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