xref: /aosp_15_r20/external/tink/java_src/examples/gcs/GcsEnvelopeAeadExample.java (revision e7b1675dde1b92d52ec075b0a92829627f2c52a5)
1*e7b1675dSTing-Kang Chang /*
2*e7b1675dSTing-Kang Chang  * Copyright 2021 Google LLC
3*e7b1675dSTing-Kang Chang  *
4*e7b1675dSTing-Kang Chang  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5*e7b1675dSTing-Kang Chang  * in compliance with the License. You may obtain a copy of the License at
6*e7b1675dSTing-Kang Chang  *
7*e7b1675dSTing-Kang Chang  * http://www.apache.org/licenses/LICENSE-2.0
8*e7b1675dSTing-Kang Chang  *
9*e7b1675dSTing-Kang Chang  * Unless required by applicable law or agreed to in writing, software distributed under the License
10*e7b1675dSTing-Kang Chang  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11*e7b1675dSTing-Kang Chang  * or implied. See the License for the specific language governing permissions and limitations under
12*e7b1675dSTing-Kang Chang  * the License.
13*e7b1675dSTing-Kang Chang  */
14*e7b1675dSTing-Kang Chang // [START gcs-envelope-aead-example]
15*e7b1675dSTing-Kang Chang package gcs;
16*e7b1675dSTing-Kang Chang 
17*e7b1675dSTing-Kang Chang import static java.nio.charset.StandardCharsets.UTF_8;
18*e7b1675dSTing-Kang Chang 
19*e7b1675dSTing-Kang Chang import com.google.auth.oauth2.GoogleCredentials;
20*e7b1675dSTing-Kang Chang import com.google.cloud.storage.BlobId;
21*e7b1675dSTing-Kang Chang import com.google.cloud.storage.BlobInfo;
22*e7b1675dSTing-Kang Chang import com.google.cloud.storage.Storage;
23*e7b1675dSTing-Kang Chang import com.google.cloud.storage.StorageOptions;
24*e7b1675dSTing-Kang Chang import com.google.crypto.tink.Aead;
25*e7b1675dSTing-Kang Chang import com.google.crypto.tink.KeyTemplates;
26*e7b1675dSTing-Kang Chang import com.google.crypto.tink.KeysetHandle;
27*e7b1675dSTing-Kang Chang import com.google.crypto.tink.aead.AeadConfig;
28*e7b1675dSTing-Kang Chang import com.google.crypto.tink.aead.KmsEnvelopeAeadKeyManager;
29*e7b1675dSTing-Kang Chang import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
30*e7b1675dSTing-Kang Chang import java.io.File;
31*e7b1675dSTing-Kang Chang import java.io.FileInputStream;
32*e7b1675dSTing-Kang Chang import java.io.FileOutputStream;
33*e7b1675dSTing-Kang Chang import java.nio.file.Files;
34*e7b1675dSTing-Kang Chang import java.nio.file.Paths;
35*e7b1675dSTing-Kang Chang import java.security.GeneralSecurityException;
36*e7b1675dSTing-Kang Chang import java.util.Arrays;
37*e7b1675dSTing-Kang Chang import java.util.Optional;
38*e7b1675dSTing-Kang Chang 
39*e7b1675dSTing-Kang Chang /**
40*e7b1675dSTing-Kang Chang  * A command-line utility for encrypting small files with envelope encryption and uploading the
41*e7b1675dSTing-Kang Chang  * results to GCS.
42*e7b1675dSTing-Kang Chang  *
43*e7b1675dSTing-Kang Chang  * <p>The CLI takes the following required arguments:
44*e7b1675dSTing-Kang Chang  *
45*e7b1675dSTing-Kang Chang  * <ul>
46*e7b1675dSTing-Kang Chang  *   <li>mode: "encrypt" or "decrypt" to indicate if you want to encrypt or decrypt.
47*e7b1675dSTing-Kang Chang  *   <li>kek-uri: The URI for the Cloud KMS key to be used for envelope encryption.
48*e7b1675dSTing-Kang Chang  *   <li>gcp-credential-file: Name of the file with the GCP credentials (in JSON format) that can
49*e7b1675dSTing-Kang Chang  *       access the Cloud KMS key and the GCS input/output blobs.
50*e7b1675dSTing-Kang Chang  *   <li>gcp-project-id: The ID of the GCP project hosting the GCS blobs that you want to encrypt or
51*e7b1675dSTing-Kang Chang  *       decrypt.
52*e7b1675dSTing-Kang Chang  * </ul>
53*e7b1675dSTing-Kang Chang  *
54*e7b1675dSTing-Kang Chang  * <p>When mode is "encrypt", it takes the following additional arguments:
55*e7b1675dSTing-Kang Chang  *
56*e7b1675dSTing-Kang Chang  * <ul>
57*e7b1675dSTing-Kang Chang  *   <li>local-input-file: Read the plaintext from this local file.
58*e7b1675dSTing-Kang Chang  *   <li>gcs-output-blob: Write the encryption result to this blob in GCS. The encryption result is
59*e7b1675dSTing-Kang Chang  *       bound to the location of this blob. That is, if you rename or move it to a different
60*e7b1675dSTing-Kang Chang  *       bucket, decryption will fail.
61*e7b1675dSTing-Kang Chang  * </ul>
62*e7b1675dSTing-Kang Chang  *
63*e7b1675dSTing-Kang Chang  * <p>When mode is "decrypt", it takes the following additional arguments:
64*e7b1675dSTing-Kang Chang  *
65*e7b1675dSTing-Kang Chang  * <ul>
66*e7b1675dSTing-Kang Chang  *   <li>gcs-input-blob: Read the ciphertext from this blob in GCS.
67*e7b1675dSTing-Kang Chang  *   <li>local-output-file: Write the decryption result to this local file.
68*e7b1675dSTing-Kang Chang  */
69*e7b1675dSTing-Kang Chang public final class GcsEnvelopeAeadExample {
70*e7b1675dSTing-Kang Chang   private static final String MODE_ENCRYPT = "encrypt";
71*e7b1675dSTing-Kang Chang   private static final String MODE_DECRYPT = "decrypt";
72*e7b1675dSTing-Kang Chang   private static final String GCS_PATH_PREFIX = "gs://";
73*e7b1675dSTing-Kang Chang 
main(String[] args)74*e7b1675dSTing-Kang Chang   public static void main(String[] args) throws Exception {
75*e7b1675dSTing-Kang Chang     if (args.length != 6) {
76*e7b1675dSTing-Kang Chang       System.err.printf("Expected 6 parameters, got %d\n", args.length);
77*e7b1675dSTing-Kang Chang       System.err.println(
78*e7b1675dSTing-Kang Chang           "Usage: java GcsEnvelopeAeadExample encrypt/decrypt kek-uri gcp-credential-file"
79*e7b1675dSTing-Kang Chang               + " gcp-project-id input-file output-file");
80*e7b1675dSTing-Kang Chang       System.exit(1);
81*e7b1675dSTing-Kang Chang     }
82*e7b1675dSTing-Kang Chang     String mode = args[0];
83*e7b1675dSTing-Kang Chang     String kekUri = args[1];
84*e7b1675dSTing-Kang Chang     String gcpCredentialFilename = args[2];
85*e7b1675dSTing-Kang Chang     String gcpProjectId = args[3];
86*e7b1675dSTing-Kang Chang 
87*e7b1675dSTing-Kang Chang     // Initialise Tink: register all AEAD key types with the Tink runtime
88*e7b1675dSTing-Kang Chang     AeadConfig.register();
89*e7b1675dSTing-Kang Chang 
90*e7b1675dSTing-Kang Chang     // Read the GCP credentials and set up client
91*e7b1675dSTing-Kang Chang     try {
92*e7b1675dSTing-Kang Chang       GcpKmsClient.register(Optional.of(kekUri), Optional.of(gcpCredentialFilename));
93*e7b1675dSTing-Kang Chang     } catch (GeneralSecurityException ex) {
94*e7b1675dSTing-Kang Chang       System.err.println("Error initializing GCP client: " + ex);
95*e7b1675dSTing-Kang Chang       System.exit(1);
96*e7b1675dSTing-Kang Chang     }
97*e7b1675dSTing-Kang Chang 
98*e7b1675dSTing-Kang Chang     // Create envelope AEAD primitive using AES256 GCM for encrypting the data
99*e7b1675dSTing-Kang Chang     Aead aead = null;
100*e7b1675dSTing-Kang Chang     try {
101*e7b1675dSTing-Kang Chang       KeysetHandle handle =
102*e7b1675dSTing-Kang Chang           KeysetHandle.generateNew(
103*e7b1675dSTing-Kang Chang               KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, KeyTemplates.get("AES256_GCM")));
104*e7b1675dSTing-Kang Chang       aead = handle.getPrimitive(Aead.class);
105*e7b1675dSTing-Kang Chang     } catch (GeneralSecurityException ex) {
106*e7b1675dSTing-Kang Chang       System.err.println("Error creating primitive: %s " + ex);
107*e7b1675dSTing-Kang Chang       System.exit(1);
108*e7b1675dSTing-Kang Chang     }
109*e7b1675dSTing-Kang Chang 
110*e7b1675dSTing-Kang Chang     GoogleCredentials credentials =
111*e7b1675dSTing-Kang Chang         GoogleCredentials.fromStream(new FileInputStream(gcpCredentialFilename))
112*e7b1675dSTing-Kang Chang             .createScoped(Arrays.asList("https://www.googleapis.com/auth/cloud-platform"));
113*e7b1675dSTing-Kang Chang     Storage storage =
114*e7b1675dSTing-Kang Chang         StorageOptions.newBuilder()
115*e7b1675dSTing-Kang Chang             .setProjectId(gcpProjectId)
116*e7b1675dSTing-Kang Chang             .setCredentials(credentials)
117*e7b1675dSTing-Kang Chang             .build()
118*e7b1675dSTing-Kang Chang             .getService();
119*e7b1675dSTing-Kang Chang 
120*e7b1675dSTing-Kang Chang     // Use the primitive to encrypt/decrypt files.
121*e7b1675dSTing-Kang Chang     if (MODE_ENCRYPT.equals(mode)) {
122*e7b1675dSTing-Kang Chang       // Encrypt the local file
123*e7b1675dSTing-Kang Chang       byte[] input = Files.readAllBytes(Paths.get(args[4]));
124*e7b1675dSTing-Kang Chang       String gcsBlobPath = args[5];
125*e7b1675dSTing-Kang Chang       // This will bind the encryption to the location of the GCS blob. That if, if you rename or
126*e7b1675dSTing-Kang Chang       // move the blob to a different bucket, decryption will fail.
127*e7b1675dSTing-Kang Chang       // See https://developers.google.com/tink/aead#associated_data.
128*e7b1675dSTing-Kang Chang       byte[] associatedData = gcsBlobPath.getBytes(UTF_8);
129*e7b1675dSTing-Kang Chang       byte[] ciphertext = aead.encrypt(input, associatedData);
130*e7b1675dSTing-Kang Chang 
131*e7b1675dSTing-Kang Chang       // Upload to GCS
132*e7b1675dSTing-Kang Chang       String bucketName = getBucketName(gcsBlobPath);
133*e7b1675dSTing-Kang Chang       String objectName = getObjectName(gcsBlobPath);
134*e7b1675dSTing-Kang Chang       BlobId blobId = BlobId.of(bucketName, objectName);
135*e7b1675dSTing-Kang Chang       BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
136*e7b1675dSTing-Kang Chang       storage.create(blobInfo, ciphertext);
137*e7b1675dSTing-Kang Chang     } else if (MODE_DECRYPT.equals(mode)) {
138*e7b1675dSTing-Kang Chang       // Download the GCS blob
139*e7b1675dSTing-Kang Chang       String gcsBlobPath = args[4];
140*e7b1675dSTing-Kang Chang       String bucketName = getBucketName(gcsBlobPath);
141*e7b1675dSTing-Kang Chang       String objectName = getObjectName(gcsBlobPath);
142*e7b1675dSTing-Kang Chang       byte[] input = storage.readAllBytes(bucketName, objectName);
143*e7b1675dSTing-Kang Chang 
144*e7b1675dSTing-Kang Chang       // Decrypt to a local file
145*e7b1675dSTing-Kang Chang       byte[] associatedData = gcsBlobPath.getBytes(UTF_8);
146*e7b1675dSTing-Kang Chang       byte[] plaintext = aead.decrypt(input, associatedData);
147*e7b1675dSTing-Kang Chang       File outputFile = new File(args[5]);
148*e7b1675dSTing-Kang Chang       try (FileOutputStream stream = new FileOutputStream(outputFile)) {
149*e7b1675dSTing-Kang Chang         stream.write(plaintext);
150*e7b1675dSTing-Kang Chang       }
151*e7b1675dSTing-Kang Chang     } else {
152*e7b1675dSTing-Kang Chang       System.err.println("The first argument must be either encrypt or decrypt, got: " + mode);
153*e7b1675dSTing-Kang Chang       System.exit(1);
154*e7b1675dSTing-Kang Chang     }
155*e7b1675dSTing-Kang Chang 
156*e7b1675dSTing-Kang Chang     System.exit(0);
157*e7b1675dSTing-Kang Chang   }
158*e7b1675dSTing-Kang Chang 
getBucketName(String gcsBlobPath)159*e7b1675dSTing-Kang Chang   private static String getBucketName(String gcsBlobPath) {
160*e7b1675dSTing-Kang Chang     if (!gcsBlobPath.startsWith(GCS_PATH_PREFIX)) {
161*e7b1675dSTing-Kang Chang       throw new IllegalArgumentException(
162*e7b1675dSTing-Kang Chang           "GCS blob paths must start with gs://, got " + gcsBlobPath);
163*e7b1675dSTing-Kang Chang     }
164*e7b1675dSTing-Kang Chang 
165*e7b1675dSTing-Kang Chang     String bucketAndObjectName = gcsBlobPath.substring(GCS_PATH_PREFIX.length());
166*e7b1675dSTing-Kang Chang     int firstSlash = bucketAndObjectName.indexOf("/");
167*e7b1675dSTing-Kang Chang     if (firstSlash == -1) {
168*e7b1675dSTing-Kang Chang       throw new IllegalArgumentException(
169*e7b1675dSTing-Kang Chang           "GCS blob paths must have format gs://my-bucket-name/my-object-name, got " + gcsBlobPath);
170*e7b1675dSTing-Kang Chang     }
171*e7b1675dSTing-Kang Chang     return bucketAndObjectName.substring(0, firstSlash);
172*e7b1675dSTing-Kang Chang   }
173*e7b1675dSTing-Kang Chang 
getObjectName(String gcsBlobPath)174*e7b1675dSTing-Kang Chang   private static String getObjectName(String gcsBlobPath) {
175*e7b1675dSTing-Kang Chang     if (!gcsBlobPath.startsWith(GCS_PATH_PREFIX)) {
176*e7b1675dSTing-Kang Chang       throw new IllegalArgumentException(
177*e7b1675dSTing-Kang Chang           "GCS blob paths must start with gs://, got " + gcsBlobPath);
178*e7b1675dSTing-Kang Chang     }
179*e7b1675dSTing-Kang Chang 
180*e7b1675dSTing-Kang Chang     String bucketAndObjectName = gcsBlobPath.substring(GCS_PATH_PREFIX.length());
181*e7b1675dSTing-Kang Chang     int firstSlash = bucketAndObjectName.indexOf("/");
182*e7b1675dSTing-Kang Chang     if (firstSlash == -1) {
183*e7b1675dSTing-Kang Chang       throw new IllegalArgumentException(
184*e7b1675dSTing-Kang Chang           "GCS blob paths must have format gs://my-bucket-name/my-object-name, got " + gcsBlobPath);
185*e7b1675dSTing-Kang Chang     }
186*e7b1675dSTing-Kang Chang     return bucketAndObjectName.substring(firstSlash + 1);
187*e7b1675dSTing-Kang Chang   }
188*e7b1675dSTing-Kang Chang 
GcsEnvelopeAeadExample()189*e7b1675dSTing-Kang Chang   private GcsEnvelopeAeadExample() {}
190*e7b1675dSTing-Kang Chang }
191*e7b1675dSTing-Kang Chang // [END gcs-envelope-aead-example]
192