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:
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user