返回

Android 密钥库密码校验:无需 JAR 文件的方案

Android

Android 密钥库:密钥密码正确性验证方案

开发 Android 应用,离不开应用的签名环节。发布应用前,需要使用一个有效的密钥进行签名,这个过程离不开 Android 密钥库(keystore)以及密码保护机制。有时,需要对 keystore 中的密钥密码的正确性进行验证。这个需求可以是为了确保密码仍然有效,或者是在自动化流程中进行预检查,避免因密码错误导致后续操作失败。

难题:不借助已签名的 JAR 文件来校验密钥密码

通常,校验 Android 密钥密码最常见的方法是尝试对一个 JAR 文件进行签名。这个做法在有待签名的 JAR 文件时是可行的,如果获取这个 JAR 文件的过程很耗时,那么就需要找到一种更为高效的方式——不依赖于待签名的 JAR 文件进行密钥密码的校验。

方案探索与解决方案

Android 的 keytooljarsigner 工具为开发者提供了签名工具支持,那么是否可以利用这两个工具实现校验目标呢?深入分析它们的运行机制,有助于寻找一个无需 JAR 文件参与的验证方法。

方法一:利用 keytool -list 命令尝试

keytool -list 命令用于列出密钥库中的条目信息,这个过程也涉及到了对密钥密码的验证。 实验发现直接结合 -list 选项和 keypass 参数去获取某个密钥条目的详细信息时,仍然无法仅依赖此命令完成校验任务。这是因为如果没有输入或使用 -storepass 参数来给出密钥库的密码,keytool 会尝试自动弹出窗口来接收密钥库的密码,从而导致一些环境下的命令无法实现全自动输入校验,因此仅依赖此方式不合适。

结合以上的问题,我们设计一种新的方法,可以完全由自动化方式验证密码的正确性。此方案在给出密钥库密码和别名以及别名密码的条件下进行,可以正确区分仅是密钥库的密码不正确还是别名密码不正确,且不受 keytool 在输入参数不全时是否自动弹出接收密钥库密码窗口的机制的影响,能满足一些场景下的自动化输入和结果收集。

实现步骤与代码示例

步骤 1: 列出所有别名

keytool -list -keystore <keystore_path> -storepass <keystore_password> | awk -F: '/Alias name/ {print $2}'

这步骤中通过 keytool -list 命令来获得别名,若无任何信息输出则可判断为 keystore_password 不正确。此步骤在给出密钥库密码但不知道其具体值是否正确时是必须的。若密钥库密码始终固定则可以忽略该步骤直接进入步骤 2。

步骤 2: 利用别名来列出具体某个密钥信息
如果只给 keytool -list 命令添加 storepass 参数,则 keytool 会打印别名所对应密钥的一些摘要信息。但是因为该过程没有解析对应密钥的内容,因此无需指定对应的密钥密码即可完成该操作。这导致无法区分密钥密码是否正确。

如果除了 storepass 参数之外还给出 keypass 参数来给 keytool -list 命令指定某个别名所对应的密钥,那么就能达到只检查别名密码的目的。需要注意别名是否存在,因为该操作仍然要求别名正确,只有别名和其对应的密码均正确才视为密码正确。若不正确则 keytool 命令仍然会执行失败并退出,此时再进一步去识别是否因为别名不正确还是因为别名密码不正确,若是别名不正确则此流程直接中止,说明提供的别名有误,无需后续的校验,若是别名密码不正确则此时通过日志确认即可确认别名密码有误。该方案支持多个别名的自动化批量校验,能提升校验效率。

完整的校验代码示例如下,其中通过 set -e 参数来实现只要任意命令执行失败就立即中止并抛出错误的自动化。此外该脚本仅返回是否成功,因此可以在该脚本基础上自行解析执行的日志并提取有效的信息以支持各种业务场景需求。

#!/bin/bash

set -e

KEYSTORE="your_keystore.jks"  # 请替换为你的密钥库文件
STOREPASS="your_store_password"  # 请替换为你的密钥库密码

# 验证密钥库密码,若无法列出别名则密钥库密码有误。若执行此步骤前已确认过密钥库密码则可跳过此步骤直接进入步骤 2
ALIAS_NAMES=$(keytool -list -keystore $KEYSTORE -storepass $STOREPASS | awk -F: '/Alias name/ {print $2}')

if [[ -z "$ALIAS_NAMES" ]]; then
  # 无法列出别名,可以确认密钥库密码不正确
  echo "Keystore password incorrect."
  exit 1
fi

# 循环校验指定别名和对应的密码,也可添加额外的参数来校验其它别名和密码
ALIAS_PASSWORD_PAIRS=(
    "your_alias_1:your_alias_1_password" # 请替换为你的别名及其密码
    "your_alias_2:your_alias_2_password" # 
)
for pair in "${ALIAS_PASSWORD_PAIRS[@]}"; do
    IFS=':' read -r -a alias_password <<< "$pair"
    alias="${alias_password[0]}"
    password="${alias_password[1]}"

    # 如果别名和密钥密码均正确,将返回具体的条目信息,否则命令执行失败
    keytool -list -v -keystore $KEYSTORE -storepass $STOREPASS -alias "$alias" -keypass "$password"
    echo "Alias [$alias] and its password are correct."
done

exit 0

上述脚本支持校验一个或多个别名及其密码,且能给出明确的结果来指明具体哪一项配置有误。开发者可根据此代码示例扩展成其它自定义化的形式以用于不同的校验目的。

方法二:通过程序实现密钥加载与校验

除了上述直接利用现有命令来进行密码验证的方案外,开发者也可以考虑通过编程的方式来实现密码的验证。此方式利用程序开发中密钥的加载功能来实现。这个过程中涉及到对密钥库以及密钥内容的加载。

实现步骤与代码示例

步骤 1: 加载密钥库

使用编程语言(这里以 Java 为例)提供的 API 来加载密钥库。

import java.io.FileInputStream;
import java.security.KeyStore;

public class KeyStoreValidator {

    public static boolean validateKeyStorePassword(String keystorePath, String keystorePassword) {
        try {
            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            FileInputStream fis = new FileInputStream(keystorePath);
            keystore.load(fis, keystorePassword.toCharArray());
            fis.close();
            return true;
        } catch (Exception e) {
            System.err.println("Error loading keystore: " + e.getMessage());
            return false;
        }
    }

    public static void main(String[] args) {
        String keystorePath = "your_keystore.jks"; // 请替换为你的密钥库路径
        String keystorePassword = "your_store_password"; // 请替换为你的密钥库密码
        if (validateKeyStorePassword(keystorePath, keystorePassword)) {
            System.out.println("Keystore password is correct.");
        } else {
            System.out.println("Keystore password is not correct.");
        }
    }
}

此代码利用了 java.security.KeyStore 类来对密钥进行加载,通过对异常进行捕获和处理可以实现对密钥库密码的验证。

步骤 2: 校验指定别名及其密码的正确性
在步骤 1 的基础上进行进一步的操作来尝试使用指定的密钥密码获取具体的密钥信息,实现更详细的校验。具体如下:

import java.io.FileInputStream;
import java.security.Key;
import java.security.KeyStore;

public class KeyStoreValidator {

    // ... validateKeyStorePassword 方法 ...
  
    public static boolean validateAliasPassword(String keystorePath, String keystorePassword, String alias, String aliasPassword) {
        try {
            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            FileInputStream fis = new FileInputStream(keystorePath);
            keystore.load(fis, keystorePassword.toCharArray());
            fis.close();
          
            // 该方法可以获取到别名所对应的 Key 对象。只有别名和别名密码同时正确时才能成功
            Key key = keystore.getKey(alias, aliasPassword.toCharArray());

            if (key != null) {
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            System.err.println("Error validating alias password: " + e.getMessage());
            return false;
        }
    }

    public static void main(String[] args) {
        // ... validateKeyStorePassword 调用 ...

        String alias = "your_alias"; // 请替换为你的别名
        String aliasPassword = "your_alias_password"; // 请替换为你的别名密码

        if (validateAliasPassword(keystorePath, keystorePassword, alias, aliasPassword)) {
            System.out.println("Alias password is correct.");
        } else {
            System.out.println("Alias password is not correct or alias does not exist.");
        }
    }
}

和上述对密钥库进行加载并校验的方法类似,这里添加了 validateAliasPassword 方法来用于具体的密钥和别名密码的验证。利用 java.security.KeyStore.getKey() 方法的返回值或者是否发生异常来确认对应密钥的信息。getKey() 方法能够正常返回结果的前置条件是必须同时给正确的别名以及别名密码。
这种方案可以给开发者提供更多的操作和校验空间,可以灵活地与已有流程集成在一起实现更为复杂的校验策略和目的。

补充的安全建议

安全保管密钥库和密钥密码至关重要。在验证密码的同时,也需遵循一些安全方面的原则:

  1. 最小权限原则 :在进行密码验证的环境中,尽量使用受限的权限运行,避免使用 root 或者管理员权限,从而避免风险进一步扩大。
  2. 安全存储密码 :密钥和密码需要存放在一个安全的位置。可以采用加密容器来存储密钥库以及相关的信息。
  3. 限制访问 :限制只有授权人员才能访问密钥库文件以及相关的信息,对密钥库和密码信息采取访问控制,记录密钥使用日志以用于检测。
  4. 避免硬编码 : 密钥以及密码等信息尽可能地避免直接硬编码到代码中,可以采取外部的配置文件或加密数据库等来管理这些敏感信息。

这些手段均可以进一步保证系统的安全性。密钥的泄漏是 Android 应用面临的主要风险之一。采取措施强化密钥以及密钥库密码的验证策略能进一步避免风险,提升安全性。