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