Skip to content

Commit 87d040b

Browse files
storage: allow custom file encryption
1 parent aa51104 commit 87d040b

File tree

5 files changed

+167
-12
lines changed

5 files changed

+167
-12
lines changed

lib/common/service-object.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
'use strict';
2222

23+
var arrify = require('arrify');
2324
var exec = require('methmeth');
2425
var extend = require('extend');
2526
var is = require('is');
@@ -301,12 +302,18 @@ ServiceObject.prototype.setMetadata = function(metadata, callback) {
301302
* @param {function} callback - The callback function passed to `request`.
302303
*/
303304
ServiceObject.prototype.request = function(reqOpts, callback) {
305+
var isAbsoluteUrl = reqOpts.uri.charAt(0) === 'h';
306+
304307
var uriComponents = [
305308
this.baseUrl,
306309
this.id,
307310
reqOpts.uri
308311
];
309312

313+
if (isAbsoluteUrl) {
314+
uriComponents.splice(0, uriComponents.indexOf(reqOpts.uri));
315+
}
316+
310317
reqOpts.uri = uriComponents
311318
.filter(exec('trim')) // Limit to non-empty strings.
312319
.map(function(uriComponent) {
@@ -315,7 +322,10 @@ ServiceObject.prototype.request = function(reqOpts, callback) {
315322
})
316323
.join('/');
317324

318-
reqOpts.interceptors_ = [].slice.call(this.interceptors);
325+
reqOpts.interceptors_ = arrify(reqOpts.interceptors_);
326+
327+
var interceptors = [].slice.call(this.interceptors);
328+
reqOpts.interceptors_ = reqOpts.interceptors_.concat(interceptors);
319329

320330
return this.parent.request(reqOpts, callback);
321331
};

lib/common/service.js

+6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ function Service(config, options) {
7171
* @param {function} callback - The callback function passed to `request`.
7272
*/
7373
Service.prototype.request = function(reqOpts, callback) {
74+
var isAbsoluteUrl = reqOpts.uri.charAt(0) === 'h';
75+
7476
var uriComponents = [
7577
this.baseUrl
7678
];
@@ -82,6 +84,10 @@ Service.prototype.request = function(reqOpts, callback) {
8284

8385
uriComponents.push(reqOpts.uri);
8486

87+
if (isAbsoluteUrl) {
88+
uriComponents.splice(0, uriComponents.indexOf(reqOpts.uri));
89+
}
90+
8591
reqOpts.uri = uriComponents
8692
.map(function(uriComponent) {
8793
var trimSlashesRegex = /^\/*|\/*$/g;

lib/storage/bucket.js

+40-6
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,8 @@ Bucket.prototype.deleteFiles = function(query, callback) {
548548
* @param {object=} options - Configuration options.
549549
* @param {string|number} options.generation - Only use a specific revision of
550550
* this file.
551+
* @param {string} options.key - A custom encryption key. See
552+
* [Customer-supplied Encryption Keys](https://cloud.google.com/storage/docs/encryption#customer-supplied).
551553
* @return {module:storage/file}
552554
*
553555
* @example
@@ -942,6 +944,8 @@ Bucket.prototype.makePublic = function(options, callback) {
942944
* bucket using the name of the local file.
943945
* @param {boolean} options.gzip - Automatically gzip the file. This will set
944946
* `options.metadata.contentEncoding` to `gzip`.
947+
* @param {string} options.key - A custom encryption key. See
948+
* [Customer-supplied Encryption Keys](https://cloud.google.com/storage/docs/encryption#customer-supplied).
945949
* @param {object} options.metadata - See an
946950
* [Objects: insert request body](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request_properties_JSON).
947951
* @param {string} options.offset - The starting byte of the upload stream, for
@@ -972,17 +976,17 @@ Bucket.prototype.makePublic = function(options, callback) {
972976
* `options.predefinedAcl = 'publicRead'`)
973977
* @param {boolean} options.resumable - Force a resumable upload. (default:
974978
* true for files larger than 5 MB).
975-
* @param {function} callback - The callback function.
976-
* @param {?error} callback.err - An error returned while making this request
977-
* @param {module:storage/file} callback.file - The uploaded File.
978-
* @param {object} callback.apiResponse - The full API response.
979979
* @param {string} options.uri - The URI for an already-created resumable
980980
* upload. See {module:storage/file#createResumableUpload}.
981981
* @param {string|boolean} options.validation - Possible values: `"md5"`,
982982
* `"crc32c"`, or `false`. By default, data integrity is validated with an
983983
* MD5 checksum for maximum reliability. CRC32c will provide better
984984
* performance with less reliability. You may also choose to skip validation
985985
* completely, however this is **not recommended**.
986+
* @param {function} callback - The callback function.
987+
* @param {?error} callback.err - An error returned while making this request
988+
* @param {module:storage/file} callback.file - The uploaded File.
989+
* @param {object} callback.apiResponse - The full API response.
986990
*
987991
* @example
988992
* //-
@@ -1044,6 +1048,32 @@ Bucket.prototype.makePublic = function(options, callback) {
10441048
* // Note:
10451049
* // The `newFile` parameter is equal to `file`.
10461050
* });
1051+
*
1052+
* //-
1053+
* // To use [Customer-supplied Encryption Keys](https://cloud.google.com/storage/docs/encryption#customer-supplied),
1054+
* // provide the `key` option.
1055+
* //-
1056+
* var crypto = require('crypto');
1057+
* var encryptionKey = crypto.randomBytes(32);
1058+
*
1059+
* bucket.upload('img.png', {
1060+
* key: encryptionKey
1061+
* }, function(err, newFile) {
1062+
* // `img.png` was uploaded with your custom encryption key.
1063+
*
1064+
* // `newFile` is already configured to use the encryption key when making
1065+
* // operations on the remote object.
1066+
*
1067+
* // However, to use your encryption key later, you must create a `File`
1068+
* // instance with the `key` supplied:
1069+
* var file = bucket.file('img.png', {
1070+
* key: encryptionKey
1071+
* });
1072+
*
1073+
* // Or with `file#setKey`:
1074+
* var file = bucket.file('img.png');
1075+
* file.setKey(encryptionKey);
1076+
* });
10471077
*/
10481078
Bucket.prototype.upload = function(localPath, options, callback) {
10491079
if (is.fn(options)) {
@@ -1060,10 +1090,14 @@ Bucket.prototype.upload = function(localPath, options, callback) {
10601090
newFile = options.destination;
10611091
} else if (is.string(options.destination)) {
10621092
// Use the string as the name of the file.
1063-
newFile = this.file(options.destination);
1093+
newFile = this.file(options.destination, {
1094+
key: options.key
1095+
});
10641096
} else {
10651097
// Resort to using the name of the incoming file.
1066-
newFile = this.file(path.basename(localPath));
1098+
newFile = this.file(path.basename(localPath), {
1099+
key: options.key
1100+
});
10671101
}
10681102

10691103
var contentType = mime.contentType(path.basename(localPath));

lib/storage/file.js

+66-5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ var STORAGE_UPLOAD_BASE_URL = 'https://www.googleapis.com/upload/storage/v1/b';
8686
* @param {string} name - The name of the remote file.
8787
* @param {object=} options - Configuration object.
8888
* @param {number} options.generation - Generation to scope the file to.
89+
* @param {string} options.key - A custom encryption key.
8990
*/
9091
/**
9192
* A File object is created from your Bucket object using
@@ -241,6 +242,10 @@ function File(bucket, name, options) {
241242
methods: methods
242243
});
243244

245+
if (options.key) {
246+
this.setKey(options.key);
247+
}
248+
244249
/**
245250
* Google Cloud Storage uses access control lists (ACLs) to manage object and
246251
* bucket access. ACLs are the mechanism you use to share objects with other
@@ -531,7 +536,7 @@ File.prototype.createReadStream = function(options) {
531536
};
532537
}
533538

534-
var requestStream = self.storage.makeAuthenticatedRequest(reqOpts);
539+
var requestStream = self.request(reqOpts);
535540
var validateStream;
536541

537542
// We listen to the response event from the request stream so that we can...
@@ -989,6 +994,53 @@ File.prototype.download = function(options, callback) {
989994
}
990995
};
991996

997+
/**
998+
* The Storage API allows you to use a custom key for server-side encryption.
999+
* Supply this method with a passphrase and the correct key (AES-256) will be
1000+
* generated and used for you.
1001+
*
1002+
* @resource [Customer-supplied Encryption Keys]{@link https://cloud.google.com/storage/docs/encryption#customer-supplied}
1003+
*
1004+
* @param {string|buffer} key - An AES-256 encryption key.
1005+
* @return {module:storage/file}
1006+
*
1007+
* @example
1008+
* var crypto = require('crypto');
1009+
* var encryptionKey = crypto.randomBytes(32);
1010+
*
1011+
* var fileWithCustomEncryption = myBucket.file('my-file');
1012+
* fileWithCustomEncryption.setKey(encryptionKey);
1013+
*
1014+
* var fileWithoutCustomEncryption = myBucket.file('my-file');
1015+
*
1016+
* fileWithCustomEncryption.save('data', function(err) {
1017+
* // Try to download with the File object that hasn't had `setKey()` called:
1018+
* fileWithoutCustomEncryption.download(function(err) {
1019+
* // We will receive an error:
1020+
* // err.message === 'Bad Request'
1021+
*
1022+
* // Try again with the File object we called `setKey()` on:
1023+
* fileWithCustomEncryption.download(function(err, contents) {
1024+
* // contents.toString() === 'data'
1025+
* });
1026+
* });
1027+
* });
1028+
*/
1029+
File.prototype.setKey = function(key) {
1030+
key = new Buffer(key).toString('base64');
1031+
var hash = crypto.createHash('sha256').update(key, 'base64').digest('base64');
1032+
1033+
this.interceptors.push({
1034+
request: function(reqOpts) {
1035+
reqOpts.headers = reqOpts.headers || {};
1036+
reqOpts.headers['x-goog-encryption-algorithm'] = 'AES256';
1037+
reqOpts.headers['x-goog-encryption-key'] = key;
1038+
reqOpts.headers['x-goog-encryption-key-sha256'] = hash;
1039+
return reqOpts;
1040+
}
1041+
});
1042+
};
1043+
9921044
/**
9931045
* Get a signed policy document to allow a user to upload data with a POST
9941046
* request.
@@ -1556,6 +1608,7 @@ File.prototype.startResumableUpload_ = function(dup, options) {
15561608
var uploadStream = resumableUpload({
15571609
authClient: this.storage.authClient,
15581610
bucket: this.bucket.name,
1611+
encryption: this.encryption,
15591612
file: this.name,
15601613
generation: this.generation,
15611614
metadata: options.metadata,
@@ -1620,12 +1673,20 @@ File.prototype.startSimpleUpload_ = function(dup, options) {
16201673
}
16211674

16221675
util.makeWritableStream(dup, {
1623-
makeAuthenticatedRequest: this.storage.makeAuthenticatedRequest,
1676+
makeAuthenticatedRequest: function(reqOpts) {
1677+
self.request(reqOpts, function(err, body, resp) {
1678+
if (err) {
1679+
dup.destroy(err);
1680+
return;
1681+
}
1682+
1683+
self.metadata = body;
1684+
dup.emit('response', resp);
1685+
dup.emit('complete');
1686+
});
1687+
},
16241688
metadata: options.metadata,
16251689
request: reqOpts
1626-
}, function(data) {
1627-
self.metadata = data;
1628-
dup.emit('complete');
16291690
});
16301691
};
16311692

system-test/storage.js

+44
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,50 @@ describe('storage', function() {
759759
});
760760
});
761761

762+
describe('customer-supplied encryption keys', function() {
763+
var encryptionKey = crypto.randomBytes(32);
764+
765+
var file = bucket.file('encrypted-file', {
766+
key: encryptionKey
767+
});
768+
var unencryptedFile = bucket.file(file.name);
769+
770+
before(function(done) {
771+
file.save('secret data', { resumable: false }, done);
772+
});
773+
774+
it('should not get the hashes from the unencrypted file', function(done) {
775+
unencryptedFile.getMetadata(function(err, metadata) {
776+
assert.ifError(err);
777+
assert.strictEqual(metadata.crc32c, undefined);
778+
done();
779+
});
780+
});
781+
782+
it('should get the hashes from the encrypted file', function(done) {
783+
file.getMetadata(function(err, metadata) {
784+
assert.ifError(err);
785+
assert.notStrictEqual(metadata.crc32c, undefined);
786+
done();
787+
});
788+
});
789+
790+
it('should not download from the unencrypted file', function(done) {
791+
unencryptedFile.download(function(err) {
792+
assert.strictEqual(err.message, 'Bad Request');
793+
done();
794+
});
795+
});
796+
797+
it('should download from the encrytped file', function(done) {
798+
file.download(function(err, contents) {
799+
assert.ifError(err);
800+
assert.strictEqual(contents.toString(), 'secret data');
801+
done();
802+
});
803+
});
804+
});
805+
762806
it('should copy an existing file', function(done) {
763807
var opts = { destination: 'CloudLogo' };
764808
bucket.upload(FILES.logo.path, opts, function(err, file) {

0 commit comments

Comments
 (0)