Skip to content

Commit 5426f5a

Browse files
authored
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
1 parent d5ac0f7 commit 5426f5a

File tree

3 files changed

+65
-5
lines changed

3 files changed

+65
-5
lines changed

spec/GridFSBucketStorageAdapter.spec.js

+16
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
3535
expect(gfsResult.toString('utf8')).toBe(originalString);
3636
});
3737

38+
it('an encypted file created in GridStore should be available in GridFS', async () => {
39+
const gsAdapter = new GridStoreAdapter(databaseURI);
40+
const gfsAdapter = new GridFSBucketAdapter(
41+
databaseURI,
42+
{},
43+
'89E4AFF1-DFE4-4603-9574-BFA16BB446FD'
44+
);
45+
await expectMissingFile(gfsAdapter, 'myFileName');
46+
const originalString = 'abcdefghi';
47+
await gfsAdapter.createFile('myFileName', originalString);
48+
const gsResult = await gsAdapter.getFileData('myFileName');
49+
expect(gsResult.toString('utf8')).not.toBe(originalString);
50+
const gfsResult = await gfsAdapter.getFileData('myFileName');
51+
expect(gfsResult.toString('utf8')).toBe(originalString);
52+
});
53+
3854
it('should save metadata', async () => {
3955
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
4056
const originalString = 'abcdefghi';

src/Adapters/Files/GridFSBucketAdapter.js

+47-4
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,30 @@
1010
import { MongoClient, GridFSBucket, Db } from 'mongodb';
1111
import { FilesAdapter, validateFilename } from './FilesAdapter';
1212
import defaults from '../../defaults';
13+
const crypto = require('crypto');
1314

1415
export class GridFSBucketAdapter extends FilesAdapter {
1516
_databaseURI: string;
1617
_connectionPromise: Promise<Db>;
1718
_mongoOptions: Object;
19+
_algorithm: string;
1820

19-
constructor(mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}) {
21+
constructor(
22+
mongoDatabaseURI = defaults.DefaultMongoURI,
23+
mongoOptions = {},
24+
fileKey = undefined
25+
) {
2026
super();
2127
this._databaseURI = mongoDatabaseURI;
22-
28+
this._algorithm = 'aes-256-gcm';
29+
this._fileKey =
30+
fileKey !== undefined
31+
? crypto
32+
.createHash('sha256')
33+
.update(String(fileKey))
34+
.digest('base64')
35+
.substr(0, 32)
36+
: null;
2337
const defaultMongoOptions = {
2438
useNewUrlParser: true,
2539
useUnifiedTopology: true,
@@ -51,7 +65,19 @@ export class GridFSBucketAdapter extends FilesAdapter {
5165
const stream = await bucket.openUploadStream(filename, {
5266
metadata: options.metadata,
5367
});
54-
await stream.write(data);
68+
if (this._fileKey !== null) {
69+
const iv = crypto.randomBytes(16);
70+
const cipher = crypto.createCipheriv(this._algorithm, this._fileKey, iv);
71+
const encryptedResult = Buffer.concat([
72+
cipher.update(data),
73+
cipher.final(),
74+
iv,
75+
cipher.getAuthTag(),
76+
]);
77+
await stream.write(encryptedResult);
78+
} else {
79+
await stream.write(data);
80+
}
5581
stream.end();
5682
return new Promise((resolve, reject) => {
5783
stream.on('finish', resolve);
@@ -82,7 +108,24 @@ export class GridFSBucketAdapter extends FilesAdapter {
82108
chunks.push(data);
83109
});
84110
stream.on('end', () => {
85-
resolve(Buffer.concat(chunks));
111+
const data = Buffer.concat(chunks);
112+
if (this._fileKey !== null) {
113+
const authTagLocation = data.length - 16;
114+
const ivLocation = data.length - 32;
115+
const authTag = data.slice(authTagLocation);
116+
const iv = data.slice(ivLocation, authTagLocation);
117+
const encrypted = data.slice(0, ivLocation);
118+
const decipher = crypto.createDecipheriv(
119+
this._algorithm,
120+
this._fileKey,
121+
iv
122+
);
123+
decipher.setAuthTag(authTag);
124+
return resolve(
125+
Buffer.concat([decipher.update(encrypted), decipher.final()])
126+
);
127+
}
128+
resolve(data);
86129
});
87130
stream.on('error', (err) => {
88131
reject(err);

src/Controllers/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,13 @@ export function getFilesController(
105105
filesAdapter,
106106
databaseAdapter,
107107
preserveFileName,
108+
fileKey,
108109
} = options;
109110
if (!filesAdapter && databaseAdapter) {
110111
throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.';
111112
}
112113
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
113-
return new GridFSBucketAdapter(databaseURI);
114+
return new GridFSBucketAdapter(databaseURI, {}, fileKey);
114115
});
115116
return new FilesController(filesControllerAdapter, appId, {
116117
preserveFileName,

0 commit comments

Comments
 (0)