在一個木函早先版本,有一個挺炫酷的功能:網頁轉App。那麼這麼一個功能是怎麼實現的呢?java
若是咱們使用IDE開發的話,這個功能徹底可使用一個WebView去實現,至於網頁對應的URL只須要在打包的時候進行配置就好了,但是並沒有法作到在已安裝App中直接出包並簽名安裝,並且在手機中,並無法直接將代碼編譯稱APK。因此猜想一個木函是將一個已有的APK進行修改,而後進行簽名。android
如上分析,若是咱們要對一個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代簽名了
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.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設備對文件進行簽名