From 8bf12783cfd5674bfe0d884b00ceead8e9cdd34c Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Thu, 18 Mar 2021 11:10:51 +0000 Subject: [PATCH 1/6] Add an example for encrypting payloads. --- .../encryptedpayloads/CryptDataConverter.java | 193 ++++++++++++++++++ .../EncryptedPayloadsActivity.java | 120 +++++++++++ 2 files changed, 313 insertions(+) create mode 100644 src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java create mode 100644 src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java new file mode 100644 index 00000000..87b4be23 --- /dev/null +++ b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.samples.encryptedpayloads; + +import com.google.common.base.Defaults; +import com.google.protobuf.ByteString; +import io.temporal.api.common.v1.Payload; +import io.temporal.api.common.v1.Payloads; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.DataConverterException; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Optional; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +class EncodingKeys { + static final String METADATA_ENCODING_KEY = "encoding"; + static final String METADATA_WRAPPED_ENCODING_KEY = "wrapped-encoding"; + + static final String METADATA_ENCODING_RAW_NAME = "binary/plain"; + static final ByteString METADATA_ENCODING_RAW = ByteString.copyFrom(METADATA_ENCODING_RAW_NAME, + StandardCharsets.UTF_8); +} + +public class CryptDataConverter implements DataConverter { + private static final String CIPHER = "AES/GCM/NoPadding"; + private static final int GCM_NONCE_LENGTH_BYTE = 12; + private static final int GCM_TAG_LENGTH_BIT = 128; + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + private DataConverter converter; + + public CryptDataConverter(DataConverter converter) { + this.converter = converter; + } + + private SecretKey getKey() { + // Key can be fetched from KMS or other secure storage. + return new SecretKeySpec("test-key-test-key-test-key-test!".getBytes(UTF_8), "AES"); + } + + private static byte[] getNonce(int size) { + byte[] nonce = new byte[size]; + new SecureRandom().nextBytes(nonce); + return nonce; + } + + private byte[] encrypt(byte[] plainData) throws Exception { + SecretKey key = this.getKey(); + byte[] nonce = getNonce(GCM_NONCE_LENGTH_BYTE); + + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonce)); + + byte[] encryptedData = cipher.doFinal(plainData); + byte[] result = ByteBuffer.allocate(nonce.length + encryptedData.length).put(nonce).put(encryptedData).array(); + + return result; + } + + private byte[] decrypt(byte[] encryptedDataWithNonce) throws Exception { + SecretKey key = this.getKey(); + ByteBuffer buffer = ByteBuffer.wrap(encryptedDataWithNonce); + + byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTE]; + buffer.get(nonce); + byte[] encryptedData = new byte[buffer.remaining()]; + buffer.get(encryptedData); + + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonce)); + + return cipher.doFinal(encryptedData); + } + + @Override + public Optional toPayload(T value) throws DataConverterException { + Optional optionalPayload = converter.toPayload(value); + + if (!optionalPayload.isPresent()) { + return optionalPayload; + } + + Payload innerPayload = optionalPayload.get(); + + ByteString encoding; + try { + encoding = innerPayload.getMetadataOrThrow(EncodingKeys.METADATA_ENCODING_KEY); + } catch (Throwable e) { + throw new DataConverterException(e); + } + + byte[] encryptedData; + try { + encryptedData = encrypt(innerPayload.getData().toByteArray()); + } catch (Throwable e) { + throw new DataConverterException(e); + } + + Payload encryptedPayload = Payload.newBuilder(innerPayload) + .setData(ByteString.copyFrom(encryptedData)) + .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, EncodingKeys.METADATA_ENCODING_RAW) + .putMetadata(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY, encoding) + .build(); + + return Optional.of(encryptedPayload); + } + + @Override + public T fromPayload(Payload payload, Class valueClass, Type valueType) { + ByteString encoding; + try { + encoding = payload.getMetadataOrThrow(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY); + } catch (Throwable e) { + return converter.fromPayload(payload, valueClass, valueType); + } + + byte[] plainData; + try { + plainData = decrypt(payload.getData().toByteArray()); + } catch (Throwable e) { + throw new DataConverterException(e); + } + + Payload decryptedPayload = Payload.newBuilder(payload) + .setData(ByteString.copyFrom(plainData)) + .removeMetadata(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY) + .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, encoding) + .build(); + + return converter.fromPayload(decryptedPayload, valueClass, valueType); + } + + @Override + public Optional toPayloads(Object... values) throws DataConverterException { + if (values == null || values.length == 0) { + return Optional.empty(); + } + try { + Payloads.Builder result = Payloads.newBuilder(); + for (Object value : values) { + Optional payload = toPayload(value); + if (payload.isPresent()) { + result.addPayloads(payload.get()); + } else { + result.addPayloads(Payload.getDefaultInstance()); + } + } + return Optional.of(result.build()); + } catch (DataConverterException e) { + throw e; + } catch (Throwable e) { + throw new DataConverterException(e); + } + } + + @Override + public T fromPayloads(int index, Optional content, Class parameterType, Type genericParameterType) + throws DataConverterException { + if (!content.isPresent()) { + return (T) Defaults.defaultValue((Class) parameterType); + } + int count = content.get().getPayloadsCount(); + // To make adding arguments a backwards compatible change + if (index >= count) { + return (T) Defaults.defaultValue((Class) parameterType); + } + return fromPayload(content.get().getPayloads(index), parameterType, genericParameterType); + } +} diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java b/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java new file mode 100644 index 00000000..a37d2639 --- /dev/null +++ b/src/main/java/io/temporal/samples/encryptedpayloads/EncryptedPayloadsActivity.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.samples.encryptedpayloads; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; +import io.temporal.activity.ActivityOptions; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowOptions; +import io.temporal.common.converter.DataConverter; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; +import java.time.Duration; + +/** + * Hello World Temporal workflow that executes a single activity. Requires a local instance the + * Temporal service to be running. + */ +public class EncryptedPayloadsActivity { + + static final String TASK_QUEUE = "EncryptedPayloads"; + + /** Workflow interface has to have at least one method annotated with @WorkflowMethod. */ + @WorkflowInterface + public interface GreetingWorkflow { + @WorkflowMethod + String getGreeting(String name); + } + + /** Activity interface is just a POJI. */ + @ActivityInterface + public interface GreetingActivities { + @ActivityMethod + String composeGreeting(String greeting, String name); + } + + /** GreetingWorkflow implementation that calls GreetingsActivities#composeGreeting. */ + public static class GreetingWorkflowImpl implements GreetingWorkflow { + + /** + * Activity stub implements activity interface and proxies calls to it to Temporal activity + * invocations. Because activities are reentrant, only a single stub can be used for multiple + * activity invocations. + */ + private final GreetingActivities activities = + Workflow.newActivityStub( + GreetingActivities.class, + ActivityOptions.newBuilder().setScheduleToCloseTimeout(Duration.ofSeconds(2)).build()); + + @Override + public String getGreeting(String name) { + // This is a blocking call that returns only after the activity has completed. + return activities.composeGreeting("Hello", name); + } + } + + static class GreetingActivitiesImpl implements GreetingActivities { + @Override + public String composeGreeting(String greeting, String name) { + return greeting + " " + name + "!"; + } + } + + public static void main(String[] args) { + // gRPC stubs wrapper that talks to the local docker instance of temporal service. + WorkflowServiceStubs service = WorkflowServiceStubs.newInstance(); + // client that can be used to start and signal workflows + WorkflowClient client = + WorkflowClient.newInstance( + service, + WorkflowClientOptions.newBuilder() + .setDataConverter(new CryptDataConverter(DataConverter.getDefaultInstance())) + .build()); + + // worker factory that can be used to create workers for specific task queues + WorkerFactory factory = WorkerFactory.newInstance(client); + // Worker that listens on a task queue and hosts both workflow and activity implementations. + Worker worker = factory.newWorker(TASK_QUEUE); + // Workflows are stateful. So you need a type to create instances. + worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class); + // Activities are stateless and thread safe. So a shared instance is used. + worker.registerActivitiesImplementations(new GreetingActivitiesImpl()); + // Start listening to the workflow and activity task queues. + factory.start(); + + // Start a workflow execution. Usually this is done from another program. + // Uses task queue from the GreetingWorkflow @WorkflowMethod annotation. + GreetingWorkflow workflow = + client.newWorkflowStub( + GreetingWorkflow.class, WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build()); + // Execute a workflow waiting for it to complete. See {@link + // io.temporal.samples.hello.HelloSignal} + // for an example of starting workflow without waiting synchronously for its result. + String greeting = workflow.getGreeting("My Secret Friend"); + System.out.println(greeting); + System.exit(0); + } +} From 27fbb88dbe1238ba9ffac52b85b2067a20598170 Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Thu, 18 Mar 2021 16:58:07 +0000 Subject: [PATCH 2/6] Don't use exceptions for flow control. --- .../encryptedpayloads/CryptDataConverter.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java index 87b4be23..8594ec8c 100644 --- a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java +++ b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java @@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Optional; +import java.util.Map; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; @@ -106,11 +107,11 @@ public Optional toPayload(T value) throws DataConverterException { Payload innerPayload = optionalPayload.get(); - ByteString encoding; - try { - encoding = innerPayload.getMetadataOrThrow(EncodingKeys.METADATA_ENCODING_KEY); - } catch (Throwable e) { - throw new DataConverterException(e); + Map metadata = innerPayload.getMetadataMap(); + ByteString encoding = metadata.get(EncodingKeys.METADATA_ENCODING_KEY); + + if (encoding == null) { + return optionalPayload; } byte[] encryptedData; @@ -131,10 +132,10 @@ public Optional toPayload(T value) throws DataConverterException { @Override public T fromPayload(Payload payload, Class valueClass, Type valueType) { - ByteString encoding; - try { - encoding = payload.getMetadataOrThrow(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY); - } catch (Throwable e) { + Map metadata = payload.getMetadataMap(); + ByteString encoding = metadata.get(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY); + + if (encoding == null) { return converter.fromPayload(payload, valueClass, valueType); } From 17fd3f9b559fcac473ac0fd25ed6bd00c6d20f0e Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Fri, 19 Mar 2021 15:44:29 +0000 Subject: [PATCH 3/6] Prefer slightly more performant method. --- .../samples/encryptedpayloads/CryptDataConverter.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java index 8594ec8c..ac3ccf21 100644 --- a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java +++ b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java @@ -31,7 +31,6 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Optional; -import java.util.Map; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; @@ -107,9 +106,7 @@ public Optional toPayload(T value) throws DataConverterException { Payload innerPayload = optionalPayload.get(); - Map metadata = innerPayload.getMetadataMap(); - ByteString encoding = metadata.get(EncodingKeys.METADATA_ENCODING_KEY); - + ByteString encoding = innerPayload.getMetadataOrDefault(EncodingKeys.METADATA_ENCODING_KEY, null); if (encoding == null) { return optionalPayload; } @@ -132,9 +129,7 @@ public Optional toPayload(T value) throws DataConverterException { @Override public T fromPayload(Payload payload, Class valueClass, Type valueType) { - Map metadata = payload.getMetadataMap(); - ByteString encoding = metadata.get(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY); - + ByteString encoding = innerPayload.getMetadataOrDefault(METADATA_WRAPPED_ENCODING_KEY, null); if (encoding == null) { return converter.fromPayload(payload, valueClass, valueType); } From ecb994b1853aae63ac896e28e238f1eb0407d0e7 Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Fri, 19 Mar 2021 15:52:53 +0000 Subject: [PATCH 4/6] Fix pasto. --- .../temporal/samples/encryptedpayloads/CryptDataConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java index ac3ccf21..1454b7ab 100644 --- a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java +++ b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java @@ -129,7 +129,7 @@ public Optional toPayload(T value) throws DataConverterException { @Override public T fromPayload(Payload payload, Class valueClass, Type valueType) { - ByteString encoding = innerPayload.getMetadataOrDefault(METADATA_WRAPPED_ENCODING_KEY, null); + ByteString encoding = payload.getMetadataOrDefault(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY, null); if (encoding == null) { return converter.fromPayload(payload, valueClass, valueType); } From 8f54423b7ee8eef17065aef7df7d6e4b60a6be91 Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Mon, 12 Apr 2021 14:52:29 +0100 Subject: [PATCH 5/6] Update based on PR feedback. --- .../encryptedpayloads/CryptDataConverter.java | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java index 1454b7ab..a6551cc9 100644 --- a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java +++ b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java @@ -36,16 +36,11 @@ import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; -class EncodingKeys { +public class CryptDataConverter implements DataConverter { static final String METADATA_ENCODING_KEY = "encoding"; - static final String METADATA_WRAPPED_ENCODING_KEY = "wrapped-encoding"; - - static final String METADATA_ENCODING_RAW_NAME = "binary/plain"; - static final ByteString METADATA_ENCODING_RAW = ByteString.copyFrom(METADATA_ENCODING_RAW_NAME, - StandardCharsets.UTF_8); -} + static final ByteString METADATA_ENCODING = + ByteString.copyFrom("binary/encrypted", StandardCharsets.UTF_8); -public class CryptDataConverter implements DataConverter { private static final String CIPHER = "AES/GCM/NoPadding"; private static final int GCM_NONCE_LENGTH_BYTE = 12; private static final int GCM_TAG_LENGTH_BIT = 128; @@ -58,7 +53,8 @@ public CryptDataConverter(DataConverter converter) { } private SecretKey getKey() { - // Key can be fetched from KMS or other secure storage. + // Key must be fetched from KMS or other secure storage. + // Hard coded here only for example purposes. return new SecretKeySpec("test-key-test-key-test-key-test!".getBytes(UTF_8), "AES"); } @@ -76,7 +72,11 @@ private byte[] encrypt(byte[] plainData) throws Exception { cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BIT, nonce)); byte[] encryptedData = cipher.doFinal(plainData); - byte[] result = ByteBuffer.allocate(nonce.length + encryptedData.length).put(nonce).put(encryptedData).array(); + byte[] result = + ByteBuffer.allocate(nonce.length + encryptedData.length) + .put(nonce) + .put(encryptedData) + .array(); return result; } @@ -98,6 +98,10 @@ private byte[] decrypt(byte[] encryptedDataWithNonce) throws Exception { @Override public Optional toPayload(T value) throws DataConverterException { + return converter.toPayload(value); + } + + public Optional toEncryptedPayload(T value) throws DataConverterException { Optional optionalPayload = converter.toPayload(value); if (!optionalPayload.isPresent()) { @@ -106,47 +110,39 @@ public Optional toPayload(T value) throws DataConverterException { Payload innerPayload = optionalPayload.get(); - ByteString encoding = innerPayload.getMetadataOrDefault(EncodingKeys.METADATA_ENCODING_KEY, null); - if (encoding == null) { - return optionalPayload; - } - byte[] encryptedData; try { - encryptedData = encrypt(innerPayload.getData().toByteArray()); + encryptedData = encrypt(innerPayload.toByteArray()); } catch (Throwable e) { throw new DataConverterException(e); } - Payload encryptedPayload = Payload.newBuilder(innerPayload) - .setData(ByteString.copyFrom(encryptedData)) - .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, EncodingKeys.METADATA_ENCODING_RAW) - .putMetadata(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY, encoding) - .build(); + Payload encryptedPayload = + Payload.newBuilder() + .putMetadata(METADATA_ENCODING_KEY, METADATA_ENCODING) + .setData(ByteString.copyFrom(encryptedData)) + .build(); return Optional.of(encryptedPayload); } @Override public T fromPayload(Payload payload, Class valueClass, Type valueType) { - ByteString encoding = payload.getMetadataOrDefault(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY, null); - if (encoding == null) { + ByteString encoding = payload.getMetadataOrDefault(METADATA_ENCODING_KEY, null); + if (!encoding.equals(METADATA_ENCODING)) { return converter.fromPayload(payload, valueClass, valueType); } byte[] plainData; + Payload decryptedPayload; + try { plainData = decrypt(payload.getData().toByteArray()); + decryptedPayload = Payload.parseFrom(plainData); } catch (Throwable e) { throw new DataConverterException(e); } - Payload decryptedPayload = Payload.newBuilder(payload) - .setData(ByteString.copyFrom(plainData)) - .removeMetadata(EncodingKeys.METADATA_WRAPPED_ENCODING_KEY) - .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, encoding) - .build(); - return converter.fromPayload(decryptedPayload, valueClass, valueType); } @@ -158,7 +154,7 @@ public Optional toPayloads(Object... values) throws DataConverterExcep try { Payloads.Builder result = Payloads.newBuilder(); for (Object value : values) { - Optional payload = toPayload(value); + Optional payload = toEncryptedPayload(value); if (payload.isPresent()) { result.addPayloads(payload.get()); } else { @@ -174,7 +170,8 @@ public Optional toPayloads(Object... values) throws DataConverterExcep } @Override - public T fromPayloads(int index, Optional content, Class parameterType, Type genericParameterType) + public T fromPayloads( + int index, Optional content, Class parameterType, Type genericParameterType) throws DataConverterException { if (!content.isPresent()) { return (T) Defaults.defaultValue((Class) parameterType); From c6d68706f39b14711a05e44e7f2df08eb0b8472f Mon Sep 17 00:00:00 2001 From: Rob Holland Date: Tue, 13 Apr 2021 09:31:24 +0100 Subject: [PATCH 6/6] Record cipher and key id as metadata. This allows key rotation to be used. --- .../encryptedpayloads/CryptDataConverter.java | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java index a6551cc9..9924db7c 100644 --- a/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java +++ b/src/main/java/io/temporal/samples/encryptedpayloads/CryptDataConverter.java @@ -42,6 +42,13 @@ public class CryptDataConverter implements DataConverter { ByteString.copyFrom("binary/encrypted", StandardCharsets.UTF_8); private static final String CIPHER = "AES/GCM/NoPadding"; + + static final String METADATA_ENCRYPTION_CIPHER_KEY = "encryption-cipher"; + static final ByteString METADATA_ENCRYPTION_CIPHER = + ByteString.copyFrom(CIPHER, StandardCharsets.UTF_8); + + static final String METADATA_ENCRYPTION_KEY_ID_KEY = "encryption-key-id"; + private static final int GCM_NONCE_LENGTH_BYTE = 12; private static final int GCM_TAG_LENGTH_BIT = 128; private static final Charset UTF_8 = StandardCharsets.UTF_8; @@ -52,7 +59,16 @@ public CryptDataConverter(DataConverter converter) { this.converter = converter; } - private SecretKey getKey() { + private String getKeyId() { + // Currently there is no context available to vary which key is used. + // Use a fixed key for all payloads. + // This still supports key rotation as the key ID is recorded on payloads allowing + // decryption to use a previous key. + + return "test"; + } + + private SecretKey getKey(String keyId) { // Key must be fetched from KMS or other secure storage. // Hard coded here only for example purposes. return new SecretKeySpec("test-key-test-key-test-key-test!".getBytes(UTF_8), "AES"); @@ -64,8 +80,7 @@ private static byte[] getNonce(int size) { return nonce; } - private byte[] encrypt(byte[] plainData) throws Exception { - SecretKey key = this.getKey(); + private byte[] encrypt(byte[] plainData, SecretKey key) throws Exception { byte[] nonce = getNonce(GCM_NONCE_LENGTH_BYTE); Cipher cipher = Cipher.getInstance(CIPHER); @@ -81,8 +96,7 @@ private byte[] encrypt(byte[] plainData) throws Exception { return result; } - private byte[] decrypt(byte[] encryptedDataWithNonce) throws Exception { - SecretKey key = this.getKey(); + private byte[] decrypt(byte[] encryptedDataWithNonce, SecretKey key) throws Exception { ByteBuffer buffer = ByteBuffer.wrap(encryptedDataWithNonce); byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTE]; @@ -110,9 +124,12 @@ public Optional toEncryptedPayload(T value) throws DataConverterExc Payload innerPayload = optionalPayload.get(); + String keyId = getKeyId(); + SecretKey key = getKey(keyId); + byte[] encryptedData; try { - encryptedData = encrypt(innerPayload.toByteArray()); + encryptedData = encrypt(innerPayload.toByteArray(), key); } catch (Throwable e) { throw new DataConverterException(e); } @@ -120,6 +137,8 @@ public Optional toEncryptedPayload(T value) throws DataConverterExc Payload encryptedPayload = Payload.newBuilder() .putMetadata(METADATA_ENCODING_KEY, METADATA_ENCODING) + .putMetadata(METADATA_ENCRYPTION_CIPHER_KEY, METADATA_ENCRYPTION_CIPHER) + .putMetadata(METADATA_ENCRYPTION_KEY_ID_KEY, ByteString.copyFromUtf8(keyId)) .setData(ByteString.copyFrom(encryptedData)) .build(); @@ -133,11 +152,19 @@ public T fromPayload(Payload payload, Class valueClass, Type valueType) { return converter.fromPayload(payload, valueClass, valueType); } + String keyId; + try { + keyId = payload.getMetadataOrThrow(METADATA_ENCRYPTION_KEY_ID_KEY).toString(UTF_8); + } catch (Exception e) { + throw new DataConverterException(payload, valueClass, e); + } + SecretKey key = getKey(keyId); + byte[] plainData; Payload decryptedPayload; try { - plainData = decrypt(payload.getData().toByteArray()); + plainData = decrypt(payload.getData().toByteArray(), key); decryptedPayload = Payload.parseFrom(plainData); } catch (Throwable e) { throw new DataConverterException(e);