@parse/fs-files-adapter
Advanced tools
Comparing version 1.0.1 to 1.1.0
# Change Log | ||
## [1.1.0](https://github.com/parse-server-modules/parse-server-fs-adapter/tree/1.1.0) (2020-10-21) | ||
- Added AES encryption/decryption using native crypto [\#15](https://github.com/parse-community/parse-server-fs-adapter/pull/15) (thanks to [Corey](https://github.com/cbaker6)) | ||
## [1.0.1](https://github.com/parse-server-modules/parse-server-fs-adapter/tree/1.0.1) (2016-08-15) | ||
@@ -4,0 +8,0 @@ **Closed issues:** |
150
index.js
@@ -10,5 +10,12 @@ 'use strict'; | ||
var pathSep = require('path').sep; | ||
const crypto = require("crypto"); | ||
const algorithm = 'aes-256-gcm'; | ||
function FileSystemAdapter(options) { | ||
options = options || {}; | ||
this._fileKey = null; | ||
if (options.fileKey !== undefined){ | ||
this._fileKey = crypto.createHash('sha256').update(String(options.fileKey)).digest('base64').substr(0, 32); | ||
} | ||
let filesSubDirectory = options.filesSubDirectory || ''; | ||
@@ -23,10 +30,34 @@ this._filesDir = filesSubDirectory; | ||
FileSystemAdapter.prototype.createFile = function(filename, data) { | ||
let filepath = this._getLocalFilePath(filename); | ||
const stream = fs.createWriteStream(filepath); | ||
return new Promise((resolve, reject) => { | ||
let filepath = this._getLocalFilePath(filename); | ||
fs.writeFile(filepath, data, (err) => { | ||
if(err !== null) { | ||
return reject(err); | ||
} | ||
resolve(data); | ||
}); | ||
try{ | ||
if(this._fileKey !== null){ | ||
const iv = crypto.randomBytes(16); | ||
const cipher = crypto.createCipheriv( | ||
algorithm, | ||
this._fileKey, | ||
iv | ||
); | ||
const encryptedResult = Buffer.concat([ | ||
cipher.update(data), | ||
cipher.final(), | ||
iv, | ||
cipher.getAuthTag(), | ||
]); | ||
stream.write(encryptedResult); | ||
stream.end(); | ||
stream.on('finish', function() { | ||
resolve(data); | ||
}); | ||
}else{ | ||
stream.write(data); | ||
stream.end(); | ||
stream.on('finish', function() { | ||
resolve(data); | ||
}); | ||
} | ||
}catch(err){ | ||
return reject(err); | ||
} | ||
}); | ||
@@ -36,11 +67,15 @@ } | ||
FileSystemAdapter.prototype.deleteFile = function(filename) { | ||
let filepath = this._getLocalFilePath(filename); | ||
const chunks = []; | ||
const stream = fs.createReadStream(filepath); | ||
return new Promise((resolve, reject) => { | ||
let filepath = this._getLocalFilePath(filename); | ||
fs.readFile( filepath , function (err, data) { | ||
if(err !== null) { | ||
return reject(err); | ||
} | ||
fs.unlink(filepath, (unlinkErr) => { | ||
if(err !== null) { | ||
return reject(unlinkErr); | ||
stream.read(); | ||
stream.on('data', (data) => { | ||
chunks.push(data); | ||
}); | ||
stream.on('end', () => { | ||
const data = Buffer.concat(chunks); | ||
fs.unlink(filepath, (err) => { | ||
if(err !== null) { | ||
return reject(err); | ||
} | ||
@@ -50,3 +85,5 @@ resolve(data); | ||
}); | ||
stream.on('error', (err) => { | ||
reject(err); | ||
}); | ||
}); | ||
@@ -56,13 +93,86 @@ } | ||
FileSystemAdapter.prototype.getFileData = function(filename) { | ||
let filepath = this._getLocalFilePath(filename); | ||
const stream = fs.createReadStream(filepath); | ||
stream.read(); | ||
return new Promise((resolve, reject) => { | ||
let filepath = this._getLocalFilePath(filename); | ||
fs.readFile( filepath , function (err, data) { | ||
if(err !== null) { | ||
return reject(err); | ||
const chunks = []; | ||
stream.on('data', (data) => { | ||
chunks.push(data); | ||
}); | ||
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); | ||
try{ | ||
const decipher = crypto.createDecipheriv(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); | ||
}); | ||
stream.on('error', (err) => { | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
FileSystemAdapter.prototype.rotateFileKey = function(options = {}) { | ||
const applicationDir = this._getApplicationDir(); | ||
var fileNames = []; | ||
var oldKeyFileAdapter = {}; | ||
if (options.oldKey !== undefined) { | ||
oldKeyFileAdapter = new FileSystemAdapter({filesSubDirectory: this._filesDir, fileKey: options.oldKey}); | ||
}else{ | ||
oldKeyFileAdapter = new FileSystemAdapter({filesSubDirectory: this._filesDir}); | ||
} | ||
if (options.fileNames !== undefined){ | ||
fileNames = options.fileNames; | ||
}else{ | ||
fileNames = fs.readdirSync(applicationDir); | ||
fileNames = fileNames.filter(fileName => fileName.indexOf('.') !== 0); | ||
} | ||
return new Promise((resolve, _reject) => { | ||
var fileNamesNotRotated = fileNames; | ||
var fileNamesRotated = []; | ||
var fileNameTotal = fileNames.length; | ||
var fileNameIndex = 0; | ||
fileNames.forEach(fileName => { | ||
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}); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
FileSystemAdapter.prototype.getFileLocation = function(config, filename) { | ||
@@ -69,0 +179,0 @@ return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename); |
{ | ||
"name": "@parse/fs-files-adapter", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "File system adapter for parse-server", | ||
"main": "index.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/parse-community/parse-server-fs-adapter" | ||
}, | ||
"scripts": { | ||
"test": "istanbul cover -x **/spec/** jasmine --captureExceptions" | ||
"test": "jasmine", | ||
"coverage": "nyc jasmine" | ||
}, | ||
@@ -18,7 +23,6 @@ "keywords": [ | ||
"devDependencies": { | ||
"codecov": "^1.0.1", | ||
"istanbul": "^0.4.2", | ||
"jasmine": "^2.4.1", | ||
"nyc": "^15.1.0", | ||
"jasmine": "^3.5.0", | ||
"parse-server-conformance-tests": "^1.0.0" | ||
} | ||
} |
@@ -5,14 +5,17 @@ # parse-server-fs-adapter | ||
parse-server file system storage adapter | ||
parse-server file system storage adapter. | ||
# installation | ||
# Multiple instances of parse-server | ||
When using parse-server-fs-adapter across multiple parse-server instances it's important to establish "centralization" of your file storage (this is the same premise as the other file adapters, you are sending/recieving files through a dedicated link). You can accomplish this at the file storage level by Samba mounting (or any other type of mounting) your storage to each of your parse-server instances, e.g if you are using parse-server via docker (volume mount your SMB drive to `- /Volumes/SMB-Drive/MyParseApp1/files:/parse-server/files`). All parse-server instances need to be able to read and write to the same storage in order for parse-server-fs-adapter to work properly with parse-server. If the file storage isn't centralized, parse-server will have trouble locating files and you will get random behavior on client-side. | ||
# Installation | ||
`npm install --save @parse/fs-files-adapter` | ||
# usage with parse-server | ||
# Usage with parse-server | ||
### using a config file | ||
### Using a config file | ||
``` | ||
```javascript | ||
{ | ||
@@ -25,3 +28,4 @@ "appId": 'my_app_id', | ||
"options": { | ||
"filesSubDirectory": "my/files/folder" // optional | ||
"filesSubDirectory": "my/files/folder", // optional | ||
"fileKey": "someKey" //optional, but mandatory if you want to encrypt files | ||
} | ||
@@ -32,10 +36,31 @@ } | ||
### passing as an instance | ||
### Passing as an instance | ||
***Notice: If used with parse-server versions <= 4.2.0, DO NOT PASS in `PARSE_SERVER_FILE_KEY` or `fileKey` from parse-server. Instead pass your key directly to `FSFilesAdapter` using your own environment variable or hardcoding the string. parse-server versions > 4.2.0 can pass in `PARSE_SERVER_FILE_KEY` or `fileKey`.*** | ||
```javascript | ||
var FSFilesAdapter = require('@parse/fs-files-adapter'); | ||
var fsAdapter = new FSFilesAdapter({ | ||
"filesSubDirectory": "my/files/folder", // optional | ||
"fileKey": "someKey" //optional, but mandatory if you want to encrypt files | ||
}); | ||
var api = new ParseServer({ | ||
appId: 'my_app', | ||
masterKey: 'master_key', | ||
filesAdapter: fsAdapter | ||
}) | ||
``` | ||
### Rotating to a new fileKey | ||
Periodically you may want to rotate your fileKey for security reasons. When this is the case, you can start up a development parse-server that has the same configuration as your production server. In the development server, initialize the file adapter with the new key and do the following in your `index.js`: | ||
#### Files were previously unencrypted and you want to encrypt | ||
```javascript | ||
var FSFilesAdapter = require('@parse/fs-files-adapter'); | ||
var fsAdapter = new FSFilesAdapter({ | ||
"filesSubDirectory": "my/files/folder" // optional | ||
}); | ||
"filesSubDirectory": "my/files/folder", // optional | ||
"fileKey": "newKey" //Use the newKey | ||
}); | ||
@@ -47,3 +72,29 @@ var api = new ParseServer({ | ||
}) | ||
//This can take awhile depending on how many files and how larger they are. It will attempt to rotate the key of all files in your filesSubDirectory | ||
//It is not recommended to do this on the production server, deploy a development server to complete the process. | ||
const {rotated, notRotated} = await api.filesAdapter.rotateFileKey(); | ||
console.log('Files rotated to newKey: ' + rotated); | ||
console.log('Files that couldn't be rotated to newKey: ' + notRotated); | ||
``` | ||
After successfully rotating your key, you should change the `fileKey` to `newKey` on your production server and then restart the server. | ||
#### Files were previously encrypted with `oldKey` and you want to encrypt with `newKey` | ||
The same process as above, but pass in your `oldKey` to `rotateFileKey()`. | ||
```javascript | ||
//This can take awhile depending on how many files and how larger they are. It will attempt to rotate the key of all files in your filesSubDirectory | ||
const {rotated, notRotated} = await api.filesAdapter.rotateFileKey({oldKey: oldKey}); | ||
console.log('Files rotated to newKey: ' + rotated); | ||
console.log('Files that couldn't be rotated to newKey: ' + notRotated); | ||
``` | ||
#### Only rotate a select list of files that were previously encrypted with `oldKey` and you want to encrypt with `newKey` | ||
This is useful if for some reason there errors and some of the files werent rotated and returned in `notRotated`. The same process as above, but pass in your `oldKey` along with the array of `fileNames` to `rotateFileKey()`. | ||
```javascript | ||
//This can take awhile depending on how many files and how larger they are. It will attempt to rotate the key of all files in your filesSubDirectory | ||
const {rotated, notRotated} = await api.filesAdapter.rotateFileKey({oldKey: oldKey, fileNames: ["fileName1.png","fileName2.png"]}); | ||
console.log('Files rotated to newKey: ' + rotated); | ||
console.log('Files that couldn't be rotated to newKey: ' + notRotated); | ||
``` |
{ | ||
"spec_dir": "spec", | ||
"spec_files": [ | ||
"test.spec.js" | ||
"test.spec.js", | ||
"secureFiles.spec.js" | ||
] | ||
} |
@@ -11,3 +11,3 @@ 'use strict'; | ||
filesAdapterTests.testAdapter("FileSystemAdapter", fsAdapter); | ||
filesAdapterTests.testAdapter("FileSystemAdapter", fsAdapter); | ||
}) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
29320
3
9
503
97
2