前言
比起你去要死要活的喜欢一个男孩, 不如有这时间好好完善自己; 让他来要死要活的喜欢你, 才会长久……
简介
Android项目以它的包名作为唯一标识,如果在同一设备上安装两个相同的应用,后面安装的应用就会覆盖前面安装的应用。为了避免这种情况的发生,我们需要对作为产品发布的应用进行签名。
签名其实有两个作用:
(1) 确定发布者的身份。防止别人用相同包名来替换你已安装的程序。
(2) 确保应用的完整性。签名会对应用包中的每个文件进行处理,以确保程序包中的文件不会被替换。
本文是基于Google提供的签名工具包ApkSig来进行Apk Sign的。
Android Apk Sign : http://www.aospxref.com/android-12.0.0_r3/xref/tools/apksig/
实现
1.引入相关jar
<!--bouncycastle Java 生成SSL证书--> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency> <!--Android apk 签名jar--> <dependency> <groupId>com.android.apksig</groupId> <artifactId>apksig</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/lib/apksig.jar</systemPath> </dependency>
2.工具类相关
①实体类ApkSignOperation
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class ApkSignOperation implements Serializable { /** * sign-签名APK */ private String opType; /** * 10-system-系统密钥 20-user-用户密钥 */ private Long keypairType; /** * 10-system-系统证书 20-user-用户证书 */ private Long certType; /** * 成功 失败 */ private int code; /** * 证书名称 */ private String certName; /** * 密钥名称 */ private String keypairName; /** * 系统/用户密钥路径 */ private String systemKeypairPath; /** * 系统/用户密钥密码 */ private String systemKeypairPassword; /** * 系统/用户证书路径 */ private String systemCertPath; /************************************Sign Apk**************************************/ /** * APK输入文件 */ private byte[] apkInFile; /** * APK输入文件路径 */ private String apkInFileName; /** * APK输出路径 */ private String apkOutFile; /** * sigAlg */ private String sigAlg; /** * 0-false 1-true * 签名版本(V1/V2/V3/V4) */ private List<Boolean> versions; public String toSignApkString() { return "SignApk{" + "code='" + code + '\'' + ", opType='" + opType + '\'' + ", keypairType='" + keypairType + '\'' + ", certType='" + certType + '\'' + ", apkInFileName='" + apkInFileName + '\'' + ", apkOutFile='" + apkOutFile + '\'' + ", sigAlg='" + sigAlg + '\'' + '}'; } }
②证书工具类CertificateUtil
public class CertificateUtil { private static final String CERTIFICATE_TYPE = "X.509"; static { try { Security.addProvider(new BouncyCastleProvider()); } catch (Exception e) { e.printStackTrace(); } } /** * 读取PKCS8 * * @param file * @param password * @return * @throws Exception */ public static PrivateKey readPKCS8(File file, String password) throws Exception { if (isPemFormat(file)) { return readPemPKCS8(file, password); } else { return readDerPKCS8(file, password); } } public static boolean isPemFormat(File certFile) { try { FileInputStream inStream = new FileInputStream(certFile); byte[] b = new byte[4]; inStream.read(b); inStream.close(); if (Arrays.equals(b, new byte[]{0x2D, 0x2D, 0x2D, 0x2D})) { return true; } return false; } catch (IOException e) { } return false; } /** * 读取PKCS8系统密钥 * * @param file * @param password * @return * @throws Exception */ public static PrivateKey readPemPKCS8(File file, String password) throws Exception { FileInputStream inputStream = new FileInputStream(file); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); PEMParser parser = new PEMParser(bufferedReader); try { Object obj = parser.readObject(); if (obj instanceof PKCS8EncryptedPrivateKeyInfo) { PKCS8EncryptedPrivateKeyInfo encryptedPrivateKey = (PKCS8EncryptedPrivateKeyInfo) obj; PrivateKeyInfo privateKeyInfo = encryptedPrivateKey.decryptPrivateKeyInfo(new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray())); return getPrivateKey(privateKeyInfo); } else if (obj instanceof PEMEncryptedKeyPair) { PEMEncryptedKeyPair pemEncryptedKeyPair = (PEMEncryptedKeyPair) parser.readObject(); PEMKeyPair pemKeyPair = pemEncryptedKeyPair.decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray())); JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(getProvider()); KeyPair keyPair = converter.getKeyPair(pemKeyPair); return keyPair.getPrivate(); } } finally { if (parser == null) { return null; } try { parser.close(); } catch (Exception e) { } } return null; } public static PrivateKey getPrivateKey(PrivateKeyInfo privateKeyInfo) throws PEMException { JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); return converter.getPrivateKey(privateKeyInfo); } /** * 读取二进制PK8 * * @param file * @param password * @return * @throws Exception */ public static PrivateKey readDerPKCS8(File file, String password) throws Exception { byte[] bs = FileUtil.readBytes(file); ASN1Sequence derseq = ASN1Sequence.getInstance(Arrays.copyOf(bs, bs.length)); PKCS8EncryptedPrivateKeyInfo encobj = new PKCS8EncryptedPrivateKeyInfo(EncryptedPrivateKeyInfo.getInstance(derseq)); JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); InputDecryptorProvider decryptionProv = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray()); PrivateKeyInfo keyInfo = encobj.decryptPrivateKeyInfo(decryptionProv); PrivateKey key = converter.getPrivateKey(keyInfo); return key; } /** * 读取系统证书-PEM * * @param certFile * @return * @throws Exception */ public static X509Certificate readPemCertificate(File certFile) throws Exception { PEMParser parser = new PEMParser(new FileReader(certFile)); Security.addProvider(new BouncyCastleProvider()); Object obj = parser.readObject(); try { if (obj != null && obj instanceof X509CertificateHolder) { return new JcaX509CertificateConverter().getCertificate((X509CertificateHolder) obj); } else { X509Certificate cert = (X509Certificate) obj; return cert; } } catch (Exception e) { e.printStackTrace(); if (obj instanceof X509CertificateHolder) { X509CertificateHolder holder = (X509CertificateHolder) obj; byte[] certBuf = holder.getEncoded(); CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC"); X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBuf)); return certificate; } else { X509Certificate cert = (X509Certificate) obj; return cert; } } finally { parser.close(); } } public static String getProvider() { return "BC"; }
③签名工具类SignApkUtil
@Slf4j public class SignApkUtil { static { try { Security.addProvider(new BouncyCastleProvider()); } catch (Exception e) { e.printStackTrace(); } } public Boolean onStart(ApkSignOperation signApk) throws Exception { Boolean result = false; File outFileIdSig = null; try{ File outDir = new File(signApk.getApkOutFile()); if (!outDir.exists()) { if (!outDir.createNewFile()) { log.info("Error: Failed to create directory: " + outDir); return result; } } ApkSigner.SignerConfig signerConfig = getCustomizeSignerConfigFromResources(signApk.getCertName(), signApk.getSystemKeypairPath(), signApk.getSystemKeypairPassword(), signApk.getSystemCertPath(), false); List<ApkSigner.SignerConfig> customizeSignerConfig = Collections.singletonList(signerConfig); File outApkFile = new File(signApk.getApkOutFile()); DataSource inApkFile = DataSources.asDataSource(ByteBuffer.wrap(IoUtil.readBytes(new FileInputStream(new File(signApk.getApkInFileName()))))); List<Boolean> signResult = new ArrayList<>(4); Map<Integer, Boolean> versionMap = new HashMap<>(4); if (CollectionUtil.isNotEmpty(signApk.getVersions())) { for (int i = 0; i < signApk.getVersions().size(); i++) { boolean versionFlg = signApk.getVersions().get(i); versionMap.put((i), versionFlg); } } // V4 outFileIdSig = new File(outApkFile.getCanonicalPath() + ".idsig"); ApkSigner apkSigner = new ApkSigner.Builder(customizeSignerConfig) .setInputApk(inApkFile) .setOutputApk(outApkFile) .setCreatedBy("wizarpos") .setVerityEnabled(true) .setV1SigningEnabled(Optional.ofNullable(versionMap.get(0)).orElse(false)) .setV2SigningEnabled(Optional.ofNullable(versionMap.get(1)).orElse(false)) .setV3SigningEnabled(Optional.ofNullable(versionMap.get(2)).orElse(false)) .setV4SigningEnabled(Optional.ofNullable(versionMap.get(3)).orElse(false)) .setV4ErrorReportingEnabled(Optional.ofNullable(versionMap.get(3)).orElse(false)) .setV4SignatureOutputFile(outFileIdSig) .build(); apkSigner.sign(); ApkVerifier.Result verifyResult = verify(outApkFile.getCanonicalPath()); if (Optional.ofNullable(versionMap.get(3)).orElse(false)) { if (!verifyResult.isVerifiedUsingV4Scheme()) { log.info("Error: APK failed to sign V4. Procedure."); }else{ File apkFile = new File(outApkFile.getCanonicalPath() + ".apk"); FileUtil.copyFile(outApkFile, apkFile); ZipUtil.zip(FileUtil.file(outApkFile), false, FileUtil.file(outFileIdSig), FileUtil.file(apkFile.getCanonicalPath()) ); } } if (Optional.ofNullable(versionMap.get(2)).orElse(false) && !verifyResult.isVerifiedUsingV3Scheme()) { log.info("Error: APK failed to sign V3. Procedure."); } if (Optional.ofNullable(versionMap.get(1)).orElse(false) && !verifyResult.isVerifiedUsingV2Scheme()) { log.info("Error: APK failed to sign V2. Procedure."); } if (Optional.ofNullable(versionMap.get(0)).orElse(false) && !verifyResult.isVerifiedUsingV1Scheme()) { log.info("Error: APK failed to sign V1. Procedure."); } signResult.add(0, verifyResult.isVerifiedUsingV1Scheme()); signResult.add(1, verifyResult.isVerifiedUsingV2Scheme()); signResult.add(2, verifyResult.isVerifiedUsingV3Scheme()); signResult.add(3, verifyResult.isVerifiedUsingV4Scheme()); signApk.setVersions(signResult); log.info("Info: Sign APK Verify Result: " + verifyResult.isVerified()); return verifyResult.isVerified(); }catch (Exception e){ System.err.println(e.getMessage()); return false; }finally { FileUtil.del(outFileIdSig); } } /** * 自定义私钥/证书 * * @param keyName * @param privateKeyFilePath * @param certificateFilePath * @param deterministicDsaSigning * @return * @throws Exception */ private static ApkSigner.SignerConfig getCustomizeSignerConfigFromResources(String keyName, String privateKeyFilePath, String privateKeyPassword, String certificateFilePath, boolean deterministicDsaSigning) throws Exception { // 读取私钥 PrivateKey privateKey = CertificateUtil.readPKCS8(new File(privateKeyFilePath), privateKeyPassword); // 读取证书 List<X509Certificate> certs = new ArrayList<>(); File certFile = new File(certificateFilePath); X509Certificate systemCertificate = CertificateUtil.readPemCertificate(certFile); certs.add(systemCertificate); return new ApkSigner.SignerConfig.Builder(keyName, privateKey, certs, deterministicDsaSigning).build(); } private Boolean check(ApkSignOperation signApk){ log.info("Sign Apk Start. Params=>{" + signApk.toSignApkString() + "}"); String apkInFilePath = signApk.getApkInFileName(); if (apkInFilePath == null || !new File(apkInFilePath).exists()) { log.info("Invalid apk file: " + apkInFilePath); } // 系统密钥路径 String pk8FilePath = signApk.getSystemKeypairPath(); if (StrUtil.isBlank(pk8FilePath) || !FileUtil.exist(pk8FilePath)) { log.info("Error: The keypair does not exist. The administrator needs to maintain it."); return false; } // 系统证书路径 String certFilePath = signApk.getSystemCertPath(); if (StrUtil.isBlank(certFilePath) || !FileUtil.exist(certFilePath)) { log.info("Error: The certificate does not exist. The administrator needs to maintain it."); return false; } return true; } /** * 验证APK * @param apkFilenameInResources * @return * @throws IOException * @throws ApkFormatException * @throws NoSuchAlgorithmException */ private ApkVerifier.Result verify(String apkFilenameInResources) throws IOException, ApkFormatException, NoSuchAlgorithmException { return verify(apkFilenameInResources, null, null); } private ApkVerifier.Result verify( String apkFilenameInResources, Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws IOException, ApkFormatException, NoSuchAlgorithmException { byte[] apkBytes = IoUtil.readBytes(new FileInputStream(new File(apkFilenameInResources))); ApkVerifier.Builder builder = new ApkVerifier.Builder(DataSources.asDataSource(ByteBuffer.wrap(apkBytes))); if (minSdkVersionOverride != null) { builder.setMinCheckedPlatformVersion(minSdkVersionOverride); } if (maxSdkVersionOverride != null) { builder.setMaxCheckedPlatformVersion(maxSdkVersionOverride); } // 验证V4 File idSig = new File(apkFilenameInResources + ".idsig"); if (idSig.exists()) { builder.setV4SignatureFile(idSig); } return builder.build().verify(); } }
3.测试
/** * Android Apk 签名测试 * * @author yl */ public class SignApkTest { public static void main(String[] args) throws Exception { // APK输入文件 String apkInFilePath = "apk/apk-nosign-v1.0.apk"; FileInputStream inputStream = new FileInputStream(apkInFilePath); // APK输出路径 String apkIOutFilePath = "/apk/apk-issign-"+System.currentTimeMillis(); // 设置签名版本V1 V2 V3 V4 List<Boolean> versions = new ArrayList<>(4); // V1 versions.add(0, true); // V2 versions.add(1, true); // V3 versions.add(2, true); // V4 versions.add(3, true); ApkSignOperation apkSignOperation = ApkSignOperation.builder() .opType("sign") .keypairType(10L).certType(10L).code(0) .certName("system").keypairName("system") .systemKeypairPath("/system/keypair/system-keypair-01.pk8").systemKeypairPassword("wizarpos") .systemCertPath("/system/certificate/system-cert-01.pem") .apkInFileName(apkInFilePath) .apkInFile(IOUtils.toByteArray(inputStream)) .apkOutFile(apkIOutFilePath) .versions(versions) .build(); SignApkUtil signApkUtil = new SignApkUtil(); signApkUtil.onStart(apkSignOperation); } } 注意:Android Sign V4 时必须选择V2或者V3,且V4签名这里会生成两个文件,打成ZIP包。 下面提供apkSign的jar包:https://download.csdn.net/download/qq_35731570/87542109
结语
以前喜欢满天的星星,现在喜欢看月亮。还很喜欢万物都静下来的夜晚,像一个无声的怀抱,更喜欢你