前言
比起你去要死要活的喜欢一个男孩, 不如有这时间好好完善自己; 让他来要死要活的喜欢你, 才会长久……
简介
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
结语
以前喜欢满天的星星,现在喜欢看月亮。还很喜欢万物都静下来的夜晚,像一个无声的怀抱,更喜欢你



