Add boot signing
This commit is contained in:
parent
2ee0829871
commit
05f41928cd
@ -54,11 +54,11 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
implementation project(':jarsigner')
|
implementation project(':crypto')
|
||||||
implementation 'com.android.support:recyclerview-v7:26.1.0'
|
implementation 'com.android.support:recyclerview-v7:27.0.0'
|
||||||
implementation 'com.android.support:cardview-v7:26.1.0'
|
implementation 'com.android.support:cardview-v7:27.0.0'
|
||||||
implementation 'com.android.support:design:26.1.0'
|
implementation 'com.android.support:design:27.0.0'
|
||||||
implementation 'com.android.support:support-v4:26.1.0'
|
implementation 'com.android.support:support-v4:27.0.0'
|
||||||
implementation 'com.jakewharton:butterknife:8.8.1'
|
implementation 'com.jakewharton:butterknife:8.8.1'
|
||||||
implementation 'com.atlassian.commonmark:commonmark:0.10.0'
|
implementation 'com.atlassian.commonmark:commonmark:0.10.0'
|
||||||
implementation 'org.kamranzafar:jtar:2.3'
|
implementation 'org.kamranzafar:jtar:2.3'
|
||||||
|
@ -2,7 +2,7 @@ package com.topjohnwu.magisk.asyncs;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
import com.topjohnwu.jarsigner.ByteArrayStream;
|
import com.topjohnwu.crypto.ByteArrayStream;
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
import com.topjohnwu.magisk.utils.WebService;
|
import com.topjohnwu.magisk.utils.WebService;
|
||||||
|
@ -3,7 +3,7 @@ package com.topjohnwu.magisk.asyncs;
|
|||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.topjohnwu.jarsigner.JarMap;
|
import com.topjohnwu.crypto.JarMap;
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
import com.topjohnwu.magisk.database.SuDatabaseHelper;
|
import com.topjohnwu.magisk.database.SuDatabaseHelper;
|
||||||
|
@ -14,6 +14,7 @@ import android.view.WindowManager;
|
|||||||
import com.topjohnwu.magisk.MagiskManager;
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
import com.topjohnwu.magisk.utils.Topic;
|
import com.topjohnwu.magisk.utils.Topic;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
public class Activity extends AppCompatActivity {
|
public class Activity extends AppCompatActivity {
|
||||||
|
|
||||||
@ -88,14 +89,9 @@ public class Activity extends AppCompatActivity {
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public void swapResources(String dexPath) {
|
public void swapResources(String dexPath) {
|
||||||
try {
|
mAssetManager = Utils.getAssets(dexPath);
|
||||||
AssetManager asset = AssetManager.class.newInstance();
|
if (mAssetManager == null)
|
||||||
AssetManager.class.getMethod("addAssetPath", String.class).invoke(asset, dexPath);
|
|
||||||
mAssetManager = asset;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
Resources res = super.getResources();
|
Resources res = super.getResources();
|
||||||
mResources = new Resources(mAssetManager, res.getDisplayMetrics(), res.getConfiguration());
|
mResources = new Resources(mAssetManager, res.getDisplayMetrics(), res.getConfiguration());
|
||||||
mResources.newTheme().setTo(super.getTheme());
|
mResources.newTheme().setTo(super.getTheme());
|
||||||
|
@ -3,8 +3,9 @@ package com.topjohnwu.magisk.services;
|
|||||||
import android.app.IntentService;
|
import android.app.IntentService;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.v7.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
|
|
||||||
public class OnBootIntentService extends IntentService {
|
public class OnBootIntentService extends IntentService {
|
||||||
@ -19,7 +20,8 @@ public class OnBootIntentService extends IntentService {
|
|||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
|
NotificationCompat.Builder builder =
|
||||||
|
new NotificationCompat.Builder(this, MagiskManager.NOTIFICATION_CHANNEL);
|
||||||
builder.setSmallIcon(R.drawable.ic_magisk)
|
builder.setSmallIcon(R.drawable.ic_magisk)
|
||||||
.setContentTitle("onBoot")
|
.setContentTitle("onBoot")
|
||||||
.setContentText("Running onBoot operations...");
|
.setContentText("Running onBoot operations...");
|
||||||
|
46
app/src/main/java/com/topjohnwu/magisk/utils/BootSigner.java
Normal file
46
app/src/main/java/com/topjohnwu/magisk/utils/BootSigner.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
|
||||||
|
import com.topjohnwu.crypto.SignBoot;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class BootSigner {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
if ("-verify".equals(args[0])) {
|
||||||
|
String certPath = "";
|
||||||
|
if (args.length >= 4 && "-certificate".equals(args[2])) {
|
||||||
|
/* args[3] is the path to a public key certificate */
|
||||||
|
certPath = args[3];
|
||||||
|
}
|
||||||
|
/* args[1] is the path to a signed boot image */
|
||||||
|
boolean signed = SignBoot.verifySignature(args[1],
|
||||||
|
certPath.isEmpty() ? null : new FileInputStream(certPath));
|
||||||
|
System.exit(signed ? 0 : 1);
|
||||||
|
} else {
|
||||||
|
/* args[0] is the target name, typically /boot
|
||||||
|
args[1] is the path to a boot image to sign
|
||||||
|
args[2] is the path where to output the signed boot image
|
||||||
|
args[3] is the path to a private key
|
||||||
|
args[4] is the path to the matching public key certificate
|
||||||
|
*/
|
||||||
|
InputStream keyIn, sigIn;
|
||||||
|
if (args.length >= 5) {
|
||||||
|
keyIn = new FileInputStream(args[3]);
|
||||||
|
sigIn = new FileInputStream(args[4]);
|
||||||
|
} else {
|
||||||
|
/* Use internal test keys */
|
||||||
|
AssetManager asset = Utils.getAssets(System.getProperty("java.class.path"));
|
||||||
|
if (asset == null)
|
||||||
|
System.exit(1);
|
||||||
|
keyIn = asset.open(ZipUtils.PRIVATE_KEY_NAME);
|
||||||
|
sigIn = asset.open(ZipUtils.PUBLIC_KEY_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
SignBoot.doSignature(args[0], args[1], args[2], keyIn, sigIn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import android.content.Context;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
@ -203,4 +204,15 @@ public class Utils {
|
|||||||
public static File getDatabasePath(Context context, String dbName) {
|
public static File getDatabasePath(Context context, String dbName) {
|
||||||
return new File(context.getFilesDir().getParent() + "/databases", dbName);
|
return new File(context.getFilesDir().getParent() + "/databases", dbName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AssetManager getAssets(String apk) {
|
||||||
|
try {
|
||||||
|
AssetManager asset = AssetManager.class.newInstance();
|
||||||
|
AssetManager.class.getMethod("addAssetPath", String.class).invoke(asset, apk);
|
||||||
|
return asset;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,8 +2,8 @@ package com.topjohnwu.magisk.utils;
|
|||||||
|
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
|
|
||||||
import com.topjohnwu.jarsigner.JarMap;
|
import com.topjohnwu.crypto.JarMap;
|
||||||
import com.topjohnwu.jarsigner.SignAPK;
|
import com.topjohnwu.crypto.SignAPK;
|
||||||
import com.topjohnwu.magisk.MagiskManager;
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
@ -16,8 +16,8 @@ import java.util.jar.JarInputStream;
|
|||||||
|
|
||||||
public class ZipUtils {
|
public class ZipUtils {
|
||||||
// File name in assets
|
// File name in assets
|
||||||
private static final String PUBLIC_KEY_NAME = "public.certificate.x509.pem";
|
static final String PUBLIC_KEY_NAME = "public.certificate.x509.pem";
|
||||||
private static final String PRIVATE_KEY_NAME = "private.key.pk8";
|
static final String PRIVATE_KEY_NAME = "private.key.pk8";
|
||||||
|
|
||||||
static {
|
static {
|
||||||
System.loadLibrary("zipadjust");
|
System.loadLibrary("zipadjust");
|
||||||
|
@ -8,13 +8,14 @@ targetCompatibility = "1.8"
|
|||||||
|
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': 'com.topjohnwu.jarsigner.CommandLine'
|
attributes 'Main-Class': 'com.topjohnwu.crypto.ZipSigner'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowJar {
|
shadowJar {
|
||||||
classifier = 'fat'
|
baseName = 'zipsigner'
|
||||||
version = null
|
classifier = null
|
||||||
|
version = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.jarsigner;
|
package com.topjohnwu.crypto;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
136
crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java
Normal file
136
crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package com.topjohnwu.crypto;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1InputStream;
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
||||||
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||||
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||||
|
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.ECPrivateKeySpec;
|
||||||
|
import java.security.spec.ECPublicKeySpec;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class CryptoUtils {
|
||||||
|
|
||||||
|
private static final Map<String, String> ID_TO_ALG;
|
||||||
|
private static final Map<String, String> ALG_TO_ID;
|
||||||
|
|
||||||
|
static {
|
||||||
|
ID_TO_ALG = new HashMap<>();
|
||||||
|
ALG_TO_ID = new HashMap<>();
|
||||||
|
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA256.getId(), "SHA256withECDSA");
|
||||||
|
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA384.getId(), "SHA384withECDSA");
|
||||||
|
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA512.getId(), "SHA512withECDSA");
|
||||||
|
ID_TO_ALG.put(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1withRSA");
|
||||||
|
ID_TO_ALG.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256withRSA");
|
||||||
|
ID_TO_ALG.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512withRSA");
|
||||||
|
ALG_TO_ID.put("SHA256withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA256.getId());
|
||||||
|
ALG_TO_ID.put("SHA384withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA384.getId());
|
||||||
|
ALG_TO_ID.put("SHA512withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA512.getId());
|
||||||
|
ALG_TO_ID.put("SHA1withRSA", PKCSObjectIdentifiers.sha1WithRSAEncryption.getId());
|
||||||
|
ALG_TO_ID.put("SHA256withRSA", PKCSObjectIdentifiers.sha256WithRSAEncryption.getId());
|
||||||
|
ALG_TO_ID.put("SHA512withRSA", PKCSObjectIdentifiers.sha512WithRSAEncryption.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getSignatureAlgorithm(Key key) throws Exception {
|
||||||
|
if ("EC".equals(key.getAlgorithm())) {
|
||||||
|
int curveSize;
|
||||||
|
KeyFactory factory = KeyFactory.getInstance("EC");
|
||||||
|
if (key instanceof PublicKey) {
|
||||||
|
ECPublicKeySpec spec = factory.getKeySpec(key, ECPublicKeySpec.class);
|
||||||
|
curveSize = spec.getParams().getCurve().getField().getFieldSize();
|
||||||
|
} else if (key instanceof PrivateKey) {
|
||||||
|
ECPrivateKeySpec spec = factory.getKeySpec(key, ECPrivateKeySpec.class);
|
||||||
|
curveSize = spec.getParams().getCurve().getField().getFieldSize();
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeySpecException();
|
||||||
|
}
|
||||||
|
if (curveSize <= 256) {
|
||||||
|
return "SHA256withECDSA";
|
||||||
|
} else if (curveSize <= 384) {
|
||||||
|
return "SHA384withECDSA";
|
||||||
|
} else {
|
||||||
|
return "SHA512withECDSA";
|
||||||
|
}
|
||||||
|
} else if ("RSA".equals(key.getAlgorithm())) {
|
||||||
|
return "SHA256withRSA";
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AlgorithmIdentifier getSignatureAlgorithmIdentifier(Key key) throws Exception {
|
||||||
|
String id = ALG_TO_ID.get(getSignatureAlgorithm(key));
|
||||||
|
if (id == null) {
|
||||||
|
throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
|
||||||
|
}
|
||||||
|
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean verify(PublicKey key, byte[] input, byte[] signature,
|
||||||
|
AlgorithmIdentifier algId) throws Exception {
|
||||||
|
String algName = ID_TO_ALG.get(algId.getAlgorithm().getId());
|
||||||
|
if (algName == null) {
|
||||||
|
throw new IllegalArgumentException("Unsupported algorithm " + algId.getAlgorithm());
|
||||||
|
}
|
||||||
|
Signature verifier = Signature.getInstance(algName);
|
||||||
|
verifier.initVerify(key);
|
||||||
|
verifier.update(input);
|
||||||
|
return verifier.verify(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] sign(PrivateKey privateKey, byte[] input) throws Exception {
|
||||||
|
Signature signer = Signature.getInstance(getSignatureAlgorithm(privateKey));
|
||||||
|
signer.initSign(privateKey);
|
||||||
|
signer.update(input);
|
||||||
|
return signer.sign();
|
||||||
|
}
|
||||||
|
|
||||||
|
static X509Certificate readPublicKey(InputStream input)
|
||||||
|
throws IOException, GeneralSecurityException {
|
||||||
|
try {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
return (X509Certificate) cf.generateCertificate(input);
|
||||||
|
} finally {
|
||||||
|
input.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read a PKCS#8 format private key. */
|
||||||
|
static PrivateKey readPrivateKey(InputStream input)
|
||||||
|
throws IOException, GeneralSecurityException {
|
||||||
|
try {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int size = input.read(buffer);
|
||||||
|
byte[] bytes = Arrays.copyOf(buffer, size);
|
||||||
|
/* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
|
||||||
|
/*
|
||||||
|
* Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
|
||||||
|
* OID and use that to construct a KeyFactory.
|
||||||
|
*/
|
||||||
|
ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
|
||||||
|
PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
|
||||||
|
String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
|
||||||
|
return KeyFactory.getInstance(algOid).generatePrivate(spec);
|
||||||
|
} finally {
|
||||||
|
input.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.jarsigner;
|
package com.topjohnwu.crypto;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
@ -1,10 +1,9 @@
|
|||||||
package com.topjohnwu.jarsigner;
|
package com.topjohnwu.crypto;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
import org.bouncycastle.asn1.ASN1InputStream;
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
import org.bouncycastle.asn1.DEROutputStream;
|
import org.bouncycastle.asn1.DEROutputStream;
|
||||||
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
||||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||||
import org.bouncycastle.cms.CMSException;
|
import org.bouncycastle.cms.CMSException;
|
||||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
@ -20,7 +19,6 @@ import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
|||||||
import org.bouncycastle.util.encoders.Base64;
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@ -31,15 +29,12 @@ import java.io.OutputStream;
|
|||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Provider;
|
import java.security.Provider;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -69,8 +64,8 @@ public class SignAPK {
|
|||||||
private static final int USE_SHA256 = 2;
|
private static final int USE_SHA256 = 2;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
SignAPK.sBouncyCastleProvider = new BouncyCastleProvider();
|
sBouncyCastleProvider = new BouncyCastleProvider();
|
||||||
Security.insertProviderAt(SignAPK.sBouncyCastleProvider, 1);
|
Security.insertProviderAt(sBouncyCastleProvider, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void signZip(InputStream publicIn, InputStream privateIn,
|
public static void signZip(InputStream publicIn, InputStream privateIn,
|
||||||
@ -78,14 +73,14 @@ public class SignAPK {
|
|||||||
int alignment = 4;
|
int alignment = 4;
|
||||||
BufferedOutputStream outputFile;
|
BufferedOutputStream outputFile;
|
||||||
int hashes = 0;
|
int hashes = 0;
|
||||||
X509Certificate publicKey = readPublicKey(publicIn);
|
X509Certificate publicKey = CryptoUtils.readPublicKey(publicIn);
|
||||||
hashes |= getDigestAlgorithm(publicKey);
|
hashes |= getDigestAlgorithm(publicKey);
|
||||||
|
|
||||||
// Set the ZIP file timestamp to the starting valid time
|
// Set the ZIP file timestamp to the starting valid time
|
||||||
// of the 0th certificate plus one hour (to match what
|
// of the 0th certificate plus one hour (to match what
|
||||||
// we've historically done).
|
// we've historically done).
|
||||||
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
||||||
PrivateKey privateKey = readPrivateKey(privateIn);
|
PrivateKey privateKey = CryptoUtils.readPrivateKey(privateIn);
|
||||||
|
|
||||||
outputFile = new BufferedOutputStream(new FileOutputStream(output));
|
outputFile = new BufferedOutputStream(new FileOutputStream(output));
|
||||||
if (minSign) {
|
if (minSign) {
|
||||||
@ -144,37 +139,7 @@ public class SignAPK {
|
|||||||
private static Pattern stripPattern =
|
private static Pattern stripPattern =
|
||||||
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
|
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
|
||||||
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
|
||||||
private static X509Certificate readPublicKey(InputStream input)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
try {
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
return (X509Certificate) cf.generateCertificate(input);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read a PKCS#8 format private key. */
|
|
||||||
private static PrivateKey readPrivateKey(InputStream input)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
try {
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
int size = input.read(buffer);
|
|
||||||
byte[] bytes = Arrays.copyOf(buffer, size);
|
|
||||||
/* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
|
|
||||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
|
|
||||||
/*
|
|
||||||
* Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
|
|
||||||
* OID and use that to construct a KeyFactory.
|
|
||||||
*/
|
|
||||||
ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
|
|
||||||
PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
|
|
||||||
String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
|
|
||||||
return KeyFactory.getInstance(algOid).generatePrivate(spec);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
234
crypto/src/main/java/com/topjohnwu/crypto/SignBoot.java
Normal file
234
crypto/src/main/java/com/topjohnwu/crypto/SignBoot.java
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package com.topjohnwu.crypto;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1Encodable;
|
||||||
|
import org.bouncycastle.asn1.ASN1EncodableVector;
|
||||||
|
import org.bouncycastle.asn1.ASN1InputStream;
|
||||||
|
import org.bouncycastle.asn1.ASN1Integer;
|
||||||
|
import org.bouncycastle.asn1.ASN1Object;
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
import org.bouncycastle.asn1.ASN1Primitive;
|
||||||
|
import org.bouncycastle.asn1.ASN1Sequence;
|
||||||
|
import org.bouncycastle.asn1.DEROctetString;
|
||||||
|
import org.bouncycastle.asn1.DERPrintableString;
|
||||||
|
import org.bouncycastle.asn1.DERSequence;
|
||||||
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class SignBoot {
|
||||||
|
|
||||||
|
static {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doSignature(String target, String imagePath, String outPath,
|
||||||
|
InputStream keyIn, InputStream certIn) throws Exception {
|
||||||
|
doSignature(target, new FileInputStream(imagePath),
|
||||||
|
new FileOutputStream(outPath), keyIn, certIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doSignature(String target, InputStream imgIn, OutputStream imgOut,
|
||||||
|
InputStream keyIn, InputStream certIn) throws Exception {
|
||||||
|
ByteArrayStream bas = new ByteArrayStream();
|
||||||
|
bas.readFrom(imgIn);
|
||||||
|
byte[] image = bas.toByteArray();
|
||||||
|
bas.close();
|
||||||
|
imgIn.close();
|
||||||
|
int signableSize = getSignableImageSize(image);
|
||||||
|
if (signableSize < image.length) {
|
||||||
|
System.err.println("NOTE: truncating input from " +
|
||||||
|
image.length + " to " + signableSize + " bytes");
|
||||||
|
image = Arrays.copyOf(image, signableSize);
|
||||||
|
} else if (signableSize > image.length) {
|
||||||
|
throw new IllegalArgumentException("Invalid image: too short, expected " +
|
||||||
|
signableSize + " bytes");
|
||||||
|
}
|
||||||
|
BootSignature bootsig = new BootSignature(target, image.length);
|
||||||
|
X509Certificate cert = CryptoUtils.readPublicKey(certIn);
|
||||||
|
bootsig.setCertificate(cert);
|
||||||
|
PrivateKey key = CryptoUtils.readPrivateKey(keyIn);
|
||||||
|
bootsig.setSignature(bootsig.sign(image, key),
|
||||||
|
CryptoUtils.getSignatureAlgorithmIdentifier(key));
|
||||||
|
byte[] encoded_bootsig = bootsig.getEncoded();
|
||||||
|
imgOut.write(image);
|
||||||
|
imgOut.write(encoded_bootsig);
|
||||||
|
imgOut.flush();
|
||||||
|
imgOut.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifySignature(String imagePath, InputStream certPath) throws Exception {
|
||||||
|
ByteArrayStream bas = new ByteArrayStream();
|
||||||
|
bas.readFrom(new FileInputStream(imagePath));
|
||||||
|
byte[] image = bas.toByteArray();
|
||||||
|
bas.close();
|
||||||
|
int signableSize = getSignableImageSize(image);
|
||||||
|
if (signableSize >= image.length) {
|
||||||
|
System.err.println("Invalid image: not signed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
byte[] signature = Arrays.copyOfRange(image, signableSize, image.length);
|
||||||
|
BootSignature bootsig = new BootSignature(signature);
|
||||||
|
if (certPath != null) {
|
||||||
|
bootsig.setCertificate(CryptoUtils.readPublicKey(certPath));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (bootsig.verify(Arrays.copyOf(image, signableSize))) {
|
||||||
|
System.err.println("Signature is VALID");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
System.err.println("Signature is INVALID");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSignableImageSize(byte[] data) throws Exception {
|
||||||
|
if (!Arrays.equals(Arrays.copyOfRange(data, 0, 8),
|
||||||
|
"ANDROID!".getBytes("US-ASCII"))) {
|
||||||
|
throw new IllegalArgumentException("Invalid image header: missing magic");
|
||||||
|
}
|
||||||
|
ByteBuffer image = ByteBuffer.wrap(data);
|
||||||
|
image.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
image.getLong(); // magic
|
||||||
|
int kernelSize = image.getInt();
|
||||||
|
image.getInt(); // kernel_addr
|
||||||
|
int ramdskSize = image.getInt();
|
||||||
|
image.getInt(); // ramdisk_addr
|
||||||
|
int secondSize = image.getInt();
|
||||||
|
image.getLong(); // second_addr + tags_addr
|
||||||
|
int pageSize = image.getInt();
|
||||||
|
int length = pageSize // include the page aligned image header
|
||||||
|
+ ((kernelSize + pageSize - 1) / pageSize) * pageSize
|
||||||
|
+ ((ramdskSize + pageSize - 1) / pageSize) * pageSize
|
||||||
|
+ ((secondSize + pageSize - 1) / pageSize) * pageSize;
|
||||||
|
length = ((length + pageSize - 1) / pageSize) * pageSize;
|
||||||
|
if (length <= 0) {
|
||||||
|
throw new IllegalArgumentException("Invalid image header: invalid length");
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class BootSignature extends ASN1Object {
|
||||||
|
private ASN1Integer formatVersion;
|
||||||
|
private ASN1Encodable certificate;
|
||||||
|
private AlgorithmIdentifier algorithmIdentifier;
|
||||||
|
private DERPrintableString target;
|
||||||
|
private ASN1Integer length;
|
||||||
|
private DEROctetString signature;
|
||||||
|
private PublicKey publicKey;
|
||||||
|
private static final int FORMAT_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the object for signing an image file
|
||||||
|
* @param target Target name, included in the signed data
|
||||||
|
* @param length Length of the image, included in the signed data
|
||||||
|
*/
|
||||||
|
public BootSignature(String target, int length) {
|
||||||
|
this.formatVersion = new ASN1Integer(FORMAT_VERSION);
|
||||||
|
this.target = new DERPrintableString(target);
|
||||||
|
this.length = new ASN1Integer(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the object for verifying a signed image file
|
||||||
|
* @param signature Signature footer
|
||||||
|
*/
|
||||||
|
public BootSignature(byte[] signature)
|
||||||
|
throws Exception {
|
||||||
|
ASN1InputStream stream = new ASN1InputStream(signature);
|
||||||
|
ASN1Sequence sequence = (ASN1Sequence) stream.readObject();
|
||||||
|
formatVersion = (ASN1Integer) sequence.getObjectAt(0);
|
||||||
|
if (formatVersion.getValue().intValue() != FORMAT_VERSION) {
|
||||||
|
throw new IllegalArgumentException("Unsupported format version");
|
||||||
|
}
|
||||||
|
certificate = sequence.getObjectAt(1);
|
||||||
|
byte[] encoded = ((ASN1Object) certificate).getEncoded();
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
X509Certificate c = (X509Certificate) cf.generateCertificate(bis);
|
||||||
|
publicKey = c.getPublicKey();
|
||||||
|
ASN1Sequence algId = (ASN1Sequence) sequence.getObjectAt(2);
|
||||||
|
algorithmIdentifier = new AlgorithmIdentifier(
|
||||||
|
(ASN1ObjectIdentifier) algId.getObjectAt(0));
|
||||||
|
ASN1Sequence attrs = (ASN1Sequence) sequence.getObjectAt(3);
|
||||||
|
target = (DERPrintableString) attrs.getObjectAt(0);
|
||||||
|
length = (ASN1Integer) attrs.getObjectAt(1);
|
||||||
|
this.signature = (DEROctetString) sequence.getObjectAt(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ASN1Object getAuthenticatedAttributes() {
|
||||||
|
ASN1EncodableVector attrs = new ASN1EncodableVector();
|
||||||
|
attrs.add(target);
|
||||||
|
attrs.add(length);
|
||||||
|
return new DERSequence(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncodedAuthenticatedAttributes() throws IOException {
|
||||||
|
return getAuthenticatedAttributes().getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignature(byte[] sig, AlgorithmIdentifier algId) {
|
||||||
|
algorithmIdentifier = algId;
|
||||||
|
signature = new DEROctetString(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCertificate(X509Certificate cert)
|
||||||
|
throws Exception, IOException, CertificateEncodingException {
|
||||||
|
ASN1InputStream s = new ASN1InputStream(cert.getEncoded());
|
||||||
|
certificate = s.readObject();
|
||||||
|
publicKey = cert.getPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] generateSignableImage(byte[] image) throws IOException {
|
||||||
|
byte[] attrs = getEncodedAuthenticatedAttributes();
|
||||||
|
byte[] signable = Arrays.copyOf(image, image.length + attrs.length);
|
||||||
|
for (int i=0; i < attrs.length; i++) {
|
||||||
|
signable[i+image.length] = attrs[i];
|
||||||
|
}
|
||||||
|
return signable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] sign(byte[] image, PrivateKey key) throws Exception {
|
||||||
|
byte[] signable = generateSignableImage(image);
|
||||||
|
return CryptoUtils.sign(key, signable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(byte[] image) throws Exception {
|
||||||
|
if (length.getValue().intValue() != image.length) {
|
||||||
|
throw new IllegalArgumentException("Invalid image length");
|
||||||
|
}
|
||||||
|
byte[] signable = generateSignableImage(image);
|
||||||
|
return CryptoUtils.verify(publicKey, signable, signature.getOctets(),
|
||||||
|
algorithmIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ASN1Primitive toASN1Primitive() {
|
||||||
|
ASN1EncodableVector v = new ASN1EncodableVector();
|
||||||
|
v.add(formatVersion);
|
||||||
|
v.add(certificate);
|
||||||
|
v.add(algorithmIdentifier);
|
||||||
|
v.add(getAuthenticatedAttributes());
|
||||||
|
v.add(signature);
|
||||||
|
return new DERSequence(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.jarsigner;
|
package com.topjohnwu.crypto;
|
||||||
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ import java.io.FileInputStream;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
|
||||||
public class CommandLine {
|
public class ZipSigner {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
boolean minSign = false;
|
boolean minSign = false;
|
||||||
int argStart = 0;
|
int argStart = 0;
|
@ -1 +1 @@
|
|||||||
include ':app', ':snet', ':jarsigner'
|
include ':app', ':snet', ':crypto'
|
||||||
|
Loading…
Reference in New Issue
Block a user