Search code examples
javaamazon-web-servicesencryptionamazon-kms

Why does decryption function of Java SDK of AWS KMS does not require an encryption context?


From the description of AWS

When an encryption context is provided in an encryption request, it is cryptographically bound to the ciphertext such that the same encryption context is required to decrypt (or decrypt and re-encrypt) the data. If the encryption context provided in the decryption request is not an exact, case-sensitive match, the decrypt request fails. Only the order of the encryption context pairs can vary.

However, in the example code of JAVA SDK, it does not specify encryption context when decrypting.

crypto.decryptString(prov, ciphertext);

These two posts sound a bit contradictory to me since I think the decryption user needs to provide the encryption context on his own. I checked the source code of AwsCrypto.decryptString(final CryptoMaterialsManager provider, final String ciphertext)) in the sdk and it seems that the encryption context is also contained in the cipher text.

May I know why it is set up this way ?


Solution

  • After some research, I found that there are at least two ways to perform encryption and decryption. Just post them here if anyone is interested. It is written in kotlin.

    class AwsEncryptionSDKWrapper(private val keyIdArn: String, region: String) {
        private val crypto = AwsCrypto()
        private val prov: KmsMasterKeyProvider = KmsMasterKeyProvider.builder().withDefaultRegion(region).withKeysForEncryption(keyIdArn).build()
    
        fun encrypt(raw: String, encryptionContext: Map<String, String>): String {
            return crypto.encryptString(prov, raw, encryptionContext).result
        }
    
        fun decrypt(cipherText: String, encryptionContext: Map<String, String>): String {
            val decryptedResponse = crypto.decryptString(prov, cipherText)
            if (decryptedResponse.masterKeyIds[0] != keyIdArn) {
                throw IllegalStateException("Wrong key ID!")
            }
    
            encryptionContext.entries.forEach { (key, value) ->
                if (value != decryptedResponse.encryptionContext[key]) {
                    throw IllegalStateException("Wrong Encryption Context!")
                }
            }
            return decryptedResponse.result
        }
    }
    
    class AwsKMSSDKWrapper(region: String) {
        private val client = AWSKMSClientBuilder.standard().withRegion(region).build()
    
        fun encrypt(keyIdArn: String, raw: String, encryptionContext: Map<String, String>): String {
            val plaintextBytes = raw.toByteArray(StandardCharsets.UTF_8)
    
            val encReq = EncryptRequest()
            encReq.keyId = keyIdArn
            encReq.plaintext = ByteBuffer.wrap(plaintextBytes)
            encReq.withEncryptionContext(encryptionContext)
            val cipherText = client.encrypt(encReq).ciphertextBlob
    
            return Base64.getEncoder().encodeToString(cipherText.array())
        }
    
        fun decrypt(base64CipherText: String, encryptionContext: Map<String, String>, keyIdArn: String): String {
            val req = DecryptRequest()
                .withCiphertextBlob(ByteBuffer.wrap(Base64.getDecoder().decode(base64CipherText)))
                .withEncryptionContext(encryptionContext)
    
            val resp = client.decrypt(req)
            if (resp.keyId == null || resp.keyId!!.contentEquals(keyIdArn))  throw IllegalStateException("keyid not match ! provided $keyIdArn, actual ${resp.keyId}");
    
            return resp.plaintext.array().toString(StandardCharsets.UTF_8)
        }
    }
    

    Special credit to @kdgregory for pointing out my confusion.