增加更换hook框架,支持修复manifest的破解方式,支持更多功能点
This commit is contained in:
parent
eb27c08bdb
commit
0f2645ee04
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ allprojects {
|
||||||
apply plugin: 'maven'
|
apply plugin: 'maven'
|
||||||
apply plugin: 'idea'
|
apply plugin: 'idea'
|
||||||
apply plugin: 'eclipse'
|
apply plugin: 'eclipse'
|
||||||
version = '2.0'
|
version = '2.0.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultTasks('clean','distZip')
|
defaultTasks('clean','distZip')
|
||||||
|
|
@ -11,8 +11,8 @@ defaultTasks('clean','distZip')
|
||||||
subprojects {
|
subprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
apply plugin: 'maven'
|
apply plugin: 'maven'
|
||||||
sourceCompatibility = 1.7
|
sourceCompatibility = 1.8
|
||||||
targetCompatibility = 1.7
|
targetCompatibility = 1.8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
rootProject.name = 'Xpatch'
|
rootProject.name = 'Xpatch'
|
||||||
include ':axmlprinter', ':xpatch'
|
include ':axmlprinter', ':xpatch', ':apksigner'
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ apply plugin: 'java-library'
|
||||||
dependencies {
|
dependencies {
|
||||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
compile project(':axmlprinter')
|
compile project(':axmlprinter')
|
||||||
|
compile project(':apksigner')
|
||||||
}
|
}
|
||||||
|
|
||||||
jar{
|
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.
Binary file not shown.
Binary file not shown.
|
|
@ -4,9 +4,14 @@ import com.storm.wind.xpatch.base.BaseCommand;
|
||||||
import com.storm.wind.xpatch.task.ApkModifyTask;
|
import com.storm.wind.xpatch.task.ApkModifyTask;
|
||||||
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
|
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
|
||||||
import com.storm.wind.xpatch.task.SaveApkSignatureTask;
|
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.task.SoAndDexCopyTask;
|
||||||
import com.storm.wind.xpatch.util.FileUtils;
|
import com.storm.wind.xpatch.util.FileUtils;
|
||||||
import com.storm.wind.xpatch.util.ManifestParser;
|
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.io.File;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
|
@ -39,16 +44,38 @@ public class MainCommand extends BaseCommand {
|
||||||
description = "disable craching the apk's signature.")
|
description = "disable craching the apk's signature.")
|
||||||
private boolean disableCrackSignature = false;
|
private boolean disableCrackSignature = false;
|
||||||
|
|
||||||
@Opt(opt = "xm", longOpt = "xposed-modules", description = "the xposed mpdule files to be packaged into the apk, " +
|
@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) ")
|
"multi files should be seperated by :(mac) or ;(win) ", argName = "xposed module file path")
|
||||||
private String xposedModules;
|
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文件的数量
|
// 原来apk中dex文件的数量
|
||||||
private int dexFileCount = 0;
|
private int dexFileCount = 0;
|
||||||
|
|
||||||
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
|
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<>();
|
private List<Runnable> mXpatchTasks = new ArrayList<>();
|
||||||
|
|
||||||
|
|
@ -79,13 +106,6 @@ public class MainCommand extends BaseCommand {
|
||||||
return;
|
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(); // 当前命令行所在的目录
|
String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录
|
||||||
if (showAllLogs) {
|
if (showAllLogs) {
|
||||||
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
|
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
|
||||||
|
|
@ -102,20 +122,27 @@ public class MainCommand extends BaseCommand {
|
||||||
return;
|
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 +
|
System.out.println(" !!!!! output apk path --> " + output +
|
||||||
" disableCrackSignature --> " + disableCrackSignature);
|
" disableCrackSignature --> " + disableCrackSignature);
|
||||||
|
|
||||||
String apkFileName = getBaseName(srcApkFile);
|
String apkFileName = getBaseName(srcApkFile);
|
||||||
|
|
||||||
// 中间文件临时存储的位置
|
// 中间文件临时存储的位置
|
||||||
String tempFilePath = srcApkFileParentPath + File.separator +
|
String tempFilePath = outputApkFileParentPath + File.separator +
|
||||||
currentTimeStr() + "-tmp" + File.separator;
|
currentTimeStr() + "-tmp" + File.separator;
|
||||||
|
|
||||||
// apk文件解压的目录
|
// apk文件解压的目录
|
||||||
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
|
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
|
||||||
|
|
||||||
if (showAllLogs) {
|
if (showAllLogs) {
|
||||||
System.out.println(" !!!!! srcApkFileParentPath = " + srcApkFileParentPath +
|
System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath +
|
||||||
"\n unzipApkFilePath = " + unzipApkFilePath);
|
"\n unzipApkFilePath = " + unzipApkFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,8 +152,13 @@ public class MainCommand extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先解压apk到指定目录下
|
// 先解压apk到指定目录下
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
FileUtils.decompressZip(apkPath, unzipApkFilePath);
|
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
|
// Get the dex count in the apk zip file
|
||||||
dexFileCount = findDexFileCount(unzipApkFilePath);
|
dexFileCount = findDexFileCount(unzipApkFilePath);
|
||||||
|
|
||||||
|
|
@ -136,33 +168,56 @@ public class MainCommand extends BaseCommand {
|
||||||
|
|
||||||
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
|
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
|
||||||
|
|
||||||
|
currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// parse the app main application full name from the manifest file
|
// parse the app main application full name from the manifest file
|
||||||
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
|
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
|
||||||
String applicationName;
|
String applicationName = null;
|
||||||
if (pair != null && pair.applicationName != null) {
|
if (pair != null && pair.applicationName != null) {
|
||||||
applicationName = pair.applicationName;
|
applicationName = pair.applicationName;
|
||||||
} else {
|
|
||||||
System.out.println(" Application name not found error !!!!!! ");
|
|
||||||
applicationName = DEFAULT_APPLICATION_NAME;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAllLogs) {
|
if (showAllLogs) {
|
||||||
|
System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime));
|
||||||
System.out.println(" Get the application name --> " + applicationName);
|
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,
|
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
|
||||||
dexFileCount));
|
dexFileCount));
|
||||||
|
}
|
||||||
|
|
||||||
// 2. copy xposed so and dex files into the unzipped apk
|
// copy xposed so and dex files into the unzipped apk
|
||||||
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath, getXposedModules(xposedModules)));
|
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));
|
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
|
||||||
|
|
||||||
// 4. excute these tasks
|
// excute these tasks
|
||||||
for (Runnable executor : mXpatchTasks) {
|
for (Runnable executor : mXpatchTasks) {
|
||||||
|
currentTime = System.currentTimeMillis();
|
||||||
executor.run();
|
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
|
// 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) {
|
private int findDexFileCount(String unzipApkFilePath) {
|
||||||
File zipfileRoot = new File(unzipApkFilePath);
|
File zipfileRoot = new File(unzipApkFilePath);
|
||||||
if (!zipfileRoot.exists()) {
|
if (!zipfileRoot.exists()) {
|
||||||
|
|
@ -208,4 +296,8 @@ public class MainCommand extends BaseCommand {
|
||||||
}
|
}
|
||||||
return modules.split(File.pathSeparator);
|
return modules.split(File.pathSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isNotEmpty(String str) {
|
||||||
|
return str != null && !str.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -251,7 +253,23 @@ public abstract class BaseCommand {
|
||||||
Option needArgOpt = null;
|
Option needArgOpt = null;
|
||||||
for (String s : args) {
|
for (String s : args) {
|
||||||
if (needArgOpt != null) {
|
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;
|
needArgOpt = null;
|
||||||
} else if (s.startsWith("-")) {// its a short or long option
|
} else if (s.startsWith("-")) {// its a short or long option
|
||||||
Option opt = optMap.get(s);
|
Option opt = optMap.get(s);
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public class ApkModifyTask implements Runnable {
|
||||||
|
|
||||||
String jarOutputPath = unzipApkFile.getParent() + File.separator + JAR_FILE_NAME;
|
String jarOutputPath = unzipApkFile.getParent() + File.separator + JAR_FILE_NAME;
|
||||||
|
|
||||||
// classes.dex
|
// classes-1.0.dex
|
||||||
String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName);
|
String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName);
|
||||||
|
|
||||||
if (showAllLogs) {
|
if (showAllLogs) {
|
||||||
|
|
@ -104,12 +104,12 @@ public class ApkModifyTask implements Runnable {
|
||||||
cmd.doMain(args);
|
cmd.doMain(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列出目录下所有dex文件,classes.dex,classes2.dex,classes3.dex .....
|
// 列出目录下所有dex文件,classes-1.0.dex,classes2.dex,classes3.dex .....
|
||||||
private ArrayList<String> createClassesDotDexFileList(int dexFileCount) {
|
private ArrayList<String> createClassesDotDexFileList(int dexFileCount) {
|
||||||
ArrayList<String> list = new ArrayList<>();
|
ArrayList<String> list = new ArrayList<>();
|
||||||
for (int i = 0; i < dexFileCount; i++) {
|
for (int i = 0; i < dexFileCount; i++) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
list.add("classes.dex");
|
list.add("classes-1.0.dex");
|
||||||
} else {
|
} else {
|
||||||
list.add("classes" + (i + 1) + ".dex");
|
list.add("classes" + (i + 1) + ".dex");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,22 @@ public class BuildAndSignApkTask implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean signApk(String apkPath, String keyStorePath, String signedApkPath, boolean useLocalJarsigner) {
|
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;
|
File localJarsignerFile = null;
|
||||||
try {
|
try {
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
|
|
@ -67,7 +83,8 @@ public class BuildAndSignApkTask implements Runnable {
|
||||||
String localJarsignerPath = (new File(apkPath)).getParent() + File.separator + "jarsigner-081688";
|
String localJarsignerPath = (new File(apkPath)).getParent() + File.separator + "jarsigner-081688";
|
||||||
localJarsignerFile = new File(localJarsignerPath);
|
localJarsignerFile = new File(localJarsignerPath);
|
||||||
FileUtils.copyFileFromJar("assets/jarsigner", 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 = new StringBuilder(localJarsignerPath + " ");
|
||||||
}
|
}
|
||||||
signCmd.append(" -keystore ")
|
signCmd.append(" -keystore ")
|
||||||
|
|
@ -96,6 +113,15 @@ public class BuildAndSignApkTask implements Runnable {
|
||||||
} else {
|
} else {
|
||||||
System.out.println("use inner jarsigner to sign apk failed, sign it yourself fail msg is :" +
|
System.out.println("use inner jarsigner to sign apk failed, sign it yourself fail msg is :" +
|
||||||
e.toString());
|
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;
|
return false;
|
||||||
} finally {
|
} 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,10 @@ import java.util.HashMap;
|
||||||
*/
|
*/
|
||||||
public class SoAndDexCopyTask implements Runnable {
|
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 XPOSED_MODULE_FILE_NAME_PREFIX = "libxpatch_xp_module_";
|
||||||
private static final String SO_FILE_SUFFIX = ".so";
|
private static final String SO_FILE_SUFFIX = ".so";
|
||||||
|
|
||||||
|
|
@ -20,22 +23,27 @@ public class SoAndDexCopyTask implements Runnable {
|
||||||
"lib/arm64-v8a/"
|
"lib/arm64-v8a/"
|
||||||
};
|
};
|
||||||
|
|
||||||
private final HashMap<String, String> SO_FILE_PATH_MAP = new HashMap<String, String>() {
|
private final HashMap<String, String> mSoFilePathMap = new HashMap<>();
|
||||||
{
|
|
||||||
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 int dexFileCount;
|
private int dexFileCount;
|
||||||
private String unzipApkFilePath;
|
private String unzipApkFilePath;
|
||||||
private String[] xposedModuleArray;
|
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.dexFileCount = dexFileCount;
|
||||||
this.unzipApkFilePath = unzipApkFilePath;
|
this.unzipApkFilePath = unzipApkFilePath;
|
||||||
this.xposedModuleArray = xposedModuleArray;
|
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
|
@Override
|
||||||
|
|
@ -49,14 +57,33 @@ public class SoAndDexCopyTask implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copySoFile() {
|
private void copySoFile() {
|
||||||
|
String[] existLibPathArray = new String[3];
|
||||||
|
int arrayIndex = 0;
|
||||||
for (String libPath : APK_LIB_PATH_ARRAY) {
|
for (String libPath : APK_LIB_PATH_ARRAY) {
|
||||||
String apkSoFullPath = fullLibPath(libPath);
|
String apkSoFullPath = fullLibPath(libPath);
|
||||||
File apkSoFullPathFile = new File(apkSoFullPath);
|
File apkSoFullPathFile = new File(apkSoFullPath);
|
||||||
if (!apkSoFullPathFile.exists()){
|
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();
|
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
|
// copy xposed modules into the lib path
|
||||||
if (xposedModuleArray != null && xposedModuleArray.length > 0) {
|
if (xposedModuleArray != null && xposedModuleArray.length > 0) {
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
|
@ -69,14 +96,13 @@ public class SoAndDexCopyTask implements Runnable {
|
||||||
if (!moduleFile.exists()) {
|
if (!moduleFile.exists()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (String libPath : APK_LIB_PATH_ARRAY) {
|
for (String libPath : existLibPathArray) {
|
||||||
|
if (libPath != null && !libPath.isEmpty()) {
|
||||||
String apkSoFullPath = fullLibPath(libPath);
|
String apkSoFullPath = fullLibPath(libPath);
|
||||||
String outputModuleName = XPOSED_MODULE_FILE_NAME_PREFIX + index + SO_FILE_SUFFIX;
|
String outputModuleName = XPOSED_MODULE_FILE_NAME_PREFIX + index + SO_FILE_SUFFIX;
|
||||||
if(new File(apkSoFullPath).exists()) {
|
|
||||||
File outputModuleSoFile = new File(apkSoFullPath, outputModuleName);
|
File outputModuleSoFile = new File(apkSoFullPath, outputModuleName);
|
||||||
FileUtils.copyFile(moduleFile, outputModuleSoFile);
|
FileUtils.copyFile(moduleFile, outputModuleSoFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +113,7 @@ public class SoAndDexCopyTask implements Runnable {
|
||||||
// copy dex file to root dir, rename it first
|
// copy dex file to root dir, rename it first
|
||||||
String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex";
|
String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex";
|
||||||
// assets/classes.dex分隔符不能使用File.seperater,否则在windows上无法读取到文件,IOException
|
// 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) {
|
private String fullLibPath(String libPath) {
|
||||||
|
|
@ -101,16 +127,15 @@ public class SoAndDexCopyTask implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the file name first
|
// get the file name first
|
||||||
int lastIndex = srcSoPath.lastIndexOf('/');
|
// int lastIndex = srcSoPath.lastIndexOf('/');
|
||||||
int length = srcSoPath.length();
|
// int length = srcSoPath.length();
|
||||||
String soFileName = srcSoPath.substring(lastIndex, length);
|
String soFileName = SO_FILE_NAME_WITH_SUFFIX;
|
||||||
|
|
||||||
// do copy
|
// do copy
|
||||||
FileUtils.copyFileFromJar(srcSoPath, new File(apkSoParentFile, soFileName).getAbsolutePath());
|
FileUtils.copyFileFromJar(srcSoPath, new File(apkSoParentFile, soFileName).getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void deleteMetaInfo() {
|
private void deleteMetaInfo() {
|
||||||
String metaInfoFilePath = "META-INF";
|
String metaInfoFilePath = "META-INF";
|
||||||
File metaInfoFileRoot = new File(unzipApkFilePath + metaInfoFilePath);
|
File metaInfoFileRoot = new File(unzipApkFilePath + metaInfoFilePath);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
* And:https://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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,29 @@ public class ShellCmdUtil {
|
||||||
return result.toString();
|
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) {
|
private static void close(Closeable stream) {
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
try {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue