Skip to content

Commit e541bc6

Browse files
authored
Merge pull request #1336 from data-integrations/add-tink-encryption-tool
Add a tool to generate sample encrypted GCS files
2 parents d4c29cd + 2a905b3 commit e541bc6

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright © 2023 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.plugin.gcp.gcs.source;
18+
19+
import com.google.cloud.storage.BlobId;
20+
import com.google.cloud.storage.BlobInfo;
21+
import com.google.cloud.storage.Storage;
22+
import com.google.cloud.storage.StorageOptions;
23+
import com.google.crypto.tink.Aead;
24+
import com.google.crypto.tink.JsonKeysetWriter;
25+
import com.google.crypto.tink.KeysetHandle;
26+
import com.google.crypto.tink.KeysetWriter;
27+
import com.google.crypto.tink.KmsClients;
28+
import com.google.crypto.tink.StreamingAead;
29+
import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
30+
import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
31+
import com.google.crypto.tink.streamingaead.StreamingAeadKeyTemplates;
32+
import org.json.JSONObject;
33+
34+
import java.io.ByteArrayOutputStream;
35+
import java.io.OutputStream;
36+
import java.nio.file.Files;
37+
import java.nio.file.Paths;
38+
import java.security.GeneralSecurityException;
39+
import java.util.Base64;
40+
41+
import static java.nio.charset.StandardCharsets.UTF_8;
42+
43+
/**
44+
* This tool is adapted from the
45+
* <a href="https://cloud.google.com/kms/docs/client-side-encryption#java">KMS example</a>.
46+
* This can be used to upload files to GCS to test the decryption logic in the GCS source.
47+
* Follow https://cloud.google.com/kms/docs/create-key for instructions on creating a KMS key.
48+
*
49+
* <p>
50+
* A command-line utility for encrypting small files with envelope encryption and uploading the
51+
* results to GCS.
52+
*
53+
* <p>The CLI takes the following required arguments:
54+
*
55+
* <ul>
56+
* <li>kek-uri: The URI for the Cloud KMS key to be used for envelope encryption.
57+
* Should be of the form gcp-kms://projects/[project]/locations/[loc]/keyRings/[keyring]/cryptoKeys/[key].
58+
* <li>gcp-project-id: The ID of the GCP project hosting the GCS blobs that you want to encrypt or
59+
* decrypt.
60+
* <li>local-input-file: Read the plaintext from this local file.
61+
* <li>gcs-output-blob: Write the encryption result to this blob in GCS. A corresponding .metadata file will also
62+
* be written, containing the information the {@link TinkDecryptor} expects.
63+
* </ul>
64+
*/
65+
public final class GCSTinkTool {
66+
private static final String GCS_PATH_PREFIX = "gs://";
67+
68+
public static void main(String[] args) throws Exception {
69+
if (args.length != 4) {
70+
System.err.printf("Expected 4 parameters, got %d\n", args.length);
71+
System.err.println(
72+
"Usage: java GcsEnvelopeAeadExample kek-uri gcp-project-id input-file output-file");
73+
System.exit(1);
74+
}
75+
String kekUri = args[0];
76+
String gcpProjectId = args[1];
77+
78+
// Initialise Tink: register all Streaming AEAD key types with the Tink runtime
79+
StreamingAeadConfig.register();
80+
81+
// Read the GCP credentials and set up client
82+
try {
83+
KmsClients.add(new GcpKmsClient(kekUri).withDefaultCredentials());
84+
} catch (GeneralSecurityException ex) {
85+
System.err.println("Error initializing GCP client: " + ex);
86+
System.exit(1);
87+
}
88+
89+
// Create envelope AEAD primitive using AES256 GCM for encrypting the data
90+
KeysetHandle handle = KeysetHandle.generateNew(StreamingAeadKeyTemplates.AES256_GCM_HKDF_4KB);
91+
StreamingAead aead = handle.getPrimitive(StreamingAead.class);
92+
93+
Storage storage =
94+
StorageOptions.newBuilder()
95+
.setProjectId(gcpProjectId)
96+
.build()
97+
.getService();
98+
99+
// Encrypt the local file
100+
byte[] input = Files.readAllBytes(Paths.get(args[2]));
101+
String gcsBlobPath = args[3];
102+
// This will bind the encryption to the location of the GCS blob. That if, if you rename or
103+
// move the blob to a different bucket, decryption will fail.
104+
// See https://developers.google.com/tink/aead#associated_data.
105+
byte[] associatedData = gcsBlobPath.getBytes(UTF_8);
106+
ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream();
107+
try (OutputStream encryptingOutputStream = aead.newEncryptingStream(ciphertextOutputStream, associatedData)) {
108+
encryptingOutputStream.write(input);
109+
}
110+
byte[] ciphertext = ciphertextOutputStream.toByteArray();
111+
// Upload encrypted file to GCS
112+
writeToGcs(storage, gcsBlobPath, ciphertext);
113+
// Upload metadata file to GCS
114+
String metadataBlobPath = gcsBlobPath + ".metadata";
115+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
116+
KeysetWriter keySetWriter = JsonKeysetWriter.withOutputStream(bos);
117+
Aead keysetAead = KmsClients.get(kekUri).getAead(kekUri);
118+
handle.write(keySetWriter, keysetAead);
119+
JSONObject keySetObj = new JSONObject(new String(bos.toByteArray(), UTF_8));
120+
121+
JSONObject metadataObj = new JSONObject();
122+
metadataObj.put("kms", kekUri);
123+
metadataObj.put("aad", Base64.getEncoder().encodeToString(associatedData));
124+
metadataObj.put("keyset", keySetObj);
125+
// verify it is valid
126+
TinkDecryptor.getDecryptInfo(metadataObj);
127+
128+
writeToGcs(storage, metadataBlobPath, metadataObj.toString(4).getBytes(UTF_8));
129+
130+
System.exit(0);
131+
}
132+
133+
private static void writeToGcs(Storage storage, String path, byte[] content) {
134+
String bucketName = getBucketName(path);
135+
String objectName = getObjectName(path);
136+
BlobId blobId = BlobId.of(bucketName, objectName);
137+
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
138+
storage.create(blobInfo, content);
139+
}
140+
141+
private static String getBucketName(String gcsBlobPath) {
142+
if (!gcsBlobPath.startsWith(GCS_PATH_PREFIX)) {
143+
throw new IllegalArgumentException(
144+
"GCS blob paths must start with gs://, got " + gcsBlobPath);
145+
}
146+
147+
String bucketAndObjectName = gcsBlobPath.substring(GCS_PATH_PREFIX.length());
148+
int firstSlash = bucketAndObjectName.indexOf("/");
149+
if (firstSlash == -1) {
150+
throw new IllegalArgumentException(
151+
"GCS blob paths must have format gs://my-bucket-name/my-object-name, got " + gcsBlobPath);
152+
}
153+
return bucketAndObjectName.substring(0, firstSlash);
154+
}
155+
156+
private static String getObjectName(String gcsBlobPath) {
157+
if (!gcsBlobPath.startsWith(GCS_PATH_PREFIX)) {
158+
throw new IllegalArgumentException(
159+
"GCS blob paths must start with gs://, got " + gcsBlobPath);
160+
}
161+
162+
String bucketAndObjectName = gcsBlobPath.substring(GCS_PATH_PREFIX.length());
163+
int firstSlash = bucketAndObjectName.indexOf("/");
164+
if (firstSlash == -1) {
165+
throw new IllegalArgumentException(
166+
"GCS blob paths must have format gs://my-bucket-name/my-object-name, got " + gcsBlobPath);
167+
}
168+
return bucketAndObjectName.substring(firstSlash + 1);
169+
}
170+
}

src/main/java/io/cdap/plugin/gcp/gcs/source/TinkDecryptor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ private DecryptInfo getDecryptInfo(FileSystem fs, Path path) throws IOException
111111
metadata = new JSONObject(new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8));
112112
}
113113

114+
// Create the DecryptInfo
115+
return getDecryptInfo(metadata);
116+
}
117+
118+
static DecryptInfo getDecryptInfo(JSONObject metadata) throws IOException {
114119
// Create the DecryptInfo
115120
try {
116121
String kmsURI = metadata.getString(KMS);

0 commit comments

Comments
 (0)