Skip to content

Commit f441b52

Browse files
committed
feat(storage): allow setting file encryption key to null to disable CSEK and fix copy behavior for non-encrypted destinations
1 parent ca43580 commit f441b52

3 files changed

Lines changed: 150 additions & 12 deletions

File tree

handwritten/storage/src/file.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ export type RenameCallback = MoveCallback;
317317
export type RotateEncryptionKeyOptions = string | Buffer | EncryptionKeyOptions;
318318

319319
export interface EncryptionKeyOptions {
320-
encryptionKey?: string | Buffer;
320+
encryptionKey?: string | Buffer | null;
321321
kmsKeyName?: string;
322322
preconditionOpts?: PreconditionOptions;
323323
}
@@ -366,7 +366,7 @@ const COMPRESSIBLE_MIME_REGEX = new RegExp(
366366

367367
export interface FileOptions {
368368
crc32cGenerator?: CRC32CValidatorGenerator;
369-
encryptionKey?: string | Buffer;
369+
encryptionKey?: string | Buffer | null;
370370
generation?: number | string;
371371
restoreToken?: string;
372372
kmsKeyName?: string;
@@ -425,7 +425,7 @@ export type DownloadCallback = (
425425

426426
export interface DownloadOptions extends CreateReadStreamOptions {
427427
destination?: string;
428-
encryptionKey?: string | Buffer;
428+
encryptionKey?: string | Buffer | null;
429429
}
430430

431431
interface CopyQuery {
@@ -600,7 +600,7 @@ class File extends ServiceObject<File, FileMetadata> {
600600
restoreToken?: string;
601601
parent!: Bucket;
602602

603-
private encryptionKey?: string | Buffer;
603+
private encryptionKey?: string | Buffer | null;
604604
private encryptionKeyBase64?: string;
605605
private encryptionKeyHash?: string;
606606
private encryptionKeyInterceptor?: Interceptor;
@@ -1102,7 +1102,7 @@ class File extends ServiceObject<File, FileMetadata> {
11021102

11031103
this.name = name;
11041104

1105-
if (options.encryptionKey) {
1105+
if (options.encryptionKey !== undefined) {
11061106
this.setEncryptionKey(options.encryptionKey);
11071107
}
11081108

@@ -1379,14 +1379,26 @@ class File extends ServiceObject<File, FileMetadata> {
13791379

13801380
const headers: {[index: string]: string | undefined} = {};
13811381

1382-
if (this.encryptionKey !== undefined) {
1382+
if (this.encryptionKey !== undefined && this.encryptionKey !== null) {
13831383
headers['x-goog-copy-source-encryption-algorithm'] = 'AES256';
13841384
headers['x-goog-copy-source-encryption-key'] = this.encryptionKeyBase64;
13851385
headers['x-goog-copy-source-encryption-key-sha256'] =
13861386
this.encryptionKeyHash;
13871387
}
13881388

1389-
if (newFile.encryptionKey !== undefined) {
1389+
let copiedKey = false;
1390+
if (
1391+
this.encryptionKey !== undefined &&
1392+
this.encryptionKey !== null &&
1393+
newFile.encryptionKey === undefined
1394+
) {
1395+
newFile.encryptionKey = this.encryptionKey;
1396+
newFile.encryptionKeyBase64 = this.encryptionKeyBase64;
1397+
newFile.encryptionKeyHash = this.encryptionKeyHash;
1398+
copiedKey = true;
1399+
}
1400+
1401+
if (newFile.encryptionKey !== undefined && !copiedKey) {
13901402
this.setEncryptionKey(newFile.encryptionKey!);
13911403
} else if (options.destinationKmsKeyName !== undefined) {
13921404
query.destinationKmsKeyName = options.destinationKmsKeyName;
@@ -1864,7 +1876,7 @@ class File extends ServiceObject<File, FileMetadata> {
18641876
),
18651877
file: this.name,
18661878
generation: this.generation,
1867-
key: this.encryptionKey,
1879+
key: this.encryptionKey === null ? undefined : this.encryptionKey,
18681880
kmsKeyName: this.kmsKeyName,
18691881
metadata: options.metadata,
18701882
offset: options.offset,
@@ -2392,7 +2404,7 @@ class File extends ServiceObject<File, FileMetadata> {
23922404
const destination = options.destination;
23932405
delete options.destination;
23942406

2395-
if (options.encryptionKey) {
2407+
if (options.encryptionKey !== undefined) {
23962408
this.setEncryptionKey(options.encryptionKey);
23972409
delete options.encryptionKey;
23982410
}
@@ -2482,8 +2494,23 @@ class File extends ServiceObject<File, FileMetadata> {
24822494
* region_tag:storage_download_encrypted_file
24832495
* Example of downloading an encrypted file:
24842496
*/
2485-
setEncryptionKey(encryptionKey: string | Buffer) {
2497+
setEncryptionKey(encryptionKey: string | Buffer | null) {
2498+
if (this.encryptionKeyInterceptor) {
2499+
const index = this.interceptors.indexOf(this.encryptionKeyInterceptor);
2500+
if (index > -1) {
2501+
this.interceptors.splice(index, 1);
2502+
}
2503+
this.encryptionKeyInterceptor = undefined;
2504+
}
2505+
24862506
this.encryptionKey = encryptionKey;
2507+
2508+
if (encryptionKey === null || encryptionKey === undefined) {
2509+
this.encryptionKeyBase64 = undefined;
2510+
this.encryptionKeyHash = undefined;
2511+
return this;
2512+
}
2513+
24872514
this.encryptionKeyBase64 = Buffer.from(encryptionKey as string).toString(
24882515
'base64',
24892516
);
@@ -2504,7 +2531,7 @@ class File extends ServiceObject<File, FileMetadata> {
25042531
},
25052532
};
25062533

2507-
this.interceptors.push(this.encryptionKeyInterceptor!);
2534+
this.interceptors.push(this.encryptionKeyInterceptor);
25082535

25092536
return this;
25102537
}
@@ -4462,7 +4489,7 @@ class File extends ServiceObject<File, FileMetadata> {
44624489
file: this.name,
44634490
generation: this.generation,
44644491
isPartialUpload: options.isPartialUpload,
4465-
key: this.encryptionKey,
4492+
key: this.encryptionKey === null ? undefined : this.encryptionKey,
44664493
kmsKeyName: this.kmsKeyName,
44674494
metadata: options.metadata,
44684495
offset: options.offset,

handwritten/storage/system-test/storage.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2793,6 +2793,24 @@ describe('storage', function () {
27932793
const [contents] = await file.download();
27942794
assert.strictEqual(contents.toString(), 'secret data');
27952795
});
2796+
2797+
it('should copy a CSEK-encrypted file to a standard non-CSEK destination when destination key is null', async () => {
2798+
const srcFile = bucket.file('encrypted-source');
2799+
srcFile.setEncryptionKey('a'.repeat(32));
2800+
2801+
await srcFile.save('csek data', { resumable: false });
2802+
2803+
const dstFile = bucket.file('non-csek-destination');
2804+
dstFile.setEncryptionKey(null);
2805+
2806+
await srcFile.copy(dstFile);
2807+
2808+
const [metadata] = await dstFile.getMetadata();
2809+
assert.strictEqual(metadata.customerEncryption, undefined);
2810+
2811+
const [contents] = await dstFile.download();
2812+
assert.strictEqual(contents.toString(), 'csek data');
2813+
});
27962814
});
27972815

27982816
describe.skip('kms keys', () => {

handwritten/storage/test/file.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,75 @@ describe('File', () => {
626626
file.copy(newFile, assert.ifError);
627627
});
628628

629+
it('should not copy encryption key or send destination headers when destination file has null encryption key', done => {
630+
file.encryptionKey = 'sourceKey';
631+
file.encryptionKeyBase64 = 'sourceKeyBase64';
632+
file.encryptionKeyHash = 'sourceKeyHash';
633+
634+
const newFile = new File(BUCKET, 'new-file');
635+
newFile.setEncryptionKey(null);
636+
637+
file.request = (reqOpts: DecorateRequestOptions) => {
638+
assert.strictEqual(newFile.encryptionKey, null);
639+
assert.strictEqual(newFile.encryptionKeyBase64, undefined);
640+
assert.strictEqual(newFile.encryptionKeyHash, undefined);
641+
642+
assert.strictEqual(
643+
reqOpts.headers!['x-goog-copy-source-encryption-algorithm'],
644+
'AES256'
645+
);
646+
assert.strictEqual(
647+
reqOpts.headers!['x-goog-copy-source-encryption-key'],
648+
'sourceKeyBase64'
649+
);
650+
assert.strictEqual(
651+
reqOpts.headers!['x-goog-copy-source-encryption-key-sha256'],
652+
'sourceKeyHash'
653+
);
654+
655+
assert.strictEqual(
656+
reqOpts.headers!['x-goog-encryption-algorithm'],
657+
undefined
658+
);
659+
assert.strictEqual(
660+
reqOpts.headers!['x-goog-encryption-key'],
661+
undefined
662+
);
663+
assert.strictEqual(
664+
reqOpts.headers!['x-goog-encryption-key-sha256'],
665+
undefined
666+
);
667+
668+
assert.strictEqual(file.encryptionKeyInterceptor, undefined);
669+
670+
done();
671+
};
672+
673+
file.copy(newFile, assert.ifError);
674+
});
675+
676+
it('should copy the source key to the destination file object if destination key is undefined', done => {
677+
file.encryptionKey = 'sourceKey';
678+
file.encryptionKeyBase64 = 'sourceKeyBase64';
679+
file.encryptionKeyHash = 'sourceKeyHash';
680+
681+
const newFile = new File(BUCKET, 'new-file');
682+
683+
file.request = (reqOpts: DecorateRequestOptions) => {
684+
assert.strictEqual(newFile.encryptionKey, file.encryptionKey);
685+
assert.strictEqual(newFile.encryptionKeyBase64, file.encryptionKeyBase64);
686+
assert.strictEqual(newFile.encryptionKeyHash, file.encryptionKeyHash);
687+
688+
assert.strictEqual(
689+
reqOpts.headers!['x-goog-copy-source-encryption-key'],
690+
file.encryptionKeyBase64
691+
);
692+
done();
693+
};
694+
695+
file.copy(newFile, assert.ifError);
696+
});
697+
629698
it('should set destination KMS key name', done => {
630699
const newFile = new File(BUCKET, 'new-file');
631700
newFile.kmsKeyName = 'kms-key-name';
@@ -5440,6 +5509,30 @@ describe('File', () => {
54405509

54415510
done();
54425511
});
5512+
5513+
describe('null key', () => {
5514+
beforeEach(() => {
5515+
file.setEncryptionKey(KEY);
5516+
file.setEncryptionKey(null);
5517+
});
5518+
5519+
it('should localize the key to null', () => {
5520+
assert.strictEqual(file.encryptionKey, null);
5521+
});
5522+
5523+
it('should clear the base64 key', () => {
5524+
assert.strictEqual(file.encryptionKeyBase64, undefined);
5525+
});
5526+
5527+
it('should clear the hash', () => {
5528+
assert.strictEqual(file.encryptionKeyHash, undefined);
5529+
});
5530+
5531+
it('should remove the request interceptor', () => {
5532+
assert.strictEqual(file.encryptionKeyInterceptor, undefined);
5533+
assert.strictEqual(file.interceptors.length, 0);
5534+
});
5535+
});
54435536
});
54445537

54455538
describe('startResumableUpload_', () => {

0 commit comments

Comments
 (0)