From 05f41928cd0f72ddab557c8db6c2fc11c3fd08e6 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Mon, 30 Oct 2017 03:45:22 +0800 Subject: [PATCH] Add boot signing --- app/build.gradle | 10 +- .../magisk/asyncs/CheckSafetyNet.java | 2 +- .../topjohnwu/magisk/asyncs/HideManager.java | 2 +- .../topjohnwu/magisk/components/Activity.java | 10 +- .../magisk/services/OnBootIntentService.java | 6 +- .../topjohnwu/magisk/utils/BootSigner.java | 46 ++++ .../com/topjohnwu/magisk/utils/Utils.java | 12 + .../com/topjohnwu/magisk/utils/ZipUtils.java | 8 +- {jarsigner => crypto}/.gitignore | 0 {jarsigner => crypto}/build.gradle | 7 +- .../topjohnwu/crypto}/ByteArrayStream.java | 2 +- .../com/topjohnwu/crypto/CryptoUtils.java | 136 ++++++++++ .../java/com/topjohnwu/crypto}/JarMap.java | 2 +- .../java/com/topjohnwu/crypto}/SignAPK.java | 45 +--- .../java/com/topjohnwu/crypto/SignBoot.java | 234 ++++++++++++++++++ .../java/com/topjohnwu/crypto/ZipSigner.java | 4 +- settings.gradle | 2 +- 17 files changed, 460 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/com/topjohnwu/magisk/utils/BootSigner.java rename {jarsigner => crypto}/.gitignore (100%) rename {jarsigner => crypto}/build.gradle (81%) rename {jarsigner/src/main/java/com/topjohnwu/jarsigner => crypto/src/main/java/com/topjohnwu/crypto}/ByteArrayStream.java (96%) create mode 100644 crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java rename {jarsigner/src/main/java/com/topjohnwu/jarsigner => crypto/src/main/java/com/topjohnwu/crypto}/JarMap.java (99%) rename {jarsigner/src/main/java/com/topjohnwu/jarsigner => crypto/src/main/java/com/topjohnwu/crypto}/SignAPK.java (91%) create mode 100644 crypto/src/main/java/com/topjohnwu/crypto/SignBoot.java rename jarsigner/src/main/java/com/topjohnwu/jarsigner/CommandLine.java => crypto/src/main/java/com/topjohnwu/crypto/ZipSigner.java (95%) diff --git a/app/build.gradle b/app/build.gradle index 4c9a6a071..d230d7a83 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,11 +54,11 @@ repositories { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation project(':jarsigner') - implementation 'com.android.support:recyclerview-v7:26.1.0' - implementation 'com.android.support:cardview-v7:26.1.0' - implementation 'com.android.support:design:26.1.0' - implementation 'com.android.support:support-v4:26.1.0' + implementation project(':crypto') + implementation 'com.android.support:recyclerview-v7:27.0.0' + implementation 'com.android.support:cardview-v7:27.0.0' + implementation 'com.android.support:design:27.0.0' + implementation 'com.android.support:support-v4:27.0.0' implementation 'com.jakewharton:butterknife:8.8.1' implementation 'com.atlassian.commonmark:commonmark:0.10.0' implementation 'org.kamranzafar:jtar:2.3' diff --git a/app/src/main/java/com/topjohnwu/magisk/asyncs/CheckSafetyNet.java b/app/src/main/java/com/topjohnwu/magisk/asyncs/CheckSafetyNet.java index cfd411430..95ec17792 100644 --- a/app/src/main/java/com/topjohnwu/magisk/asyncs/CheckSafetyNet.java +++ b/app/src/main/java/com/topjohnwu/magisk/asyncs/CheckSafetyNet.java @@ -2,7 +2,7 @@ package com.topjohnwu.magisk.asyncs; import android.app.Activity; -import com.topjohnwu.jarsigner.ByteArrayStream; +import com.topjohnwu.crypto.ByteArrayStream; import com.topjohnwu.magisk.MagiskManager; import com.topjohnwu.magisk.utils.Shell; import com.topjohnwu.magisk.utils.WebService; diff --git a/app/src/main/java/com/topjohnwu/magisk/asyncs/HideManager.java b/app/src/main/java/com/topjohnwu/magisk/asyncs/HideManager.java index 3c20fb8ba..2083c7aa3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/asyncs/HideManager.java +++ b/app/src/main/java/com/topjohnwu/magisk/asyncs/HideManager.java @@ -3,7 +3,7 @@ package com.topjohnwu.magisk.asyncs; import android.os.Environment; import android.widget.Toast; -import com.topjohnwu.jarsigner.JarMap; +import com.topjohnwu.crypto.JarMap; import com.topjohnwu.magisk.MagiskManager; import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.database.SuDatabaseHelper; diff --git a/app/src/main/java/com/topjohnwu/magisk/components/Activity.java b/app/src/main/java/com/topjohnwu/magisk/components/Activity.java index 4e43e8bcb..910583925 100644 --- a/app/src/main/java/com/topjohnwu/magisk/components/Activity.java +++ b/app/src/main/java/com/topjohnwu/magisk/components/Activity.java @@ -14,6 +14,7 @@ import android.view.WindowManager; import com.topjohnwu.magisk.MagiskManager; import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.utils.Topic; +import com.topjohnwu.magisk.utils.Utils; public class Activity extends AppCompatActivity { @@ -88,14 +89,9 @@ public class Activity extends AppCompatActivity { @Keep public void swapResources(String dexPath) { - try { - AssetManager asset = AssetManager.class.newInstance(); - AssetManager.class.getMethod("addAssetPath", String.class).invoke(asset, dexPath); - mAssetManager = asset; - } catch (Exception e) { - e.printStackTrace(); + mAssetManager = Utils.getAssets(dexPath); + if (mAssetManager == null) return; - } Resources res = super.getResources(); mResources = new Resources(mAssetManager, res.getDisplayMetrics(), res.getConfiguration()); mResources.newTheme().setTo(super.getTheme()); diff --git a/app/src/main/java/com/topjohnwu/magisk/services/OnBootIntentService.java b/app/src/main/java/com/topjohnwu/magisk/services/OnBootIntentService.java index 38a6bfe35..a2386c1a5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/services/OnBootIntentService.java +++ b/app/src/main/java/com/topjohnwu/magisk/services/OnBootIntentService.java @@ -3,8 +3,9 @@ package com.topjohnwu.magisk.services; import android.app.IntentService; import android.content.Intent; 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; public class OnBootIntentService extends IntentService { @@ -19,7 +20,8 @@ public class OnBootIntentService extends IntentService { public void onCreate() { super.onCreate(); 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) .setContentTitle("onBoot") .setContentText("Running onBoot operations..."); diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/BootSigner.java b/app/src/main/java/com/topjohnwu/magisk/utils/BootSigner.java new file mode 100644 index 000000000..41867aef2 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/BootSigner.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java index f4892a8e2..e8a3a81be 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.java @@ -7,6 +7,7 @@ import android.content.Context; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.AssetManager; import android.content.res.Configuration; import android.database.Cursor; import android.net.ConnectivityManager; @@ -203,4 +204,15 @@ public class Utils { public static File getDatabasePath(Context context, String 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; + } + } } \ No newline at end of file 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 aa97c0ca2..6fc0f3841 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java +++ b/app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java @@ -2,8 +2,8 @@ package com.topjohnwu.magisk.utils; import android.content.res.AssetManager; -import com.topjohnwu.jarsigner.JarMap; -import com.topjohnwu.jarsigner.SignAPK; +import com.topjohnwu.crypto.JarMap; +import com.topjohnwu.crypto.SignAPK; import com.topjohnwu.magisk.MagiskManager; import java.io.BufferedInputStream; @@ -16,8 +16,8 @@ import java.util.jar.JarInputStream; public class ZipUtils { // File name in assets - private static final String PUBLIC_KEY_NAME = "public.certificate.x509.pem"; - private static final String PRIVATE_KEY_NAME = "private.key.pk8"; + static final String PUBLIC_KEY_NAME = "public.certificate.x509.pem"; + static final String PRIVATE_KEY_NAME = "private.key.pk8"; static { System.loadLibrary("zipadjust"); diff --git a/jarsigner/.gitignore b/crypto/.gitignore similarity index 100% rename from jarsigner/.gitignore rename to crypto/.gitignore diff --git a/jarsigner/build.gradle b/crypto/build.gradle similarity index 81% rename from jarsigner/build.gradle rename to crypto/build.gradle index 59afb7317..9a9a419ec 100644 --- a/jarsigner/build.gradle +++ b/crypto/build.gradle @@ -8,13 +8,14 @@ targetCompatibility = "1.8" jar { manifest { - attributes 'Main-Class': 'com.topjohnwu.jarsigner.CommandLine' + attributes 'Main-Class': 'com.topjohnwu.crypto.ZipSigner' } } shadowJar { - classifier = 'fat' - version = null + baseName = 'zipsigner' + classifier = null + version = 1.0 } buildscript { diff --git a/jarsigner/src/main/java/com/topjohnwu/jarsigner/ByteArrayStream.java b/crypto/src/main/java/com/topjohnwu/crypto/ByteArrayStream.java similarity index 96% rename from jarsigner/src/main/java/com/topjohnwu/jarsigner/ByteArrayStream.java rename to crypto/src/main/java/com/topjohnwu/crypto/ByteArrayStream.java index 5c71946f8..ef6c7bc6d 100644 --- a/jarsigner/src/main/java/com/topjohnwu/jarsigner/ByteArrayStream.java +++ b/crypto/src/main/java/com/topjohnwu/crypto/ByteArrayStream.java @@ -1,4 +1,4 @@ -package com.topjohnwu.jarsigner; +package com.topjohnwu.crypto; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java b/crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java new file mode 100644 index 000000000..45bf1655c --- /dev/null +++ b/crypto/src/main/java/com/topjohnwu/crypto/CryptoUtils.java @@ -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 ID_TO_ALG; + private static final Map 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(); + } + } +} diff --git a/jarsigner/src/main/java/com/topjohnwu/jarsigner/JarMap.java b/crypto/src/main/java/com/topjohnwu/crypto/JarMap.java similarity index 99% rename from jarsigner/src/main/java/com/topjohnwu/jarsigner/JarMap.java rename to crypto/src/main/java/com/topjohnwu/crypto/JarMap.java index e6f85cec2..334c5b8db 100644 --- a/jarsigner/src/main/java/com/topjohnwu/jarsigner/JarMap.java +++ b/crypto/src/main/java/com/topjohnwu/crypto/JarMap.java @@ -1,4 +1,4 @@ -package com.topjohnwu.jarsigner; +package com.topjohnwu.crypto; import java.io.Closeable; import java.io.File; diff --git a/jarsigner/src/main/java/com/topjohnwu/jarsigner/SignAPK.java b/crypto/src/main/java/com/topjohnwu/crypto/SignAPK.java similarity index 91% rename from jarsigner/src/main/java/com/topjohnwu/jarsigner/SignAPK.java rename to crypto/src/main/java/com/topjohnwu/crypto/SignAPK.java index 11f16e5c2..a89691165 100644 --- a/jarsigner/src/main/java/com/topjohnwu/jarsigner/SignAPK.java +++ b/crypto/src/main/java/com/topjohnwu/crypto/SignAPK.java @@ -1,10 +1,9 @@ -package com.topjohnwu.jarsigner; +package com.topjohnwu.crypto; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DEROutputStream; import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.CMSProcessableByteArray; @@ -20,7 +19,6 @@ import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.bouncycastle.util.encoders.Base64; import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -31,15 +29,12 @@ import java.io.OutputStream; import java.io.PrintStream; import java.security.DigestOutputStream; import java.security.GeneralSecurityException; -import java.security.KeyFactory; import java.security.MessageDigest; import java.security.PrivateKey; import java.security.Provider; import java.security.Security; import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -69,8 +64,8 @@ public class SignAPK { private static final int USE_SHA256 = 2; static { - SignAPK.sBouncyCastleProvider = new BouncyCastleProvider(); - Security.insertProviderAt(SignAPK.sBouncyCastleProvider, 1); + sBouncyCastleProvider = new BouncyCastleProvider(); + Security.insertProviderAt(sBouncyCastleProvider, 1); } public static void signZip(InputStream publicIn, InputStream privateIn, @@ -78,14 +73,14 @@ public class SignAPK { int alignment = 4; BufferedOutputStream outputFile; int hashes = 0; - X509Certificate publicKey = readPublicKey(publicIn); + X509Certificate publicKey = CryptoUtils.readPublicKey(publicIn); hashes |= getDigestAlgorithm(publicKey); // Set the ZIP file timestamp to the starting valid time // of the 0th certificate plus one hour (to match what // we've historically done). long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; - PrivateKey privateKey = readPrivateKey(privateIn); + PrivateKey privateKey = CryptoUtils.readPrivateKey(privateIn); outputFile = new BufferedOutputStream(new FileOutputStream(output)); if (minSign) { @@ -144,37 +139,7 @@ public class SignAPK { private static Pattern stripPattern = Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" + 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 * necessary. diff --git a/crypto/src/main/java/com/topjohnwu/crypto/SignBoot.java b/crypto/src/main/java/com/topjohnwu/crypto/SignBoot.java new file mode 100644 index 000000000..4145b5e1b --- /dev/null +++ b/crypto/src/main/java/com/topjohnwu/crypto/SignBoot.java @@ -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); + } + + } +} diff --git a/jarsigner/src/main/java/com/topjohnwu/jarsigner/CommandLine.java b/crypto/src/main/java/com/topjohnwu/crypto/ZipSigner.java similarity index 95% rename from jarsigner/src/main/java/com/topjohnwu/jarsigner/CommandLine.java rename to crypto/src/main/java/com/topjohnwu/crypto/ZipSigner.java index 9eb7888b2..6b4599dbf 100644 --- a/jarsigner/src/main/java/com/topjohnwu/jarsigner/CommandLine.java +++ b/crypto/src/main/java/com/topjohnwu/crypto/ZipSigner.java @@ -1,4 +1,4 @@ -package com.topjohnwu.jarsigner; +package com.topjohnwu.crypto; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -7,7 +7,7 @@ import java.io.FileInputStream; import java.io.InputStream; import java.security.Security; -public class CommandLine { +public class ZipSigner { public static void main(String[] args) { boolean minSign = false; int argStart = 0; diff --git a/settings.gradle b/settings.gradle index f255cbc46..9f2cf784e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':snet', ':jarsigner' \ No newline at end of file +include ':app', ':snet', ':crypto'