增加更换hook框架,支持修复manifest的破解方式,支持更多功能点

This commit is contained in:
Windy 2020-02-07 01:39:48 +08:00
parent eb27c08bdb
commit 0f2645ee04
37 changed files with 2770 additions and 56 deletions

1
apksigner/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

11
apksigner/build.gradle Normal file
View File

@ -0,0 +1,11 @@
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'commons-cli:commons-cli:1.4'
implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.56.0.0'
}
sourceCompatibility = "8"
targetCompatibility = "8"

View File

@ -0,0 +1,21 @@
package net.fornwall.apksigner;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.spongycastle.util.encoders.Base64Encoder;
/** Base64 encoding handling in a portable way across Android and JSE. */
public class Base64 {
public static String encode(byte[] data) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new Base64Encoder().encode(data, 0, data.length, baos);
} catch (IOException e) {
throw new RuntimeException(e);
}
return new String(baos.toByteArray());
}
}

View File

@ -0,0 +1,158 @@
package net.fornwall.apksigner;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Vector;
import org.spongycastle.asn1.ASN1ObjectIdentifier;
import org.spongycastle.asn1.x500.style.BCStyle;
import org.spongycastle.jce.X509Principal;
import org.spongycastle.x509.X509V3CertificateGenerator;
/** All methods create self-signed certificates. */
public class CertCreator {
/** Helper class for dealing with the distinguished name RDNs. */
@SuppressWarnings("serial")
public static class DistinguishedNameValues extends LinkedHashMap<ASN1ObjectIdentifier, String> {
public DistinguishedNameValues() {
put(BCStyle.C, null);
put(BCStyle.ST, null);
put(BCStyle.L, null);
put(BCStyle.STREET, null);
put(BCStyle.O, null);
put(BCStyle.OU, null);
put(BCStyle.CN, null);
}
@Override
public String put(ASN1ObjectIdentifier oid, String value) {
if (value != null && value.equals(""))
value = null;
if (containsKey(oid))
super.put(oid, value); // preserve original ordering
else {
super.put(oid, value);
// String cn = remove(BCStyle.CN); // CN will always be last.
// put(BCStyle.CN,cn);
}
return value;
}
public void setCountry(String country) {
put(BCStyle.C, country);
}
public void setState(String state) {
put(BCStyle.ST, state);
}
public void setLocality(String locality) {
put(BCStyle.L, locality);
}
public void setStreet(String street) {
put(BCStyle.STREET, street);
}
public void setOrganization(String organization) {
put(BCStyle.O, organization);
}
public void setOrganizationalUnit(String organizationalUnit) {
put(BCStyle.OU, organizationalUnit);
}
public void setCommonName(String commonName) {
put(BCStyle.CN, commonName);
}
@Override
public int size() {
int result = 0;
for (String value : values()) {
if (value != null)
result += 1;
}
return result;
}
public X509Principal getPrincipal() {
Vector<ASN1ObjectIdentifier> oids = new Vector<>();
Vector<String> values = new Vector<>();
for (Map.Entry<ASN1ObjectIdentifier, String> entry : entrySet()) {
if (entry.getValue() != null && !entry.getValue().equals("")) {
oids.add(entry.getKey());
values.add(entry.getValue());
}
}
return new X509Principal(oids, values);
}
}
public static KeySet createKeystoreAndKey(String storePath, char[] storePass, String keyAlgorithm, int keySize,
String keyName, char[] keyPass, String certSignatureAlgorithm, int certValidityYears,
DistinguishedNameValues distinguishedNameValues) {
try {
KeySet keySet = createKey(keyAlgorithm, keySize, certSignatureAlgorithm, certValidityYears,
distinguishedNameValues);
KeyStore privateKS = KeyStoreFileManager.createKeyStore(storePass);
privateKS.setKeyEntry(keyName, keySet.privateKey, keyPass,
new java.security.cert.Certificate[] { keySet.publicKey });
File sfile = new File(storePath);
if (sfile.exists()) {
throw new IOException("File already exists: " + storePath);
}
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
return keySet;
} catch (RuntimeException x) {
throw x;
} catch (Exception x) {
throw new RuntimeException(x.getMessage(), x);
}
}
private static KeySet createKey(String keyAlgorithm, int keySize, String certSignatureAlgorithm,
int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
keyPairGenerator.initialize(keySize);
KeyPair KPair = keyPairGenerator.generateKeyPair();
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
X509Principal principal = distinguishedNameValues.getPrincipal();
// generate a positive serial number
BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
while (serialNumber.compareTo(BigInteger.ZERO) < 0)
serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
v3CertGen.setSerialNumber(serialNumber);
v3CertGen.setIssuerDN(principal);
v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L));
v3CertGen.setNotAfter(
new Date(System.currentTimeMillis() + (1000L * 60L * 60L * 24L * 366L * certValidityYears)));
v3CertGen.setSubjectDN(principal);
v3CertGen.setPublicKey(KPair.getPublic());
v3CertGen.setSignatureAlgorithm(certSignatureAlgorithm);
X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(),
KeyStoreFileManager.SECURITY_PROVIDER.getName());
return new KeySet(PKCertificate, KPair.getPrivate(), null);
} catch (Exception x) {
throw new RuntimeException(x.getMessage(), x);
}
}
}

View File

@ -0,0 +1,478 @@
/* JKS.java -- implementation of the "JKS" key store.
Copyright (C) 2003 Casey Marshall <rsdio@metastatic.org>
Permission to use, copy, modify, distribute, and sell this software and
its documentation for any purpose is hereby granted without fee,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation. No representations are made about the
suitability of this software for any purpose. It is provided "as is"
without express or implied warranty.
This program was derived by reverse-engineering Sun's own
implementation, using only the public API that is available in the 1.4.1
JDK. Hence nothing in this program is, or is derived from, anything
copyrighted by Sun Microsystems. While the "Binary Evaluation License
Agreement" that the JDK is licensed under contains blanket statements
that forbid reverse-engineering (among other things), it is my position
that US copyright law does not and cannot forbid reverse-engineering of
software to produce a compatible implementation. There are, in fact,
numerous clauses in copyright law that specifically allow
reverse-engineering, and therefore I believe it is outside of Sun's
power to enforce restrictions on reverse-engineering of their software,
and it is irresponsible for them to claim they can. */
package net.fornwall.apksigner;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.DigestInputStream;
import java.security.DigestOutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Vector;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.spec.SecretKeySpec;
/**
* This is an implementation of Sun's proprietary key store algorithm, called "JKS" for "Java Key Store". This
* implementation was created entirely through reverse-engineering.
*
* <p>
* The format of JKS files is, from the start of the file:
*
* <ol>
* <li>Magic bytes. This is a four-byte integer, in big-endian byte order, equal to <code>0xFEEDFEED</code>.</li>
* <li>The version number (probably), as a four-byte integer (all multibyte integral types are in big-endian byte
* order). The current version number (in modern distributions of the JDK) is 2.</li>
* <li>The number of entries in this keystore, as a four-byte integer. Call this value <i>n</i></li>
* <li>Then, <i>n</i> times:
* <ol>
* <li>The entry type, a four-byte int. The value 1 denotes a private key entry, and 2 denotes a trusted certificate.</li>
* <li>The entry's alias, formatted as strings such as those written by <a
* href="http://java.sun.com/j2se/1.4.1/docs/api/java/io/DataOutput.html#writeUTF(java.lang.String)"
* >DataOutput.writeUTF(String)</a>.</li>
* <li>An eight-byte integer, representing the entry's creation date, in milliseconds since the epoch.
*
* <p>
* Then, if the entry is a private key entry:
* <ol>
* <li>The size of the encoded key as a four-byte int, then that number of bytes. The encoded key is the DER encoded
* bytes of the <a
* href="http://java.sun.com/j2se/1.4.1/docs/api/javax/crypto/EncryptedPrivateKeyInfo.html">EncryptedPrivateKeyInfo</a>
* structure (the encryption algorithm is discussed later).</li>
* <li>A four-byte integer, followed by that many encoded certificates, encoded as described in the trusted certificates
* section.</li>
* </ol>
*
* <p>
* Otherwise, the entry is a trusted certificate, which is encoded as the name of the encoding algorithm (e.g. X.509),
* encoded the same way as alias names. Then, a four-byte integer representing the size of the encoded certificate, then
* that many bytes representing the encoded certificate (e.g. the DER bytes in the case of X.509).</li>
* </ol>
* </li>
* <li>Then, the signature.</li>
* </ol>
* </ol> </li> </ol>
*
* <p>
* (See <a href="http://metastatic.org/source/genkey.java">this file</a> for some idea of how I was able to figure out these algorithms)
* </p>
*
* <p>
* Decrypting the key works as follows:
*
* <ol>
* <li>The key length is the length of the ciphertext minus 40. The encrypted key, <code>ekey</code>, is the middle
* bytes of the ciphertext.</li>
* <li>Take the first 20 bytes of the encrypted key as a seed value, <code>K[0]</code>.</li>
* <li>Compute <code>K[1] ... K[n]</code>, where <code>|K[i]| = 20</code>, <code>n = ceil(|ekey| / 20)</code>, and
* <code>K[i] = SHA-1(UTF-16BE(password) + K[i-1])</code>.</li>
* <li><code>key = ekey ^ (K[1] + ... + K[n])</code>.</li>
* <li>The last 20 bytes are the checksum, computed as <code>H =
* SHA-1(UTF-16BE(password) + key)</code>. If this value does not match the last 20 bytes of the ciphertext, output
* <code>FAIL</code>. Otherwise, output <code>key</code>.</li>
* </ol>
*
* <p>
* The signature is defined as <code>SHA-1(UTF-16BE(password) +
* US_ASCII("Mighty Aphrodite") + encoded_keystore)</code> (yup, Sun engineers are just that clever).
*
* <p>
* (Above, SHA-1 denotes the secure hash algorithm, UTF-16BE the big-endian byte representation of a UTF-16 string, and
* US_ASCII the ASCII byte representation of the string.)
*
* <p>
* The original source code by Casey Marshall of this class should be available in the file <a
* href="http://metastatic.org/source/JKS.java">http://metastatic.org/source/JKS.java</a>.
*
* <p>
* Changes by Ken Ellinwood:
* <ul>
* <li>Fixed a NullPointerException in engineLoad(). This method must return gracefully if the keystore input stream is
* null.</li>
* <li>engineGetCertificateEntry() was updated to return the first cert in the chain for private key entries.</li>
* <li>Lowercase the alias names, otherwise keytool chokes on the file created by this code.</li>
* <li>Fixed the integrity check in engineLoad(), previously the exception was never thrown regardless of password
* value.</li>
* </ul>
*
* @author Casey Marshall (rsdio@metastatic.org)
* @author Ken Ellinwood
*/
public class JKS extends KeyStoreSpi {
/** Ah, Sun. So goddamned clever with those magic bytes. */
private static final int MAGIC = 0xFEEDFEED;
private static final int PRIVATE_KEY = 1;
private static final int TRUSTED_CERT = 2;
private final Vector<String> aliases = new Vector<>();
private final HashMap<String, Certificate> trustedCerts = new HashMap<>();
private final HashMap<String, byte[]> privateKeys = new HashMap<>();
private final HashMap<String, Certificate[]> certChains = new HashMap<>();
private final HashMap<String, Date> dates = new HashMap<>();
@Override
public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException {
alias = alias.toLowerCase();
if (!privateKeys.containsKey(alias))
return null;
byte[] key = decryptKey(privateKeys.get(alias), charsToBytes(password));
Certificate[] chain = engineGetCertificateChain(alias);
if (chain.length > 0) {
try {
// Private and public keys MUST have the same algorithm.
KeyFactory fact = KeyFactory.getInstance(chain[0].getPublicKey().getAlgorithm());
return fact.generatePrivate(new PKCS8EncodedKeySpec(key));
} catch (InvalidKeySpecException x) {
throw new UnrecoverableKeyException(x.getMessage());
}
} else
return new SecretKeySpec(key, alias);
}
@Override
public Certificate[] engineGetCertificateChain(String alias) {
return certChains.get(alias.toLowerCase());
}
@Override
public Certificate engineGetCertificate(String alias) {
alias = alias.toLowerCase();
if (engineIsKeyEntry(alias)) {
Certificate[] certChain = certChains.get(alias);
if (certChain != null && certChain.length > 0)
return certChain[0];
}
return trustedCerts.get(alias);
}
@Override
public Date engineGetCreationDate(String alias) {
alias = alias.toLowerCase();
return dates.get(alias);
}
// XXX implement writing methods.
@Override
public void engineSetKeyEntry(String alias, Key key, char[] passwd, Certificate[] certChain)
throws KeyStoreException {
alias = alias.toLowerCase();
if (trustedCerts.containsKey(alias))
throw new KeyStoreException("\"" + alias + " is a trusted certificate entry");
privateKeys.put(alias, encryptKey(key, charsToBytes(passwd)));
if (certChain != null)
certChains.put(alias, certChain);
else
certChains.put(alias, new Certificate[0]);
if (!aliases.contains(alias)) {
dates.put(alias, new Date());
aliases.add(alias);
}
}
@SuppressWarnings("unused")
@Override
public void engineSetKeyEntry(String alias, byte[] encodedKey, Certificate[] certChain) throws KeyStoreException {
alias = alias.toLowerCase();
if (trustedCerts.containsKey(alias))
throw new KeyStoreException("\"" + alias + "\" is a trusted certificate entry");
try {
new EncryptedPrivateKeyInfo(encodedKey);
} catch (IOException ioe) {
throw new KeyStoreException("encoded key is not an EncryptedPrivateKeyInfo");
}
privateKeys.put(alias, encodedKey);
if (certChain != null)
certChains.put(alias, certChain);
else
certChains.put(alias, new Certificate[0]);
if (!aliases.contains(alias)) {
dates.put(alias, new Date());
aliases.add(alias);
}
}
@Override
public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
alias = alias.toLowerCase();
if (privateKeys.containsKey(alias))
throw new KeyStoreException("\"" + alias + "\" is a private key entry");
if (cert == null)
throw new NullPointerException();
trustedCerts.put(alias, cert);
if (!aliases.contains(alias)) {
dates.put(alias, new Date());
aliases.add(alias);
}
}
@Override
public void engineDeleteEntry(String alias) throws KeyStoreException {
alias = alias.toLowerCase();
aliases.remove(alias);
}
@Override
public Enumeration<String> engineAliases() {
return aliases.elements();
}
@Override
public boolean engineContainsAlias(String alias) {
alias = alias.toLowerCase();
return aliases.contains(alias);
}
@Override
public int engineSize() {
return aliases.size();
}
@Override
public boolean engineIsKeyEntry(String alias) {
alias = alias.toLowerCase();
return privateKeys.containsKey(alias);
}
@Override
public boolean engineIsCertificateEntry(String alias) {
alias = alias.toLowerCase();
return trustedCerts.containsKey(alias);
}
@Override
public String engineGetCertificateAlias(Certificate cert) {
for (String alias : trustedCerts.keySet())
if (cert.equals(trustedCerts.get(alias)))
return alias;
return null;
}
@Override
public void engineStore(OutputStream out, char[] passwd) throws IOException, NoSuchAlgorithmException,
CertificateException {
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(charsToBytes(passwd));
md.update("Mighty Aphrodite".getBytes(StandardCharsets.UTF_8));
DataOutputStream dout = new DataOutputStream(new DigestOutputStream(out, md));
dout.writeInt(MAGIC);
dout.writeInt(2);
dout.writeInt(aliases.size());
for (Enumeration<String> e = aliases.elements(); e.hasMoreElements();) {
String alias = e.nextElement();
if (trustedCerts.containsKey(alias)) {
dout.writeInt(TRUSTED_CERT);
dout.writeUTF(alias);
dout.writeLong(dates.get(alias).getTime());
writeCert(dout, trustedCerts.get(alias));
} else {
dout.writeInt(PRIVATE_KEY);
dout.writeUTF(alias);
dout.writeLong(dates.get(alias).getTime());
byte[] key = privateKeys.get(alias);
dout.writeInt(key.length);
dout.write(key);
Certificate[] chain = certChains.get(alias);
dout.writeInt(chain.length);
for (int i = 0; i < chain.length; i++)
writeCert(dout, chain[i]);
}
}
byte[] digest = md.digest();
dout.write(digest);
}
@Override
public void engineLoad(InputStream in, char[] passwd) throws IOException, NoSuchAlgorithmException,
CertificateException {
MessageDigest md = MessageDigest.getInstance("SHA");
if (passwd != null)
md.update(charsToBytes(passwd));
md.update("Mighty Aphrodite".getBytes(StandardCharsets.UTF_8));
aliases.clear();
trustedCerts.clear();
privateKeys.clear();
certChains.clear();
dates.clear();
if (in == null)
return;
DataInputStream din = new DataInputStream(new DigestInputStream(in, md));
if (din.readInt() != MAGIC)
throw new IOException("not a JavaKeyStore");
din.readInt(); // version no.
final int n = din.readInt();
aliases.ensureCapacity(n);
if (n < 0)
throw new LoadKeystoreException("Malformed key store");
for (int i = 0; i < n; i++) {
int type = din.readInt();
String alias = din.readUTF();
aliases.add(alias);
dates.put(alias, new Date(din.readLong()));
switch (type) {
case PRIVATE_KEY:
int len = din.readInt();
byte[] encoded = new byte[len];
din.read(encoded);
privateKeys.put(alias, encoded);
int count = din.readInt();
Certificate[] chain = new Certificate[count];
for (int j = 0; j < count; j++)
chain[j] = readCert(din);
certChains.put(alias, chain);
break;
case TRUSTED_CERT:
trustedCerts.put(alias, readCert(din));
break;
default:
throw new LoadKeystoreException("Malformed key store");
}
}
if (passwd != null) {
byte[] computedHash = md.digest();
byte[] storedHash = new byte[20];
din.read(storedHash);
if (!MessageDigest.isEqual(storedHash, computedHash)) {
throw new LoadKeystoreException("Incorrect password, or integrity check failed.");
}
}
}
// Own methods.
// ------------------------------------------------------------------------
private static Certificate readCert(DataInputStream in) throws IOException, CertificateException {
String type = in.readUTF();
int len = in.readInt();
byte[] encoded = new byte[len];
in.read(encoded);
CertificateFactory factory = CertificateFactory.getInstance(type);
return factory.generateCertificate(new ByteArrayInputStream(encoded));
}
private static void writeCert(DataOutputStream dout, Certificate cert) throws IOException, CertificateException {
dout.writeUTF(cert.getType());
byte[] b = cert.getEncoded();
dout.writeInt(b.length);
dout.write(b);
}
private static byte[] decryptKey(byte[] encryptedPKI, byte[] passwd) throws UnrecoverableKeyException {
try {
EncryptedPrivateKeyInfo epki = new EncryptedPrivateKeyInfo(encryptedPKI);
byte[] encr = epki.getEncryptedData();
byte[] keystream = new byte[20];
System.arraycopy(encr, 0, keystream, 0, 20);
byte[] check = new byte[20];
System.arraycopy(encr, encr.length - 20, check, 0, 20);
byte[] key = new byte[encr.length - 40];
MessageDigest sha = MessageDigest.getInstance("SHA1");
int count = 0;
while (count < key.length) {
sha.reset();
sha.update(passwd);
sha.update(keystream);
sha.digest(keystream, 0, keystream.length);
for (int i = 0; i < keystream.length && count < key.length; i++) {
key[count] = (byte) (keystream[i] ^ encr[count + 20]);
count++;
}
}
sha.reset();
sha.update(passwd);
sha.update(key);
if (!MessageDigest.isEqual(check, sha.digest()))
throw new UnrecoverableKeyException("checksum mismatch");
return key;
} catch (Exception x) {
throw new UnrecoverableKeyException(x.getMessage());
}
}
private static byte[] encryptKey(Key key, byte[] passwd) throws KeyStoreException {
try {
MessageDigest sha = MessageDigest.getInstance("SHA1");
// SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
byte[] k = key.getEncoded();
byte[] encrypted = new byte[k.length + 40];
byte[] keystream = SecureRandom.getSeed(20);
System.arraycopy(keystream, 0, encrypted, 0, 20);
int count = 0;
while (count < k.length) {
sha.reset();
sha.update(passwd);
sha.update(keystream);
sha.digest(keystream, 0, keystream.length);
for (int i = 0; i < keystream.length && count < k.length; i++) {
encrypted[count + 20] = (byte) (keystream[i] ^ k[count]);
count++;
}
}
sha.reset();
sha.update(passwd);
sha.update(k);
sha.digest(encrypted, encrypted.length - 20, 20);
// 1.3.6.1.4.1.42.2.17.1.1 is Sun's private OID for this encryption algorithm.
return new EncryptedPrivateKeyInfo("1.3.6.1.4.1.42.2.17.1.1", encrypted).getEncoded();
} catch (Exception x) {
throw new KeyStoreException(x.getMessage());
}
}
private static byte[] charsToBytes(char[] passwd) {
byte[] buf = new byte[passwd.length * 2];
for (int i = 0, j = 0; i < passwd.length; i++) {
buf[j++] = (byte) (passwd[i] >>> 8);
buf[j++] = (byte) passwd[i];
}
return buf;
}
}

View File

@ -0,0 +1,20 @@
package net.fornwall.apksigner;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
public class KeySet {
/** Certificate. */
public final X509Certificate publicKey;
/** Private key. */
public final PrivateKey privateKey;
public final String signatureAlgorithm;
public KeySet(X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm) {
this.publicKey = publicKey;
this.privateKey = privateKey;
this.signatureAlgorithm = (signatureAlgorithm != null) ? signatureAlgorithm : "SHA1withRSA";
}
}

View File

@ -0,0 +1,104 @@
package net.fornwall.apksigner;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.KeyStore;
import java.security.Provider;
import java.security.Security;
import org.spongycastle.jce.provider.BouncyCastleProvider;
public class KeyStoreFileManager {
public static final Provider SECURITY_PROVIDER = new BouncyCastleProvider();
static {
// Add the spongycastle version of the BC provider so that the implementation classes returned from the keystore
// are all from the spongycastle libs.
Security.addProvider(SECURITY_PROVIDER);
}
private static class JksKeyStore extends KeyStore {
public JksKeyStore() {
super(new JKS(), SECURITY_PROVIDER, "jks");
}
}
public static KeyStore createKeyStore(char[] password) throws Exception {
KeyStore ks = new JksKeyStore();
ks.load(null, password);
return ks;
}
public static KeyStore loadKeyStore(String keystorePath, char[] password) throws Exception {
KeyStore ks = new JksKeyStore();
try (FileInputStream fis = new FileInputStream(keystorePath)) {
ks.load(fis, password);
}
return ks;
}
public static void writeKeyStore(KeyStore ks, String keystorePath, char[] password) throws Exception {
File keystoreFile = new File(keystorePath);
try {
if (keystoreFile.exists()) {
// I've had some trouble saving new versions of the key store file in which the file becomes
// empty/corrupt. Saving the new version to a new file and creating a backup of the old version.
File tmpFile = File.createTempFile(keystoreFile.getName(), null, keystoreFile.getParentFile());
try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
ks.store(fos, password);
}
/*
* create a backup of the previous version int i = 1; File backup = new File( keystorePath + "." + i +
* ".bak"); while (backup.exists()) { i += 1; backup = new File( keystorePath + "." + i + ".bak"); }
* renameTo(keystoreFile, backup);
*/
renameTo(tmpFile, keystoreFile);
} else {
try (FileOutputStream fos = new FileOutputStream(keystorePath)) {
ks.store(fos, password);
}
}
} catch (Exception x) {
try {
File logfile = File.createTempFile("zipsigner-error", ".log", keystoreFile.getParentFile());
try (PrintWriter pw = new PrintWriter(new FileWriter(logfile))) {
x.printStackTrace(pw);
}
} catch (Exception y) {
}
throw x;
}
}
static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
if (destFile.exists() && destFile.isDirectory())
throw new IOException("Destination '" + destFile + "' exists but is a directory");
try (FileInputStream input = new FileInputStream(srcFile)) {
try (FileOutputStream output = new FileOutputStream(destFile)) {
byte[] buffer = new byte[4096];
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
}
}
}
if (srcFile.length() != destFile.length()) {
throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile + "'");
}
if (preserveFileDate)
destFile.setLastModified(srcFile.lastModified());
}
public static void renameTo(File fromFile, File toFile) throws IOException {
copyFile(fromFile, toFile, true);
if (!fromFile.delete())
throw new IOException("Failed to delete " + fromFile);
}
}

View File

@ -0,0 +1,13 @@
package net.fornwall.apksigner;
import java.io.IOException;
/** Thrown by JKS.engineLoad() for errors that occur after determining the keystore is actually a JKS keystore. */
@SuppressWarnings("serial")
public class LoadKeystoreException extends IOException {
public LoadKeystoreException(String message) {
super(message);
}
}

View File

@ -0,0 +1,97 @@
package net.fornwall.apksigner;
import java.io.File;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.List;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
/** Sign files from the command line using zipsigner-lib. */
public class Main {
static void usage(Options options) {
new HelpFormatter().printHelp("apksigner [-p password] keystore input-apk output-apk",
"Sign an input APK file using the specified keystore to produce a signed and zipaligned output APK." +
" The keystore file will be created with the specified password if it does not exist. Options:",
options, "");
System.exit(1);
}
public static void main(String... args) throws Exception {
Options options = new Options();
CommandLine cmdLine = null;
Option helpOption = new Option("h", "help", false, "Display usage information.");
options.addOption(helpOption);
Option keyPasswordOption = new Option("p", "password", false, "Password for private key (default:android).");
keyPasswordOption.setArgs(1);
options.addOption(keyPasswordOption);
try {
cmdLine = new BasicParser().parse(options, args);
} catch (ParseException x) {
System.err.println(x.getMessage());
usage(options);
}
if (cmdLine.hasOption(helpOption.getOpt())) {
usage(options);
}
@SuppressWarnings("unchecked")
List<String> argList = cmdLine.getArgList();
if (argList.size() != 3) {
usage(options);
}
String keystorePath = argList.get(0);
String inputFile = argList.get(1);
String outputFile = argList.get(2);
char[] keyPassword;
if (cmdLine.hasOption(keyPasswordOption.getOpt())) {
String optionValue = cmdLine.getOptionValue(keyPasswordOption.getOpt());
if (optionValue == null || optionValue.equals("")) {
keyPassword = null;
} else {
keyPassword = optionValue.toCharArray();
}
} else {
keyPassword = "android".toCharArray();
}
File keystoreFile = new File(keystorePath);
if (!keystoreFile.exists()) {
String alias = "alias";
System.out.println("Creating new keystore (using '" + new String(keyPassword) + "' as password and '"
+ alias + "' as the key alias).");
CertCreator.DistinguishedNameValues nameValues = new CertCreator.DistinguishedNameValues();
nameValues.setCommonName("APK Signer");
nameValues.setOrganization("Earth");
nameValues.setOrganizationalUnit("Earth");
CertCreator.createKeystoreAndKey(keystorePath, keyPassword, "RSA", 2048, alias, keyPassword, "SHA1withRSA",
30, nameValues);
}
KeyStore keyStore = KeyStoreFileManager.loadKeyStore(keystorePath, null);
String alias = keyStore.aliases().nextElement();
X509Certificate publicKey = (X509Certificate) keyStore.getCertificate(alias);
try {
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, keyPassword);
ZipSigner.signZip(publicKey, privateKey, "SHA1withRSA", inputFile, outputFile);
} catch (UnrecoverableKeyException e) {
System.err.println("apksigner: Invalid key password.");
System.exit(1);
}
}
}

View File

@ -0,0 +1,56 @@
package net.fornwall.apksigner;
import org.spongycastle.cert.jcajce.JcaCertStore;
import org.spongycastle.cms.*;
import org.spongycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.spongycastle.operator.ContentSigner;
import org.spongycastle.operator.DigestCalculatorProvider;
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
import org.spongycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.spongycastle.util.Store;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
public class SignatureBlockGenerator {
/**
* Sign the given content using the private and public keys from the keySet and return the encoded CMS (PKCS#7)
* data. Use of direct signature and DER encoding produces a block that is verifiable by Android recovery programs.
*/
public static byte[] generate(KeySet keySet, byte[] content) {
try {
List<X509Certificate> certList = new ArrayList<>();
CMSTypedData msg = new CMSProcessableByteArray(content);
certList.add(keySet.publicKey);
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.signatureAlgorithm)
.setProvider("SC");
ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.privateKey);
JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder()
.setProvider("SC");
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(
digestCalculatorProvider);
jcaSignerInfoGeneratorBuilder.setDirectSignature(true);
SignerInfoGenerator signerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey);
gen.addSignerInfoGenerator(signerInfoGenerator);
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(msg, false);
return sigData.toASN1Structure().getEncoded("DER");
} catch (Exception x) {
throw new RuntimeException(x.getMessage(), x);
}
}
}

View File

@ -0,0 +1,186 @@
package net.fornwall.apksigner;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import net.fornwall.apksigner.zipio.ZioEntry;
import net.fornwall.apksigner.zipio.ZipInput;
import net.fornwall.apksigner.zipio.ZipOutput;
/**
* This is a modified copy of com.android.signapk.SignApk.java. It provides an API to sign JAR files (including APKs and
* Zip/OTA updates) in a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
*/
public class ZipSigner {
static {
if (!KeyStoreFileManager.SECURITY_PROVIDER.getName().equals("SC")) {
throw new RuntimeException("Invalid security provider");
}
}
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
// Files matching this pattern are not copied to the output.
private static final Pattern stripPattern = Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
/** Add the SHA1 of every file to the manifest, creating it if necessary. */
private static Manifest addDigestsToManifest(Map<String, ZioEntry> entries)
throws IOException, GeneralSecurityException {
Manifest input = null;
ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME);
if (manifestEntry != null) {
input = new Manifest();
input.read(manifestEntry.getInputStream());
}
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 = MessageDigest.getInstance("SHA1");
byte[] buffer = new byte[512];
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, ZioEntry> byName = new TreeMap<>();
byName.putAll(entries);
// if (debug) getLogger().debug("Manifest entries:");
for (ZioEntry entry : byName.values()) {
String name = entry.getName();
// if (debug) getLogger().debug(name);
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && !name.equals(CERT_SF_NAME)
&& !name.equals(CERT_RSA_NAME) && (stripPattern == null || !stripPattern.matcher(name).matches())) {
InputStream data = entry.getInputStream();
while ((num = data.read(buffer)) > 0) {
md.update(buffer, 0, num);
}
Attributes attr = null;
if (input != null) {
java.util.jar.Attributes inAttr = input.getAttributes(name);
if (inAttr != null)
attr = new Attributes(inAttr);
}
if (attr == null)
attr = new Attributes();
attr.putValue("SHA1-Digest", Base64.encode(md.digest()));
output.getEntries().put(name, attr);
}
}
return output;
}
/** Write the signature file to the given output stream. */
private static byte[] generateSignatureFile(Manifest manifest) throws IOException, GeneralSecurityException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(("Signature-Version: 1.0\r\n").getBytes());
out.write(("Created-By: 1.0 (Android SignApk)\r\n").getBytes());
MessageDigest md = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md), true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
out.write(("SHA1-Digest-Manifest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
String nameEntry = "Name: " + entry.getKey() + "\r\n";
print.print(nameEntry);
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
out.write(nameEntry.getBytes());
out.write(("SHA1-Digest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
}
return out.toByteArray();
}
/**
* Sign the file using the given public key cert, private key, and signature block template. The signature block
* template parameter may be null, but if so android-sun-jarsign-support.jar must be in the classpath.
*/
public static void signZip(X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm,
String inputZipFilename, String outputZipFilename) throws IOException, GeneralSecurityException {
KeySet keySet = new KeySet(publicKey, privateKey, signatureAlgorithm);
File inFile = new File(inputZipFilename).getCanonicalFile();
File outFile = new File(outputZipFilename).getCanonicalFile();
if (inFile.equals(outFile))
throw new IllegalArgumentException("Input and output files are the same");
try (ZipInput input = new ZipInput(inputZipFilename)) {
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
// MANIFEST.MF
Manifest manifest = addDigestsToManifest(input.entries);
ZioEntry ze = new ZioEntry(JarFile.MANIFEST_NAME);
ze.setTime(timestamp);
manifest.write(ze.getOutputStream());
zipOutput.write(ze);
byte[] certSfBytes = generateSignatureFile(manifest);
// CERT.SF
ze = new ZioEntry(CERT_SF_NAME);
ze.setTime(timestamp);
ze.getOutputStream().write(certSfBytes);
zipOutput.write(ze);
// CERT.RSA
ze = new ZioEntry(CERT_RSA_NAME);
ze.setTime(timestamp);
ze.getOutputStream().write(SignatureBlockGenerator.generate(keySet, certSfBytes));
zipOutput.write(ze);
// Copy all the files in a manifest from input to output. We set the modification times in the output to
// a fixed time, so as to reduce variation in the output file and make incremental OTAs more efficient.
Map<String, Attributes> entries = manifest.getEntries();
List<String> names = new ArrayList<>(entries.keySet());
Collections.sort(names);
for (String name : names) {
ZioEntry inEntry = input.entries.get(name);
inEntry.setTime(timestamp);
zipOutput.write(inEntry);
}
}
}
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2010 Ken Ellinwood
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fornwall.apksigner.zipio;
import java.io.IOException;
class CentralEnd {
public int signature = 0x06054b50; // end of central dir signature 4 bytes
public short numberThisDisk = 0; // number of this disk 2 bytes
public short centralStartDisk = 0; // number of the disk with the start of
// the central directory 2 bytes
public short numCentralEntries; // total number of entries in the central
// directory on this disk 2 bytes
public short totalCentralEntries; // total number of entries in the central
// directory 2 bytes
public int centralDirectorySize; // size of the central directory 4 bytes
public int centralStartOffset; // offset of start of central directory with
// respect to the starting disk number 4
// bytes
public String fileComment; // .ZIP file comment (variable size)
public static CentralEnd read(ZipInput input) throws IOException {
int signature = input.readInt();
if (signature != 0x06054b50) {
// Back up to the signature.
input.seek(input.getFilePointer() - 4);
return null;
}
CentralEnd entry = new CentralEnd();
entry.numberThisDisk = input.readShort();
entry.centralStartDisk = input.readShort();
entry.numCentralEntries = input.readShort();
entry.totalCentralEntries = input.readShort();
entry.centralDirectorySize = input.readInt();
entry.centralStartOffset = input.readInt();
short zipFileCommentLen = input.readShort();
entry.fileComment = input.readString(zipFileCommentLen);
return entry;
}
public void write(ZipOutput output) throws IOException {
output.writeInt(signature);
output.writeShort(numberThisDisk);
output.writeShort(centralStartDisk);
output.writeShort(numCentralEntries);
output.writeShort(totalCentralEntries);
output.writeInt(centralDirectorySize);
output.writeInt(centralStartOffset);
output.writeShort((short) fileComment.length());
output.writeString(fileComment);
}
}

View File

@ -0,0 +1,474 @@
/*
* Copyright (C) 2010 Ken Ellinwood
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fornwall.apksigner.zipio;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.Date;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
public final class ZioEntry implements Cloneable {
private ZipInput zipInput;
// public int signature = 0x02014b50;
private short versionMadeBy;
private short versionRequired;
private short generalPurposeBits;
private short compression;
private short modificationTime;
private short modificationDate;
private int crc32;
private int compressedSize;
private int size;
private String filename;
private byte[] extraData;
private short numAlignBytes = 0;
private String fileComment;
private short diskNumberStart;
private short internalAttributes;
private int externalAttributes;
private int localHeaderOffset;
private long dataPosition = -1;
private byte[] data = null;
private ZioEntryOutputStream entryOut = null;
private static byte[] alignBytes = new byte[4];
public ZioEntry(ZipInput input) throws IOException {
this.zipInput = input;
// 0 4 Central directory header signature = 0x02014b50
int signature = input.readInt();
if (signature != 0x02014b50) {
// back up to the signature
input.seek(input.getFilePointer() - 4);
throw new IOException("Central directory header signature not found");
}
// 4 2 Version needed to extract (minimum)
versionMadeBy = input.readShort();
// 4 2 Version required
versionRequired = input.readShort();
// 6 2 General purpose bit flag
generalPurposeBits = input.readShort();
// Bits 1, 2, 3, and 11 are allowed to be set (first bit is bit zero). Any others are a problem.
if ((generalPurposeBits & 0xF7F1) != 0x0000) {
throw new IllegalStateException("Can't handle general purpose bits == "
+ String.format("0x%04x", generalPurposeBits));
}
// 8 2 Compression method
compression = input.readShort();
// 10 2 File last modification time
modificationTime = input.readShort();
// 12 2 File last modification date
modificationDate = input.readShort();
// 14 4 CRC-32
crc32 = input.readInt();
// 18 4 Compressed size
compressedSize = input.readInt();
// 22 4 Uncompressed size
size = input.readInt();
// 26 2 File name length (n)
short fileNameLen = input.readShort();
// log.debug(String.format("File name length: 0x%04x", fileNameLen));
// 28 2 Extra field length (m)
short extraLen = input.readShort();
// log.debug(String.format("Extra length: 0x%04x", extraLen));
short fileCommentLen = input.readShort();
diskNumberStart = input.readShort();
internalAttributes = input.readShort();
externalAttributes = input.readInt();
localHeaderOffset = input.readInt();
// 30 n File name
filename = input.readString(fileNameLen);
extraData = input.readBytes(extraLen);
fileComment = input.readString(fileCommentLen);
generalPurposeBits = (short) (generalPurposeBits & 0x0800); // Don't
// write a
// data
// descriptor,
// preserve
// UTF-8
// encoded
// filename
// bit
// Don't write zero-length entries with compression.
if (size == 0) {
compressedSize = 0;
compression = 0;
crc32 = 0;
}
}
public ZioEntry(String name) {
filename = name;
fileComment = "";
compression = 8;
extraData = new byte[0];
setTime(System.currentTimeMillis());
}
public void readLocalHeader() throws IOException {
ZipInput input = zipInput;
input.seek(localHeaderOffset);
// 0 4 Local file header signature = 0x04034b50
int signature = input.readInt();
if (signature != 0x04034b50) {
throw new IllegalStateException(String.format("Local header not found at pos=0x%08x, file=%s",
input.getFilePointer(), filename));
}
// This method is usually called just before the data read, so
// its only purpose currently is to position the file pointer
// for the data read. The entry's attributes might also have
// been changed since the central dir entry was read (e.g.,
// filename), so throw away the values here.
// 4 2 Version needed to extract (minimum)
/* versionRequired */input.readShort();
// 6 2 General purpose bit flag
/* generalPurposeBits */input.readShort();
// 8 2 Compression method
/* compression */input.readShort();
// 10 2 File last modification time
/* modificationTime */input.readShort();
// 12 2 File last modification date
/* modificationDate */input.readShort();
// 14 4 CRC-32
/* crc32 */input.readInt();
// 18 4 Compressed size
/* compressedSize */input.readInt();
// 22 4 Uncompressed size
/* size */input.readInt();
// 26 2 File name length (n)
short fileNameLen = input.readShort();
// 28 2 Extra field length (m)
short extraLen = input.readShort();
// 30 n File name
/* String localFilename = */input.readString(fileNameLen);
// Extra data. FIXME: Avoid useless memory allocation.
/* byte[] extra = */input.readBytes(extraLen);
// Record the file position of this entry's data.
dataPosition = input.getFilePointer();
}
public void writeLocalEntry(ZipOutput output) throws IOException {
if (data == null && dataPosition < 0 && zipInput != null)
readLocalHeader();
localHeaderOffset = output.getFilePointer();
if (entryOut != null) {
entryOut.close();
size = entryOut.getSize();
data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray();
compressedSize = data.length;
crc32 = entryOut.getCRC();
}
output.writeInt(0x04034b50);
output.writeShort(versionRequired);
output.writeShort(generalPurposeBits);
output.writeShort(compression);
output.writeShort(modificationTime);
output.writeShort(modificationDate);
output.writeInt(crc32);
output.writeInt(compressedSize);
output.writeInt(size);
output.writeShort((short) filename.length());
numAlignBytes = 0;
// Zipalign if the file is uncompressed, i.e., "Stored", and file size is not zero.
if (compression == 0) {
long dataPos = output.getFilePointer() + // current position
2 + // plus size of extra data length
filename.length() + // plus filename
extraData.length; // plus extra data
short dataPosMod4 = (short) (dataPos % 4);
if (dataPosMod4 > 0) {
numAlignBytes = (short) (4 - dataPosMod4);
}
}
// 28 2 Extra field length (m)
output.writeShort((short) (extraData.length + numAlignBytes));
// 30 n File name
output.writeString(filename);
// Extra data
output.writeBytes(extraData);
// Zipalign bytes
if (numAlignBytes > 0)
output.writeBytes(alignBytes, 0, numAlignBytes);
if (data != null) {
output.writeBytes(data);
} else {
zipInput.seek(dataPosition);
int bufferSize = Math.min(compressedSize, 8096);
byte[] buffer = new byte[bufferSize];
long totalCount = 0;
while (totalCount != compressedSize) {
int numRead = zipInput.in.read(buffer, 0, (int) Math.min(compressedSize - totalCount, bufferSize));
if (numRead > 0) {
output.writeBytes(buffer, 0, numRead);
// if (debug)
// getLogger().debug(
// String.format("Wrote %d bytes", numRead));
totalCount += numRead;
} else
throw new IllegalStateException(String.format(
"EOF reached while copying %s with %d bytes left to go", filename, compressedSize
- totalCount));
}
}
}
/** Returns the entry's data. */
public byte[] getData() throws IOException {
if (data != null)
return data;
byte[] tmpdata = new byte[size];
InputStream din = getInputStream();
int count = 0;
while (count != size) {
int numRead = din.read(tmpdata, count, size - count);
if (numRead < 0)
throw new IllegalStateException(String.format("Read failed, expecting %d bytes, got %d instead", size,
count));
count += numRead;
}
return tmpdata;
}
// Returns an input stream for reading the entry's data.
public InputStream getInputStream() throws IOException {
if (entryOut != null) {
entryOut.close();
size = entryOut.getSize();
data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray();
compressedSize = data.length;
crc32 = entryOut.getCRC();
entryOut = null;
InputStream rawis = new ByteArrayInputStream(data);
if (compression == 0)
return rawis;
else {
// Hacky, inflate using a sequence of input streams that returns
// 1 byte more than the actual length of the data.
// This extra dummy byte is required by InflaterInputStream when
// the data doesn't have the header and crc fields (as it is in
// zip files).
return new InflaterInputStream(new SequenceInputStream(rawis, new ByteArrayInputStream(new byte[1])),
new Inflater(true));
}
}
ZioEntryInputStream dataStream;
dataStream = new ZioEntryInputStream(this);
if (compression != 0) {
// Note: When using nowrap=true with Inflater it is also necessary to provide
// an extra "dummy" byte as input. This is required by the ZLIB native library
// in order to support certain optimizations.
dataStream.setReturnDummyByte(true);
return new InflaterInputStream(dataStream, new Inflater(true));
} else
return dataStream;
}
// Returns an output stream for writing an entry's data.
public OutputStream getOutputStream() {
entryOut = new ZioEntryOutputStream(compression, new ByteArrayOutputStream());
return entryOut;
}
public void write(ZipOutput output) throws IOException {
output.writeInt(0x02014b50);
output.writeShort(versionMadeBy);
output.writeShort(versionRequired);
output.writeShort(generalPurposeBits);
output.writeShort(compression);
output.writeShort(modificationTime);
output.writeShort(modificationDate);
output.writeInt(crc32);
output.writeInt(compressedSize);
output.writeInt(size);
output.writeShort((short) filename.length());
output.writeShort((short) (extraData.length + numAlignBytes));
output.writeShort((short) fileComment.length());
output.writeShort(diskNumberStart);
output.writeShort(internalAttributes);
output.writeInt(externalAttributes);
output.writeInt(localHeaderOffset);
output.writeString(filename);
output.writeBytes(extraData);
if (numAlignBytes > 0)
output.writeBytes(alignBytes, 0, numAlignBytes);
output.writeString(fileComment);
}
/** Returns time in Java format. */
public long getTime() {
int year = ((modificationDate >> 9) & 0x007f) + 80;
int month = ((modificationDate >> 5) & 0x000f) - 1;
int day = modificationDate & 0x001f;
int hour = (modificationTime >> 11) & 0x001f;
int minute = (modificationTime >> 5) & 0x003f;
int seconds = (modificationTime << 1) & 0x003e;
Date d = new Date(year, month, day, hour, minute, seconds);
return d.getTime();
}
/** Set the file timestamp (using a Java time value). */
public void setTime(long time) {
Date d = new Date(time);
long dtime;
int year = d.getYear() + 1900;
if (year < 1980) {
dtime = (1 << 21) | (1 << 16);
} else {
dtime = (year - 1980) << 25 | (d.getMonth() + 1) << 21 | d.getDate() << 16 | d.getHours() << 11
| d.getMinutes() << 5 | d.getSeconds() >> 1;
}
modificationDate = (short) (dtime >> 16);
modificationTime = (short) (dtime & 0xFFFF);
}
public boolean isDirectory() {
return filename.endsWith("/");
}
public String getName() {
return filename;
}
public void setName(String filename) {
this.filename = filename;
}
/** Use 0 (STORED), or 8 (DEFLATE). */
public void setCompression(int compression) {
this.compression = (short) compression;
}
public short getVersionMadeBy() {
return versionMadeBy;
}
public short getVersionRequired() {
return versionRequired;
}
public short getGeneralPurposeBits() {
return generalPurposeBits;
}
public short getCompression() {
return compression;
}
public int getCrc32() {
return crc32;
}
public int getCompressedSize() {
return compressedSize;
}
public int getSize() {
return size;
}
public byte[] getExtraData() {
return extraData;
}
public String getFileComment() {
return fileComment;
}
public short getDiskNumberStart() {
return diskNumberStart;
}
public short getInternalAttributes() {
return internalAttributes;
}
public int getExternalAttributes() {
return externalAttributes;
}
public int getLocalHeaderOffset() {
return localHeaderOffset;
}
public long getDataPosition() {
return dataPosition;
}
public ZioEntryOutputStream getEntryOut() {
return entryOut;
}
public ZipInput getZipInput() {
return zipInput;
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (C) 2010 Ken Ellinwood
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fornwall.apksigner.zipio;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
/** Input stream used to read just the data from a zip file entry. */
final class ZioEntryInputStream extends InputStream {
RandomAccessFile raf;
int size;
int offset;
boolean returnDummyByte = false;
public ZioEntryInputStream(ZioEntry entry) throws IOException {
offset = 0;
size = entry.getCompressedSize();
raf = entry.getZipInput().in;
long dpos = entry.getDataPosition();
if (dpos >= 0) {
raf.seek(entry.getDataPosition());
} else {
// seeks to, then reads, the local header, causing the
// file pointer to be positioned at the start of the data.
entry.readLocalHeader();
}
}
public void setReturnDummyByte(boolean returnExtraByte) {
returnDummyByte = returnExtraByte;
}
@Override
public void close() throws IOException {
}
@Override
public boolean markSupported() {
return false;
}
@Override
public int available() throws IOException {
int available = size - offset;
// log.debug(String.format("Available = %d", available));
if (available == 0 && returnDummyByte)
return 1;
else
return available;
}
@Override
public int read() throws IOException {
if ((size - offset) == 0) {
if (returnDummyByte) {
returnDummyByte = false;
return 0;
} else
return -1;
}
int b = raf.read();
if (b >= 0) {
offset += 1;
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return readBytes(b, off, len);
}
private int readBytes(byte[] b, int off, int len) throws IOException {
if ((size - offset) == 0) {
if (returnDummyByte) {
returnDummyByte = false;
b[off] = 0;
return 1;
} else
return -1;
}
int numToRead = Math.min(len, available());
int numRead = raf.read(b, off, numToRead);
if (numRead > 0) {
offset += numRead;
}
return numRead;
}
@Override
public int read(byte[] b) throws IOException {
return readBytes(b, 0, b.length);
}
@Override
public long skip(long n) throws IOException {
long numToSkip = Math.min(n, available());
raf.seek(raf.getFilePointer() + numToSkip);
return numToSkip;
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2010 Ken Ellinwood
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fornwall.apksigner.zipio;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
final class ZioEntryOutputStream extends OutputStream {
int size = 0; // tracks uncompressed size of data
final CRC32 crc = new CRC32();
int crcValue = 0;
final OutputStream wrapped;
final OutputStream downstream;
public ZioEntryOutputStream(int compression, OutputStream wrapped) {
this.wrapped = wrapped;
downstream = (compression == 0) ? wrapped : new DeflaterOutputStream(wrapped, new Deflater(
Deflater.BEST_COMPRESSION, true));
}
@Override
public void close() throws IOException {
downstream.close();
crcValue = (int) crc.getValue();
}
public int getCRC() {
return crcValue;
}
@Override
public void flush() throws IOException {
downstream.flush();
}
@Override
public void write(byte[] b) throws IOException {
downstream.write(b);
crc.update(b);
size += b.length;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
downstream.write(b, off, len);
crc.update(b, off, len);
size += len;
}
@Override
public void write(int b) throws IOException {
downstream.write(b);
crc.update(b);
size += 1;
}
public int getSize() {
return size;
}
}

View File

@ -0,0 +1,113 @@
package net.fornwall.apksigner.zipio;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.jar.Manifest;
public final class ZipInput implements AutoCloseable {
final RandomAccessFile in;
final long fileLength;
int scanIterations = 0;
public final Map<String, ZioEntry> entries = new LinkedHashMap<>();
final CentralEnd centralEnd;
Manifest manifest;
public ZipInput(String filename) throws IOException {
in = new RandomAccessFile(filename, "r");
fileLength = in.length();
long posEOCDR = scanForEOCDR((int) Math.min(fileLength, 256));
in.seek(posEOCDR);
centralEnd = CentralEnd.read(this);
in.seek(centralEnd.centralStartOffset);
for (int i = 0; i < centralEnd.totalCentralEntries; i++) {
ZioEntry entry = new ZioEntry(this);
entries.put(entry.getName(), entry);
}
}
public Manifest getManifest() throws IOException {
if (manifest == null) {
ZioEntry e = entries.get("META-INF/MANIFEST.MF");
if (e != null)
manifest = new Manifest(e.getInputStream());
}
return manifest;
}
/**
* Scan the end of the file for the end of central directory record (EOCDR). Returns the file offset of the EOCD
* signature. The size parameter is an initial buffer size (e.g., 256).
*/
public long scanForEOCDR(int size) throws IOException {
if (size > fileLength || size > 65536)
throw new IllegalStateException("End of central directory not found");
int scanSize = (int) Math.min(fileLength, size);
byte[] scanBuf = new byte[scanSize];
in.seek(fileLength - scanSize);
in.readFully(scanBuf);
for (int i = scanSize - 22; i >= 0; i--) {
scanIterations += 1;
if (scanBuf[i] == 0x50 && scanBuf[i + 1] == 0x4b && scanBuf[i + 2] == 0x05 && scanBuf[i + 3] == 0x06) {
return fileLength - scanSize + i;
}
}
return scanForEOCDR(size * 2);
}
@Override
public void close() {
if (in != null)
try {
in.close();
} catch (Throwable t) {
}
}
public long getFilePointer() throws IOException {
return in.getFilePointer();
}
public void seek(long position) throws IOException {
in.seek(position);
}
public int readInt() throws IOException {
int result = 0;
for (int i = 0; i < 4; i++)
result |= (in.readUnsignedByte() << (8 * i));
return result;
}
public short readShort() throws IOException {
short result = 0;
for (int i = 0; i < 2; i++)
result |= (in.readUnsignedByte() << (8 * i));
return result;
}
public String readString(int length) throws IOException {
byte[] buffer = new byte[length];
for (int i = 0; i < length; i++)
buffer[i] = in.readByte();
return new String(buffer);
}
public byte[] readBytes(int length) throws IOException {
byte[] buffer = new byte[length];
for (int i = 0; i < length; i++) {
buffer[i] = in.readByte();
}
return buffer;
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright (C) 2010 Ken Ellinwood
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fornwall.apksigner.zipio;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public final class ZipOutput implements AutoCloseable {
final OutputStream out;
int filePointer = 0;
final List<ZioEntry> entriesWritten = new LinkedList<>();
final Set<String> namesWritten = new HashSet<>();
public ZipOutput(OutputStream out) {
this.out = out;
}
public void write(ZioEntry entry) throws IOException {
String entryName = entry.getName();
if (namesWritten.contains(entryName)) {
System.err.println("Skipping duplicate file in output: " + entryName);
return;
}
entry.writeLocalEntry(this);
entriesWritten.add(entry);
namesWritten.add(entryName);
}
public int getFilePointer() {
return filePointer;
}
public void writeInt(int value) throws IOException {
byte[] data = new byte[4];
for (int i = 0; i < 4; i++) {
data[i] = (byte) (value & 0xFF);
value = value >> 8;
}
out.write(data);
filePointer += 4;
}
public void writeShort(short value) throws IOException {
byte[] data = new byte[2];
for (int i = 0; i < 2; i++) {
data[i] = (byte) (value & 0xFF);
value = (short) (value >> 8);
}
out.write(data);
filePointer += 2;
}
public void writeString(String value) throws IOException {
byte[] data = value.getBytes();
out.write(data);
filePointer += data.length;
}
public void writeBytes(byte[] value) throws IOException {
out.write(value);
filePointer += value.length;
}
public void writeBytes(byte[] value, int offset, int length) throws IOException {
out.write(value, offset, length);
filePointer += length;
}
@Override
public void close() throws IOException {
CentralEnd centralEnd = new CentralEnd();
centralEnd.centralStartOffset = getFilePointer();
centralEnd.numCentralEntries = centralEnd.totalCentralEntries = (short) entriesWritten.size();
for (ZioEntry entry : entriesWritten)
entry.write(this);
centralEnd.centralDirectorySize = (getFilePointer() - centralEnd.centralStartOffset);
centralEnd.fileComment = "";
centralEnd.write(this);
if (out != null)
try {
out.close();
} catch (Throwable t) {
}
}
}

View File

@ -3,7 +3,7 @@ allprojects {
apply plugin: 'maven'
apply plugin: 'idea'
apply plugin: 'eclipse'
version = '2.0'
version = '2.0.1'
}
defaultTasks('clean','distZip')
@ -11,8 +11,8 @@ defaultTasks('clean','distZip')
subprojects {
apply plugin: 'java'
apply plugin: 'maven'
sourceCompatibility = 1.7
targetCompatibility = 1.7
sourceCompatibility = 1.8
targetCompatibility = 1.8

View File

@ -1,2 +1,2 @@
rootProject.name = 'Xpatch'
include ':axmlprinter', ':xpatch'
include ':axmlprinter', ':xpatch', ':apksigner'

View File

@ -3,6 +3,7 @@ apply plugin: 'java-library'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':axmlprinter')
compile project(':apksigner')
}
jar{

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,9 +4,14 @@ import com.storm.wind.xpatch.base.BaseCommand;
import com.storm.wind.xpatch.task.ApkModifyTask;
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
import com.storm.wind.xpatch.task.SaveApkSignatureTask;
import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask;
import com.storm.wind.xpatch.task.SoAndDexCopyTask;
import com.storm.wind.xpatch.util.FileUtils;
import com.storm.wind.xpatch.util.ManifestParser;
import com.wind.meditor.core.FileProcesser;
import com.wind.meditor.property.AttributeItem;
import com.wind.meditor.property.ModificationProperty;
import com.wind.meditor.utils.NodeValue;
import java.io.File;
import java.text.SimpleDateFormat;
@ -39,16 +44,38 @@ public class MainCommand extends BaseCommand {
description = "disable craching the apk's signature.")
private boolean disableCrackSignature = false;
@Opt(opt = "xm", longOpt = "xposed-modules", description = "the xposed mpdule files to be packaged into the apk, " +
"multi files should be seperated by :(mac) or ;(win) ")
@Opt(opt = "xm", longOpt = "xposed-modules", description = "the xposed module files to be packaged into the apk, " +
"multi files should be seperated by :(mac) or ;(win) ", argName = "xposed module file path")
private String xposedModules;
// 使用dex文件中插入代码的方式修改apk而不是默认的修改Manifest中Application name的方式
@Opt(opt = "dex", longOpt = "dex", hasArg = false, description = "insert code into the dex file, not modify manifest application name attribute")
private boolean dexModificationMode = false;
@Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name")
private String newPackageName;
@Opt(opt = "d", longOpt = "debuggable", description = "set 1 to make the app debuggable = true, " +
"set 0 to make the app debuggable = false", argName = "0 or 1")
private int debuggable = -1; // 0: debuggable = false 1: debuggable = true
@Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code",
argName = "new-version-code")
private int versionCode;
@Opt(opt = "vn", longOpt = "versionName", description = "set the app version name",
argName = "new-version-name")
private String versionName;
@Opt(opt = "w", longOpt = "whale", hasArg = false, description = "Change hook framework to Lody's whale")
private boolean useWhaleHookFramework = false; // 是否使用whale hook框架默认使用的是SandHook
// 原来apk中dex文件的数量
private int dexFileCount = 0;
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
private static final String DEFAULT_APPLICATION_NAME = "android.app.Application";
private static final String PROXY_APPLICATION_NAME = "com.wind.xpatch.proxy.XpatchProxyApplication";
private List<Runnable> mXpatchTasks = new ArrayList<>();
@ -79,13 +106,6 @@ public class MainCommand extends BaseCommand {
return;
}
String srcApkFileParentPath = srcApkFile.getParent();
if (srcApkFileParentPath == null) {
String absPath = srcApkFile.getAbsolutePath();
int index = absPath.lastIndexOf(File.separatorChar);
srcApkFileParentPath = absPath.substring(0, index);
}
String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录
if (showAllLogs) {
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
@ -102,20 +122,27 @@ public class MainCommand extends BaseCommand {
return;
}
String outputApkFileParentPath = outputFile.getParent();
if (outputApkFileParentPath == null) {
String absPath = outputFile.getAbsolutePath();
int index = absPath.lastIndexOf(File.separatorChar);
outputApkFileParentPath = absPath.substring(0, index);
}
System.out.println(" !!!!! output apk path --> " + output +
" disableCrackSignature --> " + disableCrackSignature);
String apkFileName = getBaseName(srcApkFile);
// 中间文件临时存储的位置
String tempFilePath = srcApkFileParentPath + File.separator +
String tempFilePath = outputApkFileParentPath + File.separator +
currentTimeStr() + "-tmp" + File.separator;
// apk文件解压的目录
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
if (showAllLogs) {
System.out.println(" !!!!! srcApkFileParentPath = " + srcApkFileParentPath +
System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath +
"\n unzipApkFilePath = " + unzipApkFilePath);
}
@ -125,8 +152,13 @@ public class MainCommand extends BaseCommand {
}
// 先解压apk到指定目录下
long currentTime = System.currentTimeMillis();
FileUtils.decompressZip(apkPath, unzipApkFilePath);
if (showAllLogs) {
System.out.println(" decompress apk cost time: " + (System.currentTimeMillis() - currentTime));
}
// Get the dex count in the apk zip file
dexFileCount = findDexFileCount(unzipApkFilePath);
@ -136,33 +168,56 @@ public class MainCommand extends BaseCommand {
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
currentTime = System.currentTimeMillis();
// parse the app main application full name from the manifest file
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
String applicationName;
String applicationName = null;
if (pair != null && pair.applicationName != null) {
applicationName = pair.applicationName;
} else {
System.out.println(" Application name not found error !!!!!! ");
applicationName = DEFAULT_APPLICATION_NAME;
}
if (showAllLogs) {
System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime));
System.out.println(" Get the application name --> " + applicationName);
}
// 1. modify the apk dex file to make xposed can run in it
// modify manifest
File manifestFile = new File(manifestFilePath);
String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml";
File manifestFileNew = new File(manifestFilePathNew);
manifestFile.renameTo(manifestFileNew);
modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName);
manifestFileNew.delete();
// save original main application name to asset file
if (isNotEmpty(applicationName)) {
mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath));
}
// modify the apk dex file to make xposed can run in it
if (dexModificationMode && isNotEmpty(applicationName)) {
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
dexFileCount));
}
// 2. copy xposed so and dex files into the unzipped apk
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath, getXposedModules(xposedModules)));
// copy xposed so and dex files into the unzipped apk
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath,
getXposedModules(xposedModules), useWhaleHookFramework));
// 3. compress all files into an apk and then sign it.
// compress all files into an apk and then sign it.
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
// 4. excute these tasks
// excute these tasks
for (Runnable executor : mXpatchTasks) {
currentTime = System.currentTimeMillis();
executor.run();
if (showAllLogs) {
System.out.println(executor.getClass().getSimpleName() + " cost time: "
+ (System.currentTimeMillis() - currentTime));
}
}
// 5. delete all the build files that is useless now
@ -177,6 +232,39 @@ public class MainCommand extends BaseCommand {
}
}
private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) {
ModificationProperty property = new ModificationProperty();
boolean modifyEnabled = false;
if (isNotEmpty(newPackageName)) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
}
if (versionCode > 0) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode));
}
if (isNotEmpty(versionName)) {
modifyEnabled = true;
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName));
}
if (debuggable >= 0) {
modifyEnabled = true;
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0));
}
if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
modifyEnabled = true;
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, PROXY_APPLICATION_NAME));
}
if (modifyEnabled) {
FileProcesser.processManifestFile(filePath, dstFilePath, property);
}
}
private int findDexFileCount(String unzipApkFilePath) {
File zipfileRoot = new File(unzipApkFilePath);
if (!zipfileRoot.exists()) {
@ -208,4 +296,8 @@ public class MainCommand extends BaseCommand {
}
return modules.split(File.pathSeparator);
}
private static boolean isNotEmpty(String str) {
return str != null && !str.isEmpty();
}
}

View File

@ -8,6 +8,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
@ -251,7 +253,23 @@ public abstract class BaseCommand {
Option needArgOpt = null;
for (String s : args) {
if (needArgOpt != null) {
needArgOpt.field.set(this, convert(s, needArgOpt.field.getType()));
Field field = needArgOpt.field;
Class clazz = field.getType();
if (clazz.equals(List.class)) {
try {
List<Object> object = ((List<Object>) field.get(this));
// 获取List对象的泛型类型
ParameterizedType listGenericType = (ParameterizedType) field.getGenericType();
Type[] listActualTypeArguments = listGenericType.getActualTypeArguments();
Class typeClazz = (Class) listActualTypeArguments[0];
object.add(convert(s, typeClazz));
} catch (Exception e) {
e.printStackTrace();
}
} else {
field.set(this, convert(s, clazz));
}
needArgOpt = null;
} else if (s.startsWith("-")) {// its a short or long option
Option opt = optMap.get(s);

View File

@ -36,7 +36,7 @@ public class ApkModifyTask implements Runnable {
String jarOutputPath = unzipApkFile.getParent() + File.separator + JAR_FILE_NAME;
// classes.dex
// classes-1.0.dex
String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName);
if (showAllLogs) {
@ -104,12 +104,12 @@ public class ApkModifyTask implements Runnable {
cmd.doMain(args);
}
// 列出目录下所有dex文件classes.dexclasses2.dexclasses3.dex .....
// 列出目录下所有dex文件classes-1.0.dexclasses2.dexclasses3.dex .....
private ArrayList<String> createClassesDotDexFileList(int dexFileCount) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < dexFileCount; i++) {
if (i == 0) {
list.add("classes.dex");
list.add("classes-1.0.dex");
} else {
list.add("classes" + (i + 1) + ".dex");
}

View File

@ -55,6 +55,22 @@ public class BuildAndSignApkTask implements Runnable {
}
private boolean signApk(String apkPath, String keyStorePath, String signedApkPath, boolean useLocalJarsigner) {
if (isAndroid()) {
boolean success = true;
try {
ShellCmdUtil.chmod((new File(apkPath)).getParent(), ShellCmdUtil.FileMode.MODE_755);
net.fornwall.apksigner.Main.main
("--password", "123456", keyStorePath, apkPath, signedApkPath);
} catch (Exception e1) {
success = false;
e1.printStackTrace();
System.out.println("use fornwall apksigner to sign apk failed, fail msg is :" + e1.toString());
}
if (success && new File(signedApkPath).exists()) {
return true;
}
}
File localJarsignerFile = null;
try {
long time = System.currentTimeMillis();
@ -67,7 +83,8 @@ public class BuildAndSignApkTask implements Runnable {
String localJarsignerPath = (new File(apkPath)).getParent() + File.separator + "jarsigner-081688";
localJarsignerFile = new File(localJarsignerPath);
FileUtils.copyFileFromJar("assets/jarsigner", localJarsignerPath);
ShellCmdUtil.execCmd("chmod -R 777 " + localJarsignerPath, null);
ShellCmdUtil.chmod(localJarsignerPath, ShellCmdUtil.FileMode.MODE_755);
// ShellCmdUtil.execCmd("chmod -R 777 " + localJarsignerPath, null);
signCmd = new StringBuilder(localJarsignerPath + " ");
}
signCmd.append(" -keystore ")
@ -96,6 +113,15 @@ public class BuildAndSignApkTask implements Runnable {
} else {
System.out.println("use inner jarsigner to sign apk failed, sign it yourself fail msg is :" +
e.toString());
try {
net.fornwall.apksigner.Main.main
("--password", "123456", keyStorePath, apkPath, signedApkPath);
} catch (Exception e1) {
e1.printStackTrace();
System.out.println("use fornwall apksigner to sign apk failed, fail msg is :" +
e1.toString());
}
}
return false;
} finally {
@ -104,4 +130,14 @@ public class BuildAndSignApkTask implements Runnable {
}
}
}
private boolean isAndroid() {
boolean isAndroid = true;
try {
Class.forName("android.content.Context");
} catch (ClassNotFoundException e) {
isAndroid = false;
}
return isAndroid;
}
}

View File

@ -0,0 +1,40 @@
package com.storm.wind.xpatch.task;
import com.storm.wind.xpatch.util.FileUtils;
import java.io.File;
/**
* Created by xiawanli on 2019/4/6
*/
public class SaveOriginalApplicationNameTask implements Runnable {
private final String applcationName;
private final String unzipApkFilePath;
private String dstFilePath;
private final String APPLICATION_NAME_ASSET_PATH = "assets/xpatch_asset/original_application_name.ini";
public SaveOriginalApplicationNameTask(String applicationName, String unzipApkFilePath) {
this.applcationName = applicationName;
this.unzipApkFilePath = unzipApkFilePath;
this.dstFilePath = (unzipApkFilePath + APPLICATION_NAME_ASSET_PATH).replace("/", File.separator);
}
@Override
public void run() {
ensureDstFileCreated();
FileUtils.writeFile(dstFilePath, applcationName);
}
private void ensureDstFileCreated() {
File dstParentFile = new File(dstFilePath);
if (!dstParentFile.getParentFile().getParentFile().exists()) {
dstParentFile.getParentFile().getParentFile().mkdirs();
}
if (!dstParentFile.getParentFile().exists()) {
dstParentFile.getParentFile().mkdirs();
}
}
}

View File

@ -10,7 +10,10 @@ import java.util.HashMap;
*/
public class SoAndDexCopyTask implements Runnable {
private static final String SO_FILE_NAME = "libsandhook.so";
private static final String SANDHOOK_SO_FILE_NAME = "libsandhook";
private static final String WHALE_SO_FILE_NAME = "libwhale";
private static final String SO_FILE_NAME_WITH_SUFFIX = "libsandhook";
private static final String XPOSED_MODULE_FILE_NAME_PREFIX = "libxpatch_xp_module_";
private static final String SO_FILE_SUFFIX = ".so";
@ -20,22 +23,27 @@ public class SoAndDexCopyTask implements Runnable {
"lib/arm64-v8a/"
};
private final HashMap<String, String> SO_FILE_PATH_MAP = new HashMap<String, String>() {
{
put(APK_LIB_PATH_ARRAY[0], "assets/lib/armeabi-v7a/" + SO_FILE_NAME);
put(APK_LIB_PATH_ARRAY[1], "assets/lib/armeabi-v7a/" + SO_FILE_NAME);
put(APK_LIB_PATH_ARRAY[2], "assets/lib/arm64-v8a/" + SO_FILE_NAME);
}
};
private final HashMap<String, String> mSoFilePathMap = new HashMap<>();
private int dexFileCount;
private String unzipApkFilePath;
private String[] xposedModuleArray;
public SoAndDexCopyTask(int dexFileCount, String unzipApkFilePath, String[] xposedModuleArray) {
public SoAndDexCopyTask(int dexFileCount, String unzipApkFilePath,
String[] xposedModuleArray, boolean useWhaleHookFramework) {
this.dexFileCount = dexFileCount;
this.unzipApkFilePath = unzipApkFilePath;
this.xposedModuleArray = xposedModuleArray;
String soFileName;
if (useWhaleHookFramework) {
soFileName = WHALE_SO_FILE_NAME;
} else {
soFileName = SANDHOOK_SO_FILE_NAME;
}
mSoFilePathMap.put(APK_LIB_PATH_ARRAY[0], "assets/lib/armeabi-v7a/" + soFileName);
mSoFilePathMap.put(APK_LIB_PATH_ARRAY[1], "assets/lib/armeabi-v7a/" + soFileName);
mSoFilePathMap.put(APK_LIB_PATH_ARRAY[2], "assets/lib/arm64-v8a/" + soFileName);
}
@Override
@ -49,14 +57,33 @@ public class SoAndDexCopyTask implements Runnable {
}
private void copySoFile() {
String[] existLibPathArray = new String[3];
int arrayIndex = 0;
for (String libPath : APK_LIB_PATH_ARRAY) {
String apkSoFullPath = fullLibPath(libPath);
File apkSoFullPathFile= new File(apkSoFullPath);
if (!apkSoFullPathFile.exists()){
File apkSoFullPathFile = new File(apkSoFullPath);
if (apkSoFullPathFile.exists()) {
existLibPathArray[arrayIndex] = libPath;
arrayIndex++;
}
}
// 不存在lib目录则创建lib/armeabi-v7 文件夹
if (arrayIndex == 0) {
String libPath = APK_LIB_PATH_ARRAY[0];
String apkSoFullPath = fullLibPath(libPath);
File apkSoFullPathFile = new File(apkSoFullPath);
apkSoFullPathFile.mkdirs();
existLibPathArray[arrayIndex] = libPath;
}
copyLibFile(apkSoFullPath, SO_FILE_PATH_MAP.get(libPath));
for (String libPath : existLibPathArray) {
if (libPath != null && !libPath.isEmpty()) {
String apkSoFullPath = fullLibPath(libPath);
copyLibFile(apkSoFullPath, mSoFilePathMap.get(libPath));
}
}
// copy xposed modules into the lib path
if (xposedModuleArray != null && xposedModuleArray.length > 0) {
int index = 0;
@ -69,14 +96,13 @@ public class SoAndDexCopyTask implements Runnable {
if (!moduleFile.exists()) {
continue;
}
for (String libPath : APK_LIB_PATH_ARRAY) {
for (String libPath : existLibPathArray) {
if (libPath != null && !libPath.isEmpty()) {
String apkSoFullPath = fullLibPath(libPath);
String outputModuleName= XPOSED_MODULE_FILE_NAME_PREFIX + index + SO_FILE_SUFFIX;
if(new File(apkSoFullPath).exists()) {
String outputModuleName = XPOSED_MODULE_FILE_NAME_PREFIX + index + SO_FILE_SUFFIX;
File outputModuleSoFile = new File(apkSoFullPath, outputModuleName);
FileUtils.copyFile(moduleFile, outputModuleSoFile);
}
}
index++;
}
@ -87,7 +113,7 @@ public class SoAndDexCopyTask implements Runnable {
// copy dex file to root dir, rename it first
String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex";
// assets/classes.dex分隔符不能使用File.seperater,否则在windows上无法读取到文件IOException
FileUtils.copyFileFromJar("assets/classes.dex", unzipApkFilePath + copiedDexFileName);
FileUtils.copyFileFromJar("assets/classes-1.0.dex", unzipApkFilePath + copiedDexFileName);
}
private String fullLibPath(String libPath) {
@ -101,16 +127,15 @@ public class SoAndDexCopyTask implements Runnable {
}
// get the file name first
int lastIndex = srcSoPath.lastIndexOf('/');
int length = srcSoPath.length();
String soFileName = srcSoPath.substring(lastIndex, length);
// int lastIndex = srcSoPath.lastIndexOf('/');
// int length = srcSoPath.length();
String soFileName = SO_FILE_NAME_WITH_SUFFIX;
// do copy
FileUtils.copyFileFromJar(srcSoPath, new File(apkSoParentFile, soFileName).getAbsolutePath());
}
private void deleteMetaInfo() {
String metaInfoFilePath = "META-INF";
File metaInfoFileRoot = new File(unzipApkFilePath + metaInfoFilePath);

View File

@ -0,0 +1,356 @@
package com.storm.wind.xpatch.util;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* Created by xiawanli on 2018/8/25
*/
public class ReflectUtils {
//获取类的实例的变量的值
public static Object getField(Object receiver, String fieldName) {
return getField(null, receiver, fieldName);
}
//获取类的静态变量的值
public static Object getField(String className, String fieldName) {
return getField(className, null, fieldName);
}
public static Object getField(Class<?> clazz, String className, String fieldName, Object receiver) {
try {
if (clazz == null) {
clazz = Class.forName(className);
}
Field field = clazz.getDeclaredField(fieldName);
if (field == null) {
return null;
}
field.setAccessible(true);
return field.get(receiver);
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
private static Object getField(String className, Object receiver, String fieldName) {
Class<?> clazz = null;
Field field;
if (className != null && className.length() > 0) {
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else {
if (receiver != null) {
clazz = receiver.getClass();
}
}
if (clazz == null) {
return null;
}
try {
field = findField(clazz, fieldName);
if (field == null) {
return null;
}
field.setAccessible(true);
return field.get(receiver);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
return null;
}
public static Object setField(Object receiver, String fieldName, Object value) {
try {
Field field;
field = findField(receiver.getClass(), fieldName);
if (field == null) {
return null;
}
field.setAccessible(true);
Object old = field.get(receiver);
field.set(receiver, value);
return old;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
return null;
}
public static Object setField(Class<?> clazz, Object receiver, String fieldName, Object value) {
try {
Field field;
field = findField(clazz, fieldName);
if (field == null) {
return null;
}
field.setAccessible(true);
Object old = field.get(receiver);
field.set(receiver, value);
return old;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
return null;
}
public static Object callMethod(Object receiver, String methodName, Object... params) {
return callMethod(null, receiver, methodName, params);
}
public static Object setField(String clazzName, Object receiver, String fieldName, Object value) {
try {
Class<?> clazz = Class.forName(clazzName);
Field field;
field = findField(clazz, fieldName);
if (field == null) {
return null;
}
field.setAccessible(true);
Object old = field.get(receiver);
field.set(receiver, value);
return old;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
public static Object callMethod(String className, String methodName, Object... params) {
return callMethod(className, null, methodName, params);
}
public static Object callMethod(Class<?> clazz, String className, String methodName, Object receiver,
Class[] types, Object... params) {
try {
if (clazz == null) {
clazz = Class.forName(className);
}
Method method = clazz.getDeclaredMethod(methodName, types);
method.setAccessible(true);
return method.invoke(receiver, params);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
private static Object callMethod(String className, Object receiver, String methodName, Object... params) {
Class<?> clazz = null;
if (className != null && className.length() > 0) {
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else {
if (receiver != null) {
clazz = receiver.getClass();
}
}
if (clazz == null) {
return null;
}
try {
Method method = findMethod(clazz, methodName, params);
if (method == null) {
return null;
}
method.setAccessible(true);
return method.invoke(receiver, params);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
private static Method findMethod(Class<?> clazz, String name, Object... arg) {
Method[] methods = clazz.getMethods();
Method method = null;
for (Method m : methods) {
if (methodFitParam(m, name, arg)) {
method = m;
break;
}
}
if (method == null) {
method = findDeclaredMethod(clazz, name, arg);
}
return method;
}
private static Method findDeclaredMethod(Class<?> clazz, String name, Object... arg) {
Method[] methods = clazz.getDeclaredMethods();
Method method = null;
for (Method m : methods) {
if (methodFitParam(m, name, arg)) {
method = m;
break;
}
}
if (method == null) {
if (clazz.equals(Object.class)) {
return null;
}
return findDeclaredMethod(clazz.getSuperclass(), name, arg);
}
return method;
}
private static boolean methodFitParam(Method method, String methodName, Object... arg) {
if (!methodName.equals(method.getName())) {
return false;
}
Class<?>[] paramTypes = method.getParameterTypes();
if (arg == null || arg.length == 0) {
return paramTypes == null || paramTypes.length == 0;
}
if (paramTypes.length != arg.length) {
return false;
}
for (int i = 0; i < arg.length; ++i) {
Object ar = arg[i];
Class<?> paramT = paramTypes[i];
if (ar == null) {
continue;
}
//TODO for primitive type
if (paramT.isPrimitive()) {
continue;
}
if (!paramT.isInstance(ar)) {
return false;
}
}
return true;
}
private static Field findField(Class<?> clazz, String name) {
try {
return clazz.getDeclaredField(name);
} catch (NoSuchFieldException e) {
if (clazz.equals(Object.class)) {
e.printStackTrace();
return null;
}
Class<?> base = clazz.getSuperclass();
return findField(base, name);
}
}
//表示Field或者Class是编译器自动生成的
private static final int SYNTHETIC = 0x00001000;
//表示Field是final的
private static final int FINAL = 0x00000010;
//内部类持有的外部类对象一定有这两个属性
private static final int SYNTHETIC_AND_FINAL = SYNTHETIC | FINAL;
private static boolean checkModifier(int mod) {
return (mod & SYNTHETIC_AND_FINAL) == SYNTHETIC_AND_FINAL;
}
//获取内部类实例持有的外部类对象
public static <T> T getExternalField(Object innerObj) {
return getExternalField(innerObj, null);
}
/**
* 内部类持有的外部类对象的形式为
* final Outer this$0;
* flags: ACC_FINAL, ACC_SYNTHETIC
* 参考https://www.jianshu.com/p/9335c15c43cf
* Andhttps://www.2cto.com/kf/201402/281879.html
*
* @param innerObj 内部类对象
* @param name 内部类持有的外部类名称默认是"this$0"
* @return 内部类持有的外部类对象
*/
private static <T> T getExternalField(Object innerObj, String name) {
Class clazz = innerObj.getClass();
if (name == null || name.isEmpty()) {
name = "this$0";
}
Field field;
try {
field = clazz.getDeclaredField(name);
} catch (NoSuchFieldException e) {
e.printStackTrace();
return null;
}
field.setAccessible(true);
if (checkModifier(field.getModifiers())) {
try {
return (T) field.get(innerObj);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return getExternalField(innerObj, name + "$");
}
//获取当前对象的泛型类 added by xia wanli
public static Class<?> getParameterizedClassType(Object object) {
Class<?> clazz;
//getGenericSuperclass()获得带有泛型的父类
//Type是 Java 中所有类型的公共高级接口包括原始类型参数化类型数组类型类型变量和基本类型
Type genericSuperclass = object.getClass().getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
//ParameterizedType参数化类型即泛型
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
//getActualTypeArguments获取参数化类型的数组泛型可能有多个
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
clazz = (Class<?>) actualTypeArguments[0];
} else {
clazz = (Class<?>) genericSuperclass;
}
return clazz;
}
//获取当前对象的泛型类 added by xia wanli
public static Type getObjectParameterizedType(Object object) {
//getGenericSuperclass()获得带有泛型的父类
//Type是 Java 中所有类型的公共高级接口包括原始类型参数化类型数组类型类型变量和基本类型
Type genericSuperclass = object.getClass().getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
//ParameterizedType参数化类型即泛型
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
//getActualTypeArguments获取参数化类型的数组泛型可能有多个
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
return actualTypeArguments[0];
} else {
throw new RuntimeException("Missing type parameter.");
}
}
}

View File

@ -57,6 +57,29 @@ public class ShellCmdUtil {
return result.toString();
}
public static void chmod(String path, int mode) throws Exception {
chmodOnAndroid(path, mode);
File file = new File(path);
String cmd = "chmod ";
if (file.isDirectory()) {
cmd += " -R ";
}
String cmode = String.format("%o", mode);
Runtime.getRuntime().exec(cmd + cmode + " " + path).waitFor();
}
private static void chmodOnAndroid(String path, int mode) {
Object sdk_int = ReflectUtils.getField("android.os.Build$VERSION", "SDK_INT");
if (!(sdk_int instanceof Integer)) {
return;
}
if ((int)sdk_int >= 21) {
System.out.println("chmod on android is called, path = " + path);
ReflectUtils.callMethod("android.system.Os", "chmod", path, mode);
}
}
private static void close(Closeable stream) {
if (stream != null) {
try {
@ -66,4 +89,23 @@ public class ShellCmdUtil {
}
}
}
public interface FileMode {
int MODE_ISUID = 04000;
int MODE_ISGID = 02000;
int MODE_ISVTX = 01000;
int MODE_IRUSR = 00400;
int MODE_IWUSR = 00200;
int MODE_IXUSR = 00100;
int MODE_IRGRP = 00040;
int MODE_IWGRP = 00020;
int MODE_IXGRP = 00010;
int MODE_IROTH = 00004;
int MODE_IWOTH = 00002;
int MODE_IXOTH = 00001;
int MODE_755 = MODE_IRUSR | MODE_IWUSR | MODE_IXUSR
| MODE_IRGRP | MODE_IXGRP
| MODE_IROTH | MODE_IXOTH;
}
}