diff --git a/aws-java-sdk-s3/pom.xml b/aws-java-sdk-s3/pom.xml deleted file mode 100644 index 579789748..000000000 --- a/aws-java-sdk-s3/pom.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - 4.0.0 - - com.amazonaws - aws-java-sdk-pom - 1.12.184-SNAPSHOT - - com.amazonaws - aws-java-sdk-s3 - AWS Java SDK for Amazon S3 - The AWS Java SDK for Amazon S3 module holds the client classes that are used for communicating with Amazon Simple Storage Service - https://aws.amazon.com/sdkforjava - - - - - aws-java-sdk-kms - com.amazonaws - false - ${awsjavasdk.version} - - - aws-java-sdk-core - com.amazonaws - false - ${awsjavasdk.version} - - - aws-java-sdk-test-utils - com.amazonaws - false - test - ${awsjavasdk.version} - - - jmespath-java - com.amazonaws - false - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - - - - - - versiondiff - - - - com.github.siom79.japicmp - japicmp-maven-plugin - 0.5.0 - - - verify - - cmp - - - - - - - com.amazonaws - aws-java-sdk-s3 - RELEASE - - - - - ${project.build.directory}/${project.artifactId}-${project.version}.jar - - - - true - public - false - false - false - - - - - - - - diff --git a/pom.xml b/pom.xml index b89d07d56..f2cbe5a45 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ 4.0.0 software.amazon.encryption - s3-client + s3 3.0-SNAPSHOT jar AWS S3 Encryption Client The AWS S3 Encryption Client provides client-side encryption for S3 - https://github.com/aws/aws-s33c-java + https://github.com/aws/aws-s3-encryption-client-java @@ -51,7 +51,7 @@ software.amazon.awssdk bom - 2.17.154 + 2.17.204 true pom import @@ -80,14 +80,14 @@ software.amazon.awssdk s3 - 2.17.154 + 2.17.204 true software.amazon.awssdk kms - 2.17.154 + 2.17.204 true diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java new file mode 100644 index 000000000..44a2245f7 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClient.java @@ -0,0 +1,224 @@ +package software.amazon.encryption.s3; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.protocols.jsoncore.JsonWriter; +import software.amazon.awssdk.protocols.jsoncore.JsonWriter.JsonGenerationException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.InvalidObjectStateException; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; +import software.amazon.encryption.s3.materials.DecryptionMaterials; +import software.amazon.encryption.s3.materials.DecryptMaterialsRequest; +import software.amazon.encryption.s3.materials.EncryptionMaterialsRequest; +import software.amazon.encryption.s3.materials.EncryptedDataKey; +import software.amazon.encryption.s3.materials.EncryptionMaterials; +import software.amazon.encryption.s3.materials.MaterialsManager; + +public class S3EncryptionClient implements S3Client { + + private final S3Client _wrappedClient; + private final MaterialsManager _materialsManager; + + public S3EncryptionClient(S3Client client, MaterialsManager materialsManager) { + _wrappedClient = client; + _materialsManager = materialsManager; + } + + @Override + public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBody requestBody) + throws AwsServiceException, SdkClientException, S3Exception { + + // TODO: This is proof-of-concept code and needs to be refactored + + // Get content encryption key + EncryptionMaterials materials = _materialsManager.getEncryptionMaterials(EncryptionMaterialsRequest.builder() + .build()); + SecretKey contentKey = materials.dataKey(); + // Encrypt content + byte[] iv = new byte[12]; // default GCM IV length + new SecureRandom().nextBytes(iv); + + final String contentEncryptionAlgorithm = "AES/GCM/NoPadding"; + final Cipher cipher; + try { + cipher = Cipher.getInstance(contentEncryptionAlgorithm); + cipher.init(Cipher.ENCRYPT_MODE, contentKey, new GCMParameterSpec(128, iv)); + } catch (NoSuchAlgorithmException + | NoSuchPaddingException + | InvalidAlgorithmParameterException + | InvalidKeyException e) { + throw new RuntimeException(e); + } + + byte[] ciphertext; + try { + byte[] input = IoUtils.toByteArray(requestBody.contentStreamProvider().newStream()); + ciphertext = cipher.doFinal(input); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (IllegalBlockSizeException e) { + throw new RuntimeException(e); + } catch (BadPaddingException e) { + throw new RuntimeException(e); + } + + // Save content metadata into request + Base64.Encoder encoder = Base64.getEncoder(); + Map metadata = new HashMap<>(putObjectRequest.metadata()); + EncryptedDataKey edk = materials.encryptedDataKeys().get(0); + metadata.put("x-amz-key-v2", encoder.encodeToString(edk.ciphertext())); + metadata.put("x-amz-iv", encoder.encodeToString(iv)); + metadata.put("x-amz-matdesc", /* TODO: JSON encoded */ "{}"); + metadata.put("x-amz-cek-alg", contentEncryptionAlgorithm); + metadata.put("x-amz-tag-len", /* TODO: take from algo suite */ "128"); + metadata.put("x-amz-wrap-alg", edk.keyProviderId()); + + try (JsonWriter jsonWriter = JsonWriter.create()) { + jsonWriter.writeStartObject(); + for (Entry entry : materials.encryptionContext().entrySet()) { + jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); + } + jsonWriter.writeEndObject(); + + String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); + metadata.put("x-amz-matdesc", jsonEncryptionContext); + } catch (JsonGenerationException e) { + throw new RuntimeException(e); + } + + putObjectRequest = putObjectRequest.toBuilder().metadata(metadata).build(); + + return _wrappedClient.putObject(putObjectRequest, RequestBody.fromBytes(ciphertext)); + } + + @Override + public T getObject(GetObjectRequest getObjectRequest, ResponseTransformer responseTransformer) + throws NoSuchKeyException, InvalidObjectStateException, AwsServiceException, SdkClientException, S3Exception { + + // TODO: This is proof-of-concept code and needs to be refactored + + ResponseInputStream objectStream = _wrappedClient.getObject(getObjectRequest); + byte[] output; + try { + output = IoUtils.toByteArray(objectStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + + GetObjectResponse response = objectStream.response(); + Map metadata = response.metadata(); + + // Build encrypted data key + Base64.Decoder decoder = Base64.getDecoder(); + byte[] edkCiphertext = decoder.decode(metadata.get("x-amz-key-v2")); + String keyProviderId = metadata.get("x-amz-wrap-alg"); + EncryptedDataKey edk = EncryptedDataKey.builder() + .ciphertext(edkCiphertext) + .keyProviderId(keyProviderId) + .build(); + List encryptedDataKeys = Collections.singletonList(edk); + + // Get encryption context + final Map encryptionContext = new HashMap<>(); + final String jsonEncryptionContext = metadata.get("x-amz-matdesc"); + try { + JsonNodeParser parser = JsonNodeParser.create(); + JsonNode objectNode = parser.parse(jsonEncryptionContext); + + for (Map.Entry entry : objectNode.asObject().entrySet()) { + encryptionContext.put(entry.getKey(), entry.getValue().asString()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + // Get decryption materials + final String contentEncryptionAlgorithm = metadata.get("x-amz-cek-alg"); + AlgorithmSuite algorithmSuite = null; + if (contentEncryptionAlgorithm.equals("AES/GCM/NoPadding")) { + algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF;; + } + + if (algorithmSuite == null) { + throw new RuntimeException("Unknown content encryption algorithm: " + contentEncryptionAlgorithm); + } + + DecryptMaterialsRequest request = DecryptMaterialsRequest.builder() + .algorithmSuite(algorithmSuite) + .encryptedDataKeys(encryptedDataKeys) + .encryptionContext(encryptionContext) + .build(); + DecryptionMaterials materials = _materialsManager.decryptMaterials(request); + + // Get content encryption information + SecretKey contentKey = new SecretKeySpec(materials.plaintextDataKey(), "AES"); + final int tagLength = Integer.parseInt(metadata.get("x-amz-tag-len")); + byte[] iv = decoder.decode(metadata.get("x-amz-iv")); + final Cipher cipher; + byte[] plaintext; + try { + cipher = Cipher.getInstance(contentEncryptionAlgorithm); + cipher.init(Cipher.DECRYPT_MODE, contentKey, new GCMParameterSpec(tagLength, iv)); + plaintext = cipher.doFinal(output); + } catch (NoSuchAlgorithmException + | NoSuchPaddingException + | InvalidAlgorithmParameterException + | InvalidKeyException e) { + throw new RuntimeException(e); + } catch (IllegalBlockSizeException e) { + throw new RuntimeException(e); + } catch (BadPaddingException e) { + throw new RuntimeException(e); + } + + try { + return responseTransformer.transform(response, + AbortableInputStream.create(new ByteArrayInputStream(plaintext))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String serviceName() { + return _wrappedClient.serviceName(); + } + + @Override + public void close() { + _wrappedClient.close(); + } +} diff --git a/src/main/java/software/amazon/encryption/s3/S3EncryptionClientException.java b/src/main/java/software/amazon/encryption/s3/S3EncryptionClientException.java new file mode 100644 index 000000000..c3effb04e --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/S3EncryptionClientException.java @@ -0,0 +1,17 @@ +package software.amazon.encryption.s3; + +import software.amazon.awssdk.core.exception.SdkClientException; + +public class S3EncryptionClientException extends SdkClientException { + + public S3EncryptionClientException(String message) { + super(SdkClientException.builder() + .message(message)); + } + + public S3EncryptionClientException(String message, Throwable cause) { + super(SdkClientException.builder() + .message(message) + .cause(cause)); + } +} diff --git a/src/main/java/software/amazon/encryption/s3/algorithms/AlgorithmSuite.java b/src/main/java/software/amazon/encryption/s3/algorithms/AlgorithmSuite.java new file mode 100644 index 000000000..7d5ce6129 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/algorithms/AlgorithmSuite.java @@ -0,0 +1,57 @@ +package software.amazon.encryption.s3.algorithms; + + +public enum AlgorithmSuite { + ALG_AES_256_GCM_IV12_TAG16_NO_KDF(0x0078, + "AES", + 256, + "AES/GCM/NoPadding", + 128, + 96, + 128, + Constants.GCM_MAX_CONTENT_LENGTH_BITS); + + private int _id; + private String _dataKeyAlgorithm; + private int _dataKeyLengthBits; + private String _cipherName; + private int _cipherBlockSizeBits; + private int _cipherNonceLengthBits; + private int _cipherTagLengthBits; + private long _cipherMaxContentLengthBits; + + AlgorithmSuite(int id, + String dataKeyAlgorithm, + int dataKeyLengthBits, + String cipherName, + int cipherBlockSizeBits, + int cipherNonceLengthBits, + int cipherTagLengthBits, + long cipherMaxContentLengthBits + ) { + this._id = id; + this._dataKeyAlgorithm = dataKeyAlgorithm; + this._dataKeyLengthBits = dataKeyLengthBits; + this._cipherName = cipherName; + this._cipherBlockSizeBits = cipherBlockSizeBits; + this._cipherNonceLengthBits = cipherNonceLengthBits; + this._cipherTagLengthBits = cipherTagLengthBits; + this._cipherMaxContentLengthBits = cipherMaxContentLengthBits; + } + + public String dataKeyAlgorithm() { + return _dataKeyAlgorithm; + } + + public int dataKeyLengthBits() { + return _dataKeyLengthBits; + } + + public String cipherName() { + return _cipherName; + } + + public int nonceLengthBytes() { + return _cipherNonceLengthBits / 8; + } +} diff --git a/src/main/java/software/amazon/encryption/s3/algorithms/Constants.java b/src/main/java/software/amazon/encryption/s3/algorithms/Constants.java new file mode 100644 index 000000000..d5466183e --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/algorithms/Constants.java @@ -0,0 +1,6 @@ +package software.amazon.encryption.s3.algorithms; + +class Constants { + // Maximum length of the content that can be encrypted in GCM mode. + static final long GCM_MAX_CONTENT_LENGTH_BITS = (1L<<39) - 256; +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/AESKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/AESKeyring.java new file mode 100644 index 000000000..cdcd22445 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/AESKeyring.java @@ -0,0 +1,146 @@ +package software.amazon.encryption.s3.materials; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; + +/** + * AESKeyring will use an AES key to wrap the data key used to encrypt content. + */ +public class AESKeyring implements Keyring { + + private static final String KEY_ALGORITHM = "AES"; + private static final String KEY_PROVIDER_ID = "AES/GCM"; + private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding"; + private static final int NONCE_LENGTH_BYTES = 12; + private static final int TAG_LENGTH_BYTES = 16; + private static final int TAG_LENGTH_BITS = TAG_LENGTH_BYTES * 8; + + private final SecretKey _wrappingKey; + private final SecureRandom _secureRandom; + private final DataKeyGenerator _dataKeyGenerator; + + private AESKeyring(Builder builder) { + _wrappingKey = builder._wrappingKey; + _secureRandom = builder._secureRandom; + _dataKeyGenerator = builder._dataKeyGenerator; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public EncryptionMaterials onEncrypt(EncryptionMaterials materials) { + if (materials.plaintextDataKey() == null) { + SecretKey dataKey = _dataKeyGenerator.generateDataKey(materials.algorithmSuite()); + materials = materials.toBuilder() + .plaintextDataKey(dataKey.getEncoded()) + .build(); + } + + try { + byte[] nonce = new byte[NONCE_LENGTH_BYTES]; + _secureRandom.nextBytes(nonce); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, nonce); + + final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, _wrappingKey, gcmParameterSpec, _secureRandom); + + AlgorithmSuite algorithmSuite = materials.algorithmSuite(); + cipher.updateAAD(algorithmSuite.cipherName().getBytes(StandardCharsets.UTF_8)); + byte[] ciphertext = cipher.doFinal(materials.plaintextDataKey()); + + // The encrypted data key is the nonce prepended to the ciphertext + byte[] encodedBytes = new byte[nonce.length + ciphertext.length]; + System.arraycopy(nonce, 0, encodedBytes, 0, nonce.length); + System.arraycopy(ciphertext, 0, encodedBytes, nonce.length, ciphertext.length); + + EncryptedDataKey encryptedDataKey = EncryptedDataKey.builder() + .keyProviderId(KEY_PROVIDER_ID) + .ciphertext(encodedBytes) + .build(); + + List encryptedDataKeys = new ArrayList<>(materials.encryptedDataKeys()); + encryptedDataKeys.add(encryptedDataKey); + + return materials.toBuilder() + .encryptedDataKeys(encryptedDataKeys) + .build(); + } catch (Exception e) { + throw new S3EncryptionClientException("Unable to " + CIPHER_ALGORITHM + " wrap", e); + } + } + + @Override + public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List encryptedDataKeys) { + if (materials.plaintextDataKey() != null) { + return materials; + } + + for (EncryptedDataKey encryptedDataKey : encryptedDataKeys) { + if (!encryptedDataKey.keyProviderId().equals(KEY_PROVIDER_ID)) { + continue; + } + + byte[] encodedBytes = encryptedDataKey.ciphertext(); + byte[] nonce = new byte[NONCE_LENGTH_BYTES]; + byte[] ciphertext = new byte[encodedBytes.length - nonce.length]; + + System.arraycopy(encodedBytes, 0, nonce, 0, nonce.length); + System.arraycopy(encodedBytes, nonce.length, ciphertext, 0, ciphertext.length); + + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH_BITS, nonce); + try { + final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, _wrappingKey, gcmParameterSpec); + + AlgorithmSuite algorithmSuite = materials.algorithmSuite(); + cipher.updateAAD(algorithmSuite.cipherName().getBytes(StandardCharsets.UTF_8)); + byte[] plaintext = cipher.doFinal(ciphertext); + + return materials.toBuilder().plaintextDataKey(plaintext).build(); + } catch (Exception e) { + throw new S3EncryptionClientException("Unable to " + CIPHER_ALGORITHM + " unwrap", e); + } + } + + return materials; + } + + public static class Builder { + private SecretKey _wrappingKey; + private SecureRandom _secureRandom = new SecureRandom(); + private DataKeyGenerator _dataKeyGenerator = new DefaultDataKeyGenerator(); + + private Builder() {} + + public Builder wrappingKey(SecretKey wrappingKey) { + if (!wrappingKey.getAlgorithm().equals(KEY_ALGORITHM)) { + throw new S3EncryptionClientException("Invalid algorithm '" + wrappingKey.getAlgorithm() + "', expecting " + KEY_ALGORITHM); + } + _wrappingKey = wrappingKey; + return this; + } + + public Builder secureRandom(SecureRandom secureRandom) { + _secureRandom = secureRandom; + return this; + } + + public Builder dataKeyGenerator(DataKeyGenerator dataKeyGenerator) { + _dataKeyGenerator = dataKeyGenerator; + return this; + } + + public AESKeyring build() { + return new AESKeyring(this); + } + } +} \ No newline at end of file diff --git a/src/main/java/software/amazon/encryption/s3/materials/DataKeyGenerator.java b/src/main/java/software/amazon/encryption/s3/materials/DataKeyGenerator.java new file mode 100644 index 000000000..4b7b2ac05 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/DataKeyGenerator.java @@ -0,0 +1,9 @@ +package software.amazon.encryption.s3.materials; + +import javax.crypto.SecretKey; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; + +@FunctionalInterface +public interface DataKeyGenerator { + SecretKey generateDataKey(AlgorithmSuite algorithmSuite); +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java b/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java new file mode 100644 index 000000000..499bccfd6 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/DecryptMaterialsRequest.java @@ -0,0 +1,68 @@ +package software.amazon.encryption.s3.materials; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; + +public class DecryptMaterialsRequest { + + private final AlgorithmSuite _algorithmSuite; + private final List _encryptedDataKeys; + private final Map _encryptionContext; + + private DecryptMaterialsRequest(Builder builder) { + this._algorithmSuite = builder._algorithmSuite; + this._encryptedDataKeys = builder._encryptedDataKeys; + this._encryptionContext = builder._encryptionContext; + } + + static public Builder builder() { + return new Builder(); + } + + public AlgorithmSuite algorithmSuite() { + return _algorithmSuite; + } + + public List encryptedDataKeys() { + return _encryptedDataKeys; + } + + public Map encryptionContext() { + return _encryptionContext; + } + + static public class Builder { + + private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + private Map _encryptionContext = Collections.emptyMap(); + private List _encryptedDataKeys = Collections.emptyList(); + + private Builder() { + } + + public Builder algorithmSuite(AlgorithmSuite algorithmSuite) { + _algorithmSuite = algorithmSuite; + return this; + } + + public Builder encryptionContext(Map encryptionContext) { + _encryptionContext = encryptionContext == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(encryptionContext); + return this; + } + + public Builder encryptedDataKeys(List encryptedDataKeys) { + _encryptedDataKeys = encryptedDataKeys == null + ? Collections.emptyList() + : Collections.unmodifiableList(encryptedDataKeys); + return this; + } + + public DecryptMaterialsRequest build() { + return new DecryptMaterialsRequest(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java b/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java new file mode 100644 index 000000000..5390e95a6 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/DecryptionMaterials.java @@ -0,0 +1,83 @@ +package software.amazon.encryption.s3.materials; + +import java.util.Collections; +import java.util.Map; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; + +final public class DecryptionMaterials { + + // Identifies what sort of crypto algorithms we want to use + private final AlgorithmSuite _algorithmSuite; + + // Additional information passed into encrypted that is required on decryption as well + // Should NOT contain sensitive information + private final Map _encryptionContext; + + private final byte[] _plaintextDataKey; + + private DecryptionMaterials(Builder builder) { + this._algorithmSuite = builder._algorithmSuite; + this._encryptionContext = builder._encryptionContext; + this._plaintextDataKey = builder._plaintextDataKey; + } + + static public Builder builder() { + return new Builder(); + } + + public AlgorithmSuite algorithmSuite() { + return _algorithmSuite; + } + + public Map encryptionContext() { + return _encryptionContext; + } + + public byte[] plaintextDataKey() { + return _plaintextDataKey; + } + + public SecretKey dataKey() { + return new SecretKeySpec(_plaintextDataKey, "AES"); + } + + public Builder toBuilder() { + return new Builder() + .algorithmSuite(_algorithmSuite) + .encryptionContext(_encryptionContext) + .plaintextDataKey(_plaintextDataKey); + } + + static public class Builder { + + private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + private Map _encryptionContext = Collections.emptyMap(); + private byte[] _plaintextDataKey = null; + + private Builder() { + } + + public Builder algorithmSuite(AlgorithmSuite algorithmSuite) { + _algorithmSuite = algorithmSuite; + return this; + } + + public Builder encryptionContext(Map encryptionContext) { + _encryptionContext = encryptionContext == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(encryptionContext); + return this; + } + + public Builder plaintextDataKey(byte[] plaintextDataKey) { + _plaintextDataKey = plaintextDataKey == null ? null : plaintextDataKey.clone(); + return this; + } + + public DecryptionMaterials build() { + return new DecryptionMaterials(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/DefaultDataKeyGenerator.java b/src/main/java/software/amazon/encryption/s3/materials/DefaultDataKeyGenerator.java new file mode 100644 index 000000000..920256c02 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/DefaultDataKeyGenerator.java @@ -0,0 +1,22 @@ +package software.amazon.encryption.s3.materials; + +import java.security.NoSuchAlgorithmException; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; + +public class DefaultDataKeyGenerator implements DataKeyGenerator { + + public SecretKey generateDataKey(AlgorithmSuite algorithmSuite) { + KeyGenerator generator; + try { + generator = KeyGenerator.getInstance(algorithmSuite.dataKeyAlgorithm()); + } catch (NoSuchAlgorithmException e) { + throw new S3EncryptionClientException("Unable to generate a(n) " + algorithmSuite.dataKeyAlgorithm() + " data key", e); + } + + generator.init(algorithmSuite.dataKeyLengthBits()); + return generator.generateKey(); + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/DefaultMaterialsManager.java b/src/main/java/software/amazon/encryption/s3/materials/DefaultMaterialsManager.java new file mode 100644 index 000000000..7e6562323 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/DefaultMaterialsManager.java @@ -0,0 +1,31 @@ +package software.amazon.encryption.s3.materials; + +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; + +public class DefaultMaterialsManager implements MaterialsManager { + private final Keyring _keyring; + + + public DefaultMaterialsManager(Keyring keyring) { + _keyring = keyring; + } + + public EncryptionMaterials getEncryptionMaterials(EncryptionMaterialsRequest request) { + EncryptionMaterials materials = EncryptionMaterials.builder() + .algorithmSuite(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF) + .encryptionContext(request.encryptionContext()) + .build(); + + return _keyring.onEncrypt(materials); + } + + public DecryptionMaterials decryptMaterials(DecryptMaterialsRequest request) { + DecryptionMaterials materials = DecryptionMaterials.builder() + .algorithmSuite(request.algorithmSuite()) + .encryptionContext(request.encryptionContext()) + .build(); + + return _keyring.onDecrypt(materials, request.encryptedDataKeys()); + } + +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/EncryptedDataKey.java b/src/main/java/software/amazon/encryption/s3/materials/EncryptedDataKey.java new file mode 100644 index 000000000..4d61a4173 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/EncryptedDataKey.java @@ -0,0 +1,62 @@ +package software.amazon.encryption.s3.materials; + +public class EncryptedDataKey { + + // forms the "domain" of the key e.g. "aws-kms" + private final String _keyProviderId; + + // a unique identifer e.g. an ARN + private final byte[] _keyProviderInfo; + private final byte[] _ciphertext; + + private EncryptedDataKey(Builder builder) { + this._keyProviderId = builder._keyProviderId; + this._keyProviderInfo = builder._keyProviderInfo; + this._ciphertext = builder._ciphertext; + } + + static public Builder builder() { + return new Builder(); + } + + public String keyProviderId() { + return _keyProviderId; + } + + public byte[] keyProviderInfo() { + return _keyProviderInfo; + } + + public byte[] ciphertext() { + return _ciphertext; + } + + static public class Builder { + + private String _keyProviderId = null; + private byte[] _keyProviderInfo = null; + private byte[] _ciphertext = null; + + private Builder() { + } + + public Builder keyProviderId(String keyProviderId) { + _keyProviderId = keyProviderId; + return this; + } + + public Builder keyProviderInfo(byte[] keyProviderInfo) { + _keyProviderInfo = keyProviderInfo == null ? null : keyProviderInfo.clone(); + return this; + } + + public Builder ciphertext(byte[] ciphertext) { + _ciphertext = ciphertext == null ? null : ciphertext.clone(); + return this; + } + + public EncryptedDataKey build() { + return new EncryptedDataKey(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java b/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java new file mode 100644 index 000000000..2c35a24d6 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterials.java @@ -0,0 +1,99 @@ +package software.amazon.encryption.s3.materials; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import software.amazon.encryption.s3.algorithms.AlgorithmSuite; + +final public class EncryptionMaterials { + + // Identifies what sort of crypto algorithms we want to use + private final AlgorithmSuite _algorithmSuite; + + // Additional information passed into encrypted that is required on decryption as well + // Should NOT contain sensitive information + private final Map _encryptionContext; + + private final List _encryptedDataKeys; + private final byte[] _plaintextDataKey; + + private EncryptionMaterials(Builder builder) { + this._algorithmSuite = builder._algorithmSuite; + this._encryptionContext = builder._encryptionContext; + this._encryptedDataKeys = builder._encryptedDataKeys; + this._plaintextDataKey = builder._plaintextDataKey; + } + + static public Builder builder() { + return new Builder(); + } + + public AlgorithmSuite algorithmSuite() { + return _algorithmSuite; + } + + public Map encryptionContext() { + return _encryptionContext; + } + + public List encryptedDataKeys() { + return _encryptedDataKeys; + } + + public byte[] plaintextDataKey() { + return _plaintextDataKey; + } + + public SecretKey dataKey() { + return new SecretKeySpec(_plaintextDataKey, "AES"); + } + + public Builder toBuilder() { + return new Builder() + .algorithmSuite(_algorithmSuite) + .encryptionContext(_encryptionContext) + .encryptedDataKeys(_encryptedDataKeys) + .plaintextDataKey(_plaintextDataKey); + } + + static public class Builder { + + private AlgorithmSuite _algorithmSuite = AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF; + private Map _encryptionContext = Collections.emptyMap(); + private List _encryptedDataKeys = Collections.emptyList(); + private byte[] _plaintextDataKey = null; + + private Builder() { + } + + public Builder algorithmSuite(AlgorithmSuite algorithmSuite) { + _algorithmSuite = algorithmSuite; + return this; + } + + public Builder encryptionContext(Map encryptionContext) { + _encryptionContext = encryptionContext == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(encryptionContext); + return this; + } + + public Builder encryptedDataKeys(List encryptedDataKeys) { + _encryptedDataKeys = encryptedDataKeys == null + ? Collections.emptyList() + : Collections.unmodifiableList(encryptedDataKeys); + return this; + } + + public Builder plaintextDataKey(byte[] plaintextDataKey) { + _plaintextDataKey = plaintextDataKey == null ? null : plaintextDataKey.clone(); + return this; + } + + public EncryptionMaterials build() { + return new EncryptionMaterials(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterialsRequest.java b/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterialsRequest.java new file mode 100644 index 000000000..85364e4db --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/EncryptionMaterialsRequest.java @@ -0,0 +1,40 @@ +package software.amazon.encryption.s3.materials; + +import java.util.Collections; +import java.util.Map; + +final public class EncryptionMaterialsRequest { + + private final Map _encryptionContext; + + private EncryptionMaterialsRequest(Builder builder) { + this._encryptionContext = builder._encryptionContext; + } + + static public Builder builder() { + return new Builder(); + } + + public Map encryptionContext() { + return _encryptionContext; + } + + static public class Builder { + + private Map _encryptionContext = Collections.emptyMap(); + + private Builder() { + } + + public Builder encryptionContext(Map encryptionContext) { + _encryptionContext = encryptionContext == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(encryptionContext); + return this; + } + + public EncryptionMaterialsRequest build() { + return new EncryptionMaterialsRequest(this); + } + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/Keyring.java b/src/main/java/software/amazon/encryption/s3/materials/Keyring.java new file mode 100644 index 000000000..0e313bab9 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/Keyring.java @@ -0,0 +1,12 @@ +package software.amazon.encryption.s3.materials; + +import java.util.List; + +/** + * Keyring defines the interface for wrapping data keys. A {@link MaterialsManager} will use + * keyrings to encrypt and decrypt data keys. + */ +public interface Keyring { + EncryptionMaterials onEncrypt(final EncryptionMaterials materials); + DecryptionMaterials onDecrypt(final DecryptionMaterials materials, final List encryptedDataKeys); +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/MaterialsManager.java b/src/main/java/software/amazon/encryption/s3/materials/MaterialsManager.java new file mode 100644 index 000000000..71277b563 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/MaterialsManager.java @@ -0,0 +1,6 @@ +package software.amazon.encryption.s3.materials; + +public interface MaterialsManager { + EncryptionMaterials getEncryptionMaterials(EncryptionMaterialsRequest request); + DecryptionMaterials decryptMaterials(DecryptMaterialsRequest request); +}