mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-05-12 20:34:31 +02:00
192 lines
7.5 KiB
Java
192 lines
7.5 KiB
Java
/*
|
|
* Copyright (C) 2008 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
/* This is just a copy/paste/cut job from original SignAPK sources. This
|
|
* adaptation adds only the whole-file signature to a ZIP(jar,apk) file, and
|
|
* doesn't do any of the per-file signing, creating manifests, etc. This is
|
|
* useful when you've changed the structure itself of an existing (signed!)
|
|
* ZIP file, but the extracted contents are still identical. Using
|
|
* the normal SignAPK may re-arrange other things inside the ZIP, which may
|
|
* be unwanted behavior. This version only changes the ZIP's tail and keeps
|
|
* the rest the same - CF
|
|
*/
|
|
|
|
package eu.chainfire.minsignapk;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.DataInputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.security.GeneralSecurityException;
|
|
import java.security.KeyFactory;
|
|
import java.security.PrivateKey;
|
|
import java.security.Signature;
|
|
import java.security.cert.CertificateFactory;
|
|
import java.security.cert.X509Certificate;
|
|
import java.security.spec.InvalidKeySpecException;
|
|
import java.security.spec.KeySpec;
|
|
import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
|
import sun.security.pkcs.ContentInfo;
|
|
import sun.security.pkcs.PKCS7;
|
|
import sun.security.pkcs.SignerInfo;
|
|
import sun.security.x509.AlgorithmId;
|
|
import sun.security.x509.X500Name;
|
|
|
|
public class MinSignAPK {
|
|
/** Write a .RSA file with a digital signature. */
|
|
private static void writeSignatureBlock(Signature signature, X509Certificate publicKey, OutputStream out)
|
|
throws IOException, GeneralSecurityException {
|
|
SignerInfo signerInfo = new SignerInfo(new X500Name(publicKey.getIssuerX500Principal().getName()),
|
|
publicKey.getSerialNumber(), AlgorithmId.get("SHA1"), AlgorithmId.get("RSA"), signature.sign());
|
|
|
|
PKCS7 pkcs7 = new PKCS7(new AlgorithmId[] { AlgorithmId.get("SHA1") }, new ContentInfo(ContentInfo.DATA_OID,
|
|
null), new X509Certificate[] { publicKey }, new SignerInfo[] { signerInfo });
|
|
|
|
pkcs7.encodeSignedData(out);
|
|
}
|
|
|
|
private static void signWholeOutputFile(byte[] zipData, OutputStream outputStream, X509Certificate publicKey,
|
|
PrivateKey privateKey) throws IOException, GeneralSecurityException {
|
|
|
|
// For a zip with no archive comment, the
|
|
// end-of-central-directory record will be 22 bytes long, so
|
|
// we expect to find the EOCD marker 22 bytes from the end.
|
|
if (zipData[zipData.length - 22] != 0x50 || zipData[zipData.length - 21] != 0x4b
|
|
|| zipData[zipData.length - 20] != 0x05 || zipData[zipData.length - 19] != 0x06) {
|
|
throw new IllegalArgumentException("zip data already has an archive comment");
|
|
}
|
|
|
|
Signature signature = Signature.getInstance("SHA1withRSA");
|
|
signature.initSign(privateKey);
|
|
signature.update(zipData, 0, zipData.length - 2);
|
|
|
|
ByteArrayOutputStream temp = new ByteArrayOutputStream();
|
|
|
|
// put a readable message and a null char at the start of the
|
|
// archive comment, so that tools that display the comment
|
|
// (hopefully) show something sensible.
|
|
// TODO: anything more useful we can put in this message?
|
|
byte[] message = "signed by SignApk".getBytes("UTF-8");
|
|
temp.write(message);
|
|
temp.write(0);
|
|
writeSignatureBlock(signature, publicKey, temp);
|
|
int total_size = temp.size() + 6;
|
|
if (total_size > 0xffff) {
|
|
throw new IllegalArgumentException("signature is too big for ZIP file comment");
|
|
}
|
|
// signature starts this many bytes from the end of the file
|
|
int signature_start = total_size - message.length - 1;
|
|
temp.write(signature_start & 0xff);
|
|
temp.write((signature_start >> 8) & 0xff);
|
|
// Why the 0xff bytes? In a zip file with no archive comment,
|
|
// bytes [-6:-2] of the file are the little-endian offset from
|
|
// the start of the file to the central directory. So for the
|
|
// two high bytes to be 0xff 0xff, the archive would have to
|
|
// be nearly 4GB in side. So it's unlikely that a real
|
|
// commentless archive would have 0xffs here, and lets us tell
|
|
// an old signed archive from a new one.
|
|
temp.write(0xff);
|
|
temp.write(0xff);
|
|
temp.write(total_size & 0xff);
|
|
temp.write((total_size >> 8) & 0xff);
|
|
temp.flush();
|
|
|
|
// Signature verification checks that the EOCD header is the
|
|
// last such sequence in the file (to avoid minzip finding a
|
|
// fake EOCD appended after the signature in its scan). The
|
|
// odds of producing this sequence by chance are very low, but
|
|
// let's catch it here if it does.
|
|
byte[] b = temp.toByteArray();
|
|
for (int i = 0; i < b.length - 3; ++i) {
|
|
if (b[i] == 0x50 && b[i + 1] == 0x4b && b[i + 2] == 0x05 && b[i + 3] == 0x06) {
|
|
throw new IllegalArgumentException("found spurious EOCD header at " + i);
|
|
}
|
|
}
|
|
|
|
outputStream.write(zipData, 0, zipData.length - 2);
|
|
outputStream.write(total_size & 0xff);
|
|
outputStream.write((total_size >> 8) & 0xff);
|
|
temp.writeTo(outputStream);
|
|
}
|
|
|
|
private static PrivateKey readPrivateKey(File file)
|
|
throws IOException, GeneralSecurityException {
|
|
DataInputStream input = new DataInputStream(new FileInputStream(file));
|
|
try {
|
|
byte[] bytes = new byte[(int) file.length()];
|
|
input.read(bytes);
|
|
|
|
// dont support encrypted keys atm
|
|
//KeySpec spec = decryptPrivateKey(bytes, file);
|
|
//if (spec == null) {
|
|
KeySpec spec = new PKCS8EncodedKeySpec(bytes);
|
|
//}
|
|
|
|
try {
|
|
return KeyFactory.getInstance("RSA").generatePrivate(spec);
|
|
} catch (InvalidKeySpecException ex) {
|
|
return KeyFactory.getInstance("DSA").generatePrivate(spec);
|
|
}
|
|
} finally {
|
|
input.close();
|
|
}
|
|
}
|
|
|
|
private static X509Certificate readPublicKey(File file)
|
|
throws IOException, GeneralSecurityException {
|
|
FileInputStream input = new FileInputStream(file);
|
|
try {
|
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
return (X509Certificate) cf.generateCertificate(input);
|
|
} finally {
|
|
input.close();
|
|
}
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
if (args.length < 4) {
|
|
System.out.println("MinSignAPK pemfile pk8file inzip outzip");
|
|
System.out.println("- only adds whole-file signature to zip");
|
|
return;
|
|
}
|
|
|
|
String pemFile = args[0];
|
|
String pk8File = args[1];
|
|
String inFile = args[2];
|
|
String outFile = args[3];
|
|
|
|
try {
|
|
X509Certificate publicKey = readPublicKey(new File(pemFile));
|
|
PrivateKey privateKey = readPrivateKey(new File(pk8File));
|
|
|
|
InputStream fis = new FileInputStream(inFile);
|
|
byte[] buffer = new byte[(int)(new File(inFile)).length()];
|
|
fis.read(buffer);
|
|
fis.close();
|
|
|
|
OutputStream fos = new FileOutputStream(outFile, false);
|
|
signWholeOutputFile(buffer, fos, publicKey, privateKey);
|
|
fos.close();
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|