From 5a15f9864a7c2721f2b80231bc0ab1031ad4fef8 Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Tue, 30 Jun 2020 15:02:00 -0400 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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); From 10fb6594c133479fd9e9b835bff4c673da2b913e Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Thu, 2 Jul 2020 00:56:28 -0400 Subject: [PATCH 05/12] add fileKey rotation for GridFSBucketAdapter --- spec/GridFSBucketStorageAdapter.spec.js | 280 +++++++++++++++++++++- src/Adapters/Files/GridFSBucketAdapter.js | 138 +++++++++-- 2 files changed, 385 insertions(+), 33 deletions(-) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 1e55d687ab..3a4752b43c 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -35,20 +35,282 @@ 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( + it('should save an encrypted file that can only be decrypted by a GridFS adapter with the fileKey', async () => { + const unencryptedAdapter = new GridFSBucketAdapter(databaseURI); + const encryptedAdapter = new GridFSBucketAdapter( databaseURI, {}, '89E4AFF1-DFE4-4603-9574-BFA16BB446FD' ); - await expectMissingFile(gfsAdapter, 'myFileName'); + await expectMissingFile(encryptedAdapter, '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); + await encryptedAdapter.createFile('myFileName', originalString); + const unencryptedResult = await unencryptedAdapter.getFileData( + 'myFileName' + ); + expect(unencryptedResult.toString('utf8')).not.toBe(originalString); + const encryptedResult = await encryptedAdapter.getFileData('myFileName'); + expect(encryptedResult.toString('utf8')).toBe(originalString); + }); + + it('should rotate key of all unencrypted GridFS files to encrypted files', async () => { + const unencryptedAdapter = new GridFSBucketAdapter(databaseURI); + const encryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + '89E4AFF1-DFE4-4603-9574-BFA16BB446FD' + ); + const fileName1 = 'file1.txt'; + const data1 = 'hello world'; + const fileName2 = 'file2.txt'; + const data2 = 'hello new world'; + //Store unecrypted files + await unencryptedAdapter.createFile(fileName1, data1); + const unencryptedResult1 = await unencryptedAdapter.getFileData(fileName1); + expect(unencryptedResult1.toString('utf8')).toBe(data1); + await unencryptedAdapter.createFile(fileName2, data2); + const unencryptedResult2 = await unencryptedAdapter.getFileData(fileName2); + expect(unencryptedResult2.toString('utf8')).toBe(data2); + //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter + const { rotated, notRotated } = await encryptedAdapter.rotateFileKey(); + expect(rotated.length).toEqual(2); + expect( + rotated.filter(function (value) { + return value === fileName1; + }).length + ).toEqual(1); + expect( + rotated.filter(function (value) { + return value === fileName2; + }).length + ).toEqual(1); + expect(notRotated.length).toEqual(0); + let result = await encryptedAdapter.getFileData(fileName1); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data1); + const encryptedData1 = await unencryptedAdapter.getFileData(fileName1); + expect(encryptedData1.toString('utf-8')).not.toEqual(unencryptedResult1); + result = await encryptedAdapter.getFileData(fileName2); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data2); + const encryptedData2 = await unencryptedAdapter.getFileData(fileName2); + expect(encryptedData2.toString('utf-8')).not.toEqual(unencryptedResult2); + }); + + it('should rotate key of all old encrypted GridFS files to encrypted files', async () => { + const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + oldFileKey + ); + const encryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + 'newKeyThatILove' + ); + const fileName1 = 'file1.txt'; + const data1 = 'hello world'; + const fileName2 = 'file2.txt'; + const data2 = 'hello new world'; + //Store unecrypted files + await oldEncryptedAdapter.createFile(fileName1, data1); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( + fileName1 + ); + expect(oldEncryptedResult1.toString('utf8')).toBe(data1); + await oldEncryptedAdapter.createFile(fileName2, data2); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( + fileName2 + ); + expect(oldEncryptedResult2.toString('utf8')).toBe(data2); + //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter + const { rotated, notRotated } = await encryptedAdapter.rotateFileKey({ + oldKey: oldFileKey, + }); + expect(rotated.length).toEqual(2); + expect( + rotated.filter(function (value) { + return value === fileName1; + }).length + ).toEqual(1); + expect( + rotated.filter(function (value) { + return value === fileName2; + }).length + ).toEqual(1); + expect(notRotated.length).toEqual(0); + let result = await encryptedAdapter.getFileData(fileName1); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data1); + let encryptedData1; + try { + encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1); + } catch (err) { + expect(err).toMatch('Error'); + } + expect(encryptedData1).toBeUndefined(); + result = await encryptedAdapter.getFileData(fileName2); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data2); + let encryptedData2; + try { + encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2); + } catch (err) { + expect(err).toMatch('Error'); + } + expect(encryptedData2).toBeUndefined(); + }); + + it('should only encrypt specified fileNames', async () => { + const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + oldFileKey + ); + const encryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + 'newKeyThatILove' + ); + const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI); + const fileName1 = 'file1.txt'; + const data1 = 'hello world'; + const fileName2 = 'file2.txt'; + const data2 = 'hello new world'; + //Store unecrypted files + await oldEncryptedAdapter.createFile(fileName1, data1); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( + fileName1 + ); + expect(oldEncryptedResult1.toString('utf8')).toBe(data1); + await oldEncryptedAdapter.createFile(fileName2, data2); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( + fileName2 + ); + expect(oldEncryptedResult2.toString('utf8')).toBe(data2); + //Inject unecrypted file to see if causes an issue + const fileName3 = 'file3.txt'; + const data3 = 'hello past world'; + await unEncryptedAdapter.createFile(fileName3, data3, 'text/utf8'); + //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter + const { rotated, notRotated } = await encryptedAdapter.rotateFileKey({ + oldKey: oldFileKey, + fileNames: [fileName1, fileName2], + }); + expect(rotated.length).toEqual(2); + expect( + rotated.filter(function (value) { + return value === fileName1; + }).length + ).toEqual(1); + expect( + rotated.filter(function (value) { + return value === fileName2; + }).length + ).toEqual(1); + expect(notRotated.length).toEqual(0); + expect( + rotated.filter(function (value) { + return value === fileName3; + }).length + ).toEqual(0); + let result = await encryptedAdapter.getFileData(fileName1); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data1); + let encryptedData1; + try { + encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1); + } catch (err) { + expect(err).toMatch('Error'); + } + expect(encryptedData1).toBeUndefined(); + result = await encryptedAdapter.getFileData(fileName2); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data2); + let encryptedData2; + try { + encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2); + } catch (err) { + expect(err).toMatch('Error'); + } + expect(encryptedData2).toBeUndefined(); + }); + + it("should return fileNames of those it can't encrypt with the new key", async () => { + const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + oldFileKey + ); + const encryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + 'newKeyThatILove' + ); + const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI); + const fileName1 = 'file1.txt'; + const data1 = 'hello world'; + const fileName2 = 'file2.txt'; + const data2 = 'hello new world'; + //Store unecrypted files + await oldEncryptedAdapter.createFile(fileName1, data1); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( + fileName1 + ); + expect(oldEncryptedResult1.toString('utf8')).toBe(data1); + await oldEncryptedAdapter.createFile(fileName2, data2); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( + fileName2 + ); + expect(oldEncryptedResult2.toString('utf8')).toBe(data2); + //Inject unecrypted file to see if causes an issue + const fileName3 = 'file3.txt'; + const data3 = 'hello past world'; + await unEncryptedAdapter.createFile(fileName3, data3, 'text/utf8'); + //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter + const { rotated, notRotated } = await encryptedAdapter.rotateFileKey({ + oldKey: oldFileKey, + }); + expect(rotated.length).toEqual(2); + expect( + rotated.filter(function (value) { + return value === fileName1; + }).length + ).toEqual(1); + expect( + rotated.filter(function (value) { + return value === fileName2; + }).length + ).toEqual(1); + expect(notRotated.length).toEqual(1); + expect( + notRotated.filter(function (value) { + return value === fileName3; + }).length + ).toEqual(1); + let result = await encryptedAdapter.getFileData(fileName1); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data1); + let encryptedData1; + try { + encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1); + } catch (err) { + expect(err).toMatch('Error'); + } + expect(encryptedData1).toBeUndefined(); + result = await encryptedAdapter.getFileData(fileName2); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data2); + let encryptedData2; + try { + encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2); + } catch (err) { + expect(err).toMatch('Error'); + } + expect(encryptedData2).toBeUndefined(); }); it('should save metadata', async () => { diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 958f9764d8..83a08e715f 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -66,15 +66,26 @@ export class GridFSBucketAdapter extends FilesAdapter { metadata: options.metadata, }); 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); + try { + 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); + } catch (err) { + stream.end(); + return new Promise((resolve, reject) => { + return reject(err); + }); + } } else { await stream.write(data); } @@ -110,22 +121,28 @@ export class GridFSBucketAdapter extends FilesAdapter { stream.on('end', () => { 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()]) - ); + try { + 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); + const decrypted = Buffer.concat([ + decipher.update(encrypted), + decipher.final(), + ]); + return resolve(decrypted); + } catch (err) { + return reject(err); + } } - resolve(data); + return resolve(data); }); stream.on('error', (err) => { reject(err); @@ -133,6 +150,79 @@ export class GridFSBucketAdapter extends FilesAdapter { }); } + async rotateFileKey(options = {}) { + var fileNames = []; + var oldKeyFileAdapter = {}; + const bucket = await this._getBucket(); + if (options.oldKey !== undefined) { + oldKeyFileAdapter = new GridFSBucketAdapter( + this._databaseURI, + this._mongoOptions, + options.oldKey + ); + } else { + oldKeyFileAdapter = new GridFSBucketAdapter( + this._databaseURI, + this._mongoOptions + ); + } + if (options.fileNames !== undefined) { + fileNames = options.fileNames; + } else { + const fileNamesIterator = await bucket.find().toArray(); + fileNamesIterator.forEach((file) => { + fileNames.push(file.filename); + }); + } + var fileNamesNotRotated = fileNames; + var fileNamesRotated = []; + var fileNameTotal = fileNames.length; + var fileNameIndex = 0; + return new Promise((resolve) => { + for (const fileName of fileNames) { + oldKeyFileAdapter + .getFileData(fileName) + .then((plainTextData) => { + //Overwrite file with data encrypted with new key + this.createFile(fileName, plainTextData) + .then(() => { + fileNamesRotated.push(fileName); + fileNamesNotRotated = fileNamesNotRotated.filter(function ( + value + ) { + return value !== fileName; + }); + fileNameIndex += 1; + if (fileNameIndex == fileNameTotal) { + resolve({ + rotated: fileNamesRotated, + notRotated: fileNamesNotRotated, + }); + } + }) + .catch(() => { + fileNameIndex += 1; + if (fileNameIndex == fileNameTotal) { + resolve({ + rotated: fileNamesRotated, + notRotated: fileNamesNotRotated, + }); + } + }); + }) + .catch(() => { + fileNameIndex += 1; + if (fileNameIndex == fileNameTotal) { + resolve({ + rotated: fileNamesRotated, + notRotated: fileNamesNotRotated, + }); + } + }); + } + }); + } + getFileLocation(config, filename) { return ( config.mount + From b822e5a58a731edf88da1615eb46ab1c4acb07be Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Thu, 2 Jul 2020 10:06:45 -0400 Subject: [PATCH 06/12] improve catching decryption errors in testcases --- spec/GridFSBucketStorageAdapter.spec.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 3a4752b43c..e755ff3f8e 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -143,22 +143,26 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { let result = await encryptedAdapter.getFileData(fileName1); expect(result instanceof Buffer).toBe(true); expect(result.toString('utf-8')).toEqual(data1); + let decryptionError1; let encryptedData1; try { encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1); } catch (err) { - expect(err).toMatch('Error'); + decryptionError1 = err; } + expect(decryptionError1).toMatch('Error'); expect(encryptedData1).toBeUndefined(); result = await encryptedAdapter.getFileData(fileName2); expect(result instanceof Buffer).toBe(true); expect(result.toString('utf-8')).toEqual(data2); + let decryptionError2; let encryptedData2; try { encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2); } catch (err) { - expect(err).toMatch('Error'); + decryptionError2 = err; } + expect(decryptionError2).toMatch('Error'); expect(encryptedData2).toBeUndefined(); }); @@ -219,22 +223,26 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { let result = await encryptedAdapter.getFileData(fileName1); expect(result instanceof Buffer).toBe(true); expect(result.toString('utf-8')).toEqual(data1); + let decryptionError1; let encryptedData1; try { encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1); } catch (err) { - expect(err).toMatch('Error'); + decryptionError1 = err; } + expect(decryptionError1).toMatch('Error'); expect(encryptedData1).toBeUndefined(); result = await encryptedAdapter.getFileData(fileName2); expect(result instanceof Buffer).toBe(true); expect(result.toString('utf-8')).toEqual(data2); + let decryptionError2; let encryptedData2; try { encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2); } catch (err) { - expect(err).toMatch('Error'); + decryptionError2 = err; } + expect(decryptionError2).toMatch('Error'); expect(encryptedData2).toBeUndefined(); }); @@ -294,22 +302,26 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { let result = await encryptedAdapter.getFileData(fileName1); expect(result instanceof Buffer).toBe(true); expect(result.toString('utf-8')).toEqual(data1); + let decryptionError1; let encryptedData1; try { encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1); } catch (err) { - expect(err).toMatch('Error'); + decryptionError1 = err; } + expect(decryptionError1).toMatch('Error'); expect(encryptedData1).toBeUndefined(); result = await encryptedAdapter.getFileData(fileName2); expect(result instanceof Buffer).toBe(true); expect(result.toString('utf-8')).toEqual(data2); + let decryptionError2; let encryptedData2; try { encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2); } catch (err) { - expect(err).toMatch('Error'); + decryptionError2 = err; } + expect(decryptionError2).toMatch('Error'); expect(encryptedData2).toBeUndefined(); }); From 113274ab46a98216bca5390e0c0976176487464e Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Thu, 2 Jul 2020 13:26:44 -0400 Subject: [PATCH 07/12] add testcase for rotating key from oldKey to noKey leaving all files decrypted --- spec/GridFSBucketStorageAdapter.spec.js | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index e755ff3f8e..f015b12415 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -166,6 +166,71 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { expect(encryptedData2).toBeUndefined(); }); + it('should rotate key of all old encrypted GridFS files to unencrypted files', async () => { + const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptedAdapter = new GridFSBucketAdapter( + databaseURI, + {}, + oldFileKey + ); + const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI); + const fileName1 = 'file1.txt'; + const data1 = 'hello world'; + const fileName2 = 'file2.txt'; + const data2 = 'hello new world'; + //Store unecrypted files + await oldEncryptedAdapter.createFile(fileName1, data1); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( + fileName1 + ); + expect(oldEncryptedResult1.toString('utf8')).toBe(data1); + await oldEncryptedAdapter.createFile(fileName2, data2); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( + fileName2 + ); + expect(oldEncryptedResult2.toString('utf8')).toBe(data2); + //Check if unEncrypted adapter can read data and make sure it's not the same as oldEncrypted adapter + const { rotated, notRotated } = await unEncryptedAdapter.rotateFileKey({ + oldKey: oldFileKey, + }); + expect(rotated.length).toEqual(2); + expect( + rotated.filter(function (value) { + return value === fileName1; + }).length + ).toEqual(1); + expect( + rotated.filter(function (value) { + return value === fileName2; + }).length + ).toEqual(1); + expect(notRotated.length).toEqual(0); + let result = await unEncryptedAdapter.getFileData(fileName1); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data1); + let decryptionError1; + let encryptedData1; + try { + encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1); + } catch (err) { + decryptionError1 = err; + } + expect(decryptionError1).toMatch('Error'); + expect(encryptedData1).toBeUndefined(); + result = await unEncryptedAdapter.getFileData(fileName2); + expect(result instanceof Buffer).toBe(true); + expect(result.toString('utf-8')).toEqual(data2); + let decryptionError2; + let encryptedData2; + try { + encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2); + } catch (err) { + decryptionError2 = err; + } + expect(decryptionError2).toMatch('Error'); + expect(encryptedData2).toBeUndefined(); + }); + it('should only encrypt specified fileNames', async () => { const oldFileKey = 'oldKeyThatILoved'; const oldEncryptedAdapter = new GridFSBucketAdapter( From d4452da2d20b8318ca764f98a29c7bdfa103d271 Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Thu, 2 Jul 2020 15:36:58 -0400 Subject: [PATCH 08/12] removed fileKey from legacy test links. From the looks of the tests and the fileKey was appended to links. This key is now an encryption key --- spec/ParseFile.spec.js | 172 ++++++++++++++--------------- src/Controllers/FilesController.js | 24 ++-- 2 files changed, 93 insertions(+), 103 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index bb1c5c512f..ecb82b2f88 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -13,7 +13,7 @@ for (let i = 0; i < str.length; i++) { describe('Parse.File testing', () => { describe('creating files', () => { - it('works with Content-Type', done => { + it('works with Content-Type', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -24,13 +24,13 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then(response => { + }).then((response) => { const b = response.data; expect(b.name).toMatch(/_file.txt$/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/ ); - request({ url: b.url }).then(response => { + request({ url: b.url }).then((response) => { const body = response.text; expect(body).toEqual('argle bargle'); done(); @@ -38,7 +38,7 @@ describe('Parse.File testing', () => { }); }); - it('works with _ContentType', done => { + it('works with _ContentType', (done) => { request({ method: 'POST', url: 'http://localhost:8378/1/files/file', @@ -48,13 +48,13 @@ describe('Parse.File testing', () => { _ContentType: 'text/html', base64: 'PGh0bWw+PC9odG1sPgo=', }), - }).then(response => { + }).then((response) => { const b = response.data; expect(b.name).toMatch(/_file.html/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/ ); - request({ url: b.url }).then(response => { + request({ url: b.url }).then((response) => { const body = response.text; try { expect(response.headers['content-type']).toMatch('^text/html'); @@ -67,7 +67,7 @@ describe('Parse.File testing', () => { }); }); - it('works without Content-Type', done => { + it('works without Content-Type', (done) => { const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', @@ -77,13 +77,13 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then(response => { + }).then((response) => { const b = response.data; expect(b.name).toMatch(/_file.txt$/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/ ); - request({ url: b.url }).then(response => { + request({ url: b.url }).then((response) => { expect(response.text).toEqual('argle bargle'); done(); }); @@ -91,7 +91,7 @@ describe('Parse.File testing', () => { }); }); - it('supports REST end-to-end file create, read, delete, read', done => { + it('supports REST end-to-end file create, read, delete, read', (done) => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -102,13 +102,13 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/testfile.txt', body: 'check one two', - }).then(response => { + }).then((response) => { const b = response.data; expect(b.name).toMatch(/_testfile.txt$/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/ ); - request({ url: b.url }).then(response => { + request({ url: b.url }).then((response) => { const body = response.text; expect(body).toEqual('check one two'); request({ @@ -119,7 +119,7 @@ describe('Parse.File testing', () => { 'X-Parse-Master-Key': 'test', }, url: 'http://localhost:8378/1/files/' + b.name, - }).then(response => { + }).then((response) => { expect(response.status).toEqual(200); request({ headers: { @@ -127,7 +127,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', }, url: b.url, - }).then(fail, response => { + }).then(fail, (response) => { expect(response.status).toEqual(404); done(); }); @@ -136,7 +136,7 @@ describe('Parse.File testing', () => { }); }); - it('blocks file deletions with missing or incorrect master-key header', done => { + it('blocks file deletions with missing or incorrect master-key header', (done) => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -147,7 +147,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/thefile.jpg', body: 'the file body', - }).then(response => { + }).then((response) => { const b = response.data; expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/ @@ -160,7 +160,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', }, url: 'http://localhost:8378/1/files/' + b.name, - }).then(fail, response => { + }).then(fail, (response) => { const del_b = response.data; expect(response.status).toEqual(403); expect(del_b.error).toMatch(/unauthorized/); @@ -173,7 +173,7 @@ describe('Parse.File testing', () => { 'X-Parse-Master-Key': 'tryagain', }, url: 'http://localhost:8378/1/files/' + b.name, - }).then(fail, response => { + }).then(fail, (response) => { const del_b2 = response.data; expect(response.status).toEqual(403); expect(del_b2.error).toMatch(/unauthorized/); @@ -183,7 +183,7 @@ describe('Parse.File testing', () => { }); }); - it('handles other filetypes', done => { + it('handles other filetypes', (done) => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -194,11 +194,11 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.jpg', body: 'argle bargle', - }).then(response => { + }).then((response) => { const b = response.data; expect(b.name).toMatch(/_file.jpg$/); expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/); - request({ url: b.url }).then(response => { + request({ url: b.url }).then((response) => { const body = response.text; expect(body).toEqual('argle bargle'); done(); @@ -216,7 +216,7 @@ describe('Parse.File testing', () => { notEqual(file.name(), 'hello.txt'); }); - it('save file in object', async done => { + it('save file in object', async (done) => { const file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const result = await file.save(); @@ -247,7 +247,7 @@ describe('Parse.File testing', () => { ok(objectAgain.get('file') instanceof Parse.File); }); - it('autosave file in object', async done => { + it('autosave file in object', async (done) => { let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const object = new Parse.Object('TestObject'); @@ -261,7 +261,7 @@ describe('Parse.File testing', () => { done(); }); - it('autosave file in object in object', async done => { + it('autosave file in object in object', async (done) => { let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); @@ -298,25 +298,25 @@ describe('Parse.File testing', () => { equal(file.name(), previousName); }); - it('two saves at the same time', done => { + it('two saves at the same time', (done) => { const file = new Parse.File('hello.txt', data, 'text/plain'); let firstName; let secondName; - const firstSave = file.save().then(function() { + const firstSave = file.save().then(function () { firstName = file.name(); }); - const secondSave = file.save().then(function() { + const secondSave = file.save().then(function () { secondName = file.name(); }); Promise.all([firstSave, secondSave]).then( - function() { + function () { equal(firstName, secondName); done(); }, - function(error) { + function (error) { ok(false, error); done(); } @@ -333,7 +333,7 @@ describe('Parse.File testing', () => { ok(object.toJSON().file.url); }); - it('content-type used with no extension', done => { + it('content-type used with no extension', (done) => { const headers = { 'Content-Type': 'text/html', 'X-Parse-Application-Id': 'test', @@ -344,17 +344,17 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file', body: 'fee fi fo', - }).then(response => { + }).then((response) => { const b = response.data; expect(b.name).toMatch(/\.html$/); - request({ url: b.url }).then(response => { + request({ url: b.url }).then((response) => { expect(response.headers['content-type']).toMatch(/^text\/html/); done(); }); }); }); - it('filename is url encoded', done => { + it('filename is url encoded', (done) => { const headers = { 'Content-Type': 'text/html', 'X-Parse-Application-Id': 'test', @@ -365,14 +365,14 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/hello world.txt', body: 'oh emm gee', - }).then(response => { + }).then((response) => { const b = response.data; expect(b.url).toMatch(/hello%20world/); done(); }); }); - it('supports array of files', done => { + it('supports array of files', (done) => { const file = { __type: 'File', url: 'http://meep.meep', @@ -387,7 +387,7 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FilesArrayTest'); return query.first(); }) - .then(result => { + .then((result) => { const filesAgain = result.get('files'); expect(filesAgain.length).toEqual(2); expect(filesAgain[0].name()).toEqual('meep'); @@ -396,7 +396,7 @@ describe('Parse.File testing', () => { }); }); - it('validates filename characters', done => { + it('validates filename characters', (done) => { const headers = { 'Content-Type': 'text/plain', 'X-Parse-Application-Id': 'test', @@ -407,14 +407,14 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/di$avowed.txt', body: 'will fail', - }).then(fail, response => { + }).then(fail, (response) => { const b = response.data; expect(b.code).toEqual(122); done(); }); }); - it('validates filename length', done => { + it('validates filename length', (done) => { const headers = { 'Content-Type': 'text/plain', 'X-Parse-Application-Id': 'test', @@ -430,14 +430,14 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/' + fileName, body: 'will fail', - }).then(fail, response => { + }).then(fail, (response) => { const b = response.data; expect(b.code).toEqual(122); done(); }); }); - it('supports a dictionary with file', done => { + it('supports a dictionary with file', (done) => { const file = { __type: 'File', url: 'http://meep.meep', @@ -454,7 +454,7 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FileObjTest'); return query.first(); }) - .then(result => { + .then((result) => { const dictAgain = result.get('obj'); expect(typeof dictAgain).toEqual('object'); const fileAgain = dictAgain['file']; @@ -462,13 +462,13 @@ describe('Parse.File testing', () => { expect(fileAgain.url()).toEqual('http://meep.meep'); done(); }) - .catch(e => { + .catch((e) => { jfail(e); done(); }); }); - it('creates correct url for old files hosted on files.parsetfss.com', done => { + it('creates correct url for old files hosted on files.parsetfss.com', (done) => { const file = { __type: 'File', url: 'http://irrelevant.elephant/', @@ -482,20 +482,20 @@ describe('Parse.File testing', () => { const query = new Parse.Query('OldFileTest'); return query.first(); }) - .then(result => { + .then((result) => { const fileAgain = result.get('oldfile'); expect(fileAgain.url()).toEqual( - 'http://files.parsetfss.com/test/tfss-123.txt' + 'http://files.parsetfss.com/tfss-123.txt' ); done(); }) - .catch(e => { + .catch((e) => { jfail(e); done(); }); }); - it('creates correct url for old files hosted on files.parse.com', done => { + it('creates correct url for old files hosted on files.parse.com', (done) => { const file = { __type: 'File', url: 'http://irrelevant.elephant/', @@ -509,20 +509,20 @@ describe('Parse.File testing', () => { const query = new Parse.Query('OldFileTest'); return query.first(); }) - .then(result => { + .then((result) => { const fileAgain = result.get('oldfile'); expect(fileAgain.url()).toEqual( - 'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt' + 'http://files.parse.com/d6e80979-a128-4c57-a167-302f874700dc-123.txt' ); done(); }) - .catch(e => { + .catch((e) => { jfail(e); done(); }); }); - it('supports files in objects without urls', done => { + it('supports files in objects without urls', (done) => { const file = { __type: 'File', name: '123.txt', @@ -535,18 +535,18 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FileTest'); return query.first(); }) - .then(result => { + .then((result) => { const fileAgain = result.get('file'); expect(fileAgain.url()).toMatch(/123.txt$/); done(); }) - .catch(e => { + .catch((e) => { jfail(e); done(); }); }); - it('return with publicServerURL when provided', done => { + it('return with publicServerURL when provided', (done) => { reconfigureServer({ publicServerURL: 'https://mydomain/parse', }) @@ -563,18 +563,18 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FileTest'); return query.first(); }) - .then(result => { + .then((result) => { const fileAgain = result.get('file'); expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0); done(); }) - .catch(e => { + .catch((e) => { jfail(e); done(); }); }); - it('fails to upload an empty file', done => { + it('fails to upload an empty file', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -585,7 +585,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: '', - }).then(fail, response => { + }).then(fail, (response) => { expect(response.status).toBe(400); const body = response.text; expect(body).toEqual('{"code":130,"error":"Invalid file upload."}'); @@ -593,7 +593,7 @@ describe('Parse.File testing', () => { }); }); - it('fails to upload without a file name', done => { + it('fails to upload without a file name', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -604,7 +604,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/', body: 'yolo', - }).then(fail, response => { + }).then(fail, (response) => { expect(response.status).toBe(400); const body = response.text; expect(body).toEqual('{"code":122,"error":"Filename not provided."}'); @@ -612,7 +612,7 @@ describe('Parse.File testing', () => { }); }); - it('fails to delete an unkown file', done => { + it('fails to delete an unkown file', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -623,7 +623,7 @@ describe('Parse.File testing', () => { method: 'DELETE', headers: headers, url: 'http://localhost:8378/1/files/file.txt', - }).then(fail, response => { + }).then(fail, (response) => { expect(response.status).toBe(400); const body = response.text; expect(typeof body).toBe('string'); @@ -636,7 +636,7 @@ describe('Parse.File testing', () => { }); xdescribe('Gridstore Range tests', () => { - it('supports range requests', done => { + it('supports range requests', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -647,7 +647,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then(response => { + }).then((response) => { const b = response.data; request({ url: b.url, @@ -657,7 +657,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=0-5', }, - }).then(response => { + }).then((response) => { const body = response.text; expect(body).toEqual('argle '); done(); @@ -665,7 +665,7 @@ describe('Parse.File testing', () => { }); }); - it('supports small range requests', done => { + it('supports small range requests', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -676,7 +676,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then(response => { + }).then((response) => { const b = response.data; request({ url: b.url, @@ -686,7 +686,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=0-2', }, - }).then(response => { + }).then((response) => { const body = response.text; expect(body).toEqual('arg'); done(); @@ -695,7 +695,7 @@ describe('Parse.File testing', () => { }); // See specs https://www.greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges - it('supports getting one byte', done => { + it('supports getting one byte', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -706,7 +706,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then(response => { + }).then((response) => { const b = response.data; request({ url: b.url, @@ -716,7 +716,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=2-2', }, - }).then(response => { + }).then((response) => { const body = response.text; expect(body).toEqual('g'); done(); @@ -724,7 +724,7 @@ describe('Parse.File testing', () => { }); }); - xit('supports getting last n bytes', done => { + xit('supports getting last n bytes', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -735,7 +735,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'something different', - }).then(response => { + }).then((response) => { const b = response.data; request({ url: b.url, @@ -745,7 +745,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=-4', }, - }).then(response => { + }).then((response) => { const body = response.text; expect(body.length).toBe(4); expect(body).toEqual('rent'); @@ -754,7 +754,7 @@ describe('Parse.File testing', () => { }); }); - it('supports getting first n bytes', done => { + it('supports getting first n bytes', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -765,7 +765,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'something different', - }).then(response => { + }).then((response) => { const b = response.data; request({ url: b.url, @@ -775,7 +775,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=10-', }, - }).then(response => { + }).then((response) => { const body = response.text; expect(body).toEqual('different'); done(); @@ -792,7 +792,7 @@ describe('Parse.File testing', () => { return s; } - it('supports large range requests', done => { + it('supports large range requests', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -803,7 +803,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: repeat('argle bargle', 100), - }).then(response => { + }).then((response) => { const b = response.data; request({ url: b.url, @@ -813,7 +813,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=13-240', }, - }).then(response => { + }).then((response) => { const body = response.text; expect(body.length).toEqual(228); expect(body.indexOf('rgle barglea')).toBe(0); @@ -822,7 +822,7 @@ describe('Parse.File testing', () => { }); }); - it('fails to stream unknown file', done => { + it('fails to stream unknown file', (done) => { request({ url: 'http://localhost:8378/1/files/test/file.txt', headers: { @@ -831,7 +831,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=13-240', }, - }).then(response => { + }).then((response) => { expect(response.status).toBe(404); const body = response.text; expect(body).toEqual('File not found.'); @@ -843,7 +843,7 @@ describe('Parse.File testing', () => { // Because GridStore is not loaded on PG, those are perfect // for fallback tests describe_only_db('postgres')('Default Range tests', () => { - it('fallback to regular request', done => { + it('fallback to regular request', (done) => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -854,7 +854,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then(response => { + }).then((response) => { const b = response.data; request({ url: b.url, @@ -864,7 +864,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=0-5', }, - }).then(response => { + }).then((response) => { const body = response.text; expect(body).toEqual('argle bargle'); done(); diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 1caf388e62..4aa144d515 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -75,24 +75,14 @@ export class FilesController extends AdaptableController { // all filenames starting with "tfss-" should be from files.parsetfss.com // all filenames starting with a "-" seperated UUID should be from files.parse.com // all other filenames have been migrated or created from Parse Server - if (config.fileKey === undefined) { - fileObject['url'] = this.adapter.getFileLocation(config, filename); + if (filename.indexOf('tfss-') === 0) { + fileObject['url'] = + 'http://files.parsetfss.com/' + encodeURIComponent(filename); + } else if (legacyFilesRegex.test(filename)) { + fileObject['url'] = + 'http://files.parse.com/' + encodeURIComponent(filename); } else { - if (filename.indexOf('tfss-') === 0) { - fileObject['url'] = - 'http://files.parsetfss.com/' + - config.fileKey + - '/' + - encodeURIComponent(filename); - } else if (legacyFilesRegex.test(filename)) { - fileObject['url'] = - 'http://files.parse.com/' + - config.fileKey + - '/' + - encodeURIComponent(filename); - } else { - fileObject['url'] = this.adapter.getFileLocation(config, filename); - } + fileObject['url'] = this.adapter.getFileLocation(config, filename); } } } From 5006b0fe947badeda23424aa7254dc7870141f7a Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Sat, 4 Jul 2020 09:26:34 -0400 Subject: [PATCH 09/12] clean up code --- src/Adapters/Files/GridFSBucketAdapter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 83a08e715f..c1bd608650 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -81,7 +81,6 @@ export class GridFSBucketAdapter extends FilesAdapter { ]); await stream.write(encryptedResult); } catch (err) { - stream.end(); return new Promise((resolve, reject) => { return reject(err); }); @@ -142,7 +141,7 @@ export class GridFSBucketAdapter extends FilesAdapter { return reject(err); } } - return resolve(data); + resolve(data); }); stream.on('error', (err) => { reject(err); From 940d91b2846a90b5367566b091a6f24caf5bfaed Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Sat, 4 Jul 2020 19:17:07 -0400 Subject: [PATCH 10/12] make more consistant with FSAdapter --- src/Adapters/Files/GridFSBucketAdapter.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index c1bd608650..0ae0ea55c5 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -173,12 +173,12 @@ export class GridFSBucketAdapter extends FilesAdapter { fileNames.push(file.filename); }); } - var fileNamesNotRotated = fileNames; - var fileNamesRotated = []; - var fileNameTotal = fileNames.length; - var fileNameIndex = 0; return new Promise((resolve) => { - for (const fileName of fileNames) { + var fileNamesNotRotated = fileNames; + var fileNamesRotated = []; + var fileNameTotal = fileNames.length; + var fileNameIndex = 0; + fileNames.forEach((fileName) => { oldKeyFileAdapter .getFileData(fileName) .then((plainTextData) => { @@ -218,7 +218,7 @@ export class GridFSBucketAdapter extends FilesAdapter { }); } }); - } + }); }); } From 26c6507bd8464ab0c72c6498c6bd7567fc33fcfc Mon Sep 17 00:00:00 2001 From: Corey MacBook Pro Date: Fri, 23 Oct 2020 21:51:17 -0400 Subject: [PATCH 11/12] use encryptionKey instead of fileKey --- spec/GridFSBucketStorageAdapter.spec.js | 42 +++--- spec/ParseFile.spec.js | 164 +++++++++++----------- src/Adapters/Files/GridFSBucketAdapter.js | 26 ++-- src/Controllers/FilesController.js | 24 +++- src/Options/Definitions.js | 4 + 5 files changed, 140 insertions(+), 120 deletions(-) diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index f015b12415..027f816dd1 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -35,7 +35,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { expect(gfsResult.toString('utf8')).toBe(originalString); }); - it('should save an encrypted file that can only be decrypted by a GridFS adapter with the fileKey', async () => { + it('should save an encrypted file that can only be decrypted by a GridFS adapter with the encryptionKey', async () => { const unencryptedAdapter = new GridFSBucketAdapter(databaseURI); const encryptedAdapter = new GridFSBucketAdapter( databaseURI, @@ -72,7 +72,10 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const unencryptedResult2 = await unencryptedAdapter.getFileData(fileName2); expect(unencryptedResult2.toString('utf8')).toBe(data2); //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter - const { rotated, notRotated } = await encryptedAdapter.rotateFileKey(); + const { + rotated, + notRotated, + } = await encryptedAdapter.rotateEncryptionKey(); expect(rotated.length).toEqual(2); expect( rotated.filter(function (value) { @@ -98,11 +101,11 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { }); it('should rotate key of all old encrypted GridFS files to encrypted files', async () => { - const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptionKey = 'oldKeyThatILoved'; const oldEncryptedAdapter = new GridFSBucketAdapter( databaseURI, {}, - oldFileKey + oldEncryptionKey ); const encryptedAdapter = new GridFSBucketAdapter( databaseURI, @@ -125,8 +128,8 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { ); expect(oldEncryptedResult2.toString('utf8')).toBe(data2); //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter - const { rotated, notRotated } = await encryptedAdapter.rotateFileKey({ - oldKey: oldFileKey, + const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({ + oldKey: oldEncryptionKey, }); expect(rotated.length).toEqual(2); expect( @@ -167,11 +170,11 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { }); it('should rotate key of all old encrypted GridFS files to unencrypted files', async () => { - const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptionKey = 'oldKeyThatILoved'; const oldEncryptedAdapter = new GridFSBucketAdapter( databaseURI, {}, - oldFileKey + oldEncryptionKey ); const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI); const fileName1 = 'file1.txt'; @@ -190,8 +193,11 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { ); expect(oldEncryptedResult2.toString('utf8')).toBe(data2); //Check if unEncrypted adapter can read data and make sure it's not the same as oldEncrypted adapter - const { rotated, notRotated } = await unEncryptedAdapter.rotateFileKey({ - oldKey: oldFileKey, + const { + rotated, + notRotated, + } = await unEncryptedAdapter.rotateEncryptionKey({ + oldKey: oldEncryptionKey, }); expect(rotated.length).toEqual(2); expect( @@ -232,11 +238,11 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { }); it('should only encrypt specified fileNames', async () => { - const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptionKey = 'oldKeyThatILoved'; const oldEncryptedAdapter = new GridFSBucketAdapter( databaseURI, {}, - oldFileKey + oldEncryptionKey ); const encryptedAdapter = new GridFSBucketAdapter( databaseURI, @@ -264,8 +270,8 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const data3 = 'hello past world'; await unEncryptedAdapter.createFile(fileName3, data3, 'text/utf8'); //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter - const { rotated, notRotated } = await encryptedAdapter.rotateFileKey({ - oldKey: oldFileKey, + const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({ + oldKey: oldEncryptionKey, fileNames: [fileName1, fileName2], }); expect(rotated.length).toEqual(2); @@ -312,11 +318,11 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { }); it("should return fileNames of those it can't encrypt with the new key", async () => { - const oldFileKey = 'oldKeyThatILoved'; + const oldEncryptionKey = 'oldKeyThatILoved'; const oldEncryptedAdapter = new GridFSBucketAdapter( databaseURI, {}, - oldFileKey + oldEncryptionKey ); const encryptedAdapter = new GridFSBucketAdapter( databaseURI, @@ -344,8 +350,8 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const data3 = 'hello past world'; await unEncryptedAdapter.createFile(fileName3, data3, 'text/utf8'); //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter - const { rotated, notRotated } = await encryptedAdapter.rotateFileKey({ - oldKey: oldFileKey, + const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({ + oldKey: oldEncryptionKey, }); expect(rotated.length).toEqual(2); expect( diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index ecb82b2f88..1d2bf2e71e 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -13,7 +13,7 @@ for (let i = 0; i < str.length; i++) { describe('Parse.File testing', () => { describe('creating files', () => { - it('works with Content-Type', (done) => { + it('works with Content-Type', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -24,13 +24,13 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then((response) => { + }).then(response => { const b = response.data; expect(b.name).toMatch(/_file.txt$/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/ ); - request({ url: b.url }).then((response) => { + request({ url: b.url }).then(response => { const body = response.text; expect(body).toEqual('argle bargle'); done(); @@ -38,7 +38,7 @@ describe('Parse.File testing', () => { }); }); - it('works with _ContentType', (done) => { + it('works with _ContentType', done => { request({ method: 'POST', url: 'http://localhost:8378/1/files/file', @@ -48,13 +48,13 @@ describe('Parse.File testing', () => { _ContentType: 'text/html', base64: 'PGh0bWw+PC9odG1sPgo=', }), - }).then((response) => { + }).then(response => { const b = response.data; expect(b.name).toMatch(/_file.html/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/ ); - request({ url: b.url }).then((response) => { + request({ url: b.url }).then(response => { const body = response.text; try { expect(response.headers['content-type']).toMatch('^text/html'); @@ -67,7 +67,7 @@ describe('Parse.File testing', () => { }); }); - it('works without Content-Type', (done) => { + it('works without Content-Type', done => { const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', @@ -77,13 +77,13 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then((response) => { + }).then(response => { const b = response.data; expect(b.name).toMatch(/_file.txt$/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/ ); - request({ url: b.url }).then((response) => { + request({ url: b.url }).then(response => { expect(response.text).toEqual('argle bargle'); done(); }); @@ -91,7 +91,7 @@ describe('Parse.File testing', () => { }); }); - it('supports REST end-to-end file create, read, delete, read', (done) => { + it('supports REST end-to-end file create, read, delete, read', done => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -102,13 +102,13 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/testfile.txt', body: 'check one two', - }).then((response) => { + }).then(response => { const b = response.data; expect(b.name).toMatch(/_testfile.txt$/); expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/ ); - request({ url: b.url }).then((response) => { + request({ url: b.url }).then(response => { const body = response.text; expect(body).toEqual('check one two'); request({ @@ -119,7 +119,7 @@ describe('Parse.File testing', () => { 'X-Parse-Master-Key': 'test', }, url: 'http://localhost:8378/1/files/' + b.name, - }).then((response) => { + }).then(response => { expect(response.status).toEqual(200); request({ headers: { @@ -127,7 +127,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', }, url: b.url, - }).then(fail, (response) => { + }).then(fail, response => { expect(response.status).toEqual(404); done(); }); @@ -136,7 +136,7 @@ describe('Parse.File testing', () => { }); }); - it('blocks file deletions with missing or incorrect master-key header', (done) => { + it('blocks file deletions with missing or incorrect master-key header', done => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -147,7 +147,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/thefile.jpg', body: 'the file body', - }).then((response) => { + }).then(response => { const b = response.data; expect(b.url).toMatch( /^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/ @@ -160,7 +160,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', }, url: 'http://localhost:8378/1/files/' + b.name, - }).then(fail, (response) => { + }).then(fail, response => { const del_b = response.data; expect(response.status).toEqual(403); expect(del_b.error).toMatch(/unauthorized/); @@ -173,7 +173,7 @@ describe('Parse.File testing', () => { 'X-Parse-Master-Key': 'tryagain', }, url: 'http://localhost:8378/1/files/' + b.name, - }).then(fail, (response) => { + }).then(fail, response => { const del_b2 = response.data; expect(response.status).toEqual(403); expect(del_b2.error).toMatch(/unauthorized/); @@ -183,7 +183,7 @@ describe('Parse.File testing', () => { }); }); - it('handles other filetypes', (done) => { + it('handles other filetypes', done => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -194,11 +194,11 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.jpg', body: 'argle bargle', - }).then((response) => { + }).then(response => { const b = response.data; expect(b.name).toMatch(/_file.jpg$/); expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/); - request({ url: b.url }).then((response) => { + request({ url: b.url }).then(response => { const body = response.text; expect(body).toEqual('argle bargle'); done(); @@ -216,7 +216,7 @@ describe('Parse.File testing', () => { notEqual(file.name(), 'hello.txt'); }); - it('save file in object', async (done) => { + it('save file in object', async done => { const file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const result = await file.save(); @@ -247,7 +247,7 @@ describe('Parse.File testing', () => { ok(objectAgain.get('file') instanceof Parse.File); }); - it('autosave file in object', async (done) => { + it('autosave file in object', async done => { let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const object = new Parse.Object('TestObject'); @@ -261,7 +261,7 @@ describe('Parse.File testing', () => { done(); }); - it('autosave file in object in object', async (done) => { + it('autosave file in object in object', async done => { let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); @@ -298,7 +298,7 @@ describe('Parse.File testing', () => { equal(file.name(), previousName); }); - it('two saves at the same time', (done) => { + it('two saves at the same time', done => { const file = new Parse.File('hello.txt', data, 'text/plain'); let firstName; @@ -333,7 +333,7 @@ describe('Parse.File testing', () => { ok(object.toJSON().file.url); }); - it('content-type used with no extension', (done) => { + it('content-type used with no extension', done => { const headers = { 'Content-Type': 'text/html', 'X-Parse-Application-Id': 'test', @@ -344,17 +344,17 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file', body: 'fee fi fo', - }).then((response) => { + }).then(response => { const b = response.data; expect(b.name).toMatch(/\.html$/); - request({ url: b.url }).then((response) => { + request({ url: b.url }).then(response => { expect(response.headers['content-type']).toMatch(/^text\/html/); done(); }); }); }); - it('filename is url encoded', (done) => { + it('filename is url encoded', done => { const headers = { 'Content-Type': 'text/html', 'X-Parse-Application-Id': 'test', @@ -365,14 +365,14 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/hello world.txt', body: 'oh emm gee', - }).then((response) => { + }).then(response => { const b = response.data; expect(b.url).toMatch(/hello%20world/); done(); }); }); - it('supports array of files', (done) => { + it('supports array of files', done => { const file = { __type: 'File', url: 'http://meep.meep', @@ -387,7 +387,7 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FilesArrayTest'); return query.first(); }) - .then((result) => { + .then(result => { const filesAgain = result.get('files'); expect(filesAgain.length).toEqual(2); expect(filesAgain[0].name()).toEqual('meep'); @@ -396,7 +396,7 @@ describe('Parse.File testing', () => { }); }); - it('validates filename characters', (done) => { + it('validates filename characters', done => { const headers = { 'Content-Type': 'text/plain', 'X-Parse-Application-Id': 'test', @@ -407,14 +407,14 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/di$avowed.txt', body: 'will fail', - }).then(fail, (response) => { + }).then(fail, response => { const b = response.data; expect(b.code).toEqual(122); done(); }); }); - it('validates filename length', (done) => { + it('validates filename length', done => { const headers = { 'Content-Type': 'text/plain', 'X-Parse-Application-Id': 'test', @@ -430,14 +430,14 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/' + fileName, body: 'will fail', - }).then(fail, (response) => { + }).then(fail, response => { const b = response.data; expect(b.code).toEqual(122); done(); }); }); - it('supports a dictionary with file', (done) => { + it('supports a dictionary with file', done => { const file = { __type: 'File', url: 'http://meep.meep', @@ -454,7 +454,7 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FileObjTest'); return query.first(); }) - .then((result) => { + .then(result => { const dictAgain = result.get('obj'); expect(typeof dictAgain).toEqual('object'); const fileAgain = dictAgain['file']; @@ -462,13 +462,13 @@ describe('Parse.File testing', () => { expect(fileAgain.url()).toEqual('http://meep.meep'); done(); }) - .catch((e) => { + .catch(e => { jfail(e); done(); }); }); - it('creates correct url for old files hosted on files.parsetfss.com', (done) => { + it('creates correct url for old files hosted on files.parsetfss.com', done => { const file = { __type: 'File', url: 'http://irrelevant.elephant/', @@ -482,20 +482,20 @@ describe('Parse.File testing', () => { const query = new Parse.Query('OldFileTest'); return query.first(); }) - .then((result) => { + .then(result => { const fileAgain = result.get('oldfile'); expect(fileAgain.url()).toEqual( - 'http://files.parsetfss.com/tfss-123.txt' + 'http://files.parsetfss.com/test/tfss-123.txt' ); done(); }) - .catch((e) => { + .catch(e => { jfail(e); done(); }); }); - it('creates correct url for old files hosted on files.parse.com', (done) => { + it('creates correct url for old files hosted on files.parse.com', done => { const file = { __type: 'File', url: 'http://irrelevant.elephant/', @@ -509,20 +509,20 @@ describe('Parse.File testing', () => { const query = new Parse.Query('OldFileTest'); return query.first(); }) - .then((result) => { + .then(result => { const fileAgain = result.get('oldfile'); expect(fileAgain.url()).toEqual( - 'http://files.parse.com/d6e80979-a128-4c57-a167-302f874700dc-123.txt' + 'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt' ); done(); }) - .catch((e) => { + .catch(e => { jfail(e); done(); }); }); - it('supports files in objects without urls', (done) => { + it('supports files in objects without urls', done => { const file = { __type: 'File', name: '123.txt', @@ -535,18 +535,18 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FileTest'); return query.first(); }) - .then((result) => { + .then(result => { const fileAgain = result.get('file'); expect(fileAgain.url()).toMatch(/123.txt$/); done(); }) - .catch((e) => { + .catch(e => { jfail(e); done(); }); }); - it('return with publicServerURL when provided', (done) => { + it('return with publicServerURL when provided', done => { reconfigureServer({ publicServerURL: 'https://mydomain/parse', }) @@ -563,18 +563,18 @@ describe('Parse.File testing', () => { const query = new Parse.Query('FileTest'); return query.first(); }) - .then((result) => { + .then(result => { const fileAgain = result.get('file'); expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0); done(); }) - .catch((e) => { + .catch(e => { jfail(e); done(); }); }); - it('fails to upload an empty file', (done) => { + it('fails to upload an empty file', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -585,7 +585,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: '', - }).then(fail, (response) => { + }).then(fail, response => { expect(response.status).toBe(400); const body = response.text; expect(body).toEqual('{"code":130,"error":"Invalid file upload."}'); @@ -593,7 +593,7 @@ describe('Parse.File testing', () => { }); }); - it('fails to upload without a file name', (done) => { + it('fails to upload without a file name', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -604,7 +604,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/', body: 'yolo', - }).then(fail, (response) => { + }).then(fail, response => { expect(response.status).toBe(400); const body = response.text; expect(body).toEqual('{"code":122,"error":"Filename not provided."}'); @@ -612,7 +612,7 @@ describe('Parse.File testing', () => { }); }); - it('fails to delete an unkown file', (done) => { + it('fails to delete an unkown file', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -623,7 +623,7 @@ describe('Parse.File testing', () => { method: 'DELETE', headers: headers, url: 'http://localhost:8378/1/files/file.txt', - }).then(fail, (response) => { + }).then(fail, response => { expect(response.status).toBe(400); const body = response.text; expect(typeof body).toBe('string'); @@ -636,7 +636,7 @@ describe('Parse.File testing', () => { }); xdescribe('Gridstore Range tests', () => { - it('supports range requests', (done) => { + it('supports range requests', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -647,7 +647,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then((response) => { + }).then(response => { const b = response.data; request({ url: b.url, @@ -657,7 +657,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=0-5', }, - }).then((response) => { + }).then(response => { const body = response.text; expect(body).toEqual('argle '); done(); @@ -665,7 +665,7 @@ describe('Parse.File testing', () => { }); }); - it('supports small range requests', (done) => { + it('supports small range requests', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -676,7 +676,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then((response) => { + }).then(response => { const b = response.data; request({ url: b.url, @@ -686,7 +686,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=0-2', }, - }).then((response) => { + }).then(response => { const body = response.text; expect(body).toEqual('arg'); done(); @@ -695,7 +695,7 @@ describe('Parse.File testing', () => { }); // See specs https://www.greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges - it('supports getting one byte', (done) => { + it('supports getting one byte', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -706,7 +706,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then((response) => { + }).then(response => { const b = response.data; request({ url: b.url, @@ -716,7 +716,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=2-2', }, - }).then((response) => { + }).then(response => { const body = response.text; expect(body).toEqual('g'); done(); @@ -724,7 +724,7 @@ describe('Parse.File testing', () => { }); }); - xit('supports getting last n bytes', (done) => { + xit('supports getting last n bytes', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -735,7 +735,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'something different', - }).then((response) => { + }).then(response => { const b = response.data; request({ url: b.url, @@ -745,7 +745,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=-4', }, - }).then((response) => { + }).then(response => { const body = response.text; expect(body.length).toBe(4); expect(body).toEqual('rent'); @@ -754,7 +754,7 @@ describe('Parse.File testing', () => { }); }); - it('supports getting first n bytes', (done) => { + it('supports getting first n bytes', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -765,7 +765,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'something different', - }).then((response) => { + }).then(response => { const b = response.data; request({ url: b.url, @@ -775,7 +775,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=10-', }, - }).then((response) => { + }).then(response => { const body = response.text; expect(body).toEqual('different'); done(); @@ -792,7 +792,7 @@ describe('Parse.File testing', () => { return s; } - it('supports large range requests', (done) => { + it('supports large range requests', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -803,7 +803,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: repeat('argle bargle', 100), - }).then((response) => { + }).then(response => { const b = response.data; request({ url: b.url, @@ -813,7 +813,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=13-240', }, - }).then((response) => { + }).then(response => { const body = response.text; expect(body.length).toEqual(228); expect(body.indexOf('rgle barglea')).toBe(0); @@ -822,7 +822,7 @@ describe('Parse.File testing', () => { }); }); - it('fails to stream unknown file', (done) => { + it('fails to stream unknown file', done => { request({ url: 'http://localhost:8378/1/files/test/file.txt', headers: { @@ -831,7 +831,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=13-240', }, - }).then((response) => { + }).then(response => { expect(response.status).toBe(404); const body = response.text; expect(body).toEqual('File not found.'); @@ -843,7 +843,7 @@ describe('Parse.File testing', () => { // Because GridStore is not loaded on PG, those are perfect // for fallback tests describe_only_db('postgres')('Default Range tests', () => { - it('fallback to regular request', (done) => { + it('fallback to regular request', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -854,7 +854,7 @@ describe('Parse.File testing', () => { headers: headers, url: 'http://localhost:8378/1/files/file.txt', body: 'argle bargle', - }).then((response) => { + }).then(response => { const b = response.data; request({ url: b.url, @@ -864,7 +864,7 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=0-5', }, - }).then((response) => { + }).then(response => { const body = response.text; expect(body).toEqual('argle bargle'); done(); diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 78fbe68537..bf9f119f4d 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 = {}, - fileKey = undefined + encryptionKey = undefined ) { super(); this._databaseURI = mongoDatabaseURI; this._algorithm = 'aes-256-gcm'; - this._fileKey = - fileKey !== undefined + this._encryptionKey = + encryptionKey !== undefined ? crypto .createHash('sha256') - .update(String(fileKey)) + .update(String(encryptionKey)) .digest('base64') .substr(0, 32) : null; @@ -65,12 +65,12 @@ export class GridFSBucketAdapter extends FilesAdapter { const stream = await bucket.openUploadStream(filename, { metadata: options.metadata, }); - if (this._fileKey !== null) { + if (this._encryptionKey !== null) { try { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv( this._algorithm, - this._fileKey, + this._encryptionKey, iv ); const encryptedResult = Buffer.concat([ @@ -119,7 +119,7 @@ export class GridFSBucketAdapter extends FilesAdapter { }); stream.on('end', () => { const data = Buffer.concat(chunks); - if (this._fileKey !== null) { + if (this._encryptionKey !== null) { try { const authTagLocation = data.length - 16; const ivLocation = data.length - 32; @@ -128,7 +128,7 @@ export class GridFSBucketAdapter extends FilesAdapter { const encrypted = data.slice(0, ivLocation); const decipher = crypto.createDecipheriv( this._algorithm, - this._fileKey, + this._encryptionKey, iv ); decipher.setAuthTag(authTag); @@ -149,7 +149,7 @@ export class GridFSBucketAdapter extends FilesAdapter { }); } - async rotateFileKey(options = {}) { + async rotateEncryptionKey(options = {}) { var fileNames = []; var oldKeyFileAdapter = {}; const bucket = await this._getBucket(); @@ -169,19 +169,19 @@ export class GridFSBucketAdapter extends FilesAdapter { fileNames = options.fileNames; } else { const fileNamesIterator = await bucket.find().toArray(); - fileNamesIterator.forEach((file) => { + fileNamesIterator.forEach(file => { fileNames.push(file.filename); }); } - return new Promise((resolve) => { + return new Promise(resolve => { var fileNamesNotRotated = fileNames; var fileNamesRotated = []; var fileNameTotal = fileNames.length; var fileNameIndex = 0; - fileNames.forEach((fileName) => { + fileNames.forEach(fileName => { oldKeyFileAdapter .getFileData(fileName) - .then((plainTextData) => { + .then(plainTextData => { //Overwrite file with data encrypted with new key this.createFile(fileName, plainTextData) .then(() => { diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index f1df32e770..579570eccd 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -75,14 +75,24 @@ export class FilesController extends AdaptableController { // all filenames starting with "tfss-" should be from files.parsetfss.com // all filenames starting with a "-" seperated UUID should be from files.parse.com // all other filenames have been migrated or created from Parse Server - if (filename.indexOf('tfss-') === 0) { - fileObject['url'] = - 'http://files.parsetfss.com/' + encodeURIComponent(filename); - } else if (legacyFilesRegex.test(filename)) { - fileObject['url'] = - 'http://files.parse.com/' + encodeURIComponent(filename); - } else { + if (config.fileKey === undefined) { fileObject['url'] = this.adapter.getFileLocation(config, filename); + } else { + if (filename.indexOf('tfss-') === 0) { + fileObject['url'] = + 'http://files.parsetfss.com/' + + config.fileKey + + '/' + + encodeURIComponent(filename); + } else if (legacyFilesRegex.test(filename)) { + fileObject['url'] = + 'http://files.parse.com/' + + config.fileKey + + '/' + + encodeURIComponent(filename); + } else { + fileObject['url'] = this.adapter.getFileLocation(config, filename); + } } } } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 1d4f2b2138..92f4f4730a 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -152,6 +152,10 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: false, }, + encryptionKey: { + env: 'PARSE_SERVER_ENCRYPTION_KEY', + help: 'Key for encrypting your files', + }, expireInactiveSessions: { env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', help: From 3daf794633fda999f4462c04d4b34075bd84b29f Mon Sep 17 00:00:00 2001 From: Corey Date: Sun, 25 Oct 2020 15:36:49 -0400 Subject: [PATCH 12/12] Update ParseFile.spec.js revert --- spec/ParseFile.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 1d2bf2e71e..bb1c5c512f 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -304,19 +304,19 @@ describe('Parse.File testing', () => { let firstName; let secondName; - const firstSave = file.save().then(function () { + const firstSave = file.save().then(function() { firstName = file.name(); }); - const secondSave = file.save().then(function () { + const secondSave = file.save().then(function() { secondName = file.name(); }); Promise.all([firstSave, secondSave]).then( - function () { + function() { equal(firstName, secondName); done(); }, - function (error) { + function(error) { ok(false, error); done(); }