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;
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<String, Attributes> entries = manifest.getEntries();
ArrayList<String> names = new ArrayList<String>(entries.keySet());
ArrayList<String> 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 {