Does the Android dev ecosystem need yet another blog post about doing AES encryption and decryption? Probably not, but just this week I stumbled with a series of problems that caught me with my guard down, so I decided to share my experience in case it helps someone else.

Android开发者生态系统是否需要 一篇关于进行AES加密和解密的博客文章? 可能不是,但是就在本周,我偶然遇到了一系列问题,这些问题使我措手不及,因此我决定分享自己的经验,以防其他人受到帮助。

TL;DR: this is how you do it. If you need to specify the IV and AAD as inputs, here’s a workaround (and the tweak needed when generating the key ) — but be extremely careful with this approach!

TL; DR: 就是您的操作方式。 如果您需要将IV和AAD指定为输入, 这是 一种解决方法(以及 生成密钥 时需要进行的调整),但是使用这种方法时要格外小心!

问题 ( The problem )

Just this week I had to design a crypto API that would allow us to use a pre-existing crypto library together with the Android KeyStore API. The idea would be that, in devices that have a hardware crypto processor, our software would rely on it for encryption and decryption; whereas in the rest of the devices it would fall back to the existing library. That called for a common, abstract, API that would allow the rest of the software to not need to know which crypto environment was being used. Encryption and decryption would be done using AES in GCM mode.

就在本周,我不得不设计一种加密API,该加密API允许我们将现有的加密库与Android KeyStore API一起使用。 这样的想法是,在具有硬件加密处理器的设备中,我们的软件将依靠它进行加密和解密。 而在其余设备中,它将退回到现有库中。 这就需要一个通用的抽象API,该API允许其余的软件不需要知道所使用的加密环境。 加密和解密将在GCM模式下使用AES进行。

So far, so good. Right?

到目前为止,一切都很好。 对?

我在网上看到的 ( What I saw on the Internet )

So, as any other developer would do, I took to the Internet and searched for the Android KeyStore API definition and examples on how to use it. In a nutshell, the summary of what I found would be something like:

因此,就像任何其他开发人员一样,我进入了互联网并搜索了Android KeyStore API定义以及如何使用它的示例。 简而言之,我发现的摘要将是这样的:

const val TAG_LENGTH = 16
fun encrypt(key: SecretKey, message: ByteArray): Pair<ByteArray, ByteArray> {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)
    val iv = cipher.iv.copyOf()
    val ciphertext = cipher.doFinal(message)
    return Pair(iv, ciphertext)
fun decrypt(key: SecretKey, iv: ByteArray, message: ByteArray): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(TAG_LENGTH * 8, iv)
    cipher.init(Cipher.DECRYPT_MODE, key, spec)
    return cipher.doFinal(message)
     

Seems ok, doesn’t it?

似乎还可以,不是吗?

Well, the problem was that the input and output parameters did not match the ones our pre-existing library was using. According to our pre-existing crypto engine:

好吧,问题在于输入和输出参数与我们现有的库正在使用的参数不匹配。 根据我们之前存在的加密引擎:

  • The encryption routine should also take an iv and aad as input parameters.

    加密例程还应将ivaad作为输入参数。

  • The encryption routine should also return a tag as output.

    加密例程还应该返回一个tag作为输出。

  • The decryption routine should take aad and tag as input parameters.

    解密例程应使用aadtag作为输入参数。

Wasn’t AES GCM a standard? How were these differences possible?

AES GCM不是标准吗? 这些差异怎么可能?

一点理论 (A bit of theory)

Skip this section if you know what the iv, aad and tag are. If you need a refresher, you should probably read it.

如果您知道 iv aad tag 什么,请跳过本节 如果您需要复习,可能应该阅读它。

The first step was investigation. I wasn’t unfamiliar with these parameters, but to be honest I didn’t know exactly how they should be generated and used. So let’s take a step back and try to fully understand them:

第一步是调查。 我并不是不熟悉这些参数,但是老实说,我并不确切地知道应该如何生成和使用它们。 因此,让我们退后一步,尝试全面了解它们:

The iv (short for Initialization Vector) is a common parameter when using block ciphers. It is used to make sure that encrypting twice the same data with the same key results in different outputs, which is why it is very important that ivs are never reused.

iv ( 初始化向量的缩写)是使用分组密码时的常用参数。 它用于确保使用相同的密钥对相同的数据加密两次会导致不同的输出,这就是为什么永远不要重用iv非常重要的原因。

On the other hand, aad and tag are only used in Authenticated Encryption ciphers (sometimes also referred to as “AEAD”, short for “Authenticated Encryption with Associated Data”), such as AES in GCM mode:

另一方面, aadtag仅用于身份验证加密密码(有时也称为“ AEAD”,是“具有关联数据的身份验证加密”的缩写),例如GCM模式下的AES:

  • The tag is an output of the encryption routine, which is then provided to the decryption routine to verify that the ciphertext has not been tampered with (that is: to authenticate that the ciphertext had been generated with the same key being used to decrypt).

    tag是加密例程的输出,然后将其提供给解密例程,以验证密文是否未被篡改(即: 验证已使用与解密相同的密钥生成密文)。

  • aad stands for “Additional Authentication Data”, and is an input to the encryption and decryption routines which is used for computing the tag.

    aad代表“附加身份验证数据”,是加密和解密例程的输入,用于计算tag

解决方案 (The solution(s))

Unfortunately, there is no single solution to all the differences I explained above. Instead, there is a different thing to be done for each parameter:

不幸的是,对于我上面解释的所有差异,没有唯一的解决方案。 而是,对每个参数都要做另一件事:

返回tag (Returning the tag)

Let’s start with the easy one. After some looking around, I found the answer in the Java docs:

让我们从简单的一开始。 环顾四周后,我在Java文档中找到了答案:

If an AEAD mode such as GCM/CCM is being used, the authentication tag is appended in the case of encryption, or verified in the case of decryption.

如果使用的是AEAD模式(例如GCM / CCM),则在加密的情况下会附加身份验证标签,而在解密的情况下会验证身份标签。

In plain words: the Cipher.doFinal() method (in GCM mode) appends the tag at the end of the ciphertext when encrypting, and verifies it (reading it from the end of the ciphertext) when decrypting. Which means that I could now write something like that:

用简单的话来说: Cipher.doFinal()方法(在GCM模式下)在加密时将tag附加在密文的末尾,并在解密时对其进行验证(从密文的末尾读取)。 这意味着我现在可以这样写:

const val TAG_LENGTH = 16
class EncryptionOutput(val iv: ByteArray,
                       val tag: ByteArray,
                       val ciphertext: ByteArray)
fun encrypt(key: SecretKey, message: ByteArray): EncryptionOutput {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)
    val iv = cipher.iv.copyOf()
    val result = cipher.doFinal(message)
    val ciphertext = result.copyOfRange(0, result.size - TAG_LENGTH)
    val tag = result.copyOfRange(result.size - TAG_LENGTH, result.size)
    return EncryptionOutput(iv, tag, ciphertext)
fun decrypt(key: SecretKey, iv: ByteArray, tag: ByteArray, ciphertext: ByteArray): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(TAG_LENGTH * 8, iv)
    cipher.init(Cipher.DECRYPT_MODE, key, spec)
    return cipher.doFinal(ciphertext + tag)
     

那ada呢? (What about the aad?)

According to the theory, the aad should also be provided when encrypting and decrypting data, but it doesn’t appear at all in the code snippets above. The answer, again, lies in the Java docs, where we find the method Cipher.updateAAD(byte[] src).

根据该理论,在加密和解密数据时也应提供aad ,但在上面的代码段中根本没有出现aad。 答案还是在Java文档中 ,我们在其中找到了Cipher.updateAAD(byte[] src)

To be completely honest, I am not sure if I understand the description of the method. What I can confirm (because I tried it) is that it actually affects the aad used: if you try to change it in encryption or decryption, decryption will fail with an AEADBadTagException, which means that authentication failed.

老实说,我不确定是否理解该方法的描述。 我可以确认(因为我试过)是,它实际上影响了aad使用:如果你试图改变它在加密或解密,解密将失败并AEADBadTagException ,这意味着身份验证失败。

If we use the Cipher.updateAAD(byte[] src) method to define the aad, our code would now look like:

如果我们使用Cipher.updateAAD(byte[] src)方法来定义aad ,那么我们的代码将如下所示:

const val AAD_LENGTH = 16
const val TAG_LENGTH = 16
class EncryptionOutput(val iv: ByteArray,
                       val aad: ByteArray,
                       val tag: ByteArray,
                       val ciphertext: ByteArray)
fun encrypt(key: SecretKey, message: ByteArray): EncryptionOutput {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)
    val iv = cipher.iv.copyOf()
    val aad = SecureRandom().generateSeed(AAD_LENGTH)
    cipher.updateAAD(aad)
    val result = cipher.doFinal(message)
    val ciphertext = result.copyOfRange(0, result.size - TAG_LENGTH)
    val tag = result.copyOfRange(result.size - TAG_LENGTH, result.size)
    return EncryptionOutput(iv, aad, tag, ciphertext)
fun decrypt(key: SecretKey, iv: ByteArray, aad: ByteArray, tag: ByteArray, ciphertext: ByteArray): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(TAG_LENGTH * 8, iv)
    cipher.init(Cipher.DECRYPT_MODE, key, spec)
    cipher.updateAAD(aad)
    return cipher.doFinal(ciphertext + tag)
     

Notice that I’ve set aad to be an output of the encryption routine, rather than an input. There is one reason for doing it like that, which I’ll explain a bit later. The important thing here is that the code above shows the approach that I would recommend to anyone looking for a safe API to implement AES GCM encryption and decryption in Android.

注意,我将aad设置为加密例程的输出 ,而不是输入。 这样做是有原因的,我将在稍后解释。 这里重要的是, 上面的代码向所有在Android中寻求安全API来实现AES GCM加密和解密的人推荐了这种方法

四世 (The IV)

But we still have one more thing that does not fit with our other crypto library: the initialisation vector is supposed to be an input to the encryption method, and not an output.

但是我们还有另外一点不适合我们的其他加密库:初始化向量应该是加密方法的输入 ,而不是输出。

Looks like using the GCMParameterSpec class we could do it:

看起来像使用GCMParameterSpec类,我们可以做到:

const val TAG_LENGTH = 16
class EncryptionOutput(val tag: ByteArray,
                       val ciphertext: ByteArray)
fun encrypt(key: SecretKey, iv: ByteArray, aad: ByteArray, message: ByteArray): EncryptionOutput {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(TAG_LENGTH * 8, iv)
    cipher.init(Cipher.ENCRYPT_MODE, key, spec)
    cipher.updateAAD(aad)
    val result = cipher.doFinal(message)
    val ciphertext = result.copyOfRange(0, result.size - TAG_LENGTH)
    val tag = result.copyOfRange(result.size - TAG_LENGTH, result.size)
    return EncryptionOutput(tag, ciphertext)
fun decrypt(key: SecretKey, iv: ByteArray, aad: ByteArray, tag: ByteArray, ciphertext: ByteArray): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(TAG_LENGTH * 8, iv)
    cipher.init(Cipher.DECRYPT_MODE, key, spec)
    cipher.updateAAD(aad)
    return cipher.doFinal(ciphertext + tag)
     

Unfortunately, if we try to execute it we will end up with an exception: java.security.InvalidAlgorithmParameterException: Caller-provided IV not permitted — which is, I believe, quite self-explanatory: Android does not allow us to specify and IV. Period.

不幸的是,如果我们尝试执行它,则会导致一个异常: java.security.InvalidAlgorithmParameterException: Caller-provided IV not permitted我相信这是不言而喻的:Android不允许我们指定IV。 期。

But, why? Well, this is a behaviour introduced in API 23 and is meant to make sure that the caller is not reusing IVs, since this could break the security of the block cipher (as I explained before). The bottom line is: you should not reuse IVs with the same key, and in order to make sure of that Android is generating a random one internally.

但为什么? 好吧,这是API 23中引入的一种行为,旨在确保调用者不会重用IV,因为这可能会破坏分组密码的安全性(如我之前所述)。 最重要的是:您不应该重复使用具有相同密钥的IV,并且要确保Android在内部生成随机密钥。

For a similar reason I think it’s best to generate the aad inside the encryption routine: to avoid a caller that doesn’t know what he is doing providing fixed values. By generating it internally we can make sure that it is random.

出于类似的原因,我认为最好在加密例程中生成aad :避免调用者不知道他在做什么,提供固定值。 通过内部生成它,我们可以确保它是随机的。

But if you really need (like I needed) to provide the iv (and aad) as input parameters, there’s one thing you can do. When the AES key is being created, you can use the setRandomizedEncryptionRequired() method to explicitly ask Android to allow you to provide the iv as an input. The key generation method would then be:

但是,如果您真的需要(像我一样)提供iv (和aad )作为输入参数,则可以做一件事。 创建AES密钥后,您可以使用setRandomizedEncryptionRequired()方法明确要求Android允许您提供iv作为输入。 密钥生成方法将是:

fun generateAesKey(): SecretKey {
    val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    val kgps = KeyGenParameterSpec.Builder("my_aes_key", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        // This is required to be able to provide the IV ourselves
        .setRandomizedEncryptionRequired(false)
        .build()
    keyGenerator.init(kgps)
    return keyGenerator.generateKey()
     

Needless to say, do this only if you really know what you are doing! Especially, if you go down that road, make sure that your iv (and aad) are generated in a proper way and are random enough for your use case.

不用说, 只有当您真正知道自己在做什么时才这样做 ! 特别是,如果您沿着这条路走,请确保您的iv (和aad )以正确的方式生成,并且对于您的用例而言足够随机。

总结思想 (Closing thoughts)

I think it is fair to say that the Java and Android APIs don’t make it easy to implement an AES GCM that is interoperable with other crypto systems. But, why? Shouldn’t that be the main point anyway?

我认为可以说Java和Android API并不容易实现可与其他加密系统互操作的AES GCM。 但为什么? 那不是主要要点吗?

In case anybody is interested, here are my thoughts on why the designers of these APIs decided to implement them that way:

如果有人感兴趣,这是我对这些API的设计者为何决定以这种方式实现它们的想法:

  • Regarding the tag being included in the result of Cipher.doFinal(), I think it is clear that here the main point was not to break the JCE API. Cipher.doFinal() has returned a byte[] for years, regardless of the encryption primitive, so they decided for it to stay like that.

    关于Cipher.doFinal()结果中包含的tag ,我认为很明显,这里的重点不是破坏JCE API。 不管加密原语如何, Cipher.doFinal()多年来一直返回byte[] ,因此他们决定像这样保留它。

  • With respect to the aad, I believe it would be nicer if it was part of the GCMParameterSpec constructor, but I guess there is a reason (which remains unknown to me) for having it as a method in the Cipher class. What I can say is that the documentation is far from clear, and more importantly it should be more specific about which aad value is used when the caller does not specify it.

    关于aad ,我相信如果它是GCMParameterSpec构造函数的一部分会更好,但是我想有理由(对我而言仍然未知)将其作为Cipher类中的方法。 我可以说的是,文档还很不清楚,更重要的是,当调用方未指定使用哪个aad值时,应该更具体地说明。

  • And, finally, the IV: I like the approach of Android (and iOS, to be fair) to provide safe-defaults for their crypto APIs. For non-crypto experts the amount of choices provided by other APIs (such as JCE) might be daunting, so it is good to provide as default the choices that are most secure.

    最后,第四点:我喜欢Android(公平地说,是iOS)为其加密API提供安全默认值的方法。 对于非加密专家而言,其他API(例如JCE)提供的选择数量可能令人望而生畏,因此最好默认提供最安全的选择。

翻译自: https://levelup.gitconnected.com/doing-aes-gcm-in-android-adventures-in-the-field-72617401269d

gcm aes

我目前正在使用aes/gcm/nopadding执行密码操作。我的加密代码:fun encrypt(plainText: ByteArray, key: Key): ByteArray? {var resultText: ByteArray? = nulltry {val cipher = Cipher.getInstance(ALGORITHM)cipher.init(Cipher.ENCRYP... 一.什么是AES加密? 常见的加密主要分为两类:对称加密和非对称加密,AES加密就是对称加密的一种,即加密和解密使用相同的一把密钥。它的全称是Advanced Encryption Standard(高级加密标准),主要是用来取代DES加密算法,目前已经被全世界广泛采用,各大处理器厂商也在各自的CPU,集成了专门的AES指令集,从而在硬件层面提升了AES加解密的速度。 具体加密过程:十分钟读懂AES加密算法 二.AES加密基本构成 1.对称加密相关概念 明文P(plainText):未经加密的数据 AES是一种对称加密算法,它的相关概念在此不赘述。 GCM ( Galois/Counter Mode) 是对称加密的一种加密模式。 在介绍AES-GCM之前,我们先了解一些相关概念。 下文出现的符号: AES-GCM AES-GCM是基于AES-CTR模式改编的,不同于CTR的是GCM在对明文进行加密的时候还会产生tag(类似签名的东西),可以有效的抵御选择明文攻击,因为GCM首先会看tag是否合法,然后才决定是否调用decrypt oracle 进行解密。加密的流程在这篇paper讲的很清楚,我把其的一部分拿了出来(只有两个明文块的情况) 具体的符号定义如下: 其:H=Enck(01... AES(Advanced Encryption Standard)即高级加密标准,由美国国家标准和技术协会(NIST)于2000年公布,它是一种对称加密算法。关于AES的更多介绍可以参考:https://blog.csdn.net/fengbingchun/article/details/100139524 AESGCM(Galois/Counter Mode)模式本质上是AES的CTR模式(计数器模式)加上GMAC(Galois Message Authentication Code, 伽罗华消息认证 GCM是一种有大吞吐能力的加密认证模式。其主要适用了CRT模式和类似CBC模式的GHASH模式。CRT模式基本上没有大多变化,GHASH则是利用有限域上的乘法进行HASH,此运算可以通常预先计算和查表优化加速。 GCM两个基本模块 GHASH和GCTR。加解密和认证验证过程都离不开这两个模块。 NIST SP 800-38D The Galois Counter Mod... Android 导入WrappedKey 本文给出WrappedKey导入AES密钥的方案。本文参考代码 CTS的导入测试。 WrappedKey 需要的格式: KeyDescription ::= SEQUENCE( keyFormat INTEGER, # Values from KeyFormat enum. keyParams AuthorizationList, SecureKeyWrap