跳过内容

Android 加密 API

概述

“移动应用加密” 章节中,我们介绍了通用的加密最佳实践,并描述了不正确使用加密时可能出现的典型问题。在本章中,我们将更详细地介绍 Android 的加密 API。我们将展示如何在源代码中识别这些 API 的使用情况,以及如何解释加密配置。审查代码时,请务必将使用的加密参数与本指南中链接的当前最佳实践进行比较。

我们可以在 Android 上识别加密系统的关键组件

Android 加密 API 基于 Java 加密架构 (JCA)。JCA 分离了接口和实现,从而可以包含多个 安全提供程序,这些提供程序可以实现多组加密算法。大多数 JCA 接口和类都在 java.security.*javax.crypto.* 包中定义。此外,还有 Android 特定的包 android.security.*android.security.keystore.*

KeyStore 和 KeyChain 提供了用于存储和使用密钥的 API(在后台,KeyChain API 使用 KeyStore 系统)。这些系统允许管理加密密钥的完整生命周期。有关加密密钥管理实施的要求和指导,请参阅 密钥管理速查表。我们可以识别以下阶段

  • 生成密钥
  • 使用密钥
  • 存储密钥
  • 归档密钥
  • 删除密钥

请注意,密钥的存储在 “测试数据存储” 章节中进行分析。

这些阶段由 Keystore/KeyChain 系统管理。但是,系统的工作方式取决于应用开发者如何实现它。对于分析过程,您应该关注应用开发者使用的函数。您应该识别并验证以下函数

以现代 API 级别为目标的应用经历了以下更改

  • 对于 Android 7.0(API 级别 24)及以上版本,Android 开发者博客显示
    • 建议停止指定安全提供程序。而是始终使用已修补的 安全提供程序
    • Crypto 提供程序的支持已下降,并且该提供程序已被弃用。对于安全随机数,其 SHA1PRNG 也是如此。
  • 对于 Android 8.1(API 级别 27)及以上版本,开发者文档显示
    • Conscrypt(称为 AndroidOpenSSL)优于使用 Bouncy Castle,它具有新的实现:AlgorithmParameters:GCMKeyGenerator:AESKeyGenerator:DESEDEKeyGenerator:HMACMD5KeyGenerator:HMACSHA1KeyGenerator:HMACSHA224KeyGenerator:HMACSHA256KeyGenerator:HMACSHA384KeyGenerator:HMACSHA512SecretKeyFactory:DESEDESignature:NONEWITHECDSA
    • 您不应再使用 IvParameterSpec.class 用于 GCM,而应使用 GCMParameterSpec.class
    • 套接字已从 OpenSSLSocketImpl 更改为 ConscryptFileDescriptorSocketConscryptEngineSocket
    • 具有空参数的 SSLSession 会给出 NullPointerException
    • 您需要具有足够大的数组作为输入字节来生成密钥,否则会抛出 InvalidKeySpecException
    • 如果套接字读取被中断,您会收到 SocketException
  • 对于 Android 9(API 级别 28)及以上版本,Android 开发者博客显示了更多的更改
    • 如果您仍然使用 getInstance 方法指定安全提供程序,并且您以低于 28 的任何 API 为目标,您将收到警告。如果您以 Android 9(API 级别 28)或更高版本为目标,您将收到错误。
    • Crypto 安全提供程序现已删除。调用它将导致 NoSuchProviderException
  • 对于 Android 10(API 级别 29),开发者文档列出了所有网络安全更改。

通用建议

在应用检查期间应考虑以下建议列表

  • 您应该确保遵循 “移动应用加密” 章节中概述的最佳实践。
  • 您应该确保安全提供程序具有最新的更新 - 更新安全提供程序
  • 您应该停止指定安全提供程序并使用默认实现(AndroidOpenSSL、Conscrypt)。
  • 您应该停止使用 Crypto 安全提供程序及其 SHA1PRNG,因为它们已被弃用。
  • 您应该仅为 Android Keystore 系统指定安全提供程序。
  • 您应该停止使用没有 IV 的基于密码的加密密码。
  • 您应该使用 KeyGenParameterSpec 而不是 KeyPairGeneratorSpec。

安全提供程序

Android 依赖于 java.security.Provider 类来实现 Java 安全服务。这些提供程序对于确保安全网络通信和依赖于加密的其他安全功能至关重要。

Android 中包含的安全提供程序列表因 Android 版本和 OEM 特定版本而异。已知旧版本中的某些安全提供程序实现安全性较低或容易受到攻击。因此,Android 应用不仅应选择正确的算法并提供良好的配置,在某些情况下还应注意旧安全提供程序中实现的强度。

您可以使用以下代码列出现有的安全提供程序集

StringBuilder builder = new StringBuilder();
for (Provider provider : Security.getProviders()) {
    builder.append("provider: ")
            .append(provider.getName())
            .append(" ")
            .append(provider.getVersion())
            .append("(")
            .append(provider.getInfo())
            .append(")\n");
}
String providers = builder.toString();
//now display the string on the screen or in the logs for debugging.

这是在具有 Google Play API 的模拟器中运行的 Android 9(API 级别 28)的输出

provider: AndroidNSSP 1.0(Android Network Security Policy Provider)
provider: AndroidOpenSSL 1.0(Android's OpenSSL-backed security provider)
provider: CertPathProvider 1.0(Provider of CertPathBuilder and CertPathVerifier)
provider: AndroidKeyStoreBCWorkaround 1.0(Android KeyStore security provider to work around Bouncy Castle)
provider: BC 1.57(BouncyCastle Security Provider v1.57)
provider: HarmonyJSSE 1.0(Harmony JSSE Provider)
provider: AndroidKeyStore 1.0(Android KeyStore security provider)

更新安全提供程序

保持最新的和已修补的组件是安全原则之一。provider 也是如此。应用应检查使用的安全提供程序是否为最新,如果不是,则 更新它

较旧的 Android 版本

对于某些支持旧版本 Android 的应用(例如,仅在低于 Android 7.0(API 级别 24)的版本中使用),捆绑最新的库可能是唯一的选择。在这种情况下,Conscrypt 库是一个不错的选择,可以使加密在不同的 API 级别上保持一致,并避免必须导入 Bouncy Castle,这是一个较重的库。

Conscrypt for Android 可以这样导入

dependencies {
  implementation 'org.conscrypt:conscrypt-android:last_version'
}

接下来,必须通过调用来注册提供程序

Security.addProvider(Conscrypt.newProvider())

密钥生成

Android SDK 允许您指定密钥的生成方式,以及在何种情况下可以使用它。Android 6.0(API 级别 23)引入了 KeyGenParameterSpec 类,可用于确保应用中密钥的正确使用。例如

String keyAlias = "MySecretKey";

KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setRandomizedEncryptionRequired(true)
        .build();

KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
        "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);

SecretKey secretKey = keyGenerator.generateKey();

KeyGenParameterSpec 指示密钥可用于加密和解密,但不能用于其他目的,例如签名或验证。它进一步指定了块模式 (CBC)、填充 (PKCS #7),并明确指定需要随机加密(这是默认设置)。接下来,我们在 KeyGenerator.getInstance 调用中输入 AndroidKeyStore 作为提供程序的名称,以确保密钥存储在 Android KeyStore 中。

GCM 是一种 AES 模式,提供 身份验证加密,通过将加密和数据身份验证集成到单个过程中来增强安全性,这与旧模式(如 CBC)需要单独的机制(如 HMAC)不同。此外,GCM 不需要填充,这简化了实现并最大限度地减少了漏洞。

尝试违反上述规范使用生成的密钥将导致安全异常。

这是一个使用该密钥进行加密的示例

String AES_MODE = KeyProperties.KEY_ALGORITHM_AES
        + "/" + KeyProperties.BLOCK_MODE_CBC
        + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7;
KeyStore AndroidKeyStore = AndroidKeyStore.getInstance("AndroidKeyStore");

// byte[] input
Key key = AndroidKeyStore.getKey(keyAlias, null);

Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.ENCRYPT_MODE, key);

byte[] encryptedBytes = cipher.doFinal(input);
byte[] iv = cipher.getIV();
// save both the IV and the encryptedBytes

IV(初始化向量)和加密的字节都需要存储;否则无法解密。

以下是如何解密该密码文本。input 是加密的字节数组,iv 是加密步骤中的初始化向量

// byte[] input
// byte[] iv
Key key = AndroidKeyStore.getKey(AES_KEY_ALIAS, null);

Cipher cipher = Cipher.getInstance(AES_MODE);
IvParameterSpec params = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, params);

byte[] result = cipher.doFinal(input);

由于 IV 每次都是随机生成的,因此应将其与密码文本 (encryptedBytes) 一起保存,以便稍后解密。

在 Android 6.0(API 级别 23)之前,不支持 AES 密钥生成。因此,许多实现选择使用 RSA 并生成公钥-私钥对以使用 KeyPairGeneratorSpec 进行非对称加密,或使用 SecureRandom 生成 AES 密钥。

这是一个使用 KeyPairGeneratorKeyPairGeneratorSpec 创建 RSA 密钥对的示例

Date startDate = Calendar.getInstance().getTime();
Calendar endCalendar = Calendar.getInstance();
endCalendar.add(Calendar.YEAR, 1);
Date endDate = endCalendar.getTime();
KeyPairGeneratorSpec keyPairGeneratorSpec = new KeyPairGeneratorSpec.Builder(context)
        .setAlias(RSA_KEY_ALIAS)
        .setKeySize(4096)
        .setSubject(new X500Principal("CN=" + RSA_KEY_ALIAS))
        .setSerialNumber(BigInteger.ONE)
        .setStartDate(startDate)
        .setEndDate(endDate)
        .build();

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA",
        "AndroidKeyStore");
keyPairGenerator.initialize(keyPairGeneratorSpec);

KeyPair keyPair = keyPairGenerator.generateKeyPair();

此示例创建密钥大小为 4096 位的 RSA 密钥对(即模数大小)。也可以以类似的方式生成椭圆曲线 (EC) 密钥。但是,截至 Android 11(API 级别 30),AndroidKeyStore 不支持使用 EC 密钥进行加密或解密。它们只能用于签名。

对称加密密钥可以通过使用基于密码的密钥派生函数版本 2 (PBKDF2) 从密码短语生成。此加密协议旨在生成可用于加密目的的加密密钥。算法的输入参数根据 不正确的密钥生成函数 部分进行调整。下面的代码清单说明了如何基于密码生成强加密密钥。

public static SecretKey generateStrongAESKey(char[] password, int keyLength)
{
    //Initialize objects and variables for later use
    int iterationCount = 10000;
    int saltLength     = keyLength / 8;
    SecureRandom random = new SecureRandom();
    //Generate the salt
    byte[] salt = new byte[saltLength];
    random.nextBytes(salt);
    KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength);
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
    byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
    return new SecretKeySpec(keyBytes, "AES");
}

上述方法需要一个包含密码的字符数组和所需的密钥长度(以位为单位),例如 128 或 256 位 AES 密钥。我们定义了一个 10,000 轮的迭代计数,PBKDF2 算法将使用它。增加迭代次数会显着增加对密码进行暴力攻击的工作量,但是它会影响性能,因为密钥派生需要更多的计算能力。我们将盐的大小定义为密钥长度除以 8,以便从位转换为字节,我们使用 SecureRandom 类随机生成盐。盐需要保持不变,以确保每次为相同的密码生成相同的加密密钥。请注意,您可以将盐私下存储在 SharedPreferences 中。建议将盐从 Android 备份机制中排除,以防止在高风险数据情况下进行同步。

请注意,如果您将植根设备或已修补(例如重新打包)的应用作为数据威胁考虑在内,则最好使用放置在 AndroidKeystore 中的密钥加密盐。基于密码的加密 (PBE) 密钥使用推荐的 PBKDF2WithHmacSHA1 算法生成,直到 Android 8.0(API 级别 26)。对于更高的 API 级别,最好使用 PBKDF2withHmacSHA256,这将最终得到更长的哈希值。

注意:有一种广泛的错误观点,即应使用 NDK 来隐藏加密操作和硬编码密钥。但是,使用此机制无效。攻击者仍然可以使用工具来查找使用的机制并转储内存中的密钥。接下来,可以使用例如 radare2 分析控制流,并借助 Frida 或两者的组合提取密钥: r2frida(请参阅 反汇编本机代码 进程探索 了解更多详细信息)。从 Android 7.0(API 级别 24)开始,不允许使用私有 API,而是需要调用公共 API,这进一步影响了隐藏它的有效性,如 Android 开发者博客 中所述

随机数生成

加密需要安全的伪随机数生成 (PRNG)。标准 Java 类(如 java.util.Random)没有提供足够的随机性,实际上可能会使攻击者能够猜测将要生成的下一个值,并使用此猜测来冒充另一个用户或访问敏感信息。

通常,应使用 SecureRandom。但是,如果支持低于 Android 4.4(API 级别 19)的 Android 版本,则需要额外注意,以解决 Android 4.1-4.3(API 级别 16-18)版本中的错误,该错误 未能正确初始化 PRNG

大多数开发者应通过不带任何参数的默认构造函数来实例化 SecureRandom。其他构造函数用于更高级的用途,如果使用不正确,可能会导致随机性和安全性降低。支持 SecureRandom 的 PRNG 提供程序使用 AndroidOpenSSL (Conscrypt) 提供程序的 SHA1PRNG

查看 Android 文档 了解更多详细信息。