In an earlier version of the App, there was a cool feature: web-to-app. So how does such a feature work?

Plan expected

If we use IDE development, this function can be fully realized by using a WebView, and the corresponding URL of the web page only needs to be configured when packaging, but it cannot be directly exported in the installed App and signed installation, and in the mobile phone, there is no way to directly compile the code as APK. So guess a wooden letter is to modify an existing APK, and then sign.

Modify APK locally

As mentioned above, if we want to modify an APK, it is difficult to modify the dex code part in the phone, but it is more possible to modify the manifest file or other resource file. Apkeditor is a Java project on Github that can modify manifest file contents and replace image resource files.

Local signature

Backward signature scheme

However, this project also has its shortcomings, after all, it was five years ago. There is the following code in KeyHelper

/** * signature prefix * First sign any APK with the keystore generated above, * @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); // Current -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);


    }
Copy the code

Obviously, the signature length is only 512, and there is such a comment in SignApk

/**
 * 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() functionBelow. * / / * * * the original code aosp build project directory/tools/signapk/signapk. How to generate the Java * privateKey and sigPrefix see {@ see KeyHelper} * /Copy the code

And a piece of code that looks like this

/**
     * 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;
    }
Copy the code

And analyzed after viewing the information of the signature file

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.(
]
]
Copy the code

It was found that SHA256withRSA was used, 512 RAS was used for signature, and from the APK of signature, only V1 signature was carried out, but now V3 signature has come out

So what’s the signature file that we’re signing now?

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
Copy the code

The above is our new signature file, this one uses SHA256withRSA, 2048 bit RAS for signature.

Use a new signature scheme

Although the above signature scheme is relatively backward, this paragraph of his notes let me find the direction

/ * * * the original code aosp build project directory/tools/signapk/signapk. How to generate the Java * privateKey and sigPrefix see {@ see KeyHelper} * /Copy the code

Apksigner. jar can be found in the Android SDK directory under build-tools/28.0.3/lib/, which is available above build-tools 24. Jarsigner works only on Android 7.0 and above and supports generation 1-3 signature technology, but it is also impossible to get Jarsigner from your computer because it is provided by Java and is an executable file, not a JAR file. But this file is found in the source/prebuilts/SDK/tools/lib/signapk jar, this file can be run under the Android 7.0, V1 signature. Through the above operations, the two JAR files can be implemented in Android7.0 below the 1 generation signature and Android7.0 above the 1-3 generation signature

Separate PK8 and PEM

There’s a file called help_sign.txt in apksigner.jar that contains this help

        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 ifapksigner 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

Copy the code

Ks2x509. Jar is a tool that can extract PK8 files and PEM files from a JKS signature file

Signapk preparation

Since signapk.jar’s entry file signapk is not public, we need to use a class with the same package name to call it

package com.android.signapk; public class ApkSignerProxy { public static void main(String[] args) { SignApk.main(args); }}Copy the code

Write a piece of test code

Once everything is ready, test it out with code

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: Start signing");
                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: End of signature");
            } catch (Exception e) {
                e.printStackTrace();
            }
            while(! outapk.exists()) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } Log.d(TAG,"Signature completed");
            runOnUiThread(() -> Toast.makeText(MainActivity.this, "Signature completed", Toast.LENGTH_SHORT).show()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }});Copy the code

After running successfully in Android6.0 devices and Android8.0 devices to sign the file

Tools to share

  • apkeditor
  • apksigner.jar
  • signapk.jar
  • ks2x509.jar