Android 密钥库系统

利用 Android 密钥库系统,您可以在容器中存储加密密钥,从而提高从设备中提取密钥的难度。在密钥进入密钥库后,可以将它们用于加密操作,而密钥材料仍不可导出。此外,您可以通过密钥库系统来限制密钥的使用场景和使用方式,例如要求进行用户身份验证才能使用密钥,或者仅限在某些加密模式中使用。如需了解详情,请参阅安全功能部分。

Keystore 系统由 Android 4.0(API 级别 14)中引入的 KeyChain API 以及在 Android 4.3(API 级别 18)中引入的 Android Keystore 提供程序功能使用。本文介绍何时以及如何使用 Android 密钥库系统。

安全功能

Android 密钥库系统可以通过两种方式保护密钥材料免遭未经授权的使用。首先,它可以防止从应用进程和整个 Android 设备中提取密钥材料,从而降低了在 Android 设备之外以未经授权的方式使用密钥材料的风险。其次,它可以让应用指定密钥的授权使用方式,并在应用进程之外强制实施这些限制,从而降低了在 Android 设备上以未经授权的方式使用密钥材料的风险。

提取防范

Android 密钥库密钥使用两项安全措施来避免密钥材料被提取:

  • 密钥材料永不进入应用进程。通过 Android 密钥库密钥执行加密操作时,应用会在后台将待签署或验证的明文、密文和消息馈送到执行加密操作的系统进程。如果应用进程受到攻击,攻击者也许能使用应用密钥,但无法提取密钥材料(例如,在 Android 设备以外使用)。
  • 密钥材料可以与 Android 设备的安全硬件(例如可信执行环境 (TEE) 或安全元件 (SE)) 绑定在一起。为密钥启用此功能时,其密钥材料永远不会暴露于安全硬件之外。如果 Android 操作系统受到攻击或者攻击者可以读取设备内部存储空间,攻击者也许能在 Android 设备上使用任意应用的 Android 密钥库,但无法从设备上提取这些数据。只有设备的安全硬件支持密钥算法、分块模式、填充方案和密钥有权配合使用的摘要的特定组合时,才可启用此功能。

    如需检查是否为密钥启用了此功能,请获取密钥的 KeyInfo。下一步取决于您的应用的目标 SDK 版本:

    • 如果您的应用以 Android 10(API 级别 29)或更高版本为目标平台,请检查 getSecurityLevel() 的返回值。如果返回值与 KeyProperties.SecurityLevelEnum.TRUSTED_ENVIRONMENT 或 KeyProperties.SecurityLevelEnum.STRONGBOX 匹配,则表示密钥位于安全硬件内。
    • 如果您的应用以 Android 9(API 级别 28)或更低版本为目标平台,请检查 KeyInfo.isInsideSecurityHardware() 返回的布尔值。

硬件安全模块

搭载 Android 9(API 级别 28)或更高版本的受支持设备可拥有 StrongBox Keymaster,它是位于硬件安全模块中的 Keymaster HAL 的一种实现。虽然硬件安全模块可以指代多种不同的密钥存储实现,其中 Linux 内核被攻破后也无法暴露它们(例如 TEE),而 StrongBox 则专指嵌入式安全元件 (eSE) 或基于 SoC 的安全处理单元 (iSE) 等设备。

该模块包含以下组成部分:

  • 自己的 CPU
  • 安全存储空间
  • 真实随机数生成器
  • 可抵御软件包篡改和未经授权旁加载应用的附加机制
  • 安全计时器
  • 重启通知引脚(或等效项),类似于通用输入/输出 (GPIO) 引脚

为支持低能耗的 StrongBox 实现,为一部分算法和密钥大小提供了支持:

  • RSA 2048
  • AES 128 和 256
  • ECDSA、ECDH P-256
  • HMAC-SHA256(支持 8-64 字节密钥大小,含首末值)
  • Triple DES
  • 扩展的长度 APDU
  • 密钥认证
  • 针对升级的修正条款 H 支持

使用 KeyStore 类生成或导入密钥时,您需要通过将 true 传递给 setIsStrongBoxBacked() 方法,指示在 StrongBox Keymaster 中存储密钥的偏好。

虽然与 TEE 相比,StrongBox 速度较慢且资源受限(这意味着它支持的并行操作较少),但 StrongBox 可以提供更好的安全保证来防范物理和边信道攻击。如果您更注重较高的安全性保证而不是应用的资源利用效率,我们建议在支持 StrongBox 的设备上使用它。如果 StrongBox 不可用,您的应用始终可以回退到使用 TEE 来存储密钥材料。

密钥使用授权

为了避免在 Android 设备上以未经授权的方式使用密钥,在生成或导入密钥时,Android 密钥库会让应用指定密钥的授权使用方式。一旦生成或导入密钥,其授权将无法更改。然后,每次使用密钥时,都会由 Android 密钥库强制执行授权。这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥以未经授权的方式使用。

支持的密钥使用授权可归为以下几个类别:

  • 加密:密钥只能与授权密钥算法、运算或目的(加密、解密、签署、验证)、填充方案、分块模式或摘要一起搭配使用。
  • 时间有效性间隔:授权仅在指定的时间段内使用密钥。
  • 用户身份验证:密钥只能在用户最近进行身份验证时使用。请参阅要求进行用户身份验证才能使用密钥。

作为一项额外的安全措施,对于密钥材料位于安全硬件内的密钥(请参阅 KeyInfo.isInsideSecurityHardware(),或者对于以 Android 10(API 级别 29)或更高版本为目标平台的应用,请参阅 KeyInfo.getSecurityLevel()),某些密钥使用授权可能会由安全硬件强制执行,具体取决于 Android 设备。安全硬件通常会强制执行加密和用户身份验证授权。但是,安全硬件通常不会强制执行时间有效性间隔授权,因为其通常没有独立且安全的实时时钟。

您可以使用 KeyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware() 查询密钥的用户身份验证授权是否由安全硬件强制执行。

选择密钥链和 Android 密钥库提供程序

如果您需要系统全局凭据,请使用 KeyChain API。在应用通过 KeyChain API 请求使用任何凭据时,用户可以通过系统提供的界面选择应用可以访问已安装的哪些凭据。因此,在用户同意的情况下多个应用可以使用同一套凭据。

使用 Android 密钥库提供程序让单个应用可以存储自己的凭据,并且只有该应用本身可以访问相应凭据。通过这种方式,应用可以管理仅可供自身使用的凭据,同时所提供的安全优势还可媲美 KeyChain API 为系统全局凭据提供的安全优势。此方法不需要用户选择凭据。

使用 Android 密钥库提供程序

如需使用此功能,请使用标准的 KeyStore 和 KeyPairGenerator 或 KeyGenerator 类,以及在 Android 4.3(API 级别 18)中引入的 AndroidKeyStore 提供程序。

AndroidKeyStore 在与KeyStore.getInstance(type) 方法搭配使用时注册为 KeyStore 类型,在与 KeyPairGenerator.getInstance(algorithm, provider) 和 KeyGenerator.getInstance(algorithm, provider) 方法搭配使用时注册为提供程序。

生成新私钥或密钥

如需生成包含 PrivateKey 的新 KeyPair,则须指定证书具备的初始 X.509 属性。之后,您可以使用 KeyStore.setKeyEntry() 将证书替换为由证书授权机构 (CA) 签署的证书。

如需生成密钥对,请使用 KeyPairGenerator 和 KeyGenParameterSpec:

Kotlin

/*
 * Generate a new EC key pair entry in the Android Keystore by
 * using the KeyPairGenerator API. The private key can only be
 * used for signing or verification and only with SHA-256 or
 * SHA-512 as the message digest.
 */
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC,
        "AndroidKeyStore"
)
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).run {
    setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
    build()
}

kpg.initialize(parameterSpec)

val kp = kpg.generateKeyPair()

Java

/*
 * Generate a new EC key pair entry in the Android Keystore by
 * using the KeyPairGenerator API. The private key can only be
 * used for signing or verification and only with SHA-256 or
 * SHA-512 as the message digest.
 */
KeyPairGenerator kpg = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
kpg.initialize(new KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
        .setDigests(KeyProperties.DIGEST_SHA256,
            KeyProperties.DIGEST_SHA512)
        .build());

KeyPair kp = kpg.generateKeyPair();

将加密密钥导入安全硬件

借助 Android 9(API 级别 28)及更高版本,您能够利用 ASN.1 编码密钥格式将已加密密钥安全导入密钥库。Keymaster 随后会在密钥库中将密钥解密,因此密钥的内容永远不会以明文形式出现在设备的主机内存中。此过程提高了密钥解密的安全性。

如需支持以安全方式将已加密密钥导入密钥库,请完成以下步骤:

  1. 生成目的为 PURPOSE_WRAP_KEY 的密钥对。我们建议您也为该密钥对添加认证。

  2. 在您信任的服务器或机器上,为 SecureKeyWrapper 生成 ASN.1 消息。

    该封装容器包含以下架构:

       KeyDescription ::= SEQUENCE {
           keyFormat INTEGER,
           authorizationList AuthorizationList
       }
    
       SecureKeyWrapper ::= SEQUENCE {
           wrapperFormatVersion INTEGER,
           encryptedTransportKey OCTET_STRING,
           initializationVector OCTET_STRING,
           keyDescription KeyDescription,
           secureKey OCTET_STRING,
           tag OCTET_STRING
       }
    
  3. 创建 WrappedKeyEntry 对象,传入字节数组形式的 ASN.1 消息。

  4. 将该 WrappedKeyEntry 对象传入接受 Keystore.Entry 对象的 setEntry() 的重载。

使用密钥库条目

您可以通过所有标准 KeyStore API 访问 AndroidKeyStore 提供程序。

列出条目

通过调用 aliases() 方法列出密钥库中的条目:

Kotlin

/*
 * Load the Android KeyStore instance using the
 * AndroidKeyStore provider to list the currently stored entries.
 */
val ks: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
   load(null)
}
val aliases: Enumeration<String> = ks.aliases()

Java

/*
 * Load the Android KeyStore instance using the
 * AndroidKeyStore provider to list the currently stored entries.
 */
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
Enumeration<String> aliases = ks.aliases();

对数据进行签名和验证

通过从密钥库提取 KeyStore.Entry 并使用 Signature API(例如 sign())来为数据签名:

Kotlin

/*
 * Use a PrivateKey in the KeyStore to create a signature over
 * some data.
 */
val ks: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
    load(null)
}
val entry: KeyStore.Entry = ks.getEntry(alias, null)
if (entry !is KeyStore.PrivateKeyEntry) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry")
    return null
}
val signature: ByteArray = Signature.getInstance("SHA256withECDSA").run {
    initSign(entry.privateKey)
    update(data)
    sign()
}

Java

/*
 * Use a PrivateKey in the KeyStore to create a signature over
 * some data.
 */
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.Entry entry = ks.getEntry(alias, null);
if (!(entry instanceof PrivateKeyEntry)) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry");
    return null;
}
Signature s = Signature.getInstance("SHA256withECDSA");
s.initSign(((PrivateKeyEntry) entry).getPrivateKey());
s.update(data);
byte[] signature = s.sign();

类似地,请使用 verify(byte[]) 方法验证数据:

Kotlin

/*
 * Verify a signature previously made by a private key in the
 * KeyStore. This uses the X.509 certificate attached to the
 * private key in the KeyStore to validate a previously
 * generated signature.
 */
val ks = KeyStore.getInstance("AndroidKeyStore").apply {
    load(null)
}
val entry = ks.getEntry(alias, null) as? KeyStore.PrivateKeyEntry
if (entry == null) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry")
    return false
}
val valid: Boolean = Signature.getInstance("SHA256withECDSA").run {
    initVerify(entry.certificate)
    update(data)
    verify(signature)
}

Java

/*
 * Verify a signature previously made by a private key in the
 * KeyStore. This uses the X.509 certificate attached to the
 * private key in the KeyStore to validate a previously
 * generated signature.
 */
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.Entry entry = ks.getEntry(alias, null);
if (!(entry instanceof PrivateKeyEntry)) {
    Log.w(TAG, "Not an instance of a PrivateKeyEntry");
    return false;
}
Signature s = Signature.getInstance("SHA256withECDSA");
s.initVerify(((PrivateKeyEntry) entry).getCertificate());
s.update(data);
boolean valid = s.verify(signature);

要求进行用户身份验证才能使用密钥

生成密钥或将密钥导入到 AndroidKeyStore 时,您可以指定仅授权经过身份验证的用户使用密钥。用户使用安全锁定屏幕凭据(图案/PIN 码/密码、生物识别凭据)的子集进行身份验证。

这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥被未经身份验证的用户使用。

如果仅授权经过身份验证的用户使用密钥,则可调用l setUserAuthenticationParameters() 将密钥配置为以下列两种模式之一运行:

授权在一段时间内有效
在用户使用指定的凭据之一进行身份验证后,即授权其使用所有密钥。
在执行特定加密操作期间进行授权

涉及特定密钥的每个操作都必须由用户单独授权。

应用可以通过对 BiometricPrompt 的实例调用 authenticate() 来启动此流程。

创建每个密钥时,您可以选择支持安全系数高的生物识别凭据、锁屏凭据,或者两种凭据皆可支持。如需确定用户是否设置了应用密钥所依赖的凭据,请调用 canAuthenticate()。

如果密钥仅支持生物识别凭据,则每当添加新的已注册生物识别信息时,该密钥都会默认失效。您可以将密钥配置为在添加新的已注册生物识别信息时依然保持有效状态。为此,请将 false 传入 setInvalidatedByBiometricEnrollment()。

详细了解如何在应用中添加生物识别验证功能,包括如何显示生物识别验证对话框。

支持的算法

博客文章

请参阅博客文章在 ICS 中统一密钥库访问。