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);
});
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 () => {
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
const originalString = 'abcdefghi';

View File

@@ -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<Db>;
_mongoOptions: Object;
_algorithm: string;
constructor(mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}) {
constructor(
mongoDatabaseURI = defaults.DefaultMongoURI,
mongoOptions = {},
fileKey = undefined
) {
super();
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 = {
useNewUrlParser: true,
useUnifiedTopology: true,
@@ -51,7 +65,19 @@ export class GridFSBucketAdapter extends FilesAdapter {
const stream = await bucket.openUploadStream(filename, {
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();
return new Promise((resolve, reject) => {
stream.on('finish', resolve);
@@ -82,7 +108,24 @@ export class GridFSBucketAdapter extends FilesAdapter {
chunks.push(data);
});
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) => {
reject(err);

View File

@@ -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,