From 2b9b458c0eb0092503792124b3ac241f5760819e Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Wed, 21 Aug 2024 21:42:43 -0700 Subject: [PATCH] chore: Add ECDH examples --- .../keyring/KmsEcdhKeyringExample.java | 591 ++++++++++++ .../keyring/RawEcdhKeyringExample.java | 877 ++++++++++++++++++ .../cryptography/examples/TestUtils.java | 4 + .../keyring/TestKmsEcdhKeyringExample.java | 47 + .../keyring/TestRawEcdhKeyringExample.java | 80 ++ 5 files changed, 1599 insertions(+) create mode 100644 Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/KmsEcdhKeyringExample.java create mode 100644 Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/RawEcdhKeyringExample.java create mode 100644 Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestKmsEcdhKeyringExample.java create mode 100644 Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestRawEcdhKeyringExample.java diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/KmsEcdhKeyringExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/KmsEcdhKeyringExample.java new file mode 100644 index 000000000..4bc016197 --- /dev/null +++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/KmsEcdhKeyringExample.java @@ -0,0 +1,591 @@ +package software.amazon.cryptography.examples.keyring; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.Security; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.model.GetPublicKeyRequest; +import software.amazon.awssdk.services.kms.model.GetPublicKeyResponse; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig; +import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateAwsKmsEcdhKeyringInput; +import software.amazon.cryptography.materialproviders.model.DBEAlgorithmSuiteId; +import software.amazon.cryptography.materialproviders.model.KmsEcdhStaticConfigurations; +import software.amazon.cryptography.materialproviders.model.KmsPrivateKeyToStaticPublicKeyInput; +import software.amazon.cryptography.materialproviders.model.KmsPublicKeyDiscoveryInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.primitives.model.ECDHCurveSpec; + +public class KmsEcdhKeyringExample { + + public static String EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME = + "KmsEccKeyringKeyringExamplePublicKeySender.pem"; + public static String EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME = + "KmsEccKeyringKeyringExamplePublicKeyRecipient.pem"; + + public static void KmsEcdhKeyringGetItemPutItem( + String ddbTableName, + String eccKeyArn, + String eccPublicKeySenderFileName, + String eccPublicKeyRecipientFileName + ) { + // 1. Load UTF-8 encoded public key PEM files as DER encoded bytes. + // You may have an ECC public key file already defined. + // If not, the main method in this class will call + // the KMS ECC key, retrieve its public key, and store it + // in a PEM file for example use. + ByteBuffer publicKeyRecipientByteBuffer = loadPublicKeyBytes( + eccPublicKeyRecipientFileName + ); + ByteBuffer publicKeySenderByteBuffer = loadPublicKeyBytes( + eccPublicKeySenderFileName + ); + + // 2. Create a KMS ECDH keyring. + // This keyring takes in: + // - kmsClient + // - kmsKeyId: Must be an ARN representing a KMS ECC key meant for KeyAgreement + // - curveSpec: The curve name where the public keys lie + // - senderPublicKey: A ByteBuffer of a UTF-8 encoded public + // key for the key passed into kmsKeyId in DER format + // - recipientPublicKey: A ByteBuffer of a UTF-8 encoded public key + // for the recipient public key. + final MaterialProviders matProv = MaterialProviders + .builder() + .MaterialProvidersConfig(MaterialProvidersConfig.builder().build()) + .build(); + final CreateAwsKmsEcdhKeyringInput createAwsKmsEcdhKeyringInput = + CreateAwsKmsEcdhKeyringInput + .builder() + .kmsClient(KmsClient.create()) + .curveSpec(ECDHCurveSpec.ECC_NIST_P256) + .KeyAgreementScheme( + KmsEcdhStaticConfigurations + .builder() + .KmsPrivateKeyToStaticPublicKey( + KmsPrivateKeyToStaticPublicKeyInput + .builder() + .senderKmsIdentifier(eccKeyArn) + .senderPublicKey(publicKeySenderByteBuffer) + .recipientPublicKey(publicKeyRecipientByteBuffer) + .build() + ) + .build() + ) + .build(); + IKeyring kmsEcdhKeyring = matProv.CreateAwsKmsEcdhKeyring( + createAwsKmsEcdhKeyringInput + ); + + PutGetItemWithKeyring(kmsEcdhKeyring, ddbTableName); + } + + public static void KmsEcdhDiscoveryGetItem( + String ddbTableName, + String eccRecipientKeyArn + ) { + // 1. Create a KMS ECDH keyring. + // This keyring takes in: + // - kmsClient + // - recipientKmsIdentifier: Must be an ARN representing a KMS ECC key meant for KeyAgreement + // - curveSpec: The curve name where the public keys lie + final MaterialProviders matProv = MaterialProviders + .builder() + .MaterialProvidersConfig(MaterialProvidersConfig.builder().build()) + .build(); + final CreateAwsKmsEcdhKeyringInput createAwsKmsEcdhKeyringInput = + CreateAwsKmsEcdhKeyringInput + .builder() + .kmsClient(KmsClient.create()) + .curveSpec(ECDHCurveSpec.ECC_NIST_P256) + .KeyAgreementScheme( + KmsEcdhStaticConfigurations + .builder() + .KmsPublicKeyDiscovery( + KmsPublicKeyDiscoveryInput + .builder() + .recipientKmsIdentifier(eccRecipientKeyArn) + .build() + ) + .build() + ) + .build(); + IKeyring kmsEcdhKeyring = matProv.CreateAwsKmsEcdhKeyring( + createAwsKmsEcdhKeyringInput + ); + + GetItemWithKeyring(kmsEcdhKeyring, ddbTableName); + } + + private static void GetItemWithKeyring( + IKeyring kmsEcdhKeyring, + String ddbTableName + ) { + // 2. Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + final Map attributeActions = new HashMap<>(); + attributeActions.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY + attributeActions.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY + attributeActions.put("sensitive_data", CryptoAction.ENCRYPT_AND_SIGN); + + // 3. Configure which attributes we expect to be included in the signature + // when reading items. There are two options for configuring this: + // + // - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + // When defining your DynamoDb schema and deciding on attribute names, + // choose a distinguishing prefix (such as ":") for all attributes that + // you do not want to include in the signature. + // This has two main benefits: + // - It is easier to reason about the security and authenticity of data within your item + // when all unauthenticated data is easily distinguishable by their attribute name. + // - If you need to add new unauthenticated attributes in the future, + // you can easily make the corresponding update to your `attributeActions` + // and immediately start writing to that new attribute, without + // any other configuration update needed. + // Once you configure this field, it is not safe to update it. + // + // - Configure `allowedUnsignedAttributes`: You may also explicitly list + // a set of attributes that should be considered unauthenticated when encountered + // on read. Be careful if you use this configuration. Do not remove an attribute + // name from this configuration, even if you are no longer writing with that attribute, + // as old items may still include this attribute, and our configuration needs to know + // to continue to exclude this attribute from the signature scope. + // If you add new attribute names to this field, you must first deploy the update to this + // field to all readers in your host fleet before deploying the update to start writing + // with that new attribute. + // + // For this example, we currently authenticate all attributes. To make it easier to + // add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + final String unsignAttrPrefix = ":"; + + // 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + // Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + // that does not use asymmetric signing. + final Map tableConfigs = + new HashMap<>(); + final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig + .builder() + .logicalTableName(ddbTableName) + .partitionKeyName("partition_key") + .sortKeyName("sort_key") + .attributeActionsOnEncrypt(attributeActions) + .keyring(kmsEcdhKeyring) + .allowedUnsignedAttributePrefix(unsignAttrPrefix) + // Specify algorithmSuite without asymmetric signing here + // As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + // ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + .algorithmSuiteId( + DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384 + ) + .build(); + tableConfigs.put(ddbTableName, config); + + // 5. Create the DynamoDb Encryption Interceptor + DynamoDbEncryptionInterceptor encryptionInterceptor = + DynamoDbEncryptionInterceptor + .builder() + .config( + DynamoDbTablesEncryptionConfig + .builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ) + .build(); + + // 6. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + final DynamoDbClient ddbClient = DynamoDbClient + .builder() + .overrideConfiguration( + ClientOverrideConfiguration + .builder() + .addExecutionInterceptor(encryptionInterceptor) + .build() + ) + .build(); + + // 7. Get the item back from our table using the client. + // The client will decrypt the item client-side using the ECDH keyring + // and return the original item. + final HashMap keyToGet = new HashMap<>(); + keyToGet.put( + "partition_key", + AttributeValue.builder().s("awsKmsEcdhKeyringItem").build() + ); + keyToGet.put("sort_key", AttributeValue.builder().n("0").build()); + + final GetItemRequest getRequest = GetItemRequest + .builder() + .key(keyToGet) + .tableName(ddbTableName) + .build(); + + final GetItemResponse getResponse = ddbClient.getItem(getRequest); + + // Demonstrate that GetItem succeeded and returned the decrypted item + assert 200 == getResponse.sdkHttpResponse().statusCode(); + final Map returnedItem = getResponse.item(); + assert returnedItem + .get("sensitive_data") + .s() + .equals("encrypt and sign me!"); + } + + private static void PutGetItemWithKeyring( + IKeyring awsKmsEcdhKeyring, + String ddbTableName + ) { + // 3. Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + final Map attributeActions = new HashMap<>(); + attributeActions.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY + attributeActions.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY + attributeActions.put("sensitive_data", CryptoAction.ENCRYPT_AND_SIGN); + + // 4. Configure which attributes we expect to be included in the signature + // when reading items. There are two options for configuring this: + // + // - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + // When defining your DynamoDb schema and deciding on attribute names, + // choose a distinguishing prefix (such as ":") for all attributes that + // you do not want to include in the signature. + // This has two main benefits: + // - It is easier to reason about the security and authenticity of data within your item + // when all unauthenticated data is easily distinguishable by their attribute name. + // - If you need to add new unauthenticated attributes in the future, + // you can easily make the corresponding update to your `attributeActions` + // and immediately start writing to that new attribute, without + // any other configuration update needed. + // Once you configure this field, it is not safe to update it. + // + // - Configure `allowedUnsignedAttributes`: You may also explicitly list + // a set of attributes that should be considered unauthenticated when encountered + // on read. Be careful if you use this configuration. Do not remove an attribute + // name from this configuration, even if you are no longer writing with that attribute, + // as old items may still include this attribute, and our configuration needs to know + // to continue to exclude this attribute from the signature scope. + // If you add new attribute names to this field, you must first deploy the update to this + // field to all readers in your host fleet before deploying the update to start writing + // with that new attribute. + // + // For this example, we currently authenticate all attributes. To make it easier to + // add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + final String unsignAttrPrefix = ":"; + + // 5. Create the DynamoDb Encryption configuration for the table we will be writing to. + // Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + // that does not use asymmetric signing. + final Map tableConfigs = + new HashMap<>(); + final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig + .builder() + .logicalTableName(ddbTableName) + .partitionKeyName("partition_key") + .sortKeyName("sort_key") + .attributeActionsOnEncrypt(attributeActions) + .keyring(awsKmsEcdhKeyring) + .allowedUnsignedAttributePrefix(unsignAttrPrefix) + // Specify algorithmSuite without asymmetric signing here + // As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + // ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + .algorithmSuiteId( + DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384 + ) + .build(); + tableConfigs.put(ddbTableName, config); + + // 6. Create the DynamoDb Encryption Interceptor + DynamoDbEncryptionInterceptor encryptionInterceptor = + DynamoDbEncryptionInterceptor + .builder() + .config( + DynamoDbTablesEncryptionConfig + .builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ) + .build(); + + // 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + final DynamoDbClient ddbClient = DynamoDbClient + .builder() + .overrideConfiguration( + ClientOverrideConfiguration + .builder() + .addExecutionInterceptor(encryptionInterceptor) + .build() + ) + .build(); + + // 8. Put an item into our table using the above client. + // Before the item gets sent to DynamoDb, it will be encrypted + // client-side, according to our configuration. + final HashMap item = new HashMap<>(); + item.put( + "partition_key", + AttributeValue.builder().s("awsKmsEcdhKeyringItem").build() + ); + item.put("sort_key", AttributeValue.builder().n("0").build()); + item.put( + "sensitive_data", + AttributeValue.builder().s("encrypt and sign me!").build() + ); + + final PutItemRequest putRequest = PutItemRequest + .builder() + .tableName(ddbTableName) + .item(item) + .build(); + + final PutItemResponse putResponse = ddbClient.putItem(putRequest); + + // Demonstrate that PutItem succeeded + assert 200 == putResponse.sdkHttpResponse().statusCode(); + + // 9. Get the item back from our table using the client. + // The client will decrypt the item client-side using the RSA keyring + // and return the original item. + final HashMap keyToGet = new HashMap<>(); + keyToGet.put( + "partition_key", + AttributeValue.builder().s("awsKmsEcdhKeyringItem").build() + ); + keyToGet.put("sort_key", AttributeValue.builder().n("0").build()); + + final GetItemRequest getRequest = GetItemRequest + .builder() + .key(keyToGet) + .tableName(ddbTableName) + .build(); + + final GetItemResponse getResponse = ddbClient.getItem(getRequest); + + // Demonstrate that GetItem succeeded and returned the decrypted item + assert 200 == getResponse.sdkHttpResponse().statusCode(); + final Map returnedItem = getResponse.item(); + assert returnedItem + .get("sensitive_data") + .s() + .equals("encrypt and sign me!"); + } + + private static ByteBuffer loadPublicKeyBytes(String eccPublicKeyFileName) { + try { + ByteBuffer publicKeyFileBytes = ByteBuffer.wrap( + Files.readAllBytes(Paths.get(eccPublicKeyFileName)) + ); + + PemReader pemReader = new PemReader( + new InputStreamReader( + new ByteArrayInputStream(publicKeyFileBytes.array()) + ) + ); + PemObject pemObject = pemReader.readPemObject(); + byte[] content = pemObject.getContent(); + SubjectPublicKeyInfo subjectPublicKeyInfo = + SubjectPublicKeyInfo.getInstance(content); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec( + subjectPublicKeyInfo.getEncoded() + ); + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); + PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); + return ByteBuffer.wrap(publicKey.getEncoded()); + } catch (IOException e) { + throw new RuntimeException( + "IOException while reading public key from file", + e + ); + } catch ( + NoSuchProviderException + | NoSuchAlgorithmException + | InvalidKeySpecException e + ) { + throw new RuntimeException(e); + } + } + + public static void KmsEcdhKeyringGetItemPutItem( + String ddbTableName, + String eccKeyArn + ) { + KmsEcdhKeyringGetItemPutItem( + ddbTableName, + eccKeyArn, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME + ); + } + + public static void main(final String[] args) { + if (args.length <= 1) { + throw new IllegalArgumentException( + "To run this example, include the ddbTable and eccKeyArn in args; optionally include eccPublicKeySenderFilename, eccPublicKeyRecipientFilename, and eccRecipientKeyArn arguments" + ); + } + final String ddbTableName = args[0]; + final String eccKeyArn = args[1]; + String eccPublicKeySenderFilename; + String eccPublicKeyRecipientFilename; + String eccRecipientKeyArn; + if (args.length == 4) { + eccPublicKeySenderFilename = args[2]; + eccPublicKeyRecipientFilename = args[3]; + } else { + eccPublicKeySenderFilename = EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME; + eccPublicKeyRecipientFilename = EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME; + } + if (args.length == 5) { + eccRecipientKeyArn = args[4]; + } else { + eccRecipientKeyArn = null; + } + + // You may provide your own ECC public keys at EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME and + // EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + // The EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME must be the public key for the ECC key represented at eccKeyArn. + // + // If these files are not present, this will write a UTF-8 encoded PEM file for you. + // In this example we use an ECC KMS Key as the recipient key. However; this is only for + // example purposes. You can use an ECC Key that is not stored in KMS as the recipient public key. + if (shouldGetNewPublicKeys()) { + writePublicKeyPemForEccKey( + eccKeyArn, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + ); + writePublicKeyPemForEccKey( + eccRecipientKeyArn, + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME + ); + } + + KmsEcdhKeyringGetItemPutItem( + ddbTableName, + eccKeyArn, + eccPublicKeySenderFilename, + eccPublicKeyRecipientFilename + ); + } + + static void writePublicKeyPemForEccKey( + String eccKeyArn, + String eccPublicKeyFilename + ) { + // Safety check: Validate file is not present + File publicKeyFile = new File(eccPublicKeyFilename); + if (publicKeyFile.exists()) { + throw new IllegalStateException( + "writePublicKeyPemForEccKey will not overwrite existing PEM files" + ); + } + // This code will call KMS to get the public key for the KMS ECC key. + // You must have kms:GetPublicKey permissions on the key for this to succeed. + // The public key will be written to the file EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + // or EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + KmsClient getterForPublicKey = KmsClient.create(); + GetPublicKeyResponse response = getterForPublicKey.getPublicKey( + GetPublicKeyRequest.builder().keyId(eccKeyArn).build() + ); + byte[] publicKeyByteArray = response.publicKey().asByteArray(); + + StringWriter publicKeyStringWriter = new StringWriter(); + PemWriter publicKeyPemWriter = new PemWriter(publicKeyStringWriter); + try { + publicKeyPemWriter.writeObject( + new PemObject("PUBLIC KEY", publicKeyByteArray) + ); + publicKeyPemWriter.close(); + } catch (IOException e) { + throw new RuntimeException("IOException while writing public key PEM", e); + } + ByteBuffer publicKeyUtf8EncodedByteBufferToWrite = + StandardCharsets.UTF_8.encode(publicKeyStringWriter.toString()); + + try { + FileChannel fc = new FileOutputStream(eccPublicKeyFilename).getChannel(); + fc.write(publicKeyUtf8EncodedByteBufferToWrite); + fc.close(); + } catch (FileNotFoundException e) { + throw new RuntimeException( + "FileNotFoundException while opening public key FileChannel", + e + ); + } catch (IOException e) { + throw new RuntimeException( + "IOException while writing public key or closing FileChannel", + e + ); + } + } + + static boolean shouldGetNewPublicKeys() { + // Check if public keys already exist + File senderPublicKeyFile = new File(EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME); + File recipientPublicKeyFile = new File( + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME + ); + + if (senderPublicKeyFile.exists() || recipientPublicKeyFile.exists()) { + return false; + } + + if (!senderPublicKeyFile.exists() && recipientPublicKeyFile.exists()) { + throw new IllegalStateException( + "Missing public key sender file at " + + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + ); + } + + if (!recipientPublicKeyFile.exists() && senderPublicKeyFile.exists()) { + throw new IllegalStateException( + "Missing public key recipient file at " + + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME + ); + } + + return true; + } +} diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/RawEcdhKeyringExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/RawEcdhKeyringExample.java new file mode 100644 index 000000000..f22c8d805 --- /dev/null +++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/RawEcdhKeyringExample.java @@ -0,0 +1,877 @@ +package software.amazon.cryptography.examples.keyring; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jce.interfaces.ECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig; +import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateRawEcdhKeyringInput; +import software.amazon.cryptography.materialproviders.model.EphemeralPrivateKeyToStaticPublicKeyInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; +import software.amazon.cryptography.materialproviders.model.PublicKeyDiscoveryInput; +import software.amazon.cryptography.materialproviders.model.RawEcdhStaticConfigurations; +import software.amazon.cryptography.materialproviders.model.RawPrivateKeyToStaticPublicKeyInput; +import software.amazon.cryptography.primitives.model.ECDHCurveSpec; + +/* + This example sets up DynamoDb Encryption for the AWS SDK client + using the raw ECDH Keyring. This keyring takes in the sender's ECC + private key and the recipient's ECC Public Key to derive a shared secret. + The keyring uses the shared secret to derive a data key to protect the + data keys that encrypt and decrypt DynamoDb table items. + + This example takes in the sender's private key, the recipient's + public key, and the algorithm definition where the ECC keys lie. + This parameter takes in the sender's private key as a + UTF8 PEM-encoded (PKCS #8 PrivateKeyInfo structures), the recipient's + DER-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI), + and the Curve Specification where the keys lie. If this example + + This example encrypts a test item using the provided ECC keys and puts the + encrypted item to the provided DynamoDb table. Then, it gets the + item from the table and decrypts it. + + Running this example requires access to the DDB Table whose name + is provided in CLI arguments. + This table must be configured with the following + primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (S) + */ +public class RawEcdhKeyringExample { + + public static String EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER = + "RawEcdhKeyringExamplePrivateKeySender.pem"; + public static String EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT = + "RawEcdhKeyringExamplePrivateKeyRecipient.pem"; + public static String EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT = + "RawEcdhKeyringExamplePublicKeyRecipient.pem"; + + public static void RawEcdhKeyringGetItemPutItem( + String ddbTableName, + ECDHCurveSpec curveSpec + ) { + // 1. Load key pair from UTF-8 encoded PEM files. + // You may provide your own PEM files to use here. + // If you do not, the main method in this class will generate PEM + // files for example use. Do not use these files for any other purpose. + ByteBuffer privateKeyUtf8EncodedByteBuffer; + try { + privateKeyUtf8EncodedByteBuffer = + ByteBuffer.wrap( + Files.readAllBytes(Paths.get(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER)) + ); + } catch (IOException e) { + throw new RuntimeException( + "IOException while reading the private key from file", + e + ); + } + + ByteBuffer publicKeyByteBuffer; + try { + ByteBuffer publicKeyUtf8EncodedByteBuffer = ByteBuffer.wrap( + Files.readAllBytes(Paths.get(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT)) + ); + PemReader pemReader = new PemReader( + new InputStreamReader( + new ByteArrayInputStream(publicKeyUtf8EncodedByteBuffer.array()) + ) + ); + PemObject pemObject = pemReader.readPemObject(); + byte[] content = pemObject.getContent(); + SubjectPublicKeyInfo subjectPublicKeyInfo = + SubjectPublicKeyInfo.getInstance(content); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec( + subjectPublicKeyInfo.getEncoded() + ); + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); + PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); + publicKeyByteBuffer = ByteBuffer.wrap(publicKey.getEncoded()); + } catch (IOException e) { + throw new RuntimeException( + "IOException while reading the public key from file", + e + ); + } catch ( + NoSuchAlgorithmException + | NoSuchProviderException + | InvalidKeySpecException e + ) { + throw new RuntimeException(e); + } + + // 2. Create the keyring. + // This keyring uses static sender and recipient keys. This configuration calls for the keys + // to be + // The DynamoDb encryption client uses this to encrypt and decrypt items. + final CreateRawEcdhKeyringInput keyringInput = CreateRawEcdhKeyringInput + .builder() + .curveSpec(curveSpec) + .KeyAgreementScheme( + RawEcdhStaticConfigurations + .builder() + .RawPrivateKeyToStaticPublicKey( + RawPrivateKeyToStaticPublicKeyInput + .builder() + // Must be a UTF8 PEM-encoded private key + .senderStaticPrivateKey(privateKeyUtf8EncodedByteBuffer) + // Must be a DER-encoded X.509 public key + .recipientPublicKey(publicKeyByteBuffer) + .build() + ) + .build() + ) + .build(); + + final MaterialProviders matProv = MaterialProviders + .builder() + .MaterialProvidersConfig(MaterialProvidersConfig.builder().build()) + .build(); + IKeyring rawEcdhKeyring = matProv.CreateRawEcdhKeyring(keyringInput); + + PutGetExampleWithKeyring(rawEcdhKeyring, ddbTableName); + } + + public static void EphemeralRawEcdhKeyringPutItem( + String ddbTableName, + ECDHCurveSpec ecdhCurveSpec + ) { + // 1. Load public key from UTF-8 encoded PEM files into a DER encoded public key. + // You may provide your own PEM files to use here. + // If you do not, the main method in this class will generate PEM + // files for example use. Do not use these files for any other purpose. + ByteBuffer publicKeyByteBuffer; + try { + ByteBuffer publicKeyUtf8EncodedByteBuffer = ByteBuffer.wrap( + Files.readAllBytes(Paths.get(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT)) + ); + PemReader pemReader = new PemReader( + new InputStreamReader( + new ByteArrayInputStream(publicKeyUtf8EncodedByteBuffer.array()) + ) + ); + PemObject pemObject = pemReader.readPemObject(); + byte[] content = pemObject.getContent(); + SubjectPublicKeyInfo subjectPublicKeyInfo = + SubjectPublicKeyInfo.getInstance(content); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec( + subjectPublicKeyInfo.getEncoded() + ); + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); + PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); + publicKeyByteBuffer = ByteBuffer.wrap(publicKey.getEncoded()); + } catch (IOException e) { + throw new RuntimeException( + "IOException while reading the public key from file", + e + ); + } catch ( + NoSuchAlgorithmException + | NoSuchProviderException + | InvalidKeySpecException e + ) { + throw new RuntimeException(e); + } + + // 2. Create the keyring. + // This keyring uses an ephemeral configuration. This configuration will always create a new + // key pair as the sender key pair for the key agreement operation. The ephemeral configuration can only + // encrypt data and CANNOT decrypt messages. + // The DynamoDb encryption client uses this to encrypt and decrypt items. + final CreateRawEcdhKeyringInput keyringInput = CreateRawEcdhKeyringInput + .builder() + .curveSpec(ecdhCurveSpec) + .KeyAgreementScheme( + RawEcdhStaticConfigurations + .builder() + .EphemeralPrivateKeyToStaticPublicKey( + EphemeralPrivateKeyToStaticPublicKeyInput + .builder() + .recipientPublicKey(publicKeyByteBuffer) + .build() + ) + .build() + ) + .build(); + + final MaterialProviders matProv = MaterialProviders + .builder() + .MaterialProvidersConfig(MaterialProvidersConfig.builder().build()) + .build(); + IKeyring rawEcdhKeyring = matProv.CreateRawEcdhKeyring(keyringInput); + + // A raw ecdh keyring with Ephemeral configuration cannot decrypt data since the key pair + // used as the sender is ephemeral. This means that at decrypt time it does not have + // the private key that corresponds to the public key that is stored on the message. + PutExampleWithKeyring(rawEcdhKeyring, ddbTableName); + } + + public static void DiscoveryRawEcdhKeyringGetItem( + String ddbTableName, + ECDHCurveSpec ecdhCurveSpec + ) { + // 1. Load key pair from UTF-8 encoded PEM files. + // You may provide your own PEM files to use here. + // If you do not, the main method in this class will generate PEM + // files for example use. Do not use these files for any other purpose. + ByteBuffer privateKeyUtf8EncodedByteBuffer; + try { + privateKeyUtf8EncodedByteBuffer = + ByteBuffer.wrap( + Files.readAllBytes( + Paths.get(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) + ) + ); + } catch (IOException e) { + throw new RuntimeException( + "IOException while reading the private key from file", + e + ); + } + + // 2. Create the keyring. + // This keyring uses an ephemeral configuration. This configuration will always create a new + // key pair as the sender key pair for the key agreement operation. The ephemeral configuration can only + // encrypt data and CANNOT decrypt messages. + // The DynamoDb encryption client uses this to encrypt and decrypt items. + final CreateRawEcdhKeyringInput keyringInput = CreateRawEcdhKeyringInput + .builder() + .curveSpec(ecdhCurveSpec) + .KeyAgreementScheme( + RawEcdhStaticConfigurations + .builder() + .PublicKeyDiscovery( + PublicKeyDiscoveryInput + .builder() + .recipientStaticPrivateKey(privateKeyUtf8EncodedByteBuffer) + .build() + ) + .build() + ) + .build(); + + final MaterialProviders matProv = MaterialProviders + .builder() + .MaterialProvidersConfig(MaterialProvidersConfig.builder().build()) + .build(); + IKeyring rawEcdhKeyring = matProv.CreateRawEcdhKeyring(keyringInput); + + // A raw ecdh keyring with discovery configuration cannot encrypt data since the keyring + // looks for its configured public key on the message. + GetExampleWithKeyring(rawEcdhKeyring, ddbTableName); + } + + public static void PutGetExampleWithKeyring( + IKeyring rawEcdhKeyring, + String ddbTableName + ) { + // 3. Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + final Map attributeActionsOnEncrypt = new HashMap<>(); + attributeActionsOnEncrypt.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put( + "sensitive_data", + CryptoAction.ENCRYPT_AND_SIGN + ); + + // 4. Configure which attributes we expect to be included in the signature + // when reading items. There are two options for configuring this: + // + // - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + // When defining your DynamoDb schema and deciding on attribute names, + // choose a distinguishing prefix (such as ":") for all attributes that + // you do not want to include in the signature. + // This has two main benefits: + // - It is easier to reason about the security and authenticity of data within your item + // when all unauthenticated data is easily distinguishable by their attribute name. + // - If you need to add new unauthenticated attributes in the future, + // you can easily make the corresponding update to your `attributeActionsOnEncrypt` + // and immediately start writing to that new attribute, without + // any other configuration update needed. + // Once you configure this field, it is not safe to update it. + // + // - Configure `allowedUnsignedAttributes`: You may also explicitly list + // a set of attributes that should be considered unauthenticated when encountered + // on read. Be careful if you use this configuration. Do not remove an attribute + // name from this configuration, even if you are no longer writing with that attribute, + // as old items may still include this attribute, and our configuration needs to know + // to continue to exclude this attribute from the signature scope. + // If you add new attribute names to this field, you must first deploy the update to this + // field to all readers in your host fleet before deploying the update to start writing + // with that new attribute. + // + // For this example, we currently authenticate all attributes. To make it easier to + // add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + final String unsignAttrPrefix = ":"; + + // 5. Create the DynamoDb Encryption configuration for the table we will be writing to. + final Map tableConfigs = + new HashMap<>(); + final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig + .builder() + .logicalTableName(ddbTableName) + .partitionKeyName("partition_key") + .sortKeyName("sort_key") + .attributeActionsOnEncrypt(attributeActionsOnEncrypt) + .keyring(rawEcdhKeyring) + .allowedUnsignedAttributePrefix(unsignAttrPrefix) + .build(); + tableConfigs.put(ddbTableName, config); + + // 6. Create the DynamoDb Encryption Interceptor + DynamoDbEncryptionInterceptor encryptionInterceptor = + DynamoDbEncryptionInterceptor + .builder() + .config( + DynamoDbTablesEncryptionConfig + .builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ) + .build(); + + // 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + final DynamoDbClient ddb = DynamoDbClient + .builder() + .overrideConfiguration( + ClientOverrideConfiguration + .builder() + .addExecutionInterceptor(encryptionInterceptor) + .build() + ) + .build(); + + // 8. Put an item into our table using the above client. + // Before the item gets sent to DynamoDb, it will be encrypted + // client-side, according to our configuration. + final HashMap item = new HashMap<>(); + item.put( + "partition_key", + AttributeValue.builder().s("rawEcdhKeyringItem").build() + ); + item.put("sort_key", AttributeValue.builder().n("0").build()); + item.put( + "sensitive_data", + AttributeValue.builder().s("encrypt and sign me!").build() + ); + + final PutItemRequest putRequest = PutItemRequest + .builder() + .tableName(ddbTableName) + .item(item) + .build(); + + final PutItemResponse putResponse = ddb.putItem(putRequest); + + // Demonstrate that PutItem succeeded + assert 200 == putResponse.sdkHttpResponse().statusCode(); + + // 9. Get the item back from our table using the same client. + // The client will decrypt the item client-side, and return + // back the original item. + final HashMap keyToGet = new HashMap<>(); + keyToGet.put( + "partition_key", + AttributeValue.builder().s("rawEcdhKeyringItem").build() + ); + keyToGet.put("sort_key", AttributeValue.builder().n("0").build()); + + final GetItemRequest getRequest = GetItemRequest + .builder() + .key(keyToGet) + .tableName(ddbTableName) + .build(); + + final GetItemResponse getResponse = ddb.getItem(getRequest); + + // Demonstrate that GetItem succeeded and returned the decrypted item + assert 200 == getResponse.sdkHttpResponse().statusCode(); + final Map returnedItem = getResponse.item(); + assert returnedItem + .get("sensitive_data") + .s() + .equals("encrypt and sign me!"); + } + + public static void PutExampleWithKeyring( + IKeyring rawEcdhKeyring, + String ddbTableName + ) { + // 3. Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + final Map attributeActionsOnEncrypt = new HashMap<>(); + attributeActionsOnEncrypt.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put( + "sensitive_data", + CryptoAction.ENCRYPT_AND_SIGN + ); + + // 4. Configure which attributes we expect to be included in the signature + // when reading items. There are two options for configuring this: + // + // - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + // When defining your DynamoDb schema and deciding on attribute names, + // choose a distinguishing prefix (such as ":") for all attributes that + // you do not want to include in the signature. + // This has two main benefits: + // - It is easier to reason about the security and authenticity of data within your item + // when all unauthenticated data is easily distinguishable by their attribute name. + // - If you need to add new unauthenticated attributes in the future, + // you can easily make the corresponding update to your `attributeActionsOnEncrypt` + // and immediately start writing to that new attribute, without + // any other configuration update needed. + // Once you configure this field, it is not safe to update it. + // + // - Configure `allowedUnsignedAttributes`: You may also explicitly list + // a set of attributes that should be considered unauthenticated when encountered + // on read. Be careful if you use this configuration. Do not remove an attribute + // name from this configuration, even if you are no longer writing with that attribute, + // as old items may still include this attribute, and our configuration needs to know + // to continue to exclude this attribute from the signature scope. + // If you add new attribute names to this field, you must first deploy the update to this + // field to all readers in your host fleet before deploying the update to start writing + // with that new attribute. + // + // For this example, we currently authenticate all attributes. To make it easier to + // add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + final String unsignAttrPrefix = ":"; + + // 5. Create the DynamoDb Encryption configuration for the table we will be writing to. + final Map tableConfigs = + new HashMap<>(); + final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig + .builder() + .logicalTableName(ddbTableName) + .partitionKeyName("partition_key") + .sortKeyName("sort_key") + .attributeActionsOnEncrypt(attributeActionsOnEncrypt) + .keyring(rawEcdhKeyring) + .allowedUnsignedAttributePrefix(unsignAttrPrefix) + .build(); + tableConfigs.put(ddbTableName, config); + + // 6. Create the DynamoDb Encryption Interceptor + DynamoDbEncryptionInterceptor encryptionInterceptor = + DynamoDbEncryptionInterceptor + .builder() + .config( + DynamoDbTablesEncryptionConfig + .builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ) + .build(); + + // 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + final DynamoDbClient ddb = DynamoDbClient + .builder() + .overrideConfiguration( + ClientOverrideConfiguration + .builder() + .addExecutionInterceptor(encryptionInterceptor) + .build() + ) + .build(); + + // 8. Put an item into our table using the above client. + // Before the item gets sent to DynamoDb, it will be encrypted + // client-side, according to our configuration. + final HashMap item = new HashMap<>(); + item.put( + "partition_key", + AttributeValue.builder().s("rawEcdhKeyringItem").build() + ); + item.put("sort_key", AttributeValue.builder().n("0").build()); + item.put( + "sensitive_data", + AttributeValue.builder().s("encrypt and sign me!").build() + ); + + final PutItemRequest putRequest = PutItemRequest + .builder() + .tableName(ddbTableName) + .item(item) + .build(); + + final PutItemResponse putResponse = ddb.putItem(putRequest); + + // Demonstrate that PutItem succeeded + assert 200 == putResponse.sdkHttpResponse().statusCode(); + } + + public static void GetExampleWithKeyring( + IKeyring rawEcdhKeyring, + String ddbTableName + ) { + // 3. Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + final Map attributeActionsOnEncrypt = new HashMap<>(); + attributeActionsOnEncrypt.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put( + "sensitive_data", + CryptoAction.ENCRYPT_AND_SIGN + ); + // 4. Configure which attributes we expect to be included in the signature + // when reading items. There are two options for configuring this: + // + // - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + // When defining your DynamoDb schema and deciding on attribute names, + // choose a distinguishing prefix (such as ":") for all attributes that + // you do not want to include in the signature. + // This has two main benefits: + // - It is easier to reason about the security and authenticity of data within your item + // when all unauthenticated data is easily distinguishable by their attribute name. + // - If you need to add new unauthenticated attributes in the future, + // you can easily make the corresponding update to your `attributeActionsOnEncrypt` + // and immediately start writing to that new attribute, without + // any other configuration update needed. + // Once you configure this field, it is not safe to update it. + // + // - Configure `allowedUnsignedAttributes`: You may also explicitly list + // a set of attributes that should be considered unauthenticated when encountered + // on read. Be careful if you use this configuration. Do not remove an attribute + // name from this configuration, even if you are no longer writing with that attribute, + // as old items may still include this attribute, and our configuration needs to know + // to continue to exclude this attribute from the signature scope. + // If you add new attribute names to this field, you must first deploy the update to this + // field to all readers in your host fleet before deploying the update to start writing + // with that new attribute. + // + // For this example, we currently authenticate all attributes. To make it easier to + // add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + final String unsignAttrPrefix = ":"; + + // 5. Create the DynamoDb Encryption configuration for the table we will be writing to. + final Map tableConfigs = + new HashMap<>(); + final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig + .builder() + .logicalTableName(ddbTableName) + .partitionKeyName("partition_key") + .sortKeyName("sort_key") + .attributeActionsOnEncrypt(attributeActionsOnEncrypt) + .keyring(rawEcdhKeyring) + .allowedUnsignedAttributePrefix(unsignAttrPrefix) + .build(); + tableConfigs.put(ddbTableName, config); + + // 6. Create the DynamoDb Encryption Interceptor + DynamoDbEncryptionInterceptor encryptionInterceptor = + DynamoDbEncryptionInterceptor + .builder() + .config( + DynamoDbTablesEncryptionConfig + .builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ) + .build(); + + // 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + final DynamoDbClient ddb = DynamoDbClient + .builder() + .overrideConfiguration( + ClientOverrideConfiguration + .builder() + .addExecutionInterceptor(encryptionInterceptor) + .build() + ) + .build(); + + // 8. Get the item back from our table using the same client. + // The client will decrypt the item client-side, and return + // back the original item. + final HashMap keyToGet = new HashMap<>(); + keyToGet.put( + "partition_key", + AttributeValue.builder().s("rawEcdhKeyringItem").build() + ); + keyToGet.put("sort_key", AttributeValue.builder().n("0").build()); + + final GetItemRequest getRequest = GetItemRequest + .builder() + .key(keyToGet) + .tableName(ddbTableName) + .build(); + + final GetItemResponse getResponse = ddb.getItem(getRequest); + + // Demonstrate that GetItem succeeded and returned the decrypted item + assert 200 == getResponse.sdkHttpResponse().statusCode(); + final Map returnedItem = getResponse.item(); + assert returnedItem + .get("sensitive_data") + .s() + .equals("encrypt and sign me!"); + } + + public static void main(final String[] args) { + if (args.length <= 0) { + throw new IllegalArgumentException( + "To run this example, include the ddbTable in args" + ); + } + final String ddbTableName = args[0]; + + RawEcdhKeyringGetItemPutItem(ddbTableName, ECDHCurveSpec.ECC_NIST_P256); + } + + public static boolean shouldGenerateNewEccKeyPairs() { + File privateKeyFileSender = new File( + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + ); + File privateKeyFileRecipient = new File( + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + ); + File publicKeyFileRecipient = new File( + EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + ); + + // If keys already exist: do not overwrite existing keys + if ( + privateKeyFileSender.exists() && + publicKeyFileRecipient.exists() && + privateKeyFileRecipient.exists() + ) { + return false; + } + + // If not all three keys are present; throw an exception + if ( + !privateKeyFileSender.exists() && + publicKeyFileRecipient.exists() && + privateKeyFileRecipient.exists() + ) { + throw new IllegalStateException( + "Missing private key sender file at " + + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + ); + } + if ( + privateKeyFileSender.exists() && + !publicKeyFileRecipient.exists() && + privateKeyFileRecipient.exists() + ) { + throw new IllegalStateException( + "Missing public key recipient file at " + + EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + ); + } + if ( + privateKeyFileSender.exists() && + publicKeyFileRecipient.exists() && + !privateKeyFileRecipient.exists() + ) { + throw new IllegalStateException( + "Missing priavet key recipeitn file at " + + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + ); + } + + // If no keys are present, generate new keys + return true; + } + + public static void generateEccKeyPairs() { + File privateKeyFileSender = new File( + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + ); + File privateKeyFileRecipient = new File( + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + ); + File publicKeyFileRecipient = new File( + EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + ); + + if ( + privateKeyFileSender.exists() || + publicKeyFileRecipient.exists() || + privateKeyFileRecipient.exists() + ) { + throw new IllegalStateException( + "generateEccKeyPairs will not overwrite existing PEM files" + ); + } + + // This code will generate new ECC key pairs for example use. + // The keys will be written to the files: + // - public_sender: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_SENDER + // - private_sender: EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + // - public_recipient: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + // This example uses BouncyCastle's KeyPairGenerator to generate the key pairs. + // In practice, you should not generate this in your code, and should instead + // retrieve this key from a secure key management system (e.g. HSM). + // These examples only demonstrate using the P256 curve while the keyring accepts + // P256, P384, or P521. + // These keys are created here for example purposes only. + KeyPairGenerator keyGen; + try { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + keyGen = KeyPairGenerator.getInstance("EC", "BC"); + keyGen.initialize( + new ECGenParameterSpec("secp256r1"), + new SecureRandom() + ); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + throw new RuntimeException("No such algorithm", e); + } catch (NoSuchProviderException e) { + throw new RuntimeException(e); + } + + final KeyPair senderKeyPair = keyGen.generateKeyPair(); + final KeyPair recipientKeyPair = keyGen.generateKeyPair(); + + writePrivateKey( + senderKeyPair.getPrivate(), + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + ); + writePrivateKey( + recipientKeyPair.getPrivate(), + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + ); + writePublicKey( + (ECPublicKey) recipientKeyPair.getPublic(), + EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + ); + } + + private static void writePrivateKey( + PrivateKey aPrivate, + String exampleEccPrivateKeyFilenameSender + ) { + StringWriter privateKeySenderStringWriter = new StringWriter(); + PemWriter privateKeyPemWriter = new PemWriter(privateKeySenderStringWriter); + + try { + privateKeyPemWriter.writeObject( + new PemObject("PRIVATE KEY", aPrivate.getEncoded()) + ); + privateKeyPemWriter.close(); + } catch (IOException e) { + throw new RuntimeException( + "IOException while writing private key PEM", + e + ); + } + + ByteBuffer privateKeyUtf8EncodedPrivateKeyByteBuffer = + StandardCharsets.UTF_8.encode(privateKeySenderStringWriter.toString()); + + // Write UTF8 encoded PEM file + try { + FileChannel fc = new FileOutputStream(exampleEccPrivateKeyFilenameSender) + .getChannel(); + fc.write(privateKeyUtf8EncodedPrivateKeyByteBuffer); + fc.close(); + } catch (FileNotFoundException e) { + throw new RuntimeException( + "FileNotFoundException while opening private key FileChannel", + e + ); + } catch (IOException e) { + throw new RuntimeException( + "IOException while writing private key or closing FileChannel", + e + ); + } + } + + private static void writePublicKey(ECPublicKey aPublic, String fileName) { + StringWriter publicKeyStringWriter = new StringWriter(); + PemWriter publicKeySenderPemWriter = new PemWriter(publicKeyStringWriter); + X509EncodedKeySpec spec = new X509EncodedKeySpec(aPublic.getEncoded()); + + try { + publicKeySenderPemWriter.writeObject( + new PemObject("PUBLIC KEY", spec.getEncoded()) + ); + publicKeySenderPemWriter.close(); + } catch (IOException e) { + throw new RuntimeException("IOException while writing public key PEM", e); + } + + ByteBuffer publicKeyUtf8EncodedPublicKeyByteBuffer = + StandardCharsets.UTF_8.encode(publicKeyStringWriter.toString()); + + try { + FileChannel fc = new FileOutputStream(fileName).getChannel(); + fc.write(publicKeyUtf8EncodedPublicKeyByteBuffer); + fc.close(); + } catch (FileNotFoundException e) { + throw new RuntimeException( + "FileNotFoundException while opening private key FileChannel", + e + ); + } catch (IOException e) { + throw new RuntimeException( + "IOException while writing private key or closing FileChannel", + e + ); + } + } +} diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/TestUtils.java b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/TestUtils.java index 76fe7cbb8..c3f9fe308 100644 --- a/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/TestUtils.java +++ b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/TestUtils.java @@ -20,6 +20,10 @@ public class TestUtils { "arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7"; public static final String TEST_MRK_REPLICA_KEY_ID_EU_WEST_1 = "arn:aws:kms:eu-west-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7"; + public static final String TEST_KMS_ECDH_KEY_ID_P256_SENDER = + "arn:aws:kms:us-west-2:370957321024:key/eabdf483-6be2-4d2d-8ee4-8c2583d416e9"; + public static final String TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT = + "arn:aws:kms:us-west-2:370957321024:key/0265c8e9-5b6a-4055-8f70-63719e09fda5"; // Our tests require access to DDB Table with this name public static final String TEST_DDB_TABLE_NAME = diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestKmsEcdhKeyringExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestKmsEcdhKeyringExample.java new file mode 100644 index 000000000..42f9f51a8 --- /dev/null +++ b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestKmsEcdhKeyringExample.java @@ -0,0 +1,47 @@ +package software.amazon.cryptography.examples.keyring; + +import static software.amazon.cryptography.examples.keyring.KmsEcdhKeyringExample.EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME; +import static software.amazon.cryptography.examples.keyring.KmsEcdhKeyringExample.EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME; +import static software.amazon.cryptography.examples.keyring.KmsEcdhKeyringExample.shouldGetNewPublicKeys; +import static software.amazon.cryptography.examples.keyring.KmsEcdhKeyringExample.writePublicKeyPemForEccKey; + +import org.testng.annotations.Test; +import software.amazon.cryptography.examples.TestUtils; + +public class TestKmsEcdhKeyringExample { + + @Test + public void TestKmsEcdhKeyringExampleStatic() { + // You may provide your own ECC public keys at EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + // and EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + // This must be the public key for the ECC key represented at eccKeyArn + // If this file is not present, this will write a UTF-8 encoded PEM file for you. + if (shouldGetNewPublicKeys()) { + writePublicKeyPemForEccKey( + TestUtils.TEST_KMS_ECDH_KEY_ID_P256_SENDER, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + ); + writePublicKeyPemForEccKey( + TestUtils.TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME + ); + } + + KmsEcdhKeyringExample.KmsEcdhKeyringGetItemPutItem( + TestUtils.TEST_DDB_TABLE_NAME, + TestUtils.TEST_KMS_ECDH_KEY_ID_P256_SENDER + ); + } + + @Test + public void TestKmsEcdhKeyringExampleDiscovery() { + // In this example you do not need to provide the recipient ECC Public Key. + // On initialization, the keyring will call KMS:getPublicKey on the configured + // recipientKmsIdentifier set on the keyring. This example uses the previous example + // to write an item meant for the recipient. + KmsEcdhKeyringExample.KmsEcdhDiscoveryGetItem( + TestUtils.TEST_DDB_TABLE_NAME, + TestUtils.TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT + ); + } +} diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestRawEcdhKeyringExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestRawEcdhKeyringExample.java new file mode 100644 index 000000000..a7f9cb252 --- /dev/null +++ b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/keyring/TestRawEcdhKeyringExample.java @@ -0,0 +1,80 @@ +package software.amazon.cryptography.examples.keyring; + +import java.nio.ByteBuffer; +import java.security.spec.ECGenParameterSpec; +import org.testng.annotations.Test; +import software.amazon.cryptography.examples.TestUtils; +import software.amazon.cryptography.primitives.model.ECDHCurveSpec; + +public class TestRawEcdhKeyringExample { + + @Test + public void TestStaticRawEcdhKeyringExample() { + // You may provide your own ECC Key pairs in the files located at + // - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + // - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + // If these files are not present, this will generate a pair for you. + // For this example we will use the curve P256. + if (RawEcdhKeyringExample.shouldGenerateNewEccKeyPairs()) { + RawEcdhKeyringExample.generateEccKeyPairs(); + } + + // Part of using these keyrings is knowing which curve the keys used in the key agreement + // lie on. The keyring will fail if the keys do not lie on the configured curve. + RawEcdhKeyringExample.RawEcdhKeyringGetItemPutItem( + TestUtils.TEST_DDB_TABLE_NAME, + ECDHCurveSpec.ECC_NIST_P256 + ); + } + + @Test + public void TestEphemeralRawEcdhKeyringExample() { + // You may provide your own ECC Public Key in the files located at + // - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + // If these files are not present, this will generate a pair for you. + // For this example we will use the curve P256. + if (RawEcdhKeyringExample.shouldGenerateNewEccKeyPairs()) { + RawEcdhKeyringExample.generateEccKeyPairs(); + } + + // Part of using these keyrings is knowing which curve the keys used in the key agreement + // lie on. The keyring will fail if the keys do not lie on the configured curve. + RawEcdhKeyringExample.EphemeralRawEcdhKeyringPutItem( + TestUtils.TEST_DDB_TABLE_NAME, + ECDHCurveSpec.ECC_NIST_P256 + ); + } + + @Test + public void TestDiscoveryRawEcdhKeyringExample() { + // You may provide your own ECC Public Key in the files located at + // - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + // - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + // If these files are not present, this will generate a pair for you. + // For this example we will use the curve P256. + if (RawEcdhKeyringExample.shouldGenerateNewEccKeyPairs()) { + RawEcdhKeyringExample.generateEccKeyPairs(); + } + + // The discovery configuration is not allowed to encrypt + // To understand this example best, we will write a record with the ephemeral configuration + // in the previous example. This means that the recipient public key configured on + // both keyrings is the same. This means that the other party has the recipient public key + // and is writing messages meant only for the owner of the recipient public key to decrypt. + + // In this call we are writing a record that is written with an ephemeral sender key pair. + // The recipient will be able to decrypt the message + RawEcdhKeyringExample.EphemeralRawEcdhKeyringPutItem( + TestUtils.TEST_DDB_TABLE_NAME, + ECDHCurveSpec.ECC_NIST_P256 + ); + + // In this call we are reading a record that was written with the recipient's public key. + // It will use the recipient's private key and the sender's public key stored in the message to + // calculate the appropriate shared secret to successfully decrypt the message. + RawEcdhKeyringExample.DiscoveryRawEcdhKeyringGetItem( + TestUtils.TEST_DDB_TABLE_NAME, + ECDHCurveSpec.ECC_NIST_P256 + ); + } +}