Decrypting Amazon Connect Encrypted Customer Data
Tags: java, aws, connect, crypto
Published: 2020-03-31 04:02:39

Amazon Connect is Amazon's telephony platform, allowing customers to create call flows that collect data from the caller, via speech recognition or DTMF input. Sometimes, sensitive information such as credit card numbers are collected. Connect provides a mechanism for encrypting sensitive data while it is held in the Connect service, but in order to process that data in a related Lambda function integration, it must be decrypted.

## Amazon's Guide and Reference Implementation
Amazon provide a reference implementation in their documentation of the feature here: https://docs.aws.amazon.com/connect/latest/adminguide/encrypt-data.html

This post is an addendum to that documentation.

## Overview
Amazon Connect uses public/private key encryption, using the AWS Encryption SDK to provide support across the range of languages supported by that SDK (Java, Javascript, Python and C). While you will need to create and upload the certificate and public key for Connect to use, the encryption algorithms are not configurable - it will always use:

* OAEP: Optimal Asymmetric Encryption Padding, describes the scheme of adding randomised padding to the message before encryption
* RSA encryption in ECB mode: This is a public/private key cipher, in a block cipher mode. Connect will encrypt the data with the public key.
* SHA-512: Uses the SHA-512 algorithm to create a hash of the message, so tampering of the message can be detected.
* MGF1: Mask Generation Function 1, part of the RSA/OAEP standard defined in in RFC 3447.
 
This means, obviously, that you have to support these algorithms to decrypt the data.

## Support in the AWS Encryption SDK

You might expect that the algorithms chosen by Amazon Connect would be supported by all the AWS Encryption SDKs. This is not the case - here's the summary:
* C - No, [does not support the SHA-512 hash length](https://github.com/aws/aws-encryption-sdk-c/blob/master/include/aws/cryptosdk/cipher.h#L42) for RSA/OAEP
* Java - Yes, AWS provides [sample code](https://docs.aws.amazon.com/connect/latest/adminguide/encrypt-data.html#sample-decryption)
* Javascript - Yes, the [AWS blog post](https://aws.amazon.com/blogs/contact-center/creating-a-secure-ivr-solution-with-amazon-connect/) for this feature has a javascript implementation in the linked CloudFormation stack.
* Python - Seems like it should with the [RSA_OAEP_SHA512_MGF1](https://github.com/aws/aws-encryption-sdk-python/blob/master/src/aws_encryption_sdk/identifiers.py#L281) wrapping algorithm.

## Sample Java decryption code
Here I've taken the AWS Java decryption sample, and reworked it slightly.

```java
// DecryptSample.java
import com.amazonaws.encryptionsdk.AwsCrypto;
import com.amazonaws.encryptionsdk.CryptoResult;
import com.amazonaws.encryptionsdk.jce.JceMasterKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.util.*;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.security.*;
import java.security.interfaces.*;
import java.security.spec.*;
 
public class DecryptionSample {
    private static final String PROVIDER = "AmazonConnect";
    private static final String WRAPPING_ALGORITHM = "RSA/ECB/OAEPWithSHA-512AndMGF1Padding";
    public static void main(String[] args) throws IOException, GeneralSecurityException {
        String keyFile = args[0]; // path to PEM encoded private key to use for decryption
        String keyId = args[1]; // this is the id used for key in your contact flow
        String cypherText = args[2]; // the Base64 encoded cypher text
        decrypt(keyFile, keyId, cypherText);
    }

    public static void decrypt(String privateKeyFile, String keyId, String cypherText) throws IOException, GeneralSecurityException {
        Security.addProvider(new BouncyCastleProvider());
        byte[] cypherBytes = Base64.getDecoder().decode(cypherText);
        String privateKeyPem = new String(Files.readAllBytes(Paths.get(privateKeyFile)), Charset.forName("UTF-8"));
        RSAPrivateKey privateKey =  getPrivateKey(privateKeyPem); 
        AwsCrypto awsCrypto = new AwsCrypto();
        JceMasterKey decMasterKey = JceMasterKey.getInstance(null,privateKey, PROVIDER, keyId, WRAPPING_ALGORITHM);
        CryptoResult<byte[], JceMasterKey> result = awsCrypto.decryptData(decMasterKey, cypherBytes);
        String plainText = new String(result.getResult());
        System.out.format("Decrypted: %s\n", plainText);
    }

    public static RSAPrivateKey getPrivateKey(String privateKeyPem) throws IOException, GeneralSecurityException {
        String privateKeyBase64 = privateKeyPem.replaceAll("-----.*-----\n","").replaceAll("\n", "");
        byte[] decoded = Base64.getDecoder().decode(privateKeyBase64);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
        RSAPrivateKey privKey = (RSAPrivateKey) kf.generatePrivate(keySpec);
        return privKey;
    }
}
```

## Important points

```java
private static final String PROVIDER = "AmazonConnect";
String keyId = args[1]; // this is the id used for key in your contact flow
JceMasterKey decMasterKey = JceMasterKey.getInstance(null,privateKey, PROVIDER, keyId, WRAPPING_ALGORITHM);
```
When decrypting the cyphertext, the AWS Encryption SDK requires you to use the same provider and key identifiers as used in the encryption when obtaining a key context. Amazon Connect will always use `AmazonConnect` as the provider value. The `keyId` is set by you when you create the Store Customer Input action in the call flow. In this sample code, we're passing the `keyId` in as a command line parameter.

## Getting this sample working
* Save the code as `DecryptSample.java`
* Get dependency jars
```
wget https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.9/commons-lang3-3.9.jar
wget https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk15to18/1.64/bcprov-jdk15to18-1.64.jar
wget https://repo1.maven.org/maven2/com/amazonaws/aws-encryption-sdk-java/1.6.1/aws-encryption-sdk-java-1.6.1.jar
```
* compile the code `javac -cp bcprov-jdk15to18-164.jar:aws-encryption-sdk-java-1.6.1.jar DecryptionSample.java`
* run it with `java -cp bcprov-jdk15to18-164.jar:aws-encryption-sdk-java-1.6.1.jar:commons-lang3-3.9.jar:. DecryptionSample [params]`

You'll need a certificate and public/private key pair. First create the certificate and private key:
```bash
openssl req -batch -x509 -sha256 -nodes -newkey rsa:4096 -keyout blog.connect.private.key -days 730 -out blog.connect.certificate.pem
```
Then create extract the public key:
```bash
openssl x509 -pubkey -noout -in blog.connect.certificate.pem > blog.connect.public.key
```

## Encryption
Here's the sample Java code to perform the encryption like Amazon Connect would.
```java
// EncryptionSample.java
import com.amazonaws.encryptionsdk.AwsCrypto;
import com.amazonaws.encryptionsdk.CryptoResult;
import com.amazonaws.encryptionsdk.jce.JceMasterKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.util.*;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.security.*;
import java.security.interfaces.*;
import java.security.spec.*;
 
public class EncryptionSample {
    private static final String PROVIDER = "AmazonConnect";
    private static final String WRAPPING_ALGORITHM = "RSA/ECB/OAEPWithSHA-512AndMGF1Padding";
    public static void main(String[] args) throws IOException, GeneralSecurityException {
        String keyFile = args[0]; // path to PEM encoded public key to use for encryption
        String keyId = args[1]; // this is the key id to identify the key used
        String plainText = args[2]; // this is the plain text to encypher
        encrypt(keyFile, keyId, plainText);
    }

    public static void encrypt(String publicKeyFile, String keyId, String plainText)  throws IOException, GeneralSecurityException {
        Security.addProvider(new BouncyCastleProvider());
        String publicKeyPem = new String(Files.readAllBytes(Paths.get(publicKeyFile)), Charset.forName("UTF-8"));
        RSAPublicKey publicKey =  getPublicKey(publicKeyPem);
        AwsCrypto awsCrypto = new AwsCrypto();
        JceMasterKey encMasterKey = JceMasterKey.getInstance(publicKey, null, PROVIDER, keyId, WRAPPING_ALGORITHM);
        byte[] plainBytes = plainText.getBytes();
        CryptoResult<byte[], JceMasterKey> result = awsCrypto.encryptData(encMasterKey, plainBytes);
        String b64Result = Base64.getEncoder().encodeToString(result.getResult());
        System.out.println(b64Result);
    }

    public static RSAPublicKey getPublicKey(String publicKeyPem) throws IOException, GeneralSecurityException {
        String publicKeyBase64 = publicKeyPem.replaceAll("-----.*-----\n","").replaceAll("\n", "");
        byte[] decoded = Base64.getDecoder().decode(publicKeyBase64);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
        RSAPublicKey pubKey = (RSAPublicKey) kf.generatePublic(keySpec);
        return pubKey;
    }
}
```
Compilation step is the same as for the code above: `javac -cp bcprov-jdk15to18-164.jar:aws-encryption-sdk-java-1.6.1.jar EncryptionSample.java`

## Putting it all together

Once you have the code compiled and have a public/private key pair, we can try it out:
```bash
# assign ciphertext output to a variable
CIPHERTEXT=$(java -cp bcprov-jdk15to18-164.jar:aws-encryption-sdk-java-1.6.1.jar:commons-lang3-3.9.jar:. EncryptionSample blog.connect.public.key my-key-id 4444333322221111)
# inspect $CIPHERTEXT with:
echo $CIPHERTEXT
# try decryption
java -cp bcprov-jdk15to18-164.jar:aws-encryption-sdk-java-1.6.1.jar:commons-lang3-3.9.jar:. DecryptionSample blog.connect.private.key my-key-id "$CIPHERTEXT"
# should result in:
# Decrypted: 4444333322221111
```

## Conclusion
This post examined the relationship between Amazon Connect's customer input encryption feature and the Encryption SDK used to implement it.

Rudimentary Java code to mimic the encryption functionality of Amazon Connect, and the companion code to decrypt it was presented.