mirror of
https://github.com/finos/SymphonyElectron.git
synced 2024-12-28 09:51:06 -06:00
Merge pull request #177 from keerthi16/SEARCH-116-GCM
Search-116 (Implement Encryption and Decryption of Index)
This commit is contained in:
commit
1f2939545b
208
js/cryptoLib/crypto.js
Normal file
208
js/cryptoLib/crypto.js
Normal file
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* AES GCM Stream
|
||||
* This module exports encrypt and decrypt stream constructors which can be
|
||||
* used to protect data with authenticated encryption.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
let stream = require('stream');
|
||||
let Transform = stream.Transform;
|
||||
let util = require('util');
|
||||
let crypto = require('crypto');
|
||||
let forge = require('node-forge');
|
||||
|
||||
let KEY_LENGTH = 32; // bytes
|
||||
let GCM_NONCE_LENGTH = 12; //bytes
|
||||
let GCM_MAC_LENGTH = 16; //bytes
|
||||
|
||||
let keyEncoding = 'base64';
|
||||
|
||||
/**
|
||||
* Private helper method to validate a key passed into the Encrypt and Decrypt streams.
|
||||
* Strings are converted it into a buffer, buffers are returned as they are.
|
||||
* @param key
|
||||
* @throws Missing, Encoding, or Length errors
|
||||
* @returns Buffer
|
||||
*/
|
||||
let validateAndConvertKey = function(key) {
|
||||
if (key && key instanceof Buffer && key.length === KEY_LENGTH) {
|
||||
return key;
|
||||
} else if (key && typeof key === 'string') {
|
||||
// This is for temporary purpose only. Will be retrieving the key from the backend
|
||||
// Todo: remove node-forge
|
||||
let md = forge.md.sha256.create();
|
||||
md.update(key);
|
||||
let bufKey = new Buffer(bits2b64(md.digest().getBytes()), keyEncoding);
|
||||
if (bufKey.length !== KEY_LENGTH) {
|
||||
let encodingErrorMessage = 'Provided key string is either of an unknown encoding (expected: ' +
|
||||
keyEncoding + ') or the wrong length.';
|
||||
throw new Error(encodingErrorMessage);
|
||||
}
|
||||
return bufKey;
|
||||
}
|
||||
let message = 'The key options property is required! Expected ' +
|
||||
keyEncoding + ' encoded string or a buffer.';
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* encode bits to b64
|
||||
* @param bits
|
||||
* @returns {*}
|
||||
*/
|
||||
let bits2b64 = function (bits) {
|
||||
return forge.util.encode64(bits);
|
||||
};
|
||||
|
||||
exports.encrypt = EncryptionStream;
|
||||
exports.decrypt = DecryptionStream;
|
||||
|
||||
|
||||
/**
|
||||
* createSalt
|
||||
* Helper method that returns a salt
|
||||
* @returns string
|
||||
* @throws error
|
||||
*/
|
||||
exports.createSalt = function(length) {
|
||||
try {
|
||||
return crypto.randomBytes(length);
|
||||
} catch (ex) {
|
||||
console.error('Problem reading random data and generating salt!');
|
||||
throw ex;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* EncryptionStream
|
||||
* A constructor which returns an encryption stream
|
||||
* The stream first outputs a 12 byte nonce then encrypted cipher text.
|
||||
* When the stream is flushed it outputs a 16 byte MAC.
|
||||
* @param options Object Object.key is the only required param
|
||||
* @returns {EncryptionStream}
|
||||
* @constructor
|
||||
*/
|
||||
function EncryptionStream(options) {
|
||||
if (!(this instanceof EncryptionStream)) {
|
||||
return new EncryptionStream(options);
|
||||
}
|
||||
|
||||
let nonce = options.nonce || exports.createSalt(12);
|
||||
|
||||
this._key = validateAndConvertKey(options.key);
|
||||
this._cipher = crypto.createCipheriv('aes-256-gcm', this._key, nonce);
|
||||
|
||||
Transform.call(this, options);
|
||||
this.push(nonce);
|
||||
}
|
||||
util.inherits(EncryptionStream, Transform);
|
||||
|
||||
EncryptionStream.prototype._transform = function(chunk, enc, cb) {
|
||||
this.push(this._cipher.update(chunk));
|
||||
cb();
|
||||
};
|
||||
|
||||
EncryptionStream.prototype._flush = function(cb) {
|
||||
// final must be called on the cipher before generating a MAC
|
||||
this._cipher.final(); // this will never output data
|
||||
this.push(this._cipher.getAuthTag()); // 16 bytes
|
||||
|
||||
cb();
|
||||
};
|
||||
|
||||
/**
|
||||
* DecryptionStream
|
||||
* A constructor which returns a decryption stream
|
||||
* The stream assumes the first 12 bytes of data are the nonce and the final
|
||||
* 16 bytes received is the MAC.
|
||||
* @param options Object Object.key is the only required param
|
||||
* @returns {DecryptionStream}
|
||||
* @constructor
|
||||
*/
|
||||
function DecryptionStream(options) {
|
||||
if (!(this instanceof DecryptionStream)) {
|
||||
return new DecryptionStream(options);
|
||||
}
|
||||
|
||||
this._started = false;
|
||||
this._nonce = new Buffer(GCM_NONCE_LENGTH);
|
||||
this._nonceBytesRead = 0;
|
||||
this._cipherTextChunks = [];
|
||||
this._key = validateAndConvertKey(options.key);
|
||||
|
||||
Transform.call(this, options);
|
||||
}
|
||||
util.inherits(DecryptionStream, Transform);
|
||||
|
||||
DecryptionStream.prototype._transform = function(chunk, enc, cb) {
|
||||
let chunkLength = chunk.length;
|
||||
let chunkOffset = 0;
|
||||
let _chunk = chunk;
|
||||
if (!this._started) {
|
||||
if (this._nonceBytesRead < GCM_NONCE_LENGTH) {
|
||||
let nonceRemaining = GCM_NONCE_LENGTH - this._nonceBytesRead;
|
||||
chunkOffset = chunkLength <= nonceRemaining ? chunkLength : nonceRemaining;
|
||||
_chunk.copy(this._nonce, this._nonceBytesRead, 0, chunkOffset);
|
||||
_chunk = _chunk.slice(chunkOffset);
|
||||
chunkLength = _chunk.length;
|
||||
this._nonceBytesRead += chunkOffset;
|
||||
}
|
||||
|
||||
|
||||
if (this._nonceBytesRead === GCM_NONCE_LENGTH) {
|
||||
this._decipher = crypto.createDecipheriv('aes-256-gcm', this._key, this._nonce);
|
||||
this._started = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We can't use an else because we have no idea how long our chunks will be
|
||||
// all we know is that once we've got a nonce we can start storing cipher text
|
||||
if (this._started) {
|
||||
this._cipherTextChunks.push(_chunk);
|
||||
}
|
||||
|
||||
cb();
|
||||
};
|
||||
|
||||
DecryptionStream.prototype._flush = function(cb) {
|
||||
let mac = pullOutMac(this._cipherTextChunks);
|
||||
if (!mac) {
|
||||
return this.emit('error', new Error('Decryption failed: bad cipher text.'));
|
||||
}
|
||||
this._decipher.setAuthTag(mac);
|
||||
let decrypted = this._cipherTextChunks.map(function(item) {
|
||||
return this._decipher.update(item);
|
||||
}, this);
|
||||
try {
|
||||
this._decipher.final();
|
||||
} catch (e) {
|
||||
return cb(e);
|
||||
}
|
||||
decrypted.forEach(function(item) {
|
||||
this.push(item);
|
||||
}, this);
|
||||
return cb();
|
||||
};
|
||||
|
||||
function pullOutMac(array) {
|
||||
let macBits = [];
|
||||
let macByteCount = 0;
|
||||
let current, macStartIndex;
|
||||
while (macByteCount !== GCM_MAC_LENGTH && array.length) {
|
||||
current = array.pop();
|
||||
if (macByteCount + current.length <= GCM_MAC_LENGTH) {
|
||||
macBits.push(current);
|
||||
macByteCount += current.length;
|
||||
} else {
|
||||
macStartIndex = (macByteCount + current.length) - GCM_MAC_LENGTH;
|
||||
macBits.push(current.slice(macStartIndex));
|
||||
array.push(current.slice(0, macStartIndex));
|
||||
macByteCount += (current.length - macStartIndex);
|
||||
}
|
||||
}
|
||||
if (macByteCount !== GCM_MAC_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
macBits.reverse();
|
||||
return Buffer.concat(macBits, GCM_MAC_LENGTH);
|
||||
}
|
178
js/cryptoLib/index.js
Normal file
178
js/cryptoLib/index.js
Normal file
@ -0,0 +1,178 @@
|
||||
'use strict';
|
||||
const electron = require('electron');
|
||||
const app = electron.app;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const archiver = require('archiver');
|
||||
const zipArchive = archiver('zip');
|
||||
const extract = require('extract-zip');
|
||||
const isDevEnv = require('../utils/misc.js').isDevEnv;
|
||||
const crypto = require('./crypto');
|
||||
|
||||
const userData = path.join(app.getPath('userData'));
|
||||
const INDEX_DATA_FOLDER = isDevEnv ? './data/search_index' : path.join(userData, 'data/search_index');
|
||||
const TEMPORARY_PATH = isDevEnv ? path.join(__dirname, '..', '..') : userData;
|
||||
|
||||
class Crypto {
|
||||
|
||||
// TODO: Need to pass key for encryption and decryption
|
||||
constructor() {
|
||||
|
||||
// will be handling after implementing in client app
|
||||
let userId = 'user_data';
|
||||
let INDEX_VERSION = 'v1';
|
||||
// will be handling after implementing in client app
|
||||
|
||||
this.indexDataFolder = INDEX_DATA_FOLDER + '_' + userId + '_' + INDEX_VERSION;
|
||||
this.permanentIndexFolderName = 'search_index_' + userId + '_' + INDEX_VERSION;
|
||||
this.dump = TEMPORARY_PATH;
|
||||
this.extractToPath = `${TEMPORARY_PATH}/data/${this.permanentIndexFolderName}`;
|
||||
this.key = "XrwVgWR4czB1a9scwvgRUNbXiN3W0oWq7oUBenyq7bo="; // temporary only
|
||||
this.encryptedIndex = `${INDEX_DATA_FOLDER + '_' + userId + '_' + INDEX_VERSION}.enc`;
|
||||
this.zipErrored = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a zip of the data folder and encrypting
|
||||
* removing the data folder and the dump files
|
||||
* @returns {Promise}
|
||||
*/
|
||||
encryption() {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (!fs.existsSync(this.indexDataFolder)){
|
||||
// will be handling after implementing in client app
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
let output = fs.createWriteStream(`${this.dump}/${this.permanentIndexFolderName}.zip`);
|
||||
|
||||
|
||||
zipArchive.on('end', () => {
|
||||
|
||||
if (!fs.existsSync(`${this.dump}/${this.permanentIndexFolderName}.zip`)){
|
||||
// will be handling after implementing in client app
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const input = fs.createReadStream(`${this.dump}/${this.permanentIndexFolderName}.zip`);
|
||||
const outputEncryption = fs.createWriteStream(this.encryptedIndex);
|
||||
let config = {
|
||||
key: this.key
|
||||
};
|
||||
const encrypt = crypto.encrypt(config);
|
||||
|
||||
input.pipe(encrypt).pipe(outputEncryption).on('finish', (err, res) => {
|
||||
if (err) {
|
||||
reject(new Error(err));
|
||||
}
|
||||
if (!this.zipErrored) {
|
||||
fs.unlinkSync(`${this.dump}/${this.permanentIndexFolderName}.zip`);
|
||||
Crypto.deleteFolderRecursive(this.indexDataFolder)
|
||||
.then(function () {
|
||||
resolve(res);
|
||||
})
|
||||
.catch(function (error) {
|
||||
reject(new Error(error))
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
zipArchive.pipe(output);
|
||||
|
||||
zipArchive.directory(this.indexDataFolder + '/', false);
|
||||
|
||||
zipArchive.finalize((err) => {
|
||||
if (err) {
|
||||
this.zipErrored = true;
|
||||
reject(new Error(err));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypting the .enc file and unzipping
|
||||
* removing the .enc file and the dump files
|
||||
* @returns {Promise}
|
||||
*/
|
||||
decryption() {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (!fs.existsSync(this.encryptedIndex)){
|
||||
// will be handling after implementing in client app
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const input = fs.createReadStream(this.encryptedIndex);
|
||||
const output = fs.createWriteStream(`${this.dump}/decrypted.zip`);
|
||||
let config = {
|
||||
key: this.key
|
||||
};
|
||||
const decrypt = crypto.decrypt(config);
|
||||
|
||||
input.pipe(decrypt).pipe(output).on('finish', () => {
|
||||
|
||||
if (!fs.existsSync(`${this.dump}/decrypted.zip`)){
|
||||
// will be handling after implementing in client app
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
let readStream = fs.createReadStream(`${this.dump}/decrypted.zip`);
|
||||
readStream
|
||||
.on('data', (data) => {
|
||||
if (!data) {
|
||||
reject(new Error("error reading zip"));
|
||||
}
|
||||
extractZip();
|
||||
})
|
||||
.on('error', (error) => {
|
||||
reject(new Error(error.message));
|
||||
});
|
||||
});
|
||||
|
||||
let extractZip = () => {
|
||||
extract(`${this.dump}/decrypted.zip`, {dir: `${this.extractToPath}`}, (err) => {
|
||||
if (err) {
|
||||
reject(new Error(err));
|
||||
}
|
||||
fs.unlink(`${this.dump}/decrypted.zip`, () => {
|
||||
fs.unlink(this.encryptedIndex, () => {
|
||||
resolve('success');
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removing all the folders and files inside the data folder
|
||||
* @param {String} location
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static deleteFolderRecursive(location) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (fs.existsSync(location)) {
|
||||
fs.readdirSync(location).forEach((file) => {
|
||||
let curPath = location + "/" + file;
|
||||
if (fs.lstatSync(curPath).isDirectory()) {
|
||||
Crypto.deleteFolderRecursive(curPath);
|
||||
} else {
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
resolve(fs.rmdirSync(location));
|
||||
} else {
|
||||
reject('no file');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Crypto;
|
35
js/main.js
35
js/main.js
@ -14,9 +14,12 @@ const childProcess = require('child_process');
|
||||
const path = require('path');
|
||||
const AppDirectory = require('appdirectory');
|
||||
const dirs = new AppDirectory('Symphony');
|
||||
const Crypto = require('./cryptoLib');
|
||||
const crypto = new Crypto();
|
||||
|
||||
require('electron-dl')();
|
||||
|
||||
|
||||
// used to check if a url was opened when the app was already open
|
||||
let isAppAlreadyOpen = false;
|
||||
|
||||
@ -68,6 +71,19 @@ if (isMac) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for demo purpose only
|
||||
* will be removing this after implementing
|
||||
* in the client-app
|
||||
*/
|
||||
crypto.decryption()
|
||||
.then(function () {
|
||||
// will be handling after implementing client app
|
||||
})
|
||||
.catch(function () {
|
||||
// will be handling after implementing client app
|
||||
});
|
||||
|
||||
/**
|
||||
* This method will be called when Electron has finished
|
||||
* initialization and is ready to create browser windows.
|
||||
@ -87,6 +103,25 @@ app.on('activate', function () {
|
||||
}
|
||||
});
|
||||
|
||||
app.on('will-quit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
/**
|
||||
* This is for demo purpose only
|
||||
* will be removing this after implementing
|
||||
* in client-app
|
||||
*/
|
||||
crypto.encryption()
|
||||
.then(function () {
|
||||
// will be handling after implementing in client app
|
||||
app.exit();
|
||||
})
|
||||
.catch(function () {
|
||||
// will be handling after implementing client app
|
||||
app.exit();
|
||||
});
|
||||
});
|
||||
|
||||
// adds 'symphony' as a protocol
|
||||
// in the system. plist file in macOS
|
||||
// and registry keys in windows
|
||||
|
@ -98,15 +98,18 @@
|
||||
"dependencies": {
|
||||
"@paulcbetts/system-idle-time": "^1.0.4",
|
||||
"appdirectory": "^0.1.0",
|
||||
"archiver": "^2.0.0",
|
||||
"async.map": "^0.5.2",
|
||||
"async.mapseries": "^0.5.2",
|
||||
"auto-launch": "^5.0.1",
|
||||
"electron-dl": "^1.9.0",
|
||||
"electron-spellchecker": "^1.2.0",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"extract-zip": "^1.6.5",
|
||||
"ffi": "^2.2.0",
|
||||
"filesize": "^3.5.10",
|
||||
"keymirror": "0.1.1",
|
||||
"node-forge": "^0.7.1",
|
||||
"randomstring": "^1.1.5",
|
||||
"ref": "^1.3.4",
|
||||
"winreg": "^1.2.3"
|
||||
|
Loading…
Reference in New Issue
Block a user