前言

比起你去要死要活的喜欢一个男孩, 不如有这时间好好完善自己; 让他来要死要活的喜欢你, 才会长久……

简介

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

结语

以前喜欢满天的星星,现在喜欢看月亮。还很喜欢万物都静下来的夜晚,像一个无声的怀抱,更喜欢你 ​​​