Android 本地打包簽名方案嘗試

在一個木函早先版本,有一個挺炫酷的功能:網頁轉App。那麼這麼一個功能是怎麼實現的呢?java

方案預想

若是咱們使用IDE開發的話,這個功能徹底可使用一個WebView去實現,至於網頁對應的URL只須要在打包的時候進行配置就好了,但是並沒有法作到在已安裝App中直接出包並簽名安裝,並且在手機中,並無法直接將代碼編譯稱APK。因此猜想一個木函是將一個已有的APK進行修改,而後進行簽名。android

本地修改APK

如上分析,若是咱們要對一個APK進行修改,dex代碼部分在手機中修改天然是有難度了,可是若是對清單文件,或者是其餘資源文件進行修改就比較有可能了。在Github上有找到一個Java項目apkeditor,運行了這個項目發現,這個項目能夠作到修改清單文件內容,替換圖片資源文件。git

本地簽名

簽名方案落後

不過一樣這個項目有其不足的地方,畢竟是5年前的項目了。在KeyHelper中有如下代碼github

/**
     * 簽名前綴
     * 首先用上面生成的keystore簽名任意一個apk,解壓出這個apk裏面 META-INF/CERT.RSA 的文件
     * @throws IOException
     */
    private static void getSigPrefix() throws IOException, URISyntaxException {
        System.out.println("----------");
        String rsaFileName="CERT.RSA";
        File file = new File(ClassLoader.getSystemClassLoader().getResource(rsaFileName).toURI());
        FileInputStream fis = new FileInputStream(file);

        /**
         * RSA-keysize signature-length
         # 512 64
         # 1024 128
         # 2048 256
         */

        int same = (int) (file.length() - 64);  //當前-keysize 512

        byte[] buff = new byte[same];
        fis.read(buff, 0, same);
        fis.close();

        String string = new String(Base64.encodeBase64(buff), "UTF-8");
        System.out.println("sigPrefix -->> " + string);


    }
複製代碼

很明顯,這個簽名長度只是512,在SignApk中有這樣一段註釋bash

/**
 * HISTORICAL NOTE:
 * <p/>
 * Prior to the keylimepie release, SignApk ignored the signature
 * algorithm specified in the certificate and always used SHA1withRSA.
 * <p/>
 * Starting with keylimepie, we support SHA256withRSA, and use the
 * signature algorithm in the certificate to select which to use
 * (SHA256withRSA or SHA1withRSA).
 * <p/>
 * Because there are old keys still in use whose certificate actually
 * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
 * for compatibility with older releases.  This can be changed by
 * altering the getAlgorithm() function below.
 */
/**
 *  原始代碼見aosp項目目錄 build/tools/signapk/SignApk.java
 *  如何生成privateKey 和 sigPrefix 見{@see KeyHelper}
 */
複製代碼

以及這樣一段代碼app

/**
     * Add the hash(es) of every file to the manifest, creating it if
     * necessary.
     */
    private Manifest addDigestsToManifest(JarFile jar)
            throws IOException, GeneralSecurityException {
        Manifest input = jar.getManifest();
        Manifest output = new Manifest();
        Attributes main = output.getMainAttributes();
        if (input != null) {
            main.putAll(input.getMainAttributes());
        } else {
            main.putValue("Manifest-Version", "1.0");
            main.putValue("Created-By", "1.0 (Android SignApk)");
        }

        MessageDigest md_sha1 = MessageDigest.getInstance("SHA1");

        byte[] buffer = new byte[4096];
        int num;

        // We sort the input entries by name, and add them to the
        // output manifest in sorted order.  We expect that the output
        // map will be deterministic.

        TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();

        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
            JarEntry entry = e.nextElement();
            byName.put(entry.getName(), entry);
        }

        for (JarEntry entry : byName.values()) {
            String name = entry.getName();
            if (!entry.isDirectory() &&
                    (stripPattern == null || !stripPattern.matcher(name).matches())) {
                InputStream data = jar.getInputStream(entry);
                while ((num = data.read(buffer)) > 0) {
                    md_sha1.update(buffer, 0, num);
                }

                Attributes attr = null;
                if (input != null) attr = input.getAttributes(name);
                attr = attr != null ? new Attributes(attr) : new Attributes();
                attr.putValue("SHA1-Digest", new String(Base64.encodeBase64(md_sha1.digest()), "ASCII"));
                output.getEntries().put(name, attr);
            }
        }

        return output;
    }
複製代碼

而且分析了查看了簽名文件的信息後ide

Creation date: Sep 22, 2015
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=z, OU=z, O=z, L=shanghai, ST=shanghai, C=cn
Issuer: CN=z, OU=z, O=z, L=shanghai, ST=shanghai, C=cn
Serial number: 139a3b79
Valid from: Tue Sep 22 20:20:51 CST 2015 until: Thu Mar 29 20:20:51 CST 2125
Certificate fingerprints:
	 SHA1: BD:1C:65:A3:39:E6:D1:33:C3:C5:AD:B0:A4:22:05:BE:90:F3:6C:CD
	 SHA256: 92:57:56:C8:CD:EF:4F:43:E9:FD:ED:2D:13:DE:47:0C:99:94:92:94:97:30:F1:B4:52:24:C5:19:A9:AC:BC:F9
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 512-bit RSA key (weak)
Version: 3

Extensions: 

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 9E 3B 67 C8 52 6E BA 7C   8F 6F E1 33 F0 5D F0 B8  .;g.Rn...o.3.]..
0010: 95 31 A8 28                                        .1.(
]
]
複製代碼

發現使用的是SHA256withRSA,512位RAS 進行簽名的,並且從簽名的APK來看,只是進行了V1簽名,但如今V3簽名都已經出來了工具

那麼咱們如今在進行簽名的簽名文件是怎樣的呢?測試

Creation date: Oct 30, 2019
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=y, OU=y, O=y, L=y, ST=y, C=y
Issuer: CN=y, OU=y, O=y, L=y, ST=y, C=y
Serial number: 693a88f7
Valid from: Wed Oct 30 20:32:02 CST 2019 until: Sun Oct 23 20:32:02 CST 2044
Certificate fingerprints:
	 SHA1: 03:71:97:17:ED:B1:8B:84:BF:D3:61:AF:A1:AC:C0:22:4B:9D:E6:75
	 SHA256: D5:E0:1D:B4:1E:9C:3F:8C:E4:3B:F0:B4:89:3D:44:F7:86:49:CE:C3:8B:BA:7A:14:C5:5F:3F:38:D5:6A:35:AC
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3
複製代碼

以上是咱們新建的簽名文件,這個使用的是SHA256withRSA,2048位RAS 進行簽名的。ui

使用新的簽名方案

雖然以上的簽名方案比較落後,可是他的這一段註釋讓我找到了方向

/**
 *  原始代碼見aosp項目目錄 build/tools/signapk/SignApk.java
 *  如何生成privateKey 和 sigPrefix 見{@see KeyHelper}
 */
複製代碼

能夠從Android源碼去尋找最新的簽名方案,從android sdk目錄下的build-tools/28.0.3/lib/裏面獲得apksigner.jar,這個是build-tools 24以上纔有的,只支持Android 7.0 以上系統運行,支持1-3代簽名技術,可是一樣的若是想要從電腦裏面獲得jarsigner就不太可能了,由於這個是由java提供的,是一個可執行文件,不是jar文件,可是在源碼中存在這個文件 /prebuilts/sdk/tools/lib/signapk.jar,這個文件能夠實如今Android 7.0如下運行,進行V1 簽名。 經過以上操做,獲得的兩個jar文件,就能夠實如今Android7.0如下進行1代簽名和Android 7.0 以上進行1-3代簽名了

分離PK8和PEM

apksigner.jar 中有個文件help_sign.txt,裏面有這樣的一段使用幫助

EXAMPLES

1. Sign an APK, in-place, using the one and only key in keystore release.jks:
$ apksigner sign --ks release.jks app.apk

1. Sign an APK, without overwriting, using the one and only key in keystore
   release.jks:
$ apksigner sign --ks release.jks --in app.apk --out app-signed.apk

3. Sign an APK using a private key and certificate stored as individual files:
$ apksigner sign --key release.pk8 --cert release.x509.pem app.apk

4. Sign an APK using two keys:
$ apksigner sign --ks release.jks --next-signer --ks magic.jks app.apk

5. Sign an APK using PKCS #11 JCA Provider:
$ apksigner sign --provider-class sun.security.pkcs11.SunPKCS11 \
    --provider-arg token.cfg --ks NONE --ks-type PKCS11 app.apk

6. Sign an APK using a non-ASCII password KeyStore created on English Windows.
   The --pass-encoding parameter is not needed if apksigner is being run on
   English Windows with Java 8 or older.
$ apksigner sign --ks release.jks --pass-encoding ibm437 app.apk

7. Sign an APK on Windows using a non-ASCII password KeyStore created on a
   modern OSX or Linux machine:
$ apksigner sign --ks release.jks --pass-encoding utf-8 app.apk

8. Sign an APK with rotated signing certificate:
$ apksigner sign --ks release.jks --next-signer --ks release2.jks \
    --lineage /path/to/signing/history/lineage app.apk

複製代碼

說明了使用簽名的幾種方案,可是爲了避免用輸入密碼,我選取了第三種方案進行簽名,這樣的話就須要獲得PK8文件和PEM文件了 網上找了ks2x509.jar這樣的一個工具,能夠從一個jks簽名文件提取生成PK8文件和PEM文件

signapk 準備

因爲signapk.jar 的入口文件SignApk並非public,因此咱們須要使用一個同包名的類才能調用到它

package com.android.signapk;

public class ApkSignerProxy {

    public static void main(String[] args) {
        SignApk.main(args);
    }

}

複製代碼

寫一段測試代碼

一切準備就行後,使用代碼測試一下

findViewById(R.id.signButton).setOnClickListener(view -> {
            File unsignFile = new File(
                    Environment.getExternalStorageDirectory(),
                    "app_debug.apk"
            );
            Log.d(TAG, "unsignFile--->" + unsignFile.getAbsolutePath());
            File outapk = new File(
                    Environment.getExternalStorageDirectory(),
                    "temp.apk"
            );
            Log.d(TAG, "outapk--->" + outapk.getAbsolutePath());
            if (outapk.exists()) {
                outapk.delete();
            }
            File pk8 = new File(
                    Environment.getExternalStorageDirectory(),
                    "testkey.pk8"
            );
            Log.d(TAG, "pk8--->" + pk8.getAbsolutePath());
            File pem = new File(
                    Environment.getExternalStorageDirectory(),
                    "testkey.x509.pem"
            );
            Log.d(TAG, "pem--->" + pem.getAbsolutePath());
            try {
                Log.d(TAG, "onClick: 簽名開始");
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
                    ApkSignerTool.main(new String[]{"sign",
                            "--key",
                            pk8.getAbsolutePath(),
                            "--cert",
                            pem.getAbsolutePath(),
                            "--v2-signing-enabled",
                            "false",
                            "--out",
                            outapk.getAbsolutePath(),
                            "--in",
                            unsignFile.getAbsolutePath()});
                } else {
                    ApkSignerProxy.main(new String[]{
                            pem.getAbsolutePath(),
                            pk8.getAbsolutePath(),
                            unsignFile.getAbsolutePath(),
                            outapk.getAbsolutePath()
                    });
                }
                Log.d(TAG, "onClick: 簽名結束");
            } catch (Exception e) {
                e.printStackTrace();
            }
            while (!outapk.exists()) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Log.d(TAG, "簽名完成");
            runOnUiThread(() -> Toast.makeText(MainActivity.this, "簽名完成", Toast.LENGTH_SHORT).show());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
複製代碼

運行後成功的在Android6.0設備和Android8.0設備對文件進行簽名

工具分享

相關文章
相關標籤/搜索