做Android项目中突然需要提取APK的公钥,本来是个很小的插曲,以为一会就可以完成,没想到居然折腾了2天,事后想想还真是挺简单的一个东西。
先贴上分别在Android环境和Java环境下获取公钥的代码,当然你有兴趣可以稍稍往下看下我们小组所犯的错误。
Android环境下获取公钥的方法1:
public static String getApkSignInfo(String apkFilePath){
byte[] readBuffer = new byte[8192];
java.security.cert.Certificate[] certs = null;
try{
JarFile jarFile = new JarFile(apkFilePath);
Enumeration entries = jarFile.entries();
while(entries.hasMoreElements()){
JarEntry je = (JarEntry)entries.nextElement();
if(je.isDirectory()){
continue;
}
if(je.getName().startsWith("META-INF/")){
continue;
}
java.security.cert.Certificate[] localCerts = loadCertificates(jarFile,je,readBuffer);
// System.out.println("File " + apkFilePath + " entry " + je.getName()+ ": certs=" + certs + " ("+ (certs != null ? certs.length : 0) + ")");
if (certs == null) {
certs = localCerts;
}else{
for(int i=0; i<certs.length; i++){
boolean found = false;
for (int j = 0; j < localCerts.length; j++) {
if (certs[i] != null && certs[i].equals(localCerts[j])) {
found = true;
break;
}
}
if (!found || certs.length != localCerts.length) {
jarFile.close();
return null;
}
}
}
}
jarFile.close();
//Log.i("wind cert=",certs[0].toString());
return certs[0].getPublicKey().toString();
}catch(Exception e){
e.printStackTrace();
}
return null;
}
private static java.security.cert.Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) {
try {
InputStream is = jarFile.getInputStream(je);
while(is.read(readBuffer,0,readBuffer.length)!=-1) {
}
is.close();
return (java.security.cert.Certificate[])(je!=null?je.getCertificates():null);
} catch (Exception e) {
e.printStackTrace();
System.err.println("Exception reading "+je.getName()+" in "+jarFile.getName()+": "+e);
}
return null;
}
其实代码就是从Android源码class PackageParser 中抠出来的,这个文件位于:frameworks\base\core\java\android\content\pm\PackageParser.java,
当然你也可以直接使用Java的反射:
方法2:
public void showUninstallAPKSignatures(String apkPath) {
String PATH_PackageParser = "android.content.pm.PackageParser";
try {
// apk包的文件路径
// 这是一个Package 解释器, 是隐藏的
// 构造函数的参数只有一个, apk文件的路径
Class pkgParserCls = Class.forName(PATH_PackageParser);
Class[] typeArgs = new Class[1];
typeArgs[0] = String.class;
Constructor pkgParserCt = pkgParserCls.getConstructor(typeArgs);
Object[] valueArgs = new Object[1];
valueArgs[0] = apkPath;
Object pkgParser = pkgParserCt.newInstance(valueArgs);
// 这个是与显示有关的, 里面涉及到一些像素显示等等, 我们使用默认的情况
DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
typeArgs = new Class[4];
typeArgs[0] = File.class;
typeArgs[1] = String.class;
typeArgs[2] = DisplayMetrics.class;
typeArgs[3] = Integer.TYPE;
Method pkgParser_parsePackageMtd = pkgParserCls.getDeclaredMethod("parsePackage",
typeArgs);
valueArgs = new Object[4];
valueArgs[0] = new File(apkPath);
valueArgs[1] = apkPath;
valueArgs[2] = metrics;
valueArgs[3] = PackageManager.GET_SIGNATURES;
Object pkgParserPkg = pkgParser_parsePackageMtd.invoke(pkgParser, valueArgs);
typeArgs = new Class[2];
typeArgs[0] = pkgParserPkg.getClass();
typeArgs[1] = Integer.TYPE;
Method pkgParser_collectCertificatesMtd = pkgParserCls.getDeclaredMethod("collectCertificates",
typeArgs);
valueArgs = new Object[2];
valueArgs[0] = pkgParserPkg;
valueArgs[1] = PackageManager.GET_SIGNATURES;
pkgParser_collectCertificatesMtd.invoke(pkgParser, valueArgs);
// 应用程序信息包, 这个公开的, 不过有些函数, 变量没公开
Field packageInfoFld = pkgParserPkg.getClass().getDeclaredField("mSignatures");
Signature[] info = (Signature[]) packageInfoFld.get(pkgParserPkg);
parseSignature(info[0].toByteArray());
} catch (Exception e) {
e.printStackTrace();
}
}
public void parseSignature(byte[] signature)
{
try{
CertificateFactory certFactory = CertificateFactory
.getInstance("X.509");
X509Certificate cert = (X509Certificate)certFactory
.generateCertificate(new ByteArrayInputStream(signature));
Log.i(TAG, cert.toString());//这里是打印证书,如果要公钥,使用函数cert.getPublicKey();
}
catch(Exception e)
{
e.printStackTrace();
}
}
JAVA环境下:
因为没有Android的sdk,所以,没法用反射,只能老老实实的来:
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
class ManifestDigest {
private static final String TAG = "ManifestDigest";
/** The digest of the manifest in our preferred order. */
private final byte[] mDigest;
/** What we print out first when toString() is called. */
private static final String TO_STRING_PREFIX = "ManifestDigest {mDigest=";
/** Digest algorithm to use. */
private static final String DIGEST_ALGORITHM = "SHA-256";
ManifestDigest(byte[] digest) {
mDigest = digest;
}
static ManifestDigest fromInputStream(InputStream fileIs) {
if (fileIs == null) {
return null;
}
final MessageDigest md;
try {
md = MessageDigest.getInstance(DIGEST_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(DIGEST_ALGORITHM + " must be available",
e);
}
final DigestInputStream dis = new DigestInputStream(
new BufferedInputStream(fileIs), md);
try {
byte[] readBuffer = new byte[8192];
while (dis.read(readBuffer, 0, readBuffer.length) != -1) {
// not using
}
} catch (IOException e) {
// Slog.w(TAG, "Could not read manifest");
return null;
} finally {
// IoUtils.closeQuietly(dis);
}
final byte[] digest = md.digest();
return new ManifestDigest(digest);
}
public int describeContents() {
return 0;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ManifestDigest)) {
return false;
}
final ManifestDigest other = (ManifestDigest) o;
return this == other || Arrays.equals(mDigest, other.mDigest);
}
@Override
public int hashCode() {
return Arrays.hashCode(mDigest);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(TO_STRING_PREFIX.length()
+ (mDigest.length * 3) + 1);
sb.append(TO_STRING_PREFIX);
final int N = mDigest.length;
for (int i = 0; i < N; i++) {
final byte b = mDigest[i];
IntegralToString.appendByteAsHex(sb, b, false);
sb.append(',');
}
sb.append('}');
return sb.toString();
}
}
public class testt {
private String mArchiveSourcePath = "D:\\workspace EE1\\RTPullListView\\bin\\com.bankcomm_205.apk";
private java.security.cert.Certificate[] loadCertificates(JarFile jarFile,
JarEntry je, byte[] readBuffer) {
try {
// We must read the stream for the JarEntry to retrieve
// its certificates.
InputStream is = new BufferedInputStream(jarFile.getInputStream(je));
while (is.read(readBuffer, 0, readBuffer.length) != -1) {
}
is.close();
return je != null ? je.getCertificates() : null;
} catch (IOException e) {
System.out.print(e.toString());
} catch (RuntimeException e) {
System.out.print(e.toString());
}
return null;
}
private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
public Signature mSignatures[];
public ManifestDigest manifestDigest;
public boolean collectCertificates() {
byte[] readBuffer = new byte[8192];
java.security.cert.Certificate[] certs = null;
try {
JarFile jarFile = new JarFile(mArchiveSourcePath);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
final JarEntry je = entries.nextElement();
if (je.isDirectory())
continue;
final String name = je.getName();
if (name.contains("RSA")) {
int a = 0;
a++;
}
if (name.startsWith("META-INF/"))
continue;
if (ANDROID_MANIFEST_FILENAME.equals(name)) {
manifestDigest = ManifestDigest.fromInputStream(jarFile
.getInputStream(je));
}
final Certificate[] localCerts = loadCertificates(jarFile, je,
readBuffer);
if (localCerts == null) {
System.out.print("localCerts is null");
jarFile.close();
return false;
} else if (certs == null) {
certs = localCerts;
} else {
// Ensure all certificates match.
for (int i = 0; i < certs.length; i++) {
boolean found = false;
for (int j = 0; j < localCerts.length; j++) {
if (certs[i] != null
&& certs[i].equals(localCerts[j])) {
found = true;
break;
}
}
if (!found || certs.length != localCerts.length) {
System.out.print(" Package "
+ " has mismatched certificates at entry "
+ je.getName() + "; ignoring!");
jarFile.close();
return false;
}
}
}
}
jarFile.close();
if (certs != null && certs.length > 0) {
final int N = certs.length;
mSignatures = new Signature[certs.length];
for (int i = 0; i < N; i++) {
mSignatures[i] = new Signature(certs[i].getEncoded());
}
} else {
System.out
.print("Package " + " has no certificates; ignoring!");
return false;
}
// Add the signing KeySet to the system
mSigningKeys = new HashSet<PublicKey>();
for (int i = 0; i < certs.length; i++) {
mSigningKeys.add(certs[i].getPublicKey());
System.out.println(certs[i].toString());
}
} catch (Exception e) {
System.out.print(e.toString());
return false;
}
return true;
}
public Set<PublicKey> mSigningKeys;
public static void main(String[] args) {
testt t = new testt();
t.collectCertificates();
}
}
这里有必要说明一下,Android下获得的公钥是16进制形式的,而在JAVA环境下获得的是10进制的,其实当天我们就做出来了,但是一对比好像这个16进制跟10进制不一样,
两个版本的证书输出如下图:【左边为Java环境下的证书内容,右边为Android环境下的证书内容】,事后发现那个公钥,十进制的那个数是一个很长的大整数,得全部转换为16进制才会相等,之前一直错误的认为是一个字节一个字节的输出。
用BigInteger转换一下就知道是一模一样的:
BigInteger src = new BigInteger(s1);
System.out.println(src.toString(16));
开始发现十进制跟16进制对不上号,因为是把16进制一个字节,4个字节转为10进制之后拼接在一起,总是不对,一直以为代码出问题,或者对apk包中如何验证理解有误,然后把Android的相关的源码都抠出来,拷到Java环境下,然后各种修改错误,最后得出的结果居然是一样的,很是郁闷,仔细研究证书的byte字节,发现完全一样,然后就没有然后了,把那个十进制按照大整数转为16进制之后就发现其实是一模一样的,一整天都在看Android源码中相关的部分,各种移植,折腾了好久。
但总的来说还是有收获的,首先在PKMS中调用PackaParse解析apk文件时,你会发现网上说的collectCertificates证书收集函数中完全略过了“META-INF”这个文件夹下的所有内容,代码中的这句:着实让我纠结很久很久。
if (name.startsWith("META-INF/")) continue;
根据我们学习安卓的经验,这个文件夹中存放的是开发者信息,证书信息,公钥以及CA签名,当然开发者自己就是个CA,公钥只能从这里提取,但是既然跳过了这个阶段,那么。。。公钥呢,没有公钥怎么验证啊,这尼玛真是坑。后来手贱的点了一下JarEntry进去发现了端倪:它是ZipEntry的扩展,我们都知道APK就是个ZIP,但为什么直接用ZipFile而用JarFile呢,公钥,证书的的处理其实就封装在JarEntry中,
JarEntry.java
public Certificate[] getCertificates() {
if (parentJar == null) {
return null;
}
JarVerifier jarVerifier = parentJar.verifier;
if (jarVerifier == null) {
return null;
}
return jarVerifier.getCertificates(getName());
}
因为我们获得公钥或者证书统统是从je.getCertificates(),je.getPublicKey(),中拿到的,而je就是JarEntry,他已经帮我们做好了公钥以及证书的提取工作了,所以在PackageParse,java这个类中我们根本找不到相关的内容,其实这里的具体工作都交给了JarVerifier对象,有点像代理模式【对设计模式不是很懂,纯属猜想】。类中的证书形式就是X.509格式。具体的解析在JarVerifier.java这个类中,这个类的第一行就定义:
private static final String[] DIGEST_ALGORITHMS = new String[] {
"SHA-512",
"SHA-384",
"SHA-256",
"SHA1",
};
所以如何解析的工作就知道在哪里了,想研究了可以把这几个类结合者研究研究,就属于密码学范畴了。