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

做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",
    };

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


评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值