diff --git a/app/src/main/java/com/topjohnwu/magisk/container/ByteArrayStream.java b/app/src/main/java/com/topjohnwu/magisk/container/ByteArrayStream.java new file mode 100644 index 000000000..c0811c588 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/container/ByteArrayStream.java @@ -0,0 +1,34 @@ +package com.topjohnwu.magisk.container; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ByteArrayStream extends ByteArrayOutputStream { + public byte[] getBuf() { + return buf; + } + public synchronized void readFrom(InputStream is) { + readFrom(is, Integer.MAX_VALUE); + } + public synchronized void readFrom(InputStream is, int len) { + int read; + byte buffer[] = new byte[4096]; + try { + while ((read = is.read(buffer, 0, len < buffer.length ? len : buffer.length)) > 0) { + write(buffer, 0, read); + len -= read; + } + } catch (IOException e) { + e.printStackTrace(); + } + } + public synchronized void writeTo(OutputStream out, int off, int len) throws IOException { + out.write(buf, off, len); + } + public ByteArrayInputStream getInputStream() { + return new ByteArrayInputStream(buf, 0, count); + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/container/JarMap.java b/app/src/main/java/com/topjohnwu/magisk/container/JarMap.java new file mode 100644 index 000000000..f66d40b34 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/container/JarMap.java @@ -0,0 +1,156 @@ +package com.topjohnwu.magisk.container; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/* +* A universal random access interface for both JarFile and JarInputStream +* +* In the case when JarInputStream is provided to constructor, the whole stream +* will be loaded into memory for random access purposes. +* On the other hand, when a JarFile is provided, it simply works as a wrapper. +* */ + +public class JarMap implements Closeable, AutoCloseable { + private JarFile jarFile; + private JarInputStream jis; + private InputStream is; + private File file; + private boolean isInputStream = false, hasLoaded = false, verify; + private LinkedHashMap bufMap = new LinkedHashMap<>(); + + public JarMap(File file) throws IOException { + this(file, true); + } + + public JarMap(File file, boolean verify) throws IOException { + this(file, verify, ZipFile.OPEN_READ); + } + + public JarMap(File file, boolean verify, int mode) throws IOException { + this.file = file; + jarFile = new JarFile(file, verify, mode); + } + + public JarMap(String name) throws IOException { + this(new File(name)); + } + + public JarMap(String name, boolean verify) throws IOException { + this(new File(name), verify); + } + + public JarMap(InputStream is) throws IOException { + this(is, true); + } + + public JarMap(InputStream is, boolean verify) throws IOException { + isInputStream = true; + this.is = is; + this.verify = verify; + } + + private void loadJarInputStream() { + if (!isInputStream || hasLoaded) return; + hasLoaded = true; + JarEntry entry; + try { + jis = new JarInputStream(is, verify); + while ((entry = jis.getNextJarEntry()) != null) { + bufMap.put(entry.getName(), new JarMapEntry(entry, jis)); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public InputStream getInputStream() { + try { + return isInputStream ? is : new FileInputStream(file); + } catch (FileNotFoundException e) { + return null; + } + } + + public Manifest getManifest() throws IOException { + loadJarInputStream(); + return isInputStream ? jis.getManifest() : jarFile.getManifest(); + } + + public InputStream getInputStream(ZipEntry ze) throws IOException { + loadJarInputStream(); + return isInputStream ? ((JarMapEntry) bufMap.get(ze.getName())).getInputStream() : + jarFile.getInputStream(ze); + } + + public OutputStream getOutputStream(ZipEntry ze) { + if (!isInputStream) // Only support inputstream mode + return null; + loadJarInputStream(); + ByteArrayStream bs = ((JarMapEntry) bufMap.get(ze.getName())).data; + bs.reset(); + return bs; + } + + public byte[] getRawData(ZipEntry ze) throws IOException { + if (isInputStream) { + loadJarInputStream(); + return ((JarMapEntry) bufMap.get(ze.getName())).data.toByteArray(); + } else { + ByteArrayStream bytes = new ByteArrayStream(); + bytes.readFrom(jarFile.getInputStream(ze)); + return bytes.toByteArray(); + } + } + + public Enumeration entries() { + loadJarInputStream(); + return isInputStream ? Collections.enumeration(bufMap.values()) : jarFile.entries(); + } + + public ZipEntry getEntry(String name) { + return getJarEntry(name); + } + + public JarEntry getJarEntry(String name) { + loadJarInputStream(); + return isInputStream ? bufMap.get(name) : jarFile.getJarEntry(name); + } + + @Override + public void close() throws IOException { + if (isInputStream) + is.close(); + else + jarFile.close(); + } + + private static class JarMapEntry extends JarEntry { + ByteArrayStream data; + JarMapEntry(JarEntry je, InputStream is) { + super(je); + data = new ByteArrayStream(); + data.readFrom(is); + } + InputStream getInputStream() { + return data.getInputStream(); + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java b/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java index d82947bb0..36a4874ca 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java @@ -1,7 +1,9 @@ package com.topjohnwu.magisk.utils; import android.content.Context; -import android.text.TextUtils; + +import com.topjohnwu.magisk.container.ByteArrayStream; +import com.topjohnwu.magisk.container.JarMap; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; @@ -90,60 +92,43 @@ public class ZipUtils { public native static void zipAdjust(String filenameIn, String filenameOut); public static String generateUnhide(Context context, File output) { - File temp = new File(context.getCacheDir(), "temp.apk"); - String pkg = ""; try { - JarInputStream source = new JarInputStream(context.getAssets().open(UNHIDE_APK)); - JarOutputStream dest = new JarOutputStream(new FileOutputStream(temp)); - JarEntry entry; - int size; - byte buffer[] = new byte[4096]; - while ((entry = source.getNextJarEntry()) != null) { - dest.putNextEntry(new JarEntry(entry.getName())); - if (TextUtils.equals(entry.getName(), ANDROID_MANIFEST)) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - while((size = source.read(buffer)) != -1) { - baos.write(buffer, 0, size); - } - int offset = -1; - byte xml[] = baos.toByteArray(); + String pkg; + JarMap apk = new JarMap(context.getAssets().open(UNHIDE_APK)); + JarEntry je = new JarEntry(ANDROID_MANIFEST); + byte xml[] = apk.getRawData(je); + int offset = -1; - // Linear search pattern offset - for (int i = 0; i < xml.length - UNHIDE_PKG_NAME.length; ++i) { - boolean match = true; - for (int j = 0; j < UNHIDE_PKG_NAME.length; ++j) { - if (xml[i + j] != UNHIDE_PKG_NAME[j]) { - match = false; - break; - } - } - if (match) { - offset = i; - break; - } - } - if (offset < 0) - return ""; - - // Patch binary XML with new package name - pkg = Utils.genPackageName("com.", UNHIDE_PKG_NAME.length - 1); - System.arraycopy(pkg.getBytes(), 0, xml, offset, pkg.length()); - dest.write(xml); - } else { - while((size = source.read(buffer)) != -1) { - dest.write(buffer, 0, size); + // Linear search pattern offset + for (int i = 0; i < xml.length - UNHIDE_PKG_NAME.length; ++i) { + boolean match = true; + for (int j = 0; j < UNHIDE_PKG_NAME.length; ++j) { + if (xml[i + j] != UNHIDE_PKG_NAME[j]) { + match = false; + break; } } + if (match) { + offset = i; + break; + } } - source.close(); - dest.close(); - signZip(context, temp, output, false); - temp.delete(); + if (offset < 0) + return ""; + + // Patch binary XML with new package name + pkg = Utils.genPackageName("com.", UNHIDE_PKG_NAME.length - 1); + System.arraycopy(pkg.getBytes(), 0, xml, offset, pkg.length()); + apk.getOutputStream(je).write(xml); + + // Sign the APK + signZip(context, apk, output, false); + + return pkg; } catch (IOException e) { e.printStackTrace(); - return pkg; + return ""; } - return pkg; } public static void removeTopFolder(InputStream in, File output) throws IOException { @@ -174,7 +159,6 @@ public class ZipUtils { dest.close(); in.close(); } catch (IOException e) { - Logger.dev("ZipUtils: removeTopFolder IO error!"); throw e; } } @@ -201,7 +185,6 @@ public class ZipUtils { } else { name = entry.getName(); } - Logger.dev("ZipUtils: Extracting: " + entry); File dest = new File(folder, name); dest.getParentFile().mkdirs(); FileOutputStream out = new FileOutputStream(dest); @@ -218,9 +201,24 @@ public class ZipUtils { } } + public static void signZip(Context context, InputStream is, File output, boolean minSign) { + try { + signZip(context, new JarMap(is, false), output, minSign); + } catch (IOException e) { + e.printStackTrace(); + } + } + public static void signZip(Context context, File input, File output, boolean minSign) { + try { + signZip(context, new JarMap(new FileInputStream(input), false), output, minSign); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void signZip(Context context, JarMap input, File output, boolean minSign) { int alignment = 4; - JarFile inputJar = null; BufferedOutputStream outputFile = null; int hashes = 0; try { @@ -235,9 +233,8 @@ public class ZipUtils { outputFile = new BufferedOutputStream(new FileOutputStream(output)); if (minSign) { - ZipUtils.signWholeFile(input, publicKey, privateKey, outputFile); + ZipUtils.signWholeFile(input.getInputStream(), publicKey, privateKey, outputFile); } else { - inputJar = new JarFile(input, false); // Don't verify. JarOutputStream outputJar = new JarOutputStream(outputFile); // For signing .apks, use the maximum compression to make // them as small as possible (since they live forever on @@ -246,16 +243,16 @@ public class ZipUtils { // and produces output that is only a tiny bit larger // (~0.1% on full OTA packages I tested). outputJar.setLevel(9); - Manifest manifest = addDigestsToManifest(inputJar, hashes); - copyFiles(manifest, inputJar, outputJar, timestamp, alignment); - signFile(manifest, inputJar, publicKey, privateKey, outputJar); + Manifest manifest = addDigestsToManifest(input, hashes); + copyFiles(manifest, input, outputJar, timestamp, alignment); + signFile(manifest, input, publicKey, privateKey, outputJar); outputJar.close(); } } catch (Exception e) { e.printStackTrace(); } finally { try { - if (inputJar != null) inputJar.close(); + if (input != null) input.close(); if (outputFile != null) outputFile.close(); } catch (IOException e) { e.printStackTrace(); @@ -335,7 +332,7 @@ public class ZipUtils { * Add the hash(es) of every file to the manifest, creating it if * necessary. */ - private static Manifest addDigestsToManifest(JarFile jar, int hashes) + private static Manifest addDigestsToManifest(JarMap jar, int hashes) throws IOException, GeneralSecurityException { Manifest input = jar.getManifest(); Manifest output = new Manifest(); @@ -490,12 +487,12 @@ public class ZipUtils { * reduce variation in the output file and make incremental OTAs * more efficient. */ - private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out, + private static void copyFiles(Manifest manifest, JarMap in, JarOutputStream out, long timestamp, int alignment) throws IOException { byte[] buffer = new byte[4096]; int num; Map entries = manifest.getEntries(); - ArrayList names = new ArrayList(entries.keySet()); + ArrayList names = new ArrayList<>(entries.keySet()); Collections.sort(names); boolean firstEntry = true; long offset = 0L; @@ -564,15 +561,15 @@ public class ZipUtils { // Used for signWholeFile private static class CMSProcessableFile implements CMSTypedData { - private File file; + private InputStream is; private ASN1ObjectIdentifier type; - private byte[] buffer; - int bufferSize = 0; + ByteArrayStream bos; - CMSProcessableFile(File file) { - this.file = file; + CMSProcessableFile(InputStream is) { + this.is = is; type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); - buffer = new byte[4096]; + bos = new ByteArrayStream(); + bos.readFrom(is); } @Override @@ -582,30 +579,20 @@ public class ZipUtils { @Override public void write(OutputStream out) throws IOException, CMSException { - FileInputStream input = new FileInputStream(file); - long len = file.length() - 2; - while ((bufferSize = input.read(buffer)) > 0) { - if (len <= bufferSize) { - out.write(buffer, 0, (int) len); - break; - } else { - out.write(buffer, 0, bufferSize); - } - len -= bufferSize; - } + bos.writeTo(out, 0, bos.size() - 2); } @Override public Object getContent() { - return file; + return is; } byte[] getTail() { - return Arrays.copyOfRange(buffer, 0, bufferSize); + return Arrays.copyOfRange(bos.getBuf(), bos.size() - 22, bos.size()); } } - private static void signWholeFile(File input, X509Certificate publicKey, + private static void signWholeFile(InputStream input, X509Certificate publicKey, PrivateKey privateKey, OutputStream outputStream) throws Exception { ByteArrayOutputStream temp = new ByteArrayOutputStream(); @@ -666,7 +653,7 @@ public class ZipUtils { outputStream.write((total_size >> 8) & 0xff); temp.writeTo(outputStream); } - private static void signFile(Manifest manifest, JarFile inputJar, + private static void signFile(Manifest manifest, JarMap inputJar, X509Certificate publicKey, PrivateKey privateKey, JarOutputStream outputJar) throws Exception {