Android,Java环境下获取apk的公钥

做Android项目中突然需要提取APK的公钥,本来是个很小的插曲,以为一会就可以完成,没想到居然折腾了2天,事后想想还真是挺简单的一个东西。

先贴上分别在Android环境和Java环境下获取公钥的代码,当然你有兴趣可以稍稍往下看下我们小组所犯的错误。


Android环境下获取公钥的方法1:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public static String getApkSignInfo(String apkFilePath){  
  2.             byte[] readBuffer = new byte[8192];  
  3.             java.security.cert.Certificate[] certs = null;  
  4.             try{  
  5.                 JarFile jarFile = new JarFile(apkFilePath);  
  6.                 Enumeration entries = jarFile.entries();  
  7.                 while(entries.hasMoreElements()){  
  8.                     JarEntry je = (JarEntry)entries.nextElement();  
  9.                     if(je.isDirectory()){  
  10.                         continue;  
  11.                     }  
  12.                     if(je.getName().startsWith("META-INF/")){  
  13.                         continue;  
  14.                     }  
  15.                     java.security.cert.Certificate[] localCerts = loadCertificates(jarFile,je,readBuffer);  
  16.                   //  System.out.println("File " + apkFilePath + " entry " + je.getName()+ ": certs=" + certs + " ("+ (certs != null ? certs.length : 0) + ")");  
  17.                     if (certs == null) {  
  18.                         certs = localCerts;  
  19.                     }else{  
  20.                         for(int i=0; i<certs.length; i++){  
  21.                             boolean found = false;  
  22.                             for (int j = 0; j < localCerts.length; j++) {  
  23.                                 if (certs[i] != null && certs[i].equals(localCerts[j])) {  
  24.                                       found = true;  
  25.                                       break;  
  26.                                 }  
  27.                             }  
  28.                             if (!found || certs.length != localCerts.length) {  
  29.                                   jarFile.close();  
  30.                                   return null;  
  31.                             }  
  32.                         }  
  33.                     }  
  34.                 }  
  35.                 jarFile.close();  
  36.                 //Log.i("wind cert=",certs[0].toString());  
  37.                 return certs[0].getPublicKey().toString();  
  38.             }catch(Exception e){  
  39.                 e.printStackTrace();  
  40.             }  
  41.             return null;  
  42.         }  
  43. private static java.security.cert.Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) {  
  44.             try {  
  45.                 InputStream is = jarFile.getInputStream(je);  
  46.                 while(is.read(readBuffer,0,readBuffer.length)!=-1) {                      
  47.                 }  
  48.                 is.close();  
  49.                 return (java.security.cert.Certificate[])(je!=null?je.getCertificates():null);  
  50.             } catch (Exception e) {  
  51.                 e.printStackTrace();  
  52.                 System.err.println("Exception reading "+je.getName()+" in "+jarFile.getName()+": "+e);  
  53.             }  
  54.             return null;  
  55.         }  
其实代码就是从Android源码class PackageParser 中抠出来的,这个文件位于:frameworks\base\core\java\android\content\pm\PackageParser.java,

当然你也可以直接使用Java的反射:

方法2:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public void showUninstallAPKSignatures(String apkPath) {     
  2.        String PATH_PackageParser = "android.content.pm.PackageParser";     
  3.        try {     
  4.            // apk包的文件路径     
  5.            // 这是一个Package 解释器, 是隐藏的     
  6.            // 构造函数的参数只有一个, apk文件的路径     
  7.            Class pkgParserCls = Class.forName(PATH_PackageParser);     
  8.            Class[] typeArgs = new Class[1];     
  9.            typeArgs[0] = String.class;     
  10.            Constructor pkgParserCt = pkgParserCls.getConstructor(typeArgs);     
  11.            Object[] valueArgs = new Object[1];     
  12.            valueArgs[0] = apkPath;     
  13.            Object pkgParser = pkgParserCt.newInstance(valueArgs);     
  14.            // 这个是与显示有关的, 里面涉及到一些像素显示等等, 我们使用默认的情况     
  15.            DisplayMetrics metrics = new DisplayMetrics();     
  16.            metrics.setToDefaults();      
  17.            typeArgs = new Class[4];     
  18.            typeArgs[0] = File.class;     
  19.            typeArgs[1] = String.class;     
  20.            typeArgs[2] = DisplayMetrics.class;     
  21.            typeArgs[3] = Integer.TYPE;     
  22.            Method pkgParser_parsePackageMtd = pkgParserCls.getDeclaredMethod("parsePackage",     
  23.                    typeArgs);     
  24.            valueArgs = new Object[4];     
  25.            valueArgs[0] = new File(apkPath);     
  26.            valueArgs[1] = apkPath;     
  27.            valueArgs[2] = metrics;     
  28.            valueArgs[3] = PackageManager.GET_SIGNATURES;     
  29.            Object pkgParserPkg = pkgParser_parsePackageMtd.invoke(pkgParser, valueArgs);     
  30.                
  31.            typeArgs = new Class[2];     
  32.            typeArgs[0] = pkgParserPkg.getClass();     
  33.            typeArgs[1] = Integer.TYPE;     
  34.            Method pkgParser_collectCertificatesMtd = pkgParserCls.getDeclaredMethod("collectCertificates",     
  35.                    typeArgs);     
  36.            valueArgs = new Object[2];     
  37.            valueArgs[0] = pkgParserPkg;     
  38.            valueArgs[1] = PackageManager.GET_SIGNATURES;     
  39.            pkgParser_collectCertificatesMtd.invoke(pkgParser, valueArgs);     
  40.            // 应用程序信息包, 这个公开的, 不过有些函数, 变量没公开     
  41.            Field packageInfoFld = pkgParserPkg.getClass().getDeclaredField("mSignatures");     
  42.            Signature[] info = (Signature[]) packageInfoFld.get(pkgParserPkg);  
  43.            parseSignature(info[0].toByteArray());  
  44.        } catch (Exception e) {     
  45.            e.printStackTrace();     
  46.        }     
  47.    }    
  48. public void parseSignature(byte[] signature)  
  49. {  
  50.  try{  
  51.      CertificateFactory certFactory = CertificateFactory  
  52.              .getInstance("X.509");  
  53.      X509Certificate cert = (X509Certificate)certFactory  
  54.              .generateCertificate(new ByteArrayInputStream(signature));  
  55.      Log.i(TAG, cert.toString());//这里是打印证书,如果要公钥,使用函数cert.getPublicKey();  
  56.      }  
  57.      catch(Exception e)  
  58.      {  
  59.          e.printStackTrace();  
  60.      }  
  61. }  

JAVA环境下:

因为没有Android的sdk,所以,没法用反射,只能老老实实的来:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. import java.io.BufferedInputStream;  
  2. import java.io.IOException;  
  3. import java.io.InputStream;  
  4. import java.security.DigestInputStream;  
  5. import java.security.MessageDigest;  
  6. import java.security.NoSuchAlgorithmException;  
  7. import java.security.PublicKey;  
  8. import java.security.cert.Certificate;  
  9. import java.util.Arrays;  
  10. import java.util.Enumeration;  
  11. import java.util.HashSet;  
  12. import java.util.Set;  
  13. import java.util.jar.JarEntry;  
  14. import java.util.jar.JarFile;  
  15.   
  16. class ManifestDigest {  
  17.     private static final String TAG = "ManifestDigest";  
  18.   
  19.     /** The digest of the manifest in our preferred order. */  
  20.     private final byte[] mDigest;  
  21.   
  22.     /** What we print out first when toString() is called. */  
  23.     private static final String TO_STRING_PREFIX = "ManifestDigest {mDigest=";  
  24.   
  25.     /** Digest algorithm to use. */  
  26.     private static final String DIGEST_ALGORITHM = "SHA-256";  
  27.   
  28.     ManifestDigest(byte[] digest) {  
  29.         mDigest = digest;  
  30.     }  
  31.   
  32.     static ManifestDigest fromInputStream(InputStream fileIs) {  
  33.         if (fileIs == null) {  
  34.             return null;  
  35.         }  
  36.   
  37.         final MessageDigest md;  
  38.         try {  
  39.             md = MessageDigest.getInstance(DIGEST_ALGORITHM);  
  40.         } catch (NoSuchAlgorithmException e) {  
  41.             throw new RuntimeException(DIGEST_ALGORITHM + " must be available",  
  42.                     e);  
  43.         }  
  44.   
  45.         final DigestInputStream dis = new DigestInputStream(  
  46.                 new BufferedInputStream(fileIs), md);  
  47.         try {  
  48.             byte[] readBuffer = new byte[8192];  
  49.             while (dis.read(readBuffer, 0, readBuffer.length) != -1) {  
  50.                 // not using  
  51.             }  
  52.         } catch (IOException e) {  
  53.             // Slog.w(TAG, "Could not read manifest");  
  54.             return null;  
  55.         } finally {  
  56.             // IoUtils.closeQuietly(dis);  
  57.         }  
  58.   
  59.         final byte[] digest = md.digest();  
  60.         return new ManifestDigest(digest);  
  61.     }  
  62.   
  63.     public int describeContents() {  
  64.         return 0;  
  65.     }  
  66.   
  67.     @Override  
  68.     public boolean equals(Object o) {  
  69.         if (!(o instanceof ManifestDigest)) {  
  70.             return false;  
  71.         }  
  72.   
  73.         final ManifestDigest other = (ManifestDigest) o;  
  74.   
  75.         return this == other || Arrays.equals(mDigest, other.mDigest);  
  76.     }  
  77.   
  78.     @Override  
  79.     public int hashCode() {  
  80.         return Arrays.hashCode(mDigest);  
  81.     }  
  82.   
  83.     @Override  
  84.     public String toString() {  
  85.         final StringBuilder sb = new StringBuilder(TO_STRING_PREFIX.length()  
  86.                 + (mDigest.length * 3) + 1);  
  87.   
  88.         sb.append(TO_STRING_PREFIX);  
  89.   
  90.         final int N = mDigest.length;  
  91.         for (int i = 0; i < N; i++) {  
  92.             final byte b = mDigest[i];  
  93.             IntegralToString.appendByteAsHex(sb, b, false);  
  94.             sb.append(',');  
  95.         }  
  96.         sb.append('}');  
  97.   
  98.         return sb.toString();  
  99.     }  
  100.   
  101. }  
  102.   
  103. public class testt {  
  104.     private String mArchiveSourcePath = "D:\\workspace EE1\\RTPullListView\\bin\\com.bankcomm_205.apk";  
  105.   
  106.     private java.security.cert.Certificate[] loadCertificates(JarFile jarFile,  
  107.             JarEntry je, byte[] readBuffer) {  
  108.         try {  
  109.             // We must read the stream for the JarEntry to retrieve  
  110.             // its certificates.  
  111.             InputStream is = new BufferedInputStream(jarFile.getInputStream(je));  
  112.             while (is.read(readBuffer, 0, readBuffer.length) != -1) {  
  113.             }  
  114.             is.close();  
  115.             return je != null ? je.getCertificates() : null;  
  116.         } catch (IOException e) {  
  117.             System.out.print(e.toString());  
  118.         } catch (RuntimeException e) {  
  119.             System.out.print(e.toString());  
  120.         }  
  121.         return null;  
  122.     }  
  123.   
  124.     private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";  
  125.     public Signature mSignatures[];  
  126.     public ManifestDigest manifestDigest;  
  127.   
  128.     public boolean collectCertificates() {  
  129.         byte[] readBuffer = new byte[8192];  
  130.         java.security.cert.Certificate[] certs = null;  
  131.         try {  
  132.             JarFile jarFile = new JarFile(mArchiveSourcePath);  
  133.   
  134.             Enumeration<JarEntry> entries = jarFile.entries();  
  135.             while (entries.hasMoreElements()) {  
  136.                 final JarEntry je = entries.nextElement();  
  137.                 if (je.isDirectory())  
  138.                     continue;  
  139.                 final String name = je.getName();  
  140.                 if (name.contains("RSA")) {  
  141.                     int a = 0;  
  142.                     a++;  
  143.   
  144.                 }  
  145.                 if (name.startsWith("META-INF/"))  
  146.                     continue;  
  147.   
  148.                 if (ANDROID_MANIFEST_FILENAME.equals(name)) {  
  149.                     manifestDigest = ManifestDigest.fromInputStream(jarFile  
  150.                             .getInputStream(je));  
  151.                 }  
  152.   
  153.                 final Certificate[] localCerts = loadCertificates(jarFile, je,  
  154.                         readBuffer);  
  155.   
  156.                 if (localCerts == null) {  
  157.                     System.out.print("localCerts is null");  
  158.                     jarFile.close();  
  159.                     return false;  
  160.                 } else if (certs == null) {  
  161.                     certs = localCerts;  
  162.                 } else {  
  163.                     // Ensure all certificates match.  
  164.                     for (int i = 0; i < certs.length; i++) {  
  165.                         boolean found = false;  
  166.                         for (int j = 0; j < localCerts.length; j++) {  
  167.                             if (certs[i] != null  
  168.                                     && certs[i].equals(localCerts[j])) {  
  169.                                 found = true;  
  170.                                 break;  
  171.                             }  
  172.                         }  
  173.                         if (!found || certs.length != localCerts.length) {  
  174.                             System.out.print(" Package "  
  175.                                     + " has mismatched certificates at entry "  
  176.                                     + je.getName() + "; ignoring!");  
  177.                             jarFile.close();  
  178.                             return false;  
  179.                         }  
  180.                     }  
  181.                 }  
  182.             }  
  183.   
  184.             jarFile.close();  
  185.   
  186.             if (certs != null && certs.length > 0) {  
  187.                 final int N = certs.length;  
  188.                 mSignatures = new Signature[certs.length];  
  189.                 for (int i = 0; i < N; i++) {  
  190.                     mSignatures[i] = new Signature(certs[i].getEncoded());  
  191.                 }  
  192.             } else {  
  193.                 System.out  
  194.                         .print("Package " + " has no certificates; ignoring!");  
  195.                 return false;  
  196.             }  
  197.   
  198.             // Add the signing KeySet to the system  
  199.             mSigningKeys = new HashSet<PublicKey>();  
  200.             for (int i = 0; i < certs.length; i++) {  
  201.                 mSigningKeys.add(certs[i].getPublicKey());  
  202.   
  203.                 System.out.println(certs[i].toString());  
  204.             }  
  205.   
  206.         } catch (Exception e) {  
  207.             System.out.print(e.toString());  
  208.             return false;  
  209.         }  
  210.         return true;  
  211.     }  
  212.   
  213.     public Set<PublicKey> mSigningKeys;  
  214.   
  215.     public static void main(String[] args) {  
  216.         testt t = new testt();  
  217.         t.collectCertificates();  
  218.   
  219.     }  
  220.   
  221. }  

这里有必要说明一下,Android下获得的公钥是16进制形式的,而在JAVA环境下获得的是10进制的,其实当天我们就做出来了,但是一对比好像这个16进制跟10进制不一样,

两个版本的证书输出如下图:【左边为Java环境下的证书内容,右边为Android环境下的证书内容】,事后发现那个公钥,十进制的那个数是一个很长的大整数,得全部转换为16进制才会相等,之前一直错误的认为是一个字节一个字节的输出。


用BigInteger转换一下就知道是一模一样的:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. BigInteger src = new BigInteger(s1);  
  2.     System.out.println(src.toString(16));  


开始发现十进制跟16进制对不上号,因为是把16进制一个字节,4个字节转为10进制之后拼接在一起,总是不对,一直以为代码出问题,或者对apk包中如何验证理解有误,然后把Android的相关的源码都抠出来,拷到Java环境下,然后各种修改错误,最后得出的结果居然是一样的,很是郁闷,仔细研究证书的byte字节,发现完全一样,然后就没有然后了,把那个十进制按照大整数转为16进制之后就发现其实是一模一样的,一整天都在看Android源码中相关的部分,各种移植,折腾了好久。

但总的来说还是有收获的,首先在PKMS中调用PackaParse解析apk文件时,你会发现网上说的collectCertificates证书收集函数中完全略过了“META-INF”这个文件夹下的所有内容,代码中的这句:着实让我纠结很久很久。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. if (name.startsWith("META-INF/")) continue;  
根据我们学习安卓的经验,这个文件夹中存放的是开发者信息,证书信息,公钥以及CA签名,当然开发者自己就是个CA,公钥只能从这里提取,但是既然跳过了这个阶段,那么。。。公钥呢,没有公钥怎么验证啊,这尼玛真是坑。后来手贱的点了一下JarEntry进去发现了端倪:它是ZipEntry的扩展,我们都知道APK就是个ZIP,但为什么直接用ZipFile而用JarFile呢,公钥,证书的的处理其实就封装在JarEntry中,

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. JarEntry.java  
  2. public Certificate[] getCertificates() {  
  3.         if (parentJar == null) {  
  4.             return null;  
  5.         }  
  6.         JarVerifier jarVerifier = parentJar.verifier;  
  7.         if (jarVerifier == null) {  
  8.             return null;  
  9.         }  
  10.         return jarVerifier.getCertificates(getName());  
  11.     }  
因为我们获得公钥或者证书统统是从je.getCertificates(),je.getPublicKey(),中拿到的,而je就是JarEntry,他已经帮我们做好了公钥以及证书的提取工作了,所以在PackageParse,java这个类中我们根本找不到相关的内容,其实这里的具体工作都交给了JarVerifier对象,有点像代理模式【对设计模式不是很懂,纯属猜想】。类中的证书形式就是X.509格式。具体的解析在JarVerifier.java这个类中,这个类的第一行就定义:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. private static final String[] DIGEST_ALGORITHMS = new String[] {  
  2.       "SHA-512",  
  3.       "SHA-384",  
  4.       "SHA-256",  
  5.       "SHA1",  
  6.   };  

所以如何解析的工作就知道在哪里了,想研究了可以把这几个类结合者研究研究,就属于密码学范畴了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值