Add InputStream mode for signing zips

This commit is contained in:
topjohnwu 2017-10-04 22:09:59 +08:00
parent 53237f3ae0
commit 963edfe8ab
3 changed files with 257 additions and 80 deletions

View File

@ -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);
}
}

View File

@ -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<String, JarEntry> 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<JarEntry> 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();
}
}
}

View File

@ -1,7 +1,9 @@
package com.topjohnwu.magisk.utils; package com.topjohnwu.magisk.utils;
import android.content.Context; 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.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@ -90,60 +92,43 @@ public class ZipUtils {
public native static void zipAdjust(String filenameIn, String filenameOut); public native static void zipAdjust(String filenameIn, String filenameOut);
public static String generateUnhide(Context context, File output) { public static String generateUnhide(Context context, File output) {
File temp = new File(context.getCacheDir(), "temp.apk");
String pkg = "";
try { try {
JarInputStream source = new JarInputStream(context.getAssets().open(UNHIDE_APK)); String pkg;
JarOutputStream dest = new JarOutputStream(new FileOutputStream(temp)); JarMap apk = new JarMap(context.getAssets().open(UNHIDE_APK));
JarEntry entry; JarEntry je = new JarEntry(ANDROID_MANIFEST);
int size; byte xml[] = apk.getRawData(je);
byte buffer[] = new byte[4096]; int offset = -1;
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();
// Linear search pattern offset // Linear search pattern offset
for (int i = 0; i < xml.length - UNHIDE_PKG_NAME.length; ++i) { for (int i = 0; i < xml.length - UNHIDE_PKG_NAME.length; ++i) {
boolean match = true; boolean match = true;
for (int j = 0; j < UNHIDE_PKG_NAME.length; ++j) { for (int j = 0; j < UNHIDE_PKG_NAME.length; ++j) {
if (xml[i + j] != UNHIDE_PKG_NAME[j]) { if (xml[i + j] != UNHIDE_PKG_NAME[j]) {
match = false; match = false;
break; 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);
} }
} }
if (match) {
offset = i;
break;
}
} }
source.close(); if (offset < 0)
dest.close(); return "";
signZip(context, temp, output, false);
temp.delete(); // 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) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return pkg; return "";
} }
return pkg;
} }
public static void removeTopFolder(InputStream in, File output) throws IOException { public static void removeTopFolder(InputStream in, File output) throws IOException {
@ -174,7 +159,6 @@ public class ZipUtils {
dest.close(); dest.close();
in.close(); in.close();
} catch (IOException e) { } catch (IOException e) {
Logger.dev("ZipUtils: removeTopFolder IO error!");
throw e; throw e;
} }
} }
@ -201,7 +185,6 @@ public class ZipUtils {
} else { } else {
name = entry.getName(); name = entry.getName();
} }
Logger.dev("ZipUtils: Extracting: " + entry);
File dest = new File(folder, name); File dest = new File(folder, name);
dest.getParentFile().mkdirs(); dest.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(dest); 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) { 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; int alignment = 4;
JarFile inputJar = null;
BufferedOutputStream outputFile = null; BufferedOutputStream outputFile = null;
int hashes = 0; int hashes = 0;
try { try {
@ -235,9 +233,8 @@ public class ZipUtils {
outputFile = new BufferedOutputStream(new FileOutputStream(output)); outputFile = new BufferedOutputStream(new FileOutputStream(output));
if (minSign) { if (minSign) {
ZipUtils.signWholeFile(input, publicKey, privateKey, outputFile); ZipUtils.signWholeFile(input.getInputStream(), publicKey, privateKey, outputFile);
} else { } else {
inputJar = new JarFile(input, false); // Don't verify.
JarOutputStream outputJar = new JarOutputStream(outputFile); JarOutputStream outputJar = new JarOutputStream(outputFile);
// For signing .apks, use the maximum compression to make // For signing .apks, use the maximum compression to make
// them as small as possible (since they live forever on // 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 // and produces output that is only a tiny bit larger
// (~0.1% on full OTA packages I tested). // (~0.1% on full OTA packages I tested).
outputJar.setLevel(9); outputJar.setLevel(9);
Manifest manifest = addDigestsToManifest(inputJar, hashes); Manifest manifest = addDigestsToManifest(input, hashes);
copyFiles(manifest, inputJar, outputJar, timestamp, alignment); copyFiles(manifest, input, outputJar, timestamp, alignment);
signFile(manifest, inputJar, publicKey, privateKey, outputJar); signFile(manifest, input, publicKey, privateKey, outputJar);
outputJar.close(); outputJar.close();
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
try { try {
if (inputJar != null) inputJar.close(); if (input != null) input.close();
if (outputFile != null) outputFile.close(); if (outputFile != null) outputFile.close();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
@ -335,7 +332,7 @@ public class ZipUtils {
* Add the hash(es) of every file to the manifest, creating it if * Add the hash(es) of every file to the manifest, creating it if
* necessary. * necessary.
*/ */
private static Manifest addDigestsToManifest(JarFile jar, int hashes) private static Manifest addDigestsToManifest(JarMap jar, int hashes)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest(); Manifest input = jar.getManifest();
Manifest output = new Manifest(); Manifest output = new Manifest();
@ -490,12 +487,12 @@ public class ZipUtils {
* reduce variation in the output file and make incremental OTAs * reduce variation in the output file and make incremental OTAs
* more efficient. * 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 { long timestamp, int alignment) throws IOException {
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
int num; int num;
Map<String, Attributes> entries = manifest.getEntries(); Map<String, Attributes> entries = manifest.getEntries();
ArrayList<String> names = new ArrayList<String>(entries.keySet()); ArrayList<String> names = new ArrayList<>(entries.keySet());
Collections.sort(names); Collections.sort(names);
boolean firstEntry = true; boolean firstEntry = true;
long offset = 0L; long offset = 0L;
@ -564,15 +561,15 @@ public class ZipUtils {
// Used for signWholeFile // Used for signWholeFile
private static class CMSProcessableFile implements CMSTypedData { private static class CMSProcessableFile implements CMSTypedData {
private File file; private InputStream is;
private ASN1ObjectIdentifier type; private ASN1ObjectIdentifier type;
private byte[] buffer; ByteArrayStream bos;
int bufferSize = 0;
CMSProcessableFile(File file) { CMSProcessableFile(InputStream is) {
this.file = file; this.is = is;
type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()); type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
buffer = new byte[4096]; bos = new ByteArrayStream();
bos.readFrom(is);
} }
@Override @Override
@ -582,30 +579,20 @@ public class ZipUtils {
@Override @Override
public void write(OutputStream out) throws IOException, CMSException { public void write(OutputStream out) throws IOException, CMSException {
FileInputStream input = new FileInputStream(file); bos.writeTo(out, 0, bos.size() - 2);
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;
}
} }
@Override @Override
public Object getContent() { public Object getContent() {
return file; return is;
} }
byte[] getTail() { 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) PrivateKey privateKey, OutputStream outputStream)
throws Exception { throws Exception {
ByteArrayOutputStream temp = new ByteArrayOutputStream(); ByteArrayOutputStream temp = new ByteArrayOutputStream();
@ -666,7 +653,7 @@ public class ZipUtils {
outputStream.write((total_size >> 8) & 0xff); outputStream.write((total_size >> 8) & 0xff);
temp.writeTo(outputStream); temp.writeTo(outputStream);
} }
private static void signFile(Manifest manifest, JarFile inputJar, private static void signFile(Manifest manifest, JarMap inputJar,
X509Certificate publicKey, PrivateKey privateKey, X509Certificate publicKey, PrivateKey privateKey,
JarOutputStream outputJar) JarOutputStream outputJar)
throws Exception { throws Exception {