将加密的 AES 密钥导入 Android 密钥库并将其存储在新别名下

Import encrypted AES key into Android Keystore and store it under new alias

我只是在熟悉 Android 密钥库 API。 我发现可以使用以下功能:

  • At least on some devices the Android Keystore is hardware backed, meaning that crypto operations run in a secure environment (TEE).
  • When the keystore is hardware backed, private RSA keys as well as secret symmetric keys that have been created within the Keystore can be configured to never leave the Keystore and the raw keys cannot be read out even with root access.

我现在想知道以下是否可行:

  1. Generate a Public/Private key pair where the private key never leaves the Keystore
  2. Upload the public key of this pair to a server
  3. On the server: create a random symmetric AES key and encrypt it with the public RSA key uploaded by the user
  4. On the device: Download this encrypted AES key
  5. Import it into the hardware backed Keystore such that it is decrypted in there with the private key of the pair and stored under a new alias
  6. Use this new key alias to perform symmetric encryption and decryption

1-4 应该是可能的,我现在缺少的 link 是此列表中的第 5 点。有人可以帮助我并告诉我这是否可行 and/or 指出正确的 API 参考资料吗?

我发现了这个: https://android.googlesource.com/platform/development/+/master/samples/Vault/src/com/example/android/vault/SecretKeyWrapper.java

但在我看来,密钥的解包似乎发生在正常环境中,并且解密后的 AES 密钥将在 App 中可用,这不能满足我的安全要求。

更新:

我使用 linked SecretKeyWrapper 创建了一个小型测试项目,这里有两个代码片段:

第一个执行以下操作:

  1. Create a random AES key (not within in the keystore, this is what would happen on a server later). Obviously the raw key can be retrieved from the generated SecretKey object what isn't a problem since the server can know the key.
  2. Encrypt/wrap the key with a RSA public key that was created in the client's Android Keystore (this would also happen on a server).
  3. Decrypt the key again with the RSA private key (this would happen on the client and actually happens within the TEE in the example).

片段 1:

SecretKeyWrapper secretKeyWrapper = new SecretKeyWrapper(this,"testKeyRsa");

// Generate a random AES key (not in the keystore) [1]
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128);
SecretKey secretKeyGenerated = keyGen.generateKey();
byte[] secretKeyGeneratedRaw = secretKeyGenerated.getEncoded();

// wrap this key with the RSA key from the keystore [2]
byte[] wrappedKey = secretKeyWrapper.wrap(secretKeyGenerated);

// unwrap it again with the RSA key from the keystore [3]
SecretKey unwrappedKey = secretKeyWrapper.unwrap(wrappedKey);

// the raw key can be read again [4]
byte[] unwrappedKeyRaw = secretKeyGenerated.getEncoded();

我想要实现的是 [3] 中的解包密钥使用新别名存储在密钥库中,而不返回原始密钥。当然,我可以轻松地将 SecretKey 对象导入此处的密钥库,但问题是,此时可以使用语句 [4] what 导致安全漏洞从对象中检索原始密钥。很明显,unwrapping/decryption 已经发生在 Keystore/TEE 中,因为用于解密的 RSA 私钥存在于密钥库中,无法检索。

如果我将此与在密钥库中创建随机秘密 AES 密钥的情况进行比较,我注意到返回了不同的类型(实现 SecretKey 接口)。在上面的示例中,类型是 SecretKeySpec,而对于从 Android 密钥库返回的密钥(参见下面的片段 2),"opaque" 类型用于 getEncoded()方法总是 returns 空。在下面的例子中,keyAesKeystore 的类型是 AndroidKeyStoreSecretKey.

片段 2:

// create a new AES key in the keystore
KeyGenerator keyGenAndroid =  KeyGenerator.getInstance("AES","AndroidKeyStore");
keyGenAndroid.init(
    new KeyGenParameterSpec.Builder("testKeyAes",
         KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)                             
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)                             
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .build());
        SecretKey keyAesKeystore = keyGenAndroid.generateKey();

// this returns null
byte[] keyAesKeystoreRaw = keyAesKeystore.getEncoded();

因此,换句话说这个问题:是否有可能以某种方式安全地将 RSA 包装的 AES 密钥导入 Android 密钥库而不向应用程序泄露密钥?

更新二:

@Robert 在下面的回答中绝对有效,如果解包发生在 TEE 或 Rich OS(应用程序)中实际上并不重要,因为应用程序(或篡改版本)可能总是稍后(在拦截包装密钥之后)只是 "use" 来自密钥库的私有 RSA 密钥来解包 AES 密钥(根本不需要访问原始私钥)。

这是另一个想法: 我发现可以在 Android 密钥库中为密钥设置密钥保护参数(参见 here)。

SecretKeyWrapper 的 linked 实现不使用此类保护参数。如下更改 generateKeyPair 方法并添加 PURPOSE_DECRYPTPURPOSE_ENCRYPT 属性后,一切仍然有效。

private static void generateKeyPair(Context context, String alias)
        throws GeneralSecurityException {
    final Calendar start = new GregorianCalendar();
    final Calendar end = new GregorianCalendar();
    end.add(Calendar.YEAR, 100);
    final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT)
            .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)

            .build();
    final KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
    gen.initialize(keyGenParameterSpec);
    gen.generateKeyPair();
}

我现在可以通过删除 PURPOSE_DECRYPT 属性 来保护 RSA 密钥,使其不能用于解密。正如预期的那样,Cipher.unwrap 方法停止工作并抛出一个 Incompatible purpose 异常。

所以我需要的是一种保护 属性,其中普通解密功能被阻止,但它允许我正在寻找的 "secure import functionality"。类似“PURPOSE_IMPORT”的东西显然不存在。

仅使用 AndroidKeystore 无法实现您想要的效果。您需要的是在 TEE 中运行的自定义代码。

原因很简单:当您使用存储在 AndroidKeystore 中的非对称密钥对设置您的应用程序并且您收到包装的 AES 密钥时,解包发生在 AndroidKeystore 内部还是外部都没有关系安卓密钥库:

存储在AndroidKeystore中的应用程序的所有密钥都可以在正常环境下被应用程序使用。是设计使然,否则您将无法使用它们。

因此,如果应用程序可以使用非对称密钥对应用程序始终能够解包收到的已包装 AES 密钥(在正常环境中使用代码)。因此,展开的位置没有任何区别。您不能保证有人在应用程序收到包装后的 AES 密钥时复制了它,然后使用 AndroidKeystore 中的非对称密钥将其解包。

API 28 级(Android 派),您正在寻找的东西现已存在。要使用你,你需要:

  • 创建一个包装密钥对,一个有目的的 RSA 密钥对 PURPOSE_WRAP_KEY。您还应该为 public 密钥生成证明,以验证私钥是安全硬件中的密钥库密钥。
  • 将 public 密钥(和证明)从您的应用程序发送到将提供包装对称密钥的服务器。
  • 在服务器上,您需要包装对称密钥。这不仅仅涉及加密,因为包装器不仅需要包含密钥 material,还需要包含定义密钥使用方式的授权列表。这是通过根据模式 documented here. There's some sample wrapping code in the CTS test 将密钥和授权信息打包在 ASN.1 DER 编码结构中来完成的。请注意,如果此格式看起来过于复杂(例如可选的 "masking key"),那是因为在未来的 Android 版本中将有相应的安全导出功能,并且其用例需要额外的复杂性。安全导出功能没有进入 Q,但可能会进入 R。
  • 将打包的密钥发送到应用程序,应用程序必须创建一个 WrappedKeyEntry and use Keystore.setEntry() 来存储它。

这应该适用于 API 级别 28 的任何设备。但是,如果设备的 Keymaster 版本小于 4(请参阅证明证书以了解存在的 Keymaster 版本),然后解包操作将 return 包装密钥 material 到 Android 用户空间。 Keymaster 版本 4(或更高版本)会将未包装的 material 保存在安全硬件中,但由于较低版本不支持包装密钥功能,因此必须对其进行模拟。

如果您的 Keymaster 版本较低,会发生以下情况:当您创建 PURPOSE_WRAP_KEY 密钥对时,安全硬件实际请求的是 PURPOSE_DECRYPT 密钥对。然后当你导入时,密钥库守护进程使用这个 PURPOSE_DECRYPT 私钥从包装器中解密秘密,然后它将秘密导入安全硬件并擦除保存它的用户空间内存。因此,密钥 material 在密钥库守护程序的内存中存在几分之一毫秒。同样,如果设备具有 Keymaster 版本 4+,它只会在安全硬件中展开并且永远不会离开。