什么是APK公钥 - 如何获取APK公钥2015-06-30 17:26:20

( 3人已投票,[高质量] )
分享: ╬收藏
31.3K

什么是APK的公钥及如何获取APK公钥,相信初次遇到这些问题的朋友来说是相当恼人的事情,其实看起来容易的事情,操作起来也许不是那么顺利。

在Android中,获取环境下公钥的代码如下所示:


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;
    }


当然这段代码是源码中的内容,文件位置:

//frameworks\base\core\java\android\content\pm\PackageParser.java,


另一种你可能想要尝试的方法:Java的反射

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进制的,可能需要转换一下,这地方需要注意!


最终获取两种证书如图所示:


什么是APK公钥 - 如何获取APK公钥

左边为Java环境下的证书内容,右边为Android环境下的证书内容



公钥看起来不同,其实用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",
 };


到这里相信大家一目明了了,希望对你有帮助!






头像

snowcoal
  • apk公钥
  • android
  • 获取公钥

收藏到我的私密空间

标题:什么是APK公钥 - 如何获取APK公钥

作者:柳岸花明

你暂未登录,请登录后才可收藏至您的私密空间 确认取消
雪炭网

键盘操作 更便捷 -雪炭网雪中送炭-乐趣无限