Skip to content

Commit 02ae002

Browse files
BrandonYtswast
authored andcommitted
Add a new example for using Customer-Supplied Encryption Keys.
1 parent 54efe62 commit 02ae002

File tree

3 files changed

+330
-1
lines changed

3 files changed

+330
-1
lines changed

storage/json-api/pom.xml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
</properties>
2020

2121
<build>
22+
<resources>
23+
<resource>
24+
<directory>${basedir}/src/main/resources</directory>
25+
<includes>
26+
<include>**/*</include>
27+
</includes>
28+
</resource>
29+
</resources>
2230
<plugins>
2331
<plugin>
2432
<groupId>org.codehaus.mojo</groupId>
@@ -42,7 +50,12 @@
4250
<dependency>
4351
<groupId>com.google.apis</groupId>
4452
<artifactId>google-api-services-storage</artifactId>
45-
<version>v1-rev18-1.19.0</version>
53+
<version>v1-rev65-1.21.0</version>
54+
</dependency>
55+
<dependency>
56+
<groupId>com.google.oauth-client</groupId>
57+
<artifactId>google-oauth-client-jetty</artifactId>
58+
<version>1.21.0</version>
4659
</dependency>
4760
<!-- Test Dependencies -->
4861
<dependency>
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import com.google.api.client.auth.oauth2.Credential;
2+
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
3+
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
4+
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
5+
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
6+
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
7+
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
8+
import com.google.api.client.http.HttpHeaders;
9+
import com.google.api.client.http.HttpTransport;
10+
import com.google.api.client.http.InputStreamContent;
11+
import com.google.api.client.json.JsonFactory;
12+
import com.google.api.client.json.jackson2.JacksonFactory;
13+
import com.google.api.client.util.store.DataStoreFactory;
14+
import com.google.api.client.util.store.FileDataStoreFactory;
15+
import com.google.api.services.storage.Storage;
16+
import com.google.api.services.storage.StorageScopes;
17+
import com.google.api.services.storage.model.RewriteResponse;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.InputStreamReader;
22+
import java.util.Collections;
23+
24+
/**
25+
* Demonstrates the use of GCS's CSEK features via the Java API client library
26+
*
27+
* This program demonstrates some quick, basic examples of using GCS's CSEK functionality.
28+
*
29+
* <p>When run, it begins by uploading an object named "encrypted_file.txt" to the specified bucket
30+
* that will be protected with a provided CSEK.</p>
31+
*
32+
* <p>Next, it will fetch that object by providing that same CSEK to GCS.</p>
33+
*
34+
* <p>Finally, it will rotate that key to a new value.</p>
35+
**/
36+
class CustomerSuppliedEncryptionKeysSamples {
37+
38+
private static final java.io.File DATA_STORE_DIR =
39+
new java.io.File(System.getProperty("user.home"), ".store/storage_sample");
40+
41+
// You can (and should) generate your own CSEK Key! Try running this from the command line:
42+
// python -c 'import base64; import os; print(base64.encodestring(os.urandom(32)))'
43+
// Also, these encryption keys are included here for simplicity, but please remember that
44+
// private keys should not be stored in source code.
45+
private static final String CSEK_KEY = "4RzDI0TeWa9M/nAvYH05qbCskPaSU/CFV5HeCxk0IUA=";
46+
47+
// You can use openssl to quickly calculate the hash of your key. Try running this:
48+
// openssl base64 -d <<< YOUR_KEY_FROM_ABOVE | openssl dgst -sha256 -binary | openssl base64
49+
private static final String CSEK_KEY_HASH = "aanjNC2nwso8e2FqcWILC3/Tt1YumvIwEj34kr6PRpI=";
50+
51+
// Used for the key rotation example
52+
private static final String ANOTHER_CESK_KEY = "oevtavYZC+TfGtV86kJBKTeytXAm1s2r3xIqam+QPKM=";
53+
private static final String ANOTHER_CSEK_KEY_HASH =
54+
"/gd0N3k3MK0SEDxnUiaswl0FFv6+5PHpo+5KD5SBCeA=";
55+
56+
private static final String OBJECT_NAME = "encrypted_file.txt";
57+
58+
/**
59+
* Downloads a CSEK-protected object from GCS. The download may continue in the background after
60+
* this method returns. The caller of this method is responsible for closing the input stream.
61+
*
62+
* @param storage A Storage object, ready for use
63+
* @param bucketName The name of the destination bucket
64+
* @param objectName The name of the destination object
65+
* @param base64CSEKey An AES256 key, encoded as a base64 string.
66+
* @param base64CSEKeyHash The SHA-256 hash of the above key, also encoded as a base64 string.
67+
* @throws IOException if there was some error download from GCS.
68+
*
69+
* @return An InputStream that contains the decrypted contents of the object.
70+
*/
71+
public static InputStream downloadObject(
72+
Storage storage,
73+
String bucketName,
74+
String objectName,
75+
String base64CSEKey,
76+
String base64CSEKeyHash)
77+
throws Exception {
78+
Storage.Objects.Get getObject = storage.objects().get(bucketName, objectName);
79+
80+
// If you're using AppEngine, turn off setDirectDownloadEnabled:
81+
// getObject.getMediaHttpDownloader().setDirectDownloadEnabled(false);
82+
83+
// Now set the CSEK headers
84+
final HttpHeaders httpHeaders = new HttpHeaders();
85+
httpHeaders.set("x-goog-encryption-algorithm", "AES256");
86+
httpHeaders.set("x-goog-encryption-key", base64CSEKey);
87+
httpHeaders.set("x-goog-encryption-key-sha256", base64CSEKeyHash);
88+
89+
// Since our request includes our private key as a header, it is a good idea to instruct caches
90+
// and proxies not to store this request.
91+
httpHeaders.setCacheControl("no-store");
92+
93+
getObject.setRequestHeaders(httpHeaders);
94+
95+
try {
96+
return getObject.executeMediaAsInputStream();
97+
} catch (GoogleJsonResponseException e) {
98+
System.out.println("Error downloading: " + e.getContent());
99+
System.exit(1);
100+
return null;
101+
}
102+
}
103+
104+
/**
105+
* Uploads an object to GCS, to be stored with a customer-supplied key (CSEK). The upload may
106+
* continue in the background after this method returns. The caller of this method is responsible
107+
* for closing the input stream.
108+
*
109+
* @param storage A Storage object, ready for use
110+
* @param bucketName The name of the destination bucket
111+
* @param objectName The name of the destination object
112+
* @param data An InputStream containing the contents of the object to upload
113+
* @param base64CSEKey An AES256 key, encoded as a base64 string.
114+
* @param base64CSEKeyHash The SHA-256 hash of the above key, also encoded as a base64 string.
115+
* @throws IOException if there was some error uploading to GCS.
116+
*/
117+
public static void uploadObject(
118+
Storage storage,
119+
String bucketName,
120+
String objectName,
121+
InputStream data,
122+
String base64CSEKey,
123+
String base64CSEKeyHash)
124+
throws IOException {
125+
InputStreamContent mediaContent = new InputStreamContent("text/plain", data);
126+
Storage.Objects.Insert insertObject =
127+
storage.objects().insert(bucketName, null, mediaContent).setName(objectName);
128+
// The client library's default gzip setting may cause objects to be stored with gzip encoding,
129+
// which can be desirable in some circumstances but has some disadvantages as well, such as
130+
// making it difficult to read only a certain range of the original object.
131+
insertObject.getMediaHttpUploader().setDisableGZipContent(true);
132+
133+
// Now set the CSEK headers
134+
final HttpHeaders httpHeaders = new HttpHeaders();
135+
httpHeaders.set("x-goog-encryption-algorithm", "AES256");
136+
httpHeaders.set("x-goog-encryption-key", base64CSEKey);
137+
httpHeaders.set("x-goog-encryption-key-sha256", base64CSEKeyHash);
138+
139+
// Since our request includes our private key as a header, it is a good idea to instruct caches
140+
// and proxies not to store this request.
141+
httpHeaders.setCacheControl("no-store");
142+
143+
insertObject.setRequestHeaders(httpHeaders);
144+
145+
try {
146+
insertObject.execute();
147+
} catch (GoogleJsonResponseException e) {
148+
System.out.println("Error uploading: " + e.getContent());
149+
System.exit(1);
150+
}
151+
}
152+
153+
/**
154+
* Given an existing, CSEK-protected object, changes the key used to store that object.
155+
*
156+
* @param storage A Storage object, ready for use
157+
* @param bucketName The name of the destination bucket
158+
* @param objectName The name of the destination object
159+
* @param originalBase64Key The AES256 key currently associated with this object,
160+
* encoded as a base64 string.
161+
* @param originalBase64KeyHash The SHA-256 hash of the above key,
162+
* also encoded as a base64 string.
163+
* @param newBase64Key An AES256 key which will replace the existing key,
164+
* encoded as a base64 string.
165+
* @param newBase64KeyHash The SHA-256 hash of the above key, also encoded as a base64 string.
166+
* @throws IOException if there was some error download from GCS.
167+
*/
168+
public static void rotateKey(
169+
Storage storage,
170+
String bucketName,
171+
String objectName,
172+
String originalBase64Key,
173+
String originalBase64KeyHash,
174+
String newBase64Key,
175+
String newBase64KeyHash)
176+
throws Exception {
177+
Storage.Objects.Rewrite rewriteObject =
178+
storage.objects().rewrite(bucketName, objectName, bucketName, objectName, null);
179+
180+
// Now set the CSEK headers
181+
final HttpHeaders httpHeaders = new HttpHeaders();
182+
183+
// Specify the exiting object's current CSEK.
184+
httpHeaders.set("x-goog-copy-source-encryption-algorithm", "AES256");
185+
httpHeaders.set("x-goog-copy-source-encryption-key", originalBase64Key);
186+
httpHeaders.set("x-goog-copy-source-encryption-key-sha256", originalBase64KeyHash);
187+
188+
// Specify the new CSEK that we would like to apply.
189+
httpHeaders.set("x-goog-encryption-algorithm", "AES256");
190+
httpHeaders.set("x-goog-encryption-key", newBase64Key);
191+
httpHeaders.set("x-goog-encryption-key-sha256", newBase64KeyHash);
192+
193+
// Since our request includes our private key as a header, it is a good idea to instruct caches
194+
// and proxies not to store this request.
195+
httpHeaders.setCacheControl("no-store");
196+
197+
rewriteObject.setRequestHeaders(httpHeaders);
198+
199+
try {
200+
RewriteResponse rewriteResponse = rewriteObject.execute();
201+
202+
// If an object is very large, you may need to continue making successive calls to
203+
// rewrite until the operation completes.
204+
while (!rewriteResponse.getDone()) {
205+
System.out.println("Rewrite did not complete. Resuming...");
206+
rewriteObject.setRewriteToken(rewriteResponse.getRewriteToken());
207+
rewriteResponse = rewriteObject.execute();
208+
}
209+
} catch (GoogleJsonResponseException e) {
210+
System.out.println("Error rotating key: " + e.getContent());
211+
System.exit(1);
212+
}
213+
}
214+
215+
public static void main(String[] args) throws Exception {
216+
if (args.length != 1) {
217+
System.out.println("\nPlease run this with one argument: "
218+
+ "the GCS bucket into which this program should upload an object.\n\n"
219+
+ "You can create a bucket using gsutil like this:\n\n\t"
220+
+ "gsutil mb gs://name-of-bucket\n\n");
221+
System.exit(1);
222+
}
223+
String bucketName = args[0];
224+
// CSEK, like the JSON API, may be used only via HTTPS.
225+
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
226+
DataStoreFactory dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR);
227+
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
228+
Credential credential = authorize(jsonFactory, httpTransport, dataStoreFactory);
229+
Storage storage =
230+
new Storage.Builder(httpTransport, jsonFactory, credential)
231+
.setApplicationName("JavaCSEKApiSample")
232+
.build();
233+
234+
InputStream dataToUpload = new ArbitrarilyLargeInputStream(10000000);
235+
236+
System.out.format("Uploading object gs://%s/%s using CSEK.\n", bucketName, OBJECT_NAME);
237+
uploadObject(storage, bucketName, OBJECT_NAME, dataToUpload, CSEK_KEY, CSEK_KEY_HASH);
238+
System.out.format("Downloading object gs://%s/%s using CSEK.\n", bucketName, OBJECT_NAME);
239+
InputStream objectData =
240+
downloadObject(storage, bucketName, OBJECT_NAME, CSEK_KEY, CSEK_KEY_HASH);
241+
readStream(objectData);
242+
System.out.println("Rotating object to use a different CSEK.");
243+
rotateKey(storage, bucketName, OBJECT_NAME, CSEK_KEY, CSEK_KEY_HASH,
244+
ANOTHER_CESK_KEY, ANOTHER_CSEK_KEY_HASH);
245+
246+
System.out.println();
247+
}
248+
249+
private static Credential authorize(
250+
JsonFactory jsonFactory, HttpTransport httpTransport, DataStoreFactory dataStoreFactory)
251+
throws Exception {
252+
253+
InputStream clientSecretStream =
254+
CustomerSuppliedEncryptionKeysSamples.class
255+
.getResourceAsStream("client_secrets.json");
256+
if (clientSecretStream == null) {
257+
throw new RuntimeException("Could not load secrets");
258+
}
259+
260+
// Load client secrets
261+
GoogleClientSecrets clientSecrets =
262+
GoogleClientSecrets.load(jsonFactory, new InputStreamReader(clientSecretStream));
263+
264+
// Set up authorization code flow
265+
GoogleAuthorizationCodeFlow flow =
266+
new GoogleAuthorizationCodeFlow.Builder(
267+
httpTransport,
268+
jsonFactory,
269+
clientSecrets,
270+
Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL))
271+
.setDataStoreFactory(dataStoreFactory)
272+
.build();
273+
274+
// Authorize
275+
Credential credential =
276+
new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
277+
278+
return credential;
279+
}
280+
281+
/**
282+
* Reads the contents of an InputStream and does nothing with it.
283+
*/
284+
private static void readStream(InputStream is) throws IOException {
285+
byte inputBuffer[] = new byte[256];
286+
while (is.read(inputBuffer) != -1) {}
287+
// The caller is responsible for closing this InputStream.
288+
is.close();
289+
}
290+
291+
/**
292+
* A helper class to provide input streams of any size.
293+
* The input streams will be full of null bytes.
294+
*/
295+
static class ArbitrarilyLargeInputStream extends InputStream {
296+
297+
private long bytesRead;
298+
private final long streamSize;
299+
300+
public ArbitrarilyLargeInputStream(long streamSizeInBytes) {
301+
bytesRead = 0;
302+
this.streamSize = streamSizeInBytes;
303+
}
304+
305+
@Override
306+
public int read() throws IOException {
307+
if (bytesRead >= streamSize) {
308+
return -1;
309+
}
310+
bytesRead++;
311+
return 0;
312+
}
313+
}
314+
315+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"installed":{"client_id":"122681785480-35mkukq7o3hh55i2uuftv0hp168hauet.apps.googleusercontent.com","project_id":"gcs-code-samples","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"hf-15vXoQQY8OonIK7qdrR0L","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}

0 commit comments

Comments
 (0)