mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
SEARCH-116-GCM This is using GCM mode
This commit is contained in:
parent
eff466d199
commit
52d6a9e867
249
js/cryptoLib/crypto.js
Normal file
249
js/cryptoLib/crypto.js
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* AES GCM Stream
|
||||||
|
* This module exports encrypt and decrypt stream constructors which can be
|
||||||
|
* used to protect data with authenticated encryption.
|
||||||
|
*
|
||||||
|
* Helper methods are also provided to do things like generate secure keys
|
||||||
|
* and salt.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let stream = require('stream');
|
||||||
|
let Transform = stream.Transform;
|
||||||
|
let util = require('util');
|
||||||
|
let crypto = require('crypto');
|
||||||
|
|
||||||
|
let PBKDF2_PASS_LENGTH = 256;
|
||||||
|
let PBKDF2_SALT_LENGTH = 32;
|
||||||
|
let PBKDF2_ITERATIONS = 5000;
|
||||||
|
let PBKDF2_DIGEST = 'sha256';
|
||||||
|
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') {
|
||||||
|
// Because we don't have a reliable way to test string encoding, we assume that a string type
|
||||||
|
// key was created by the createEncodedKey method and that it is using the keyEncoding which
|
||||||
|
// has been set on the module, whether that's the default or something explicitly set by the
|
||||||
|
// user via the setKeyEncoding method.
|
||||||
|
let bufKey = new Buffer(key, 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;
|
||||||
|
} else {
|
||||||
|
let message = 'The key options property is required! Expected ' +
|
||||||
|
keyEncoding + ' encoded string or a buffer.';
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.encrypt = EncryptionStream;
|
||||||
|
exports.decrypt = DecryptionStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getEncoding
|
||||||
|
* Helper which returns the current encoding being used for keys
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
exports.getKeyEncoding = function() {
|
||||||
|
return keyEncoding;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setEncoding
|
||||||
|
* Helper to set the encoding being used for keys
|
||||||
|
* @param enc
|
||||||
|
*/
|
||||||
|
exports.setKeyEncoding = function(enc) {
|
||||||
|
keyEncoding = Buffer.isEncoding(enc) ? enc : keyEncoding;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createKeyBuffer
|
||||||
|
* Method which returns a buffer representing a secure key generated with PBKDF2
|
||||||
|
* @returns Buffer
|
||||||
|
*/
|
||||||
|
exports.createKeyBuffer = function() {
|
||||||
|
try {
|
||||||
|
let passphrase = crypto.randomBytes(PBKDF2_PASS_LENGTH);
|
||||||
|
let salt = this.createSalt(PBKDF2_SALT_LENGTH);
|
||||||
|
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('Problem reading random data and generating a key!');
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createEncodedKey
|
||||||
|
* Helper method that returns an encoded key
|
||||||
|
* @returns string
|
||||||
|
* @throws error
|
||||||
|
*/
|
||||||
|
exports.createEncodedKey = function() {
|
||||||
|
return exports.createKeyBuffer().toString(keyEncoding);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
macBits.reverse();
|
||||||
|
return Buffer.concat(macBits, GCM_MAC_LENGTH);
|
||||||
|
}
|
@ -3,26 +3,31 @@ const electron = require('electron');
|
|||||||
const app = electron.app;
|
const app = electron.app;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const crypto = require('crypto');
|
|
||||||
const archiver = require('archiver');
|
const archiver = require('archiver');
|
||||||
const zipArchive = archiver('zip');
|
const zipArchive = archiver('zip');
|
||||||
const extract = require('extract-zip');
|
const extract = require('extract-zip');
|
||||||
const isDevEnv = require('../utils/misc.js').isDevEnv;
|
const isDevEnv = require('../utils/misc.js').isDevEnv;
|
||||||
|
const crypto = require('./crypto');
|
||||||
|
|
||||||
const userData = path.join(app.getPath('userData'));
|
const userData = path.join(app.getPath('userData'));
|
||||||
const INDEX_DATA_FOLDER = isDevEnv ? './msgsjson' : path.join(userData, 'data');
|
const INDEX_DATA_FOLDER = isDevEnv ? './data' : path.join(userData, 'data');
|
||||||
const MODE = 'aes-256-ctr';
|
const TEMPORARY_PATH = isDevEnv ? path.join(__dirname, '..', '..') : userData;
|
||||||
|
|
||||||
class Crypto {
|
class Crypto {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.indexDataFolder = INDEX_DATA_FOLDER;
|
this.indexDataFolder = INDEX_DATA_FOLDER;
|
||||||
this.key = '53796d70686f6e792074657374206b657920666f7220656e6372797074696f6e20';
|
this.dump = TEMPORARY_PATH;
|
||||||
this.dump = path.join(__dirname, '..', '..');
|
this.key = "XrwVgWR4czB1a9scwvgRUNbXiN3W0oWq7oUBenyq7bo="; // temporary only
|
||||||
this.encryptedIndex = 'encryptedIndex.enc';
|
this.encryptedIndex = 'encryptedIndex.enc';
|
||||||
this.zipErrored = false;
|
this.zipErrored = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a zip of the data folder and encrypting
|
||||||
|
* removing the data folder and the dump files
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
encryption() {
|
encryption() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
@ -32,9 +37,12 @@ class Crypto {
|
|||||||
|
|
||||||
const input = fs.createReadStream(`${this.dump}/content.zip`);
|
const input = fs.createReadStream(`${this.dump}/content.zip`);
|
||||||
const outputEncryption = fs.createWriteStream(this.encryptedIndex);
|
const outputEncryption = fs.createWriteStream(this.encryptedIndex);
|
||||||
const cipher = crypto.createCipher(MODE, this.key);
|
let config = {
|
||||||
|
key: this.key
|
||||||
|
};
|
||||||
|
const encrypt = crypto.encrypt(config);
|
||||||
|
|
||||||
input.pipe(cipher).pipe(outputEncryption).on('finish', (err, res) => {
|
input.pipe(encrypt).pipe(outputEncryption).on('finish', (err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(new Error(err));
|
reject(new Error(err));
|
||||||
}
|
}
|
||||||
@ -64,52 +72,66 @@ class Crypto {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypting the .enc file and unzipping
|
||||||
|
* removing the .enc file and the dump files
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
decryption() {
|
decryption() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const input = fs.createReadStream(this.encryptedIndex);
|
const input = fs.createReadStream(this.encryptedIndex);
|
||||||
const output = fs.createWriteStream(`${this.dump}/decrypted.zip`);
|
const output = fs.createWriteStream(`${this.dump}/decrypted.zip`);
|
||||||
const deCipher = crypto.createDecipher(MODE, this.key);
|
let config = {
|
||||||
|
key: this.key
|
||||||
|
};
|
||||||
|
const decrypt = crypto.decrypt(config);
|
||||||
|
|
||||||
input.pipe(deCipher).pipe(output).on('finish', () => {
|
input.pipe(decrypt).pipe(output).on('finish', () => {
|
||||||
let readStream = fs.createReadStream(`${this.dump}/decrypted.zip`);
|
let readStream = fs.createReadStream(`${this.dump}/decrypted.zip`);
|
||||||
readStream
|
readStream
|
||||||
.on('data', (data) => {
|
.on('data', (data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
reject(new Error("error reading zip"));
|
reject(new Error("error reading zip"));
|
||||||
}
|
}
|
||||||
unzip();
|
zip();
|
||||||
})
|
})
|
||||||
.on('error', (error) => {
|
.on('error', (error) => {
|
||||||
reject(new Error(error.message));
|
reject(new Error(error.message));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let unzip = () => {
|
let zip = () => {
|
||||||
let temp = path.join(__dirname, '..', '..');
|
extract(`${this.dump}/decrypted.zip`, {dir: TEMPORARY_PATH}, (err) => {
|
||||||
extract(`${this.dump}/decrypted.zip`, {dir: temp}, (err) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(new Error(err));
|
reject(new Error(err));
|
||||||
}
|
}
|
||||||
fs.unlink(`${this.dump}/decrypted.zip`, () => {
|
fs.unlink(`${this.dump}/decrypted.zip`, () => {
|
||||||
resolve('success')
|
fs.unlink(this.encryptedIndex, () => {
|
||||||
|
resolve('success');
|
||||||
|
})
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static deleteFolderRecursive(pt) {
|
/**
|
||||||
|
* Removing all the folders and files inside the data folder
|
||||||
|
* @param {String} location
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
static deleteFolderRecursive(location) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (fs.existsSync(pt)) {
|
if (fs.existsSync(location)) {
|
||||||
fs.readdirSync(pt).forEach((file) => {
|
fs.readdirSync(location).forEach((file) => {
|
||||||
let curPath = pt + "/" + file;
|
let curPath = location + "/" + file;
|
||||||
if (fs.lstatSync(curPath).isDirectory()) {
|
if (fs.lstatSync(curPath).isDirectory()) {
|
||||||
Crypto.deleteFolderRecursive(curPath);
|
Crypto.deleteFolderRecursive(curPath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(curPath);
|
fs.unlinkSync(curPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
resolve(fs.rmdirSync(pt));
|
resolve(fs.rmdirSync(location));
|
||||||
} else {
|
} else {
|
||||||
reject('no file');
|
reject('no file');
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user