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

2022-09-04 20:50:47

我只是在熟悉 Android Keystore API。我发现以下功能可用:

  • 至少在某些设备上,Android密钥库是硬件支持的,这意味着加密操作在安全环境(TEE)中运行。
  • 当密钥库得到硬件支持时,可以将私有 RSA 密钥以及在密钥库中创建的秘密对称密钥配置为从不离开密钥库,并且即使具有 root 访问权限也无法读出原始密钥。

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

  1. 生成公钥/私钥对,其中私钥永远不会离开密钥库
  2. 将此对的公钥上传到服务器
  3. 在服务器上:创建一个随机对称的 AES 密钥,并使用用户上传的公共 RSA 密钥对其进行加密
  4. 在设备上:下载此加密的 AES 密钥
  5. 将其导入硬件支持的密钥库,以便使用该对的私钥在其中对其进行解密,并存储在新的别名下
  6. 使用此新密钥别名执行对称加密和解密

1-4应该是可能的,我现在缺少的环节是第5点。在此列表中。有人可以帮助我并告诉我这是否可能和/或指向我正确的API参考吗?

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

但在我看来,密钥的解包似乎发生在正常环境中,解密的AES密钥将在应用程序中可用,这不符合我的安全要求。

更新:

我使用链接创建了一个小型测试项目,下面是两个代码片段:SecretKeyWrapper

第一个执行以下操作:

  1. 创建一个随机的AES密钥(不在密钥库中,这是稍后在服务器上发生的情况)。显然,可以从生成的对象中检索原始密钥,这不是问题,因为服务器可以知道密钥。SecretKey
  2. 使用在客户端的 Android 密钥库中创建的 RSA 公钥加密/包装密钥(这也会发生在服务器上)。
  3. 使用 RSA 私钥再次解密密钥(这将在客户端上发生,并且在示例中的 TEE 中实际发生)。

片段 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] 的未包装密钥存储在密钥库中,具有新的别名,而不返回原始密钥。当然,我可以很容易地将对象导入到这里的密钥库中,但问题是,此时可以从对象中检索原始密钥,并使用导致安全漏洞的语句[4]。很明显,解包/解密已经发生在密钥库/TEE 中,因为用于解密的私有 RSA 密钥存在于密钥库中,无法检索。SecretKey

如果我将此与在密钥库中创建随机秘密AES密钥的情况进行比较,我注意到返回了不同的类型(实现接口)。在上面的示例中,类型为 ,而对于从 Android 密钥库返回的密钥(请参阅下面的代码段 2),使用“不透明”类型,其中该方法始终返回 null。在下面的示例中,的类型为 。SecretKeySecretKeySpecgetEncoded()keyAesKeystoreAndroidKeyStoreSecretKey

片段 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密钥库,而不会泄露应用程序的密钥?

更新 2:

@Robert在下面的答案中绝对有效,即无论在TEE中还是在Rich OS(应用程序)中进行解包实际上并不重要,因为应用程序(或篡改的版本)总是可以在以后(在拦截包装的密钥后)只是“使用”密钥库中的私有RSA密钥来解开AES密钥(根本不需要访问原始私钥)。

这是另一个想法:我发现可以为Android密钥库中的密钥设置密钥保护参数(请参阅此处)。

的链接实现不使用此类保护参数。按如下方式更改方法并添加和属性后,一切仍然有效。SecretKeyWrappergenerateKeyPairPURPOSE_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();
}

我现在可以通过删除该属性来保护RSA密钥,使其不能用于解密。正如预期的那样,该方法停止工作并引发异常。PURPOSE_DECRYPTCipher.unwrapIncompatible purpose

因此,我需要的是一个保护属性,其中普通解密功能被阻止,但允许我正在寻找的这种“安全导入功能”。像“”这样的东西显然不存在。PURPOSE_IMPORT


答案 1

您现在正在寻找的内容已经存在,截至API级别28(Android Pie)。要使用,您需要:

  • 创建包装密钥对,即具有专用PURPOSE_WRAP_KEY的 RSA 密钥对。您还应该为公钥生成证明,以验证私钥是否是安全硬件中的密钥库密钥。
  • 将公钥(和证明)从应用发送到将提供包装对称密钥的服务器。
  • 在服务器上,需要包装对称密钥。这不仅仅涉及加密它,因为包装器不仅需要包含密钥材料,还需要包含定义如何使用密钥的授权列表。这是通过将密钥和授权信息打包到 ASN.1 DER 编码结构中,根据此处记录的架构来完成的。CTS 测试中有一些示例包装代码。请注意,如果这种格式看起来过于复杂(例如,可选的“屏蔽密钥”),那是因为在将来的Android版本中将有相应的安全导出功能,并且其用例需要额外的复杂性。安全导出函数没有进入Q,但可能会进入R。
  • 将包装的密钥发送到应用程序,该应用程序必须创建一个 WrappedKeyEntry 并使用 Keystore.setEntry() 来存储它。

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

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


答案 2

您想要实现的目标仅通过简单地使用AndroidKeystore是不可能的。您需要的是在 TEE 中运行的自定义代码。

原因很简单:当您使用存储在AndroidKeystore中的非对称密钥对设置应用程序并且收到包装的AES密钥时,无论打包是在AndroidKeystore内部还是外部进行,都无关紧要:

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

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