Add file bucket encryption using fileKey (#6765)

* add fileKey encryption to GridFSBucketStorageAdapter

* remove fileAdapter options from test spec

* ensure promise doesn't fall through in getFileData

* switch secretKey to fileKey
This commit is contained in:
Corey
2020-07-01 19:43:26 -04:00
committed by GitHub
parent d5ac0f7748
commit 5426f5a4f7
3 changed files with 65 additions and 5 deletions

View File

@@ -35,6 +35,22 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
expect(gfsResult.toString('utf8')).toBe(originalString); 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,
{},
'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 () => { it('should save metadata', async () => {
const gfsAdapter = new GridFSBucketAdapter(databaseURI); const gfsAdapter = new GridFSBucketAdapter(databaseURI);
const originalString = 'abcdefghi'; const originalString = 'abcdefghi';

View File

@@ -10,16 +10,30 @@
import { MongoClient, GridFSBucket, Db } from 'mongodb'; import { MongoClient, GridFSBucket, Db } from 'mongodb';
import { FilesAdapter, validateFilename } from './FilesAdapter'; import { FilesAdapter, validateFilename } from './FilesAdapter';
import defaults from '../../defaults'; import defaults from '../../defaults';
const crypto = require('crypto');
export class GridFSBucketAdapter extends FilesAdapter { export class GridFSBucketAdapter extends FilesAdapter {
_databaseURI: string; _databaseURI: string;
_connectionPromise: Promise<Db>; _connectionPromise: Promise<Db>;
_mongoOptions: Object; _mongoOptions: Object;
_algorithm: string;
constructor(mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}) { constructor(
mongoDatabaseURI = defaults.DefaultMongoURI,
mongoOptions = {},
fileKey = undefined
) {
super(); super();
this._databaseURI = mongoDatabaseURI; this._databaseURI = mongoDatabaseURI;
this._algorithm = 'aes-256-gcm';
this._fileKey =
fileKey !== undefined
? crypto
.createHash('sha256')
.update(String(fileKey))
.digest('base64')
.substr(0, 32)
: null;
const defaultMongoOptions = { const defaultMongoOptions = {
useNewUrlParser: true, useNewUrlParser: true,
useUnifiedTopology: true, useUnifiedTopology: true,
@@ -51,7 +65,19 @@ export class GridFSBucketAdapter extends FilesAdapter {
const stream = await bucket.openUploadStream(filename, { const stream = await bucket.openUploadStream(filename, {
metadata: options.metadata, metadata: options.metadata,
}); });
await stream.write(data); if (this._fileKey !== null) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this._algorithm, this._fileKey, iv);
const encryptedResult = Buffer.concat([
cipher.update(data),
cipher.final(),
iv,
cipher.getAuthTag(),
]);
await stream.write(encryptedResult);
} else {
await stream.write(data);
}
stream.end(); stream.end();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
stream.on('finish', resolve); stream.on('finish', resolve);
@@ -82,7 +108,24 @@ export class GridFSBucketAdapter extends FilesAdapter {
chunks.push(data); chunks.push(data);
}); });
stream.on('end', () => { stream.on('end', () => {
resolve(Buffer.concat(chunks)); const data = Buffer.concat(chunks);
if (this._fileKey !== 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._fileKey,
iv
);
decipher.setAuthTag(authTag);
return resolve(
Buffer.concat([decipher.update(encrypted), decipher.final()])
);
}
resolve(data);
}); });
stream.on('error', (err) => { stream.on('error', (err) => {
reject(err); reject(err);

View File

@@ -105,12 +105,13 @@ export function getFilesController(
filesAdapter, filesAdapter,
databaseAdapter, databaseAdapter,
preserveFileName, preserveFileName,
fileKey,
} = options; } = options;
if (!filesAdapter && databaseAdapter) { if (!filesAdapter && databaseAdapter) {
throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.'; throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.';
} }
const filesControllerAdapter = loadAdapter(filesAdapter, () => { const filesControllerAdapter = loadAdapter(filesAdapter, () => {
return new GridFSBucketAdapter(databaseURI); return new GridFSBucketAdapter(databaseURI, {}, fileKey);
}); });
return new FilesController(filesControllerAdapter, appId, { return new FilesController(filesControllerAdapter, appId, {
preserveFileName, preserveFileName,