Skip to content

Commit 56fb8d7

Browse files
Add sample showing AWS encryption SDK (temporalio#700)
Add sample showing AWS encryption SDK
1 parent 5c96864 commit 56fb8d7

File tree

6 files changed

+294
-1
lines changed

6 files changed

+294
-1
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ See the README.md file in each main sample directory for cut/paste Gradle comman
142142

143143
- [**Set up OpenTracing and/or OpenTelemetry with Jaeger**](/core/src/main/java/io/temporal/samples/tracing): Demonstrates how to set up OpenTracing and/or OpenTelemetry and view traces using Jaeger.
144144

145+
#### Encryption Support
146+
147+
- [**Encrypted Payloads**](/core/src/main/java/io/temporal/samples/encryptedpayloads): Demonstrates how to use simple codec to encrypt and decrypt payloads.
148+
149+
- [**AWS Encryption SDK**](/core/src/main/java/io/temporal/samples/keymanagementencryption/awsencryptionsdk): Demonstrates how to use the AWS Encryption SDK to encrypt and decrypt payloads with AWS KMS.
145150

146151
<!-- @@@SNIPEND -->
147152

core/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ dependencies {
3030
implementation group: 'net.thisptr', name: 'jackson-jq', version: '1.0.0-preview.20240207'
3131
implementation group: 'commons-cli', name: 'commons-cli', version: '1.9.0'
3232

33+
// Used in AWS Encryption SDK sample
34+
implementation group: 'com.amazonaws', name: 'aws-encryption-sdk-java', version: '3.0.1'
35+
implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.2")
36+
implementation(platform("software.amazon.awssdk:bom:2.20.91"))
37+
implementation("software.amazon.awssdk:kms")
38+
implementation("software.amazon.awssdk:dynamodb")
39+
3340
// we don't update it to 2.1.0 because 2.1.0 requires Java 11
3441
implementation 'com.codingrodent:jackson-json-crypto:1.1.0'
3542

core/src/main/java/io/temporal/samples/hello/HelloActivity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public String getGreeting(String name) {
108108
}
109109

110110
/** Simple activity implementation, that concatenates two strings. */
111-
static class GreetingActivitiesImpl implements GreetingActivities {
111+
public static class GreetingActivitiesImpl implements GreetingActivities {
112112
private static final Logger log = LoggerFactory.getLogger(GreetingActivitiesImpl.class);
113113

114114
@Override
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.keymanagementencryption.awsencryptionsdk;
21+
22+
import io.temporal.client.WorkflowClient;
23+
import io.temporal.client.WorkflowClientOptions;
24+
import io.temporal.client.WorkflowOptions;
25+
import io.temporal.common.converter.CodecDataConverter;
26+
import io.temporal.common.converter.DefaultDataConverter;
27+
import io.temporal.samples.hello.HelloActivity;
28+
import io.temporal.serviceclient.WorkflowServiceStubs;
29+
import io.temporal.worker.Worker;
30+
import io.temporal.worker.WorkerFactory;
31+
import java.util.Collections;
32+
import software.amazon.cryptography.materialproviders.IKeyring;
33+
import software.amazon.cryptography.materialproviders.MaterialProviders;
34+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMultiKeyringInput;
35+
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
36+
37+
public class EncryptedPayloads {
38+
39+
static final String TASK_QUEUE = "EncryptedPayloads";
40+
41+
public static void main(String[] args) {
42+
// Configure your keyring. In this sample we are configuring a basic AWS KMS keyring, but the
43+
// AWS encryption SDK has multiple options depending on your use case.
44+
//
45+
// See more here:
46+
// https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/which-keyring.html
47+
String generatorKey = System.getenv("AWS_KEY_ARN");
48+
49+
final MaterialProviders materialProviders =
50+
MaterialProviders.builder()
51+
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
52+
.build();
53+
// Create the AWS KMS keyring
54+
final CreateAwsKmsMultiKeyringInput keyringInput =
55+
CreateAwsKmsMultiKeyringInput.builder().generator(generatorKey).build();
56+
final IKeyring kmsKeyring = materialProviders.CreateAwsKmsMultiKeyring(keyringInput);
57+
// gRPC stubs wrapper that talks to the local docker instance of temporal service.
58+
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
59+
// client that can be used to start and signal workflows
60+
WorkflowClient client =
61+
WorkflowClient.newInstance(
62+
service,
63+
WorkflowClientOptions.newBuilder()
64+
.setDataConverter(
65+
new CodecDataConverter(
66+
DefaultDataConverter.newDefaultInstance(),
67+
// Create our encryption codec
68+
Collections.singletonList(new KeyringCodec(kmsKeyring))))
69+
.build());
70+
71+
// worker factory that can be used to create workers for specific task queues
72+
WorkerFactory factory = WorkerFactory.newInstance(client);
73+
// Worker that listens on a task queue and hosts both workflow and activity implementations.
74+
Worker worker = factory.newWorker(TASK_QUEUE);
75+
// Register the workflows and activities
76+
worker.registerWorkflowImplementationTypes(HelloActivity.GreetingWorkflowImpl.class);
77+
worker.registerActivitiesImplementations(new HelloActivity.GreetingActivitiesImpl());
78+
// Start listening to the workflow and activity task queues.
79+
factory.start();
80+
81+
// Start a workflow execution.
82+
HelloActivity.GreetingWorkflow workflow =
83+
client.newWorkflowStub(
84+
HelloActivity.GreetingWorkflow.class,
85+
WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build());
86+
// Execute a workflow waiting for it to complete.
87+
String greeting = workflow.getGreeting("My Secret Friend");
88+
System.out.println(greeting);
89+
System.exit(0);
90+
}
91+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.samples.keymanagementencryption.awsencryptionsdk;
21+
22+
import com.amazonaws.encryptionsdk.AwsCrypto;
23+
import com.google.protobuf.ByteString;
24+
import com.google.protobuf.InvalidProtocolBufferException;
25+
import io.temporal.api.common.v1.Payload;
26+
import io.temporal.common.converter.EncodingKeys;
27+
import io.temporal.payload.codec.PayloadCodec;
28+
import io.temporal.payload.context.ActivitySerializationContext;
29+
import io.temporal.payload.context.HasWorkflowSerializationContext;
30+
import io.temporal.payload.context.SerializationContext;
31+
import io.temporal.workflow.unsafe.WorkflowUnsafe;
32+
import java.nio.charset.StandardCharsets;
33+
import java.util.Collections;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.stream.Collectors;
37+
import javax.annotation.Nonnull;
38+
import javax.annotation.Nullable;
39+
import org.jetbrains.annotations.NotNull;
40+
import software.amazon.cryptography.materialproviders.IKeyring;
41+
42+
/**
43+
* KeyringCodec is a {@link PayloadCodec} that encrypts and decrypts payloads using the AWS
44+
* Encryption SDK. It uses the provided {@link IKeyring} to encrypt and decrypt payloads. It can
45+
* optionally support using a {@link SerializationContext}.
46+
*/
47+
class KeyringCodec implements PayloadCodec {
48+
// Metadata encoding key for the AWS Encryption SDK
49+
static final ByteString METADATA_ENCODING =
50+
ByteString.copyFrom("awsencriptionsdk/binary/encrypted", StandardCharsets.UTF_8);
51+
52+
private final AwsCrypto crypto;
53+
private final IKeyring kmsKeyring;
54+
private final boolean useSerializationContext;
55+
@Nullable private final SerializationContext serializationContext;
56+
57+
/**
58+
* Constructs a new KeyringCodec with the provided {@link IKeyring}. The codec will not use a
59+
* {@link SerializationContext}.
60+
*
61+
* @param kmsKeyring the keyring to use for encryption and decryption.
62+
*/
63+
public KeyringCodec(IKeyring kmsKeyring) {
64+
this.crypto = AwsCrypto.standard();
65+
this.kmsKeyring = kmsKeyring;
66+
this.useSerializationContext = false;
67+
this.serializationContext = null;
68+
}
69+
70+
/**
71+
* Constructs a new KeyringCodec with the provided {@link IKeyring}.
72+
*
73+
* @param crypto the AWS Crypto object to use for encryption and decryption.
74+
* @param kmsKeyring the keyring to use for encryption and decryption.
75+
* @param useSerializationContext whether to use a {@link SerializationContext} for encoding and
76+
* decoding payloads.
77+
*/
78+
public KeyringCodec(AwsCrypto crypto, IKeyring kmsKeyring, boolean useSerializationContext) {
79+
this.crypto = crypto;
80+
this.kmsKeyring = kmsKeyring;
81+
this.useSerializationContext = useSerializationContext;
82+
this.serializationContext = null;
83+
}
84+
85+
private KeyringCodec(
86+
AwsCrypto crypto, IKeyring kmsKeyring, SerializationContext serializationContext) {
87+
this.crypto = crypto;
88+
this.kmsKeyring = kmsKeyring;
89+
this.useSerializationContext = true;
90+
this.serializationContext = serializationContext;
91+
}
92+
93+
@NotNull
94+
@Override
95+
public List<Payload> encode(@NotNull List<Payload> payloads) {
96+
// Disable deadlock detection for encoding payloads because this may make a network call
97+
// to encrypt the data.
98+
return WorkflowUnsafe.deadlockDetectorOff(
99+
() -> payloads.stream().map(this::encodePayload).collect(Collectors.toList()));
100+
}
101+
102+
@NotNull
103+
@Override
104+
public List<Payload> decode(@NotNull List<Payload> payloads) {
105+
// Disable deadlock detection for decoding payloads because this may make a network call
106+
// to decrypt the data.
107+
return WorkflowUnsafe.deadlockDetectorOff(
108+
() -> payloads.stream().map(this::decodePayload).collect(Collectors.toList()));
109+
}
110+
111+
@NotNull
112+
@Override
113+
public PayloadCodec withContext(@Nonnull SerializationContext context) {
114+
if (!useSerializationContext) {
115+
return this;
116+
}
117+
return new KeyringCodec(crypto, kmsKeyring, context);
118+
}
119+
120+
private Map<String, String> getEncryptionContext() {
121+
// If we are not using a serialization context, return an empty map
122+
// There may not be a serialization context if certain cases, such as when the codec is used
123+
// for encoding/decoding payloads for a Nexus operation.
124+
if (!useSerializationContext
125+
|| serializationContext == null
126+
|| !(serializationContext instanceof HasWorkflowSerializationContext)) {
127+
return Collections.emptyMap();
128+
}
129+
String workflowId = ((HasWorkflowSerializationContext) serializationContext).getWorkflowId();
130+
String activityType = null;
131+
if (serializationContext instanceof ActivitySerializationContext) {
132+
activityType = ((ActivitySerializationContext) serializationContext).getActivityType();
133+
}
134+
String signature = activityType != null ? workflowId + activityType : workflowId;
135+
return Collections.singletonMap("signature", signature);
136+
}
137+
138+
private Payload encodePayload(Payload payload) {
139+
byte[] plaintext = payload.toByteArray();
140+
byte[] ciphertext =
141+
crypto.encryptData(kmsKeyring, plaintext, getEncryptionContext()).getResult();
142+
return Payload.newBuilder()
143+
.setData(ByteString.copyFrom(ciphertext))
144+
.putMetadata(EncodingKeys.METADATA_ENCODING_KEY, METADATA_ENCODING)
145+
.build();
146+
}
147+
148+
private Payload decodePayload(Payload payload) {
149+
if (METADATA_ENCODING.equals(
150+
payload.getMetadataOrDefault(EncodingKeys.METADATA_ENCODING_KEY, null))) {
151+
byte[] ciphertext = payload.getData().toByteArray();
152+
byte[] plaintext =
153+
crypto.decryptData(kmsKeyring, ciphertext, getEncryptionContext()).getResult();
154+
try {
155+
return Payload.parseFrom(plaintext);
156+
} catch (InvalidProtocolBufferException e) {
157+
throw new RuntimeException(e);
158+
}
159+
}
160+
return payload;
161+
}
162+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## AWS Encryption SDK Sample
2+
3+
This sample demonstrates how a user can leverage the [AWS Encryption](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/java.html) SDK to build a `PayloadCodec` to encrypt and decrypt payloads using [AWS KMS](https://aws.amazon.com/kms/) and envelope encryption.
4+
5+
### About the AWS Encryption SDK:
6+
7+
>The AWS Encryption SDK is a client-side encryption library designed to make it easy for everyone to encrypt and decrypt data using industry standards and best practices. It enables you to focus on the core functionality of your application, rather than on how to best encrypt and decrypt your data. The AWS Encryption SDK is provided free of charge under the Apache 2.0 license.
8+
9+
For more details please see [Amazons Documentation](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/introduction.html)
10+
11+
### Choosing a Key Ring
12+
13+
This sample uses am [AWS KMS keyring](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html). This approach is convenient as you don't need to manage or secure your own keys. One drawback of this approach is it will require a call to KMS every time you need encrypt or decrypt data. If this is a concern you may want to consider using an [AWS KMS Hierarchical keyring](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-hierarchical-keyring.html).
14+
15+
Note: You can also use the AWS Encryption SDK without any AWS services using the raw keyrings.
16+
17+
For more details please see [Amazons Documentation on choosing a key ring](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/which-keyring.html).
18+
19+
### Running this sample
20+
21+
Make sure your AWS account credentials are up-to-date and can access KMS.
22+
23+
Export the following environment variables
24+
- `AWS_KEY_ARN`: Your AWS key ARN.
25+
26+
```bash
27+
./gradlew -q execute -PmainClass=io.temporal.samples.keymanagementencryption.awsencryptionsdk.EncryptedPayloads
28+
```

0 commit comments

Comments
 (0)