Skip to content

Commit e3afe62

Browse files
authored
Add new method of generating key for GBEK (#36891)
* Add new method of generating key for GBEK * Java version * fix deps * Imports * Secret parsing tests * docs * more docs * formatting + test cleanup * lint * lint * lint * lint * import order * Deps + style exemption * reuse key: * reuse key * Feedback * Test fixes
1 parent 2110932 commit e3afe62

File tree

13 files changed

+782
-13
lines changed

13 files changed

+782
-13
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run.",
3-
"modification": 32
3+
"modification": 33
44
}
55

buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,7 @@ class BeamModulePlugin implements Plugin<Project> {
755755
google_cloud_dataflow_java_proto_library_all: "com.google.cloud.dataflow:google-cloud-dataflow-java-proto-library-all:0.5.160304",
756756
google_cloud_datastore_v1_proto_client : "com.google.cloud.datastore:datastore-v1-proto-client:2.33.0", // [bomupgrader] sets version
757757
google_cloud_firestore : "com.google.cloud:google-cloud-firestore", // google_cloud_platform_libraries_bom sets version
758+
google_cloud_kms : "com.google.cloud:google-cloud-kms", // google_cloud_platform_libraries_bom sets version
758759
google_cloud_pubsub : "com.google.cloud:google-cloud-pubsub", // google_cloud_platform_libraries_bom sets version
759760
google_cloud_pubsublite : "com.google.cloud:google-cloud-pubsublite", // google_cloud_platform_libraries_bom sets version
760761
// [bomupgrader] the BOM version is set by scripts/tools/bomupgrader.py. If update manually, also update
@@ -765,6 +766,7 @@ class BeamModulePlugin implements Plugin<Project> {
765766
google_cloud_spanner_bom : "com.google.cloud:google-cloud-spanner-bom:$google_cloud_spanner_version",
766767
google_cloud_spanner : "com.google.cloud:google-cloud-spanner", // google_cloud_platform_libraries_bom sets version
767768
google_cloud_spanner_test : "com.google.cloud:google-cloud-spanner:$google_cloud_spanner_version:tests",
769+
google_cloud_tink : "com.google.crypto.tink:tink:1.19.0",
768770
google_cloud_vertexai : "com.google.cloud:google-cloud-vertexai", // google_cloud_platform_libraries_bom sets version
769771
google_code_gson : "com.google.code.gson:gson:$google_code_gson_version",
770772
// google-http-client's version is explicitly declared for sdks/java/maven-archetypes/examples
@@ -866,6 +868,7 @@ class BeamModulePlugin implements Plugin<Project> {
866868
proto_google_cloud_datacatalog_v1beta1 : "com.google.api.grpc:proto-google-cloud-datacatalog-v1beta1", // google_cloud_platform_libraries_bom sets version
867869
proto_google_cloud_datastore_v1 : "com.google.api.grpc:proto-google-cloud-datastore-v1", // google_cloud_platform_libraries_bom sets version
868870
proto_google_cloud_firestore_v1 : "com.google.api.grpc:proto-google-cloud-firestore-v1", // google_cloud_platform_libraries_bom sets version
871+
proto_google_cloud_kms_v1 : "com.google.api.grpc:proto-google-cloud-kms-v1", // google_cloud_platform_libraries_bom sets version
869872
proto_google_cloud_pubsub_v1 : "com.google.api.grpc:proto-google-cloud-pubsub-v1", // google_cloud_platform_libraries_bom sets version
870873
proto_google_cloud_pubsublite_v1 : "com.google.api.grpc:proto-google-cloud-pubsublite-v1", // google_cloud_platform_libraries_bom sets version
871874
proto_google_cloud_secret_manager_v1 : "com.google.api.grpc:proto-google-cloud-secretmanager-v1", // google_cloud_platform_libraries_bom sets version

sdks/java/build-tools/src/main/resources/beam/checkstyle/suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
<!-- gRPC/protobuf exceptions -->
5858
<!-- Non-vendored gRPC/protobuf imports are allowed for files that depend on libraries that expose gRPC/protobuf in its public API -->
5959
<suppress id="ForbidNonVendoredGrpcProtobuf" files=".*sdk.*extensions.*protobuf.*" />
60+
<suppress id="ForbidNonVendoredGrpcProtobuf" files=".*sdk.*core.*GcpHsmGeneratedSecret.*" />
6061
<suppress id="ForbidNonVendoredGrpcProtobuf" files=".*sdk.*core.*GroupByEncryptedKeyTest.*" />
6162
<suppress id="ForbidNonVendoredGrpcProtobuf" files=".*sdk.*core.*GroupByKeyTest.*" />
6263
<suppress id="ForbidNonVendoredGrpcProtobuf" files=".*sdk.*core.*GroupByKeyIT.*" />

sdks/java/core/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ dependencies {
102102
shadow library.java.snappy_java
103103
shadow library.java.joda_time
104104
implementation enforcedPlatform(library.java.google_cloud_platform_libraries_bom)
105+
implementation library.java.gax
106+
implementation library.java.google_cloud_kms
107+
implementation library.java.proto_google_cloud_kms_v1
108+
implementation library.java.google_cloud_tink
105109
implementation library.java.google_cloud_secret_manager
106110
implementation library.java.proto_google_cloud_secret_manager_v1
107111
implementation library.java.protobuf_java
@@ -130,6 +134,8 @@ dependencies {
130134
shadowTest library.java.log4j2_api
131135
shadowTest library.java.jamm
132136
shadowTest 'com.google.cloud:google-cloud-secretmanager:2.75.0'
137+
shadowTest 'com.google.cloud:google-cloud-kms:2.75.0'
138+
shadowTest 'com.google.crypto.tink:tink:1.19.0'
133139
testRuntimeOnly library.java.slf4j_jdk14
134140
}
135141

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.util;
19+
20+
import com.google.api.gax.rpc.AlreadyExistsException;
21+
import com.google.api.gax.rpc.NotFoundException;
22+
import com.google.cloud.kms.v1.CryptoKeyName;
23+
import com.google.cloud.kms.v1.EncryptResponse;
24+
import com.google.cloud.kms.v1.KeyManagementServiceClient;
25+
import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
26+
import com.google.cloud.secretmanager.v1.ProjectName;
27+
import com.google.cloud.secretmanager.v1.Replication;
28+
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
29+
import com.google.cloud.secretmanager.v1.SecretName;
30+
import com.google.cloud.secretmanager.v1.SecretPayload;
31+
import com.google.cloud.secretmanager.v1.SecretVersionName;
32+
import com.google.crypto.tink.subtle.Hkdf;
33+
import com.google.protobuf.ByteString;
34+
import java.io.IOException;
35+
import java.security.GeneralSecurityException;
36+
import java.security.SecureRandom;
37+
import java.util.Base64;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
40+
41+
/**
42+
* A {@link org.apache.beam.sdk.util.Secret} manager implementation that generates a secret using
43+
* entropy from a GCP HSM key and stores it in Google Cloud Secret Manager. If the secret already
44+
* exists, it will be retrieved.
45+
*/
46+
public class GcpHsmGeneratedSecret implements Secret {
47+
private static final Logger LOG = LoggerFactory.getLogger(GcpHsmGeneratedSecret.class);
48+
private final String projectId;
49+
private final String locationId;
50+
private final String keyRingId;
51+
private final String keyId;
52+
private final String secretId;
53+
54+
private final SecureRandom random = new SecureRandom();
55+
56+
public GcpHsmGeneratedSecret(
57+
String projectId, String locationId, String keyRingId, String keyId, String jobName) {
58+
this.projectId = projectId;
59+
this.locationId = locationId;
60+
this.keyRingId = keyRingId;
61+
this.keyId = keyId;
62+
this.secretId = "HsmGeneratedSecret_" + jobName;
63+
}
64+
65+
/**
66+
* Returns the secret as a byte array. Assumes that the current active service account has
67+
* permissions to read the secret.
68+
*
69+
* @return The secret as a byte array.
70+
*/
71+
@Override
72+
public byte[] getSecretBytes() {
73+
try (SecretManagerServiceClient client = SecretManagerServiceClient.create()) {
74+
SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, "1");
75+
76+
try {
77+
AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName);
78+
return response.getPayload().getData().toByteArray();
79+
} catch (NotFoundException e) {
80+
LOG.info(
81+
"Secret version {} not found. Creating new secret and version.",
82+
secretVersionName.toString());
83+
}
84+
85+
ProjectName projectName = ProjectName.of(projectId);
86+
SecretName secretName = SecretName.of(projectId, secretId);
87+
try {
88+
com.google.cloud.secretmanager.v1.Secret secret =
89+
com.google.cloud.secretmanager.v1.Secret.newBuilder()
90+
.setReplication(
91+
Replication.newBuilder()
92+
.setAutomatic(Replication.Automatic.newBuilder().build()))
93+
.build();
94+
client.createSecret(projectName, secretId, secret);
95+
} catch (AlreadyExistsException e) {
96+
LOG.info("Secret {} already exists. Adding new version.", secretName.toString());
97+
}
98+
99+
byte[] newKey = generateDek();
100+
101+
try {
102+
// Always retrieve remote secret as source-of-truth in case another thread created it
103+
AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName);
104+
return response.getPayload().getData().toByteArray();
105+
} catch (NotFoundException e) {
106+
LOG.info(
107+
"Secret version {} not found after re-check. Creating new secret and version.",
108+
secretVersionName.toString());
109+
}
110+
111+
SecretPayload payload =
112+
SecretPayload.newBuilder().setData(ByteString.copyFrom(newKey)).build();
113+
client.addSecretVersion(secretName, payload);
114+
AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName);
115+
return response.getPayload().getData().toByteArray();
116+
117+
} catch (IOException | GeneralSecurityException e) {
118+
throw new RuntimeException("Failed to retrieve or create secret bytes", e);
119+
}
120+
}
121+
122+
private byte[] generateDek() throws IOException, GeneralSecurityException {
123+
int dekSize = 32;
124+
try (KeyManagementServiceClient client = KeyManagementServiceClient.create()) {
125+
// 1. Generate nonce_one. This doesn't need to have baked in randomness since the
126+
// actual randomness comes from KMS.
127+
byte[] nonceOne = new byte[dekSize];
128+
random.nextBytes(nonceOne);
129+
130+
// 2. Encrypt to get nonce_two
131+
CryptoKeyName keyName = CryptoKeyName.of(projectId, locationId, keyRingId, keyId);
132+
EncryptResponse response = client.encrypt(keyName, ByteString.copyFrom(nonceOne));
133+
byte[] nonceTwo = response.getCiphertext().toByteArray();
134+
135+
// 3. Generate DK
136+
byte[] dk = new byte[dekSize];
137+
random.nextBytes(dk);
138+
139+
// 4. Derive DEK using HKDF
140+
byte[] dek = Hkdf.computeHkdf("HmacSha256", dk, nonceTwo, new byte[0], dekSize);
141+
142+
// 5. Base64 encode
143+
return Base64.getUrlEncoder().encode(dek);
144+
}
145+
}
146+
147+
/**
148+
* Returns the project ID of the secret.
149+
*
150+
* @return The project ID as a String.
151+
*/
152+
public String getProjectId() {
153+
return projectId;
154+
}
155+
156+
/**
157+
* Returns the location ID of the secret.
158+
*
159+
* @return The location ID as a String.
160+
*/
161+
public String getLocationId() {
162+
return locationId;
163+
}
164+
165+
/**
166+
* Returns the key ring ID of the secret.
167+
*
168+
* @return The key ring ID as a String.
169+
*/
170+
public String getKeyRingId() {
171+
return keyRingId;
172+
}
173+
174+
/**
175+
* Returns the key ID of the secret.
176+
*
177+
* @return The key ID as a String.
178+
*/
179+
public String getKeyId() {
180+
return keyId;
181+
}
182+
183+
/**
184+
* Returns the secret ID of the secret.
185+
*
186+
* @return The secret ID as a String.
187+
*/
188+
public String getSecretId() {
189+
return secretId;
190+
}
191+
}

sdks/java/core/src/main/java/org/apache/beam/sdk/util/Secret.java

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.HashSet;
2424
import java.util.Map;
2525
import java.util.Set;
26+
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions;
2627

2728
/**
2829
* A secret management interface used for handling sensitive data.
@@ -70,16 +71,48 @@ static Secret parseSecretOption(String secretOption) {
7071
paramName, gcpSecretParams));
7172
}
7273
}
73-
String versionName = paramMap.get("version_name");
74-
if (versionName == null) {
75-
throw new RuntimeException(
76-
"version_name must contain a valid value for versionName parameter");
77-
}
74+
String versionName =
75+
Preconditions.checkNotNull(
76+
paramMap.get("version_name"),
77+
"version_name must contain a valid value for versionName parameter");
7878
return new GcpSecret(versionName);
79+
case "gcphsmgeneratedsecret":
80+
Set<String> gcpHsmGeneratedSecretParams =
81+
new HashSet<>(
82+
Arrays.asList("project_id", "location_id", "key_ring_id", "key_id", "job_name"));
83+
for (String paramName : paramMap.keySet()) {
84+
if (!gcpHsmGeneratedSecretParams.contains(paramName)) {
85+
throw new RuntimeException(
86+
String.format(
87+
"Invalid secret parameter %s, GcpHsmGeneratedSecret only supports the following parameters: %s",
88+
paramName, gcpHsmGeneratedSecretParams));
89+
}
90+
}
91+
String projectId =
92+
Preconditions.checkNotNull(
93+
paramMap.get("project_id"),
94+
"project_id must contain a valid value for projectId parameter");
95+
String locationId =
96+
Preconditions.checkNotNull(
97+
paramMap.get("location_id"),
98+
"location_id must contain a valid value for locationId parameter");
99+
String keyRingId =
100+
Preconditions.checkNotNull(
101+
paramMap.get("key_ring_id"),
102+
"key_ring_id must contain a valid value for keyRingId parameter");
103+
String keyId =
104+
Preconditions.checkNotNull(
105+
paramMap.get("key_id"), "key_id must contain a valid value for keyId parameter");
106+
String jobName =
107+
Preconditions.checkNotNull(
108+
paramMap.get("job_name"),
109+
"job_name must contain a valid value for jobName parameter");
110+
return new GcpHsmGeneratedSecret(projectId, locationId, keyRingId, keyId, jobName);
79111
default:
80112
throw new RuntimeException(
81113
String.format(
82-
"Invalid secret type %s, currently only GcpSecret is supported", secretType));
114+
"Invalid secret type %s, currently only GcpSecret and GcpHsmGeneratedSecret are supported",
115+
secretType));
83116
}
84117
}
85118
}

0 commit comments

Comments
 (0)