Android 密钥库系统

Android 密钥库系统允许您将加密密钥存储在容器中,从而更难从设备中提取它们。密钥一旦进入密钥库,您就可以将其用于加密操作,而密钥材料将保持不可导出状态。此外,密钥库系统允许您限制密钥的使用时间和方式,例如要求用户身份验证才能使用密钥,或限制密钥只能在某些加密模式下使用。有关详细信息,请参阅安全功能部分。

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

安全功能

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

提取预防

Android 密钥库密钥的密钥材料通过两种安全措施保护,防止提取。

  • 密钥材料永远不会进入应用进程。当应用使用 Android 密钥库密钥执行加密操作时,在后台,明文、密文和要签名或验证的消息会被馈送到一个执行加密操作的系统进程。如果应用的进程被破坏,攻击者可能能够使用应用的密钥,但无法提取其密钥材料(例如,在 Android 设备外部使用)。
  • 密钥材料可以绑定到 Android 设备的安全硬件,例如可信执行环境 (TEE)或安全元件 (SE)。当此功能为密钥启用时,其密钥材料永远不会暴露在安全硬件之外。如果 Android 操作系统被破坏,或者攻击者可以读取设备的内部存储器,攻击者可能能够使用 Android 设备上的任何应用的 Android 密钥库密钥,但无法从设备中提取它们。此功能仅在设备的安全硬件支持密钥被授权使用的特定密钥算法、块模式、填充方案和摘要组合时才启用。

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

    • 如果您的应用的目标是 Android 10(API 级别 29)或更高版本,请检查getSecurityLevel()的返回值。返回值与KeyProperties.SecurityLevelEnum.TRUSTED_ENVIRONMENTKeyProperties.SecurityLevelEnum.STRONGBOX匹配,表明密钥驻留在安全硬件中。
    • 如果您的应用面向 Android 9(API 级别 28)或更低版本,请检查 KeyInfo.isInsideSecurityHardware() 的布尔返回值。

硬件安全模块

运行 Android 9(API 级别 28)或更高版本的受支持设备可能具有 StrongBox Keymaster,它是在类似硬件安全模块的安全元件中驻留的 Keymaster 或 Keymint HAL 的实现。虽然硬件安全模块可以指代密钥存储的许多不同实现方式(例如 TEE),在这些实现方式中,Linux 内核泄露无法泄露密钥,但 StrongBox 明确指的是嵌入式安全元件 (eSE) 或片上安全处理单元 (iSE) 等设备。

该模块包含以下内容

  • 它自己的 CPU
  • 安全存储
  • 真正的随机数生成器
  • 其他机制来抵御软件包篡改和未经授权的应用侧载
  • 安全计时器
  • 重启通知引脚(或等效引脚),例如通用输入/输出 (GPIO)

为了支持低功耗 StrongBox 实现,支持一部分算法和密钥大小

  • RSA 2048
  • AES 128 和 256
  • ECDSA、ECDH P-256
  • HMAC-SHA256(支持 8 字节到 64 字节(含)之间的密钥大小)
  • 三重 DES
  • 扩展长度 APDU
  • 密钥证明
  • 支持修订案 H 进行升级

在使用 KeyStore 类生成或导入密钥时,您可以通过向 setIsStrongBoxBacked() 方法传递 true 来表明您希望将密钥存储在 StrongBox Keymaster 中。

虽然 StrongBox 比 TEE 速度稍慢,资源限制也更大(意味着它支持更少的并发操作),但 StrongBox 提供了更好的安全性保障,可以抵御物理攻击和侧信道攻击。如果您想将更高的安全性保障置于应用资源效率之上,建议您在支持的设备上使用 StrongBox。在不支持 StrongBox 的情况下,您的应用始终可以回退到 TEE 来存储密钥材料。

密钥使用授权

为了避免在 Android 设备上未经授权使用密钥,Android Keystore 允许应用在生成或导入密钥时指定其密钥的授权用途。一旦密钥生成或导入,其授权就无法更改。Android Keystore 在每次使用密钥时都会强制执行授权。这是一项高级安全功能,通常仅在您的需求是应用进程在密钥生成/导入后(但在之前或期间除外)的泄露不会导致密钥被未经授权使用时才有用。

支持的密钥使用授权分为以下类别

  • 加密:密钥只能与授权的密钥算法、操作或目的(加密、解密、签名、验证)、填充方案、块模式或摘要一起使用。
  • 时间有效期:密钥仅在定义的时间段内授权使用。
  • 用户身份验证:密钥仅在用户最近被身份验证后才能使用。请参阅 要求用户身份验证才能使用密钥

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

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

在密钥链和 Android Keystore 提供程序之间进行选择

当您需要系统范围内的凭据时,请使用 KeyChain API。当应用通过 KeyChain API 请求使用任何凭据时,用户可以通过系统提供的 UI 选择应用可以访问的已安装凭据。这允许多个应用在用户同意的情况下使用同一组凭据。

使用 Android Keystore 提供程序允许单个应用存储其自己的凭据,只有该应用可以访问这些凭据。这为应用提供了一种管理凭据的方法,只有它们自己可以使用这些凭据,同时提供与 KeyChain API 为系统范围内的凭据提供的相同安全优势。此方法不需要用户选择凭据。

使用 Android Keystore 提供程序

要使用此功能,您需要使用标准的 KeyStoreKeyPairGeneratorKeyGenerator 类,以及 Android 4.3(API 级别 18)中引入的 AndroidKeyStore 提供程序。

AndroidKeyStore 注册为 KeyStore 类型,供与 KeyStore.getInstance(type) 方法一起使用,并注册为提供程序,供与 KeyPairGenerator.getInstance(algorithm, provider)KeyGenerator.getInstance(algorithm, provider) 方法一起使用。

由于加密操作可能很耗时,应用应避免在其主线程上使用 AndroidKeyStore,以确保应用的 UI 保持响应。(StrictMode 可以帮助您找到不符合此要求的地方。)

生成新的私钥或密钥

要生成包含 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 对象传递到 setEntry() 的重载方法中,该方法接受 Keystore.Entry 对象。

处理密钥库条目

您可以通过所有标准的 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/密码、生物识别凭据)的子集进行身份验证。

这是一项高级安全功能,通常仅在您的需求是应用进程在密钥生成/导入后(但在之前或期间除外)的泄露不会绕过用户身份验证才能使用密钥的要求时才有用。

当密钥仅在用户经过身份验证后才能授权使用时,您可以调用 setUserAuthenticationParameters() 来将其配置为以下模式之一

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

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

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

对于您创建的每个密钥,可以选择支持 强生物识别凭据锁屏凭据 或两种类型的凭据。要确定用户是否已设置应用密钥所依赖的凭据,请调用 canAuthenticate()

如果密钥仅支持生物识别凭据,则默认情况下,每当添加新的生物识别注册信息时,密钥就会失效。您可以配置密钥,使其在添加新的生物识别注册信息时保持有效。为此,请将 false 传递到 setInvalidatedByBiometricEnrollment()

了解有关如何将生物识别身份验证功能添加到应用程序的更多信息,包括如何 显示生物识别身份验证对话框

支持的算法

博文

查看博文 ICS 中统一密钥存储访问