做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;
- }
当然你也可以直接使用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;
- JarEntry.java
- public Certificate[] getCertificates() {
- if (parentJar == null) {
- return null;
- }
- JarVerifier jarVerifier = parentJar.verifier;
- if (jarVerifier == null) {
- return null;
- }
- return jarVerifier.getCertificates(getName());
- }
- private static final String[] DIGEST_ALGORITHMS = new String[] {
- "SHA-512",
- "SHA-384",
- "SHA-256",
- "SHA1",
- };
所以如何解析的工作就知道在哪里了,想研究了可以把这几个类结合者研究研究,就属于密码学范畴了。