From 5a15f9864a7c2721f2b80231bc0ab1031ad4fef8 Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Tue, 30 Jun 2020 15:02:00 -0400 Subject: [PATCH 1/4] add fileKey encryption to GridFSBucketStorageAdapter --- spec/GridFSBucketStorageAdapter.spec.js | 16 +++++++ src/Adapters/Files/GridFSBucketAdapter.js | 55 +++++++++++++++++++++-- src/Controllers/index.js | 3 +- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index b54fa22a05..8871dd4ac6 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -35,6 +35,22 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { expect(gfsResult.toString('utf8')).toBe(originalString); }); + it('an encypted file created in GridStore should be available in GridFS', async () => { + const gsAdapter = new GridStoreAdapter(databaseURI); + const gfsAdapter = new GridFSBucketAdapter( + databaseURI, + { useNewUrlParser: true, useUnifiedTopology: true }, + '89E4AFF1-DFE4-4603-9574-BFA16BB446FD' + ); + await expectMissingFile(gfsAdapter, 'myFileName'); + const originalString = 'abcdefghi'; + await gfsAdapter.createFile('myFileName', originalString); + const gsResult = await gsAdapter.getFileData('myFileName'); + expect(gsResult.toString('utf8')).not.toBe(originalString); + const gfsResult = await gfsAdapter.getFileData('myFileName'); + expect(gfsResult.toString('utf8')).toBe(originalString); + }); + it('should save metadata', async () => { const gfsAdapter = new GridFSBucketAdapter(databaseURI); const originalString = 'abcdefghi'; diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 11370ccc61..7d0b85b7ea 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -10,16 +10,30 @@ import { MongoClient, GridFSBucket, Db } from 'mongodb'; import { FilesAdapter, validateFilename } from './FilesAdapter'; import defaults from '../../defaults'; +const crypto = require('crypto'); export class GridFSBucketAdapter extends FilesAdapter { _databaseURI: string; _connectionPromise: Promise; _mongoOptions: Object; + _algorithm: string; - constructor(mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}) { + constructor( + mongoDatabaseURI = defaults.DefaultMongoURI, + mongoOptions = {}, + secretKey = undefined + ) { super(); this._databaseURI = mongoDatabaseURI; - + this._algorithm = 'aes-256-gcm'; + this._secretKey = + secretKey !== undefined + ? crypto + .createHash('sha256') + .update(String(secretKey)) + .digest('base64') + .substr(0, 32) + : null; const defaultMongoOptions = { useNewUrlParser: true, useUnifiedTopology: true, @@ -51,7 +65,23 @@ export class GridFSBucketAdapter extends FilesAdapter { const stream = await bucket.openUploadStream(filename, { metadata: options.metadata, }); - await stream.write(data); + if (this._secretKey !== null) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv( + this._algorithm, + this._secretKey, + iv + ); + const encryptedResult = Buffer.concat([ + cipher.update(data), + cipher.final(), + iv, + cipher.getAuthTag(), + ]); + await stream.write(encryptedResult); + } else { + await stream.write(data); + } stream.end(); return new Promise((resolve, reject) => { stream.on('finish', resolve); @@ -82,7 +112,24 @@ export class GridFSBucketAdapter extends FilesAdapter { chunks.push(data); }); stream.on('end', () => { - resolve(Buffer.concat(chunks)); + const data = Buffer.concat(chunks); + if (this._secretKey !== null) { + const authTagLocation = data.length - 16; + const ivLocation = data.length - 32; + const authTag = data.slice(authTagLocation); + const iv = data.slice(ivLocation, authTagLocation); + const encrypted = data.slice(0, ivLocation); + const decipher = crypto.createDecipheriv( + this._algorithm, + this._secretKey, + iv + ); + decipher.setAuthTag(authTag); + resolve( + Buffer.concat([decipher.update(encrypted), decipher.final()]) + ); + } + resolve(data); }); stream.on('error', (err) => { reject(err); diff --git a/src/Controllers/index.js b/src/Controllers/index.js index d10ad8001c..5bbd9308f1 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -105,12 +105,13 @@ export function getFilesController( filesAdapter, databaseAdapter, preserveFileName, + fileKey, } = options; if (!filesAdapter && databaseAdapter) { throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.'; } const filesControllerAdapter = loadAdapter(filesAdapter, () => { - return new GridFSBucketAdapter(databaseURI); + return new GridFSBucketAdapter(databaseURI, {}, fileKey); }); return new FilesController(filesControllerAdapter, appId, { preserveFileName, From 18ae9ceaea57cb1728843b64a332bbadd9e4be3c Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Tue, 30 Jun 2020 16:00:28 -0400 Subject: [PATCH 2/4] remove fileAdapter options from test spec --- spec/GridFSBucketStorageAdapter.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 8871dd4ac6..1e55d687ab 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -39,7 +39,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const gsAdapter = new GridStoreAdapter(databaseURI); const gfsAdapter = new GridFSBucketAdapter( databaseURI, - { useNewUrlParser: true, useUnifiedTopology: true }, + {}, '89E4AFF1-DFE4-4603-9574-BFA16BB446FD' ); await expectMissingFile(gfsAdapter, 'myFileName'); From 451693c9fedee21c177574cb8eb4760cd006da4a Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Tue, 30 Jun 2020 16:07:45 -0400 Subject: [PATCH 3/4] ensure promise doesn't fall through in getFileData --- src/Adapters/Files/GridFSBucketAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 7d0b85b7ea..f7cd57ad82 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -125,7 +125,7 @@ export class GridFSBucketAdapter extends FilesAdapter { iv ); decipher.setAuthTag(authTag); - resolve( + return resolve( Buffer.concat([decipher.update(encrypted), decipher.final()]) ); } From 38422d10d81b7400631f042d7eb0cf02aceebcf8 Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Wed, 1 Jul 2020 07:59:29 -0400 Subject: [PATCH 4/4] switch secretKey to fileKey --- src/Adapters/Files/GridFSBucketAdapter.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index f7cd57ad82..958f9764d8 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -21,16 +21,16 @@ export class GridFSBucketAdapter extends FilesAdapter { constructor( mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}, - secretKey = undefined + fileKey = undefined ) { super(); this._databaseURI = mongoDatabaseURI; this._algorithm = 'aes-256-gcm'; - this._secretKey = - secretKey !== undefined + this._fileKey = + fileKey !== undefined ? crypto .createHash('sha256') - .update(String(secretKey)) + .update(String(fileKey)) .digest('base64') .substr(0, 32) : null; @@ -65,13 +65,9 @@ export class GridFSBucketAdapter extends FilesAdapter { const stream = await bucket.openUploadStream(filename, { metadata: options.metadata, }); - if (this._secretKey !== null) { + if (this._fileKey !== null) { const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv( - this._algorithm, - this._secretKey, - iv - ); + const cipher = crypto.createCipheriv(this._algorithm, this._fileKey, iv); const encryptedResult = Buffer.concat([ cipher.update(data), cipher.final(), @@ -113,7 +109,7 @@ export class GridFSBucketAdapter extends FilesAdapter { }); stream.on('end', () => { const data = Buffer.concat(chunks); - if (this._secretKey !== null) { + if (this._fileKey !== null) { const authTagLocation = data.length - 16; const ivLocation = data.length - 32; const authTag = data.slice(authTagLocation); @@ -121,7 +117,7 @@ export class GridFSBucketAdapter extends FilesAdapter { const encrypted = data.slice(0, ivLocation); const decipher = crypto.createDecipheriv( this._algorithm, - this._secretKey, + this._fileKey, iv ); decipher.setAuthTag(authTag);