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

来源:互联网 发布:excel宏编程入门 编辑:程序博客网 时间:2024/05/22 11:55

做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;}@Overridepublic boolean equals(Object o) {if (!(o instanceof ManifestDigest)) {return false;}final ManifestDigest other = (ManifestDigest) o;return this == other || Arrays.equals(mDigest, other.mDigest);}@Overridepublic int hashCode() {return Arrays.hashCode(mDigest);}@Overridepublic 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 systemmSigningKeys = 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.javapublic 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",    };

所以如何解析的工作就知道在哪里了,想研究了可以把这几个类结合者研究研究,就属于密码学范畴了。


1 0
原创粉丝点击