mirror of
https://github.com/moparisthebest/mail
synced 2024-12-22 07:18:49 -05:00
Implement and test crypto module
This commit is contained in:
parent
a7a562bef6
commit
18d1c39b0a
4
.gitignore
vendored
4
.gitignore
vendored
@ -8,7 +8,5 @@ dist/
|
||||
release/
|
||||
test/integration/src/
|
||||
src/lib/*.js
|
||||
src/js/crypto/aes-cbc.js
|
||||
src/js/crypto/crypto-batch.js
|
||||
src/js/crypto/rsa.js
|
||||
src/js/crypto/aes-gcm.js
|
||||
src/js/crypto/util.js
|
||||
|
@ -10,7 +10,7 @@
|
||||
"start": "grunt && grunt dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1",
|
||||
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.2.0",
|
||||
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.3",
|
||||
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.3.3",
|
||||
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.4",
|
||||
|
@ -18,7 +18,7 @@ requirejs([
|
||||
'js/controller/read',
|
||||
'js/controller/write',
|
||||
'js/controller/navigation',
|
||||
'cryptoLib/util',
|
||||
'js/crypto/util',
|
||||
'js/util/error',
|
||||
'fastclick',
|
||||
'angularSanitize',
|
||||
|
@ -2,7 +2,7 @@ define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore'),
|
||||
util = require('cryptoLib/util'),
|
||||
util = require('js/crypto/util'),
|
||||
config = require('js/app-config').config,
|
||||
outboxDb = 'email_OUTBOX';
|
||||
|
||||
|
@ -4,8 +4,8 @@ define(function(require) {
|
||||
var angular = require('angular'),
|
||||
_ = require('underscore'),
|
||||
appController = require('js/app-controller'),
|
||||
aes = require('cryptoLib/aes-cbc'),
|
||||
util = require('cryptoLib/util'),
|
||||
aes = require('js/crypto/aes-gcm'),
|
||||
util = require('js/crypto/util'),
|
||||
str = require('js/app-config').string,
|
||||
crypto, emailDao, outbox, keychainDao;
|
||||
|
||||
|
@ -1,93 +0,0 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// import web worker dependencies
|
||||
importScripts('../../lib/require.js');
|
||||
|
||||
/**
|
||||
* In the web worker thread context, 'this' and 'self' can be used as a global
|
||||
* variable namespace similar to the 'window' object in the main thread
|
||||
*/
|
||||
self.onmessage = function(e) {
|
||||
// fetch dependencies via require.js
|
||||
require(['../../require-config'], function() {
|
||||
require.config({
|
||||
baseUrl: '../../lib'
|
||||
});
|
||||
|
||||
require(['cryptoLib/crypto-batch'], function(batch) {
|
||||
|
||||
var output;
|
||||
|
||||
try {
|
||||
output = doOperation(batch, e.data);
|
||||
} catch (e) {
|
||||
output = {
|
||||
err: {
|
||||
errMsg: (e.message) ? e.message : e
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// pass output back to main thread
|
||||
self.postMessage(output);
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function doOperation(batch, i) {
|
||||
var output;
|
||||
|
||||
//
|
||||
// Asymmetric encryption
|
||||
//
|
||||
|
||||
if (i.type === 'asymEncrypt' && i.receiverPubkeys && i.senderPrivkey && i.list) {
|
||||
// start encryption
|
||||
output = batch.encryptListForUser(i.list, i.receiverPubkeys, i.senderPrivkey);
|
||||
|
||||
} else if (i.type === 'asymDecrypt' && i.senderPubkeys && i.receiverPrivkey && i.list) {
|
||||
// start decryption
|
||||
output = batch.decryptListForUser(i.list, i.senderPubkeys, i.receiverPrivkey);
|
||||
}
|
||||
|
||||
//
|
||||
// Symmetric encryption
|
||||
//
|
||||
else if (i.type === 'symEncrypt' && i.list) {
|
||||
// start encryption
|
||||
output = batch.authEncryptList(i.list);
|
||||
|
||||
} else if (i.type === 'symDecrypt' && i.list && i.keys) {
|
||||
// start decryption
|
||||
output = batch.authDecryptList(i.list, i.keys);
|
||||
}
|
||||
|
||||
//
|
||||
// Reencryption of asymmetric items to symmetric items
|
||||
//
|
||||
else if (i.type === 'reencrypt' && i.senderPubkeys && i.receiverPrivkey && i.list && i.symKey) {
|
||||
// start validation and re-encryption
|
||||
output = batch.reencryptListKeysForUser(i.list, i.senderPubkeys, i.receiverPrivkey, i.symKey);
|
||||
|
||||
} else if (i.type === 'decryptItems' && i.symKey && i.list) {
|
||||
// start decryption
|
||||
output = batch.decryptKeysAndList(i.list, i.symKey);
|
||||
}
|
||||
|
||||
//
|
||||
// Error
|
||||
//
|
||||
else {
|
||||
output = {
|
||||
err: {
|
||||
errMsg: 'Not all arguments for web worker crypto are defined!'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}());
|
@ -5,120 +5,54 @@
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var util = require('cryptoLib/util'),
|
||||
aes = require('cryptoLib/aes-cbc'),
|
||||
rsa = require('cryptoLib/rsa'),
|
||||
cryptoBatch = require('cryptoLib/crypto-batch'),
|
||||
var aes = require('js/crypto/aes-gcm'),
|
||||
pbkdf2 = require('js/crypto/pbkdf2'),
|
||||
config = require('js/app-config').config;
|
||||
|
||||
var passBasedKey,
|
||||
BATCH_WORKER = '/crypto/crypto-batch-worker.js',
|
||||
PBKDF2_WORKER = '/crypto/pbkdf2-worker.js';
|
||||
var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js';
|
||||
|
||||
var Crypto = function() {
|
||||
|
||||
};
|
||||
var Crypto = function() {};
|
||||
|
||||
/**
|
||||
* Initializes the crypto modules by fetching the user's
|
||||
* encrypted secret key from storage and storing it in memory.
|
||||
* Encrypt plaintext using AES-GCM.
|
||||
* @param {String} plaintext The input string in UTF-16
|
||||
* @param {String} key The base64 encoded key
|
||||
* @param {String} iv The base64 encoded IV
|
||||
* @param {Function} callback(error, ciphertext)
|
||||
* @return {String} The base64 encoded ciphertext
|
||||
*/
|
||||
Crypto.prototype.init = function(args, callback) {
|
||||
var self = this;
|
||||
Crypto.prototype.encrypt = function(plaintext, key, iv, callback) {
|
||||
var ct;
|
||||
|
||||
// valdiate input
|
||||
if (!args.emailAddress || !args.keySize || !args.rsaKeySize || typeof args.password !== 'string' || !args.salt) {
|
||||
callback({
|
||||
errMsg: 'Crypto init failed. Not all args set!'
|
||||
});
|
||||
try {
|
||||
ct = aes.encrypt(plaintext, key, iv);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
self.emailAddress = args.emailAddress;
|
||||
self.keySize = args.keySize;
|
||||
self.ivSize = args.keySize;
|
||||
self.rsaKeySize = args.rsaKeySize;
|
||||
callback(null, ct);
|
||||
};
|
||||
|
||||
// derive PBKDF2 from password in web worker thread
|
||||
self.deriveKey(args.password, args.salt, self.keySize, function(err, derivedKey) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Decrypt ciphertext suing AES-GCM
|
||||
* @param {String} ciphertext The base64 encoded ciphertext
|
||||
* @param {String} key The base64 encoded key
|
||||
* @param {String} iv The base64 encoded IV
|
||||
* @param {Function} callback(error, plaintext)
|
||||
* @return {String} The decrypted plaintext in UTF-16
|
||||
*/
|
||||
Crypto.prototype.decrypt = function(ciphertext, key, iv, callback) {
|
||||
var pt;
|
||||
|
||||
// remember pbkdf2 for later use
|
||||
passBasedKey = derivedKey;
|
||||
|
||||
// check if key exists
|
||||
if (!args.storedKeypair) {
|
||||
// generate keys, encrypt and persist if none exists
|
||||
generateKeypair(derivedKey);
|
||||
} else {
|
||||
// decrypt key
|
||||
decryptKeypair(args.storedKeypair, derivedKey);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function generateKeypair(derivedKey) {
|
||||
// generate RSA keypair in web worker
|
||||
rsa.generateKeypair(self.rsaKeySize, function(err, generatedKeypair) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// encrypt keypair
|
||||
var iv = util.random(self.ivSize);
|
||||
var encryptedPrivateKey = aes.encrypt(generatedKeypair.privkeyPem, derivedKey, iv);
|
||||
|
||||
// new encrypted keypair object
|
||||
var newKeypair = {
|
||||
publicKey: {
|
||||
_id: generatedKeypair._id,
|
||||
userId: self.emailAddress,
|
||||
publicKey: generatedKeypair.pubkeyPem
|
||||
},
|
||||
privateKey: {
|
||||
_id: generatedKeypair._id,
|
||||
userId: self.emailAddress,
|
||||
encryptedKey: encryptedPrivateKey,
|
||||
iv: iv
|
||||
}
|
||||
};
|
||||
|
||||
// return generated keypair for storage in keychain dao
|
||||
callback(null, newKeypair);
|
||||
});
|
||||
try {
|
||||
pt = aes.decrypt(ciphertext, key, iv);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
function decryptKeypair(storedKeypair, derivedKey) {
|
||||
var decryptedPrivateKey;
|
||||
|
||||
// validate input
|
||||
if (!storedKeypair || !storedKeypair.privateKey || !storedKeypair.privateKey.encryptedKey || !storedKeypair.privateKey.iv) {
|
||||
callback({
|
||||
errMsg: 'Incomplete arguments for private key decryption!'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// try to decrypt with derivedKey
|
||||
try {
|
||||
var prK = storedKeypair.privateKey;
|
||||
decryptedPrivateKey = aes.decrypt(prK.encryptedKey, derivedKey, prK.iv);
|
||||
} catch (ex) {
|
||||
callback({
|
||||
errMsg: 'Wrong password!'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// set rsa keys
|
||||
rsa.init(storedKeypair.publicKey.publicKey, decryptedPrivateKey, storedKeypair.publicKey._id);
|
||||
|
||||
callback();
|
||||
}
|
||||
callback(null, pt);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -139,181 +73,6 @@ define(function(require) {
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// En/Decrypt a list of items with AES in a WebWorker thread
|
||||
//
|
||||
|
||||
Crypto.prototype.symEncryptList = function(list, callback) {
|
||||
var self = this,
|
||||
key, envelope, envelopes = [];
|
||||
|
||||
// generate single secret key shared for all list items
|
||||
key = util.random(self.keySize);
|
||||
|
||||
// package objects into batchable envelope format
|
||||
list.forEach(function(i) {
|
||||
envelope = {
|
||||
id: i.id,
|
||||
plaintext: i,
|
||||
key: key,
|
||||
iv: util.random(self.ivSize)
|
||||
};
|
||||
envelopes.push(envelope);
|
||||
});
|
||||
|
||||
startWorker({
|
||||
script: BATCH_WORKER,
|
||||
args: {
|
||||
type: 'symEncrypt',
|
||||
list: envelopes
|
||||
},
|
||||
callback: function(err, encryptedList) {
|
||||
// return generated secret key
|
||||
callback(err, {
|
||||
key: key,
|
||||
list: encryptedList
|
||||
});
|
||||
},
|
||||
noWorker: function() {
|
||||
return cryptoBatch.authEncryptList(envelopes);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Crypto.prototype.symDecryptList = function(list, keys, callback) {
|
||||
startWorker({
|
||||
script: BATCH_WORKER,
|
||||
args: {
|
||||
type: 'symDecrypt',
|
||||
list: list,
|
||||
keys: keys
|
||||
},
|
||||
callback: callback,
|
||||
noWorker: function() {
|
||||
return cryptoBatch.authDecryptList(list, keys);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// En/Decrypt something speficially using the user's secret key
|
||||
//
|
||||
|
||||
Crypto.prototype.encryptListForUser = function(list, receiverPubkeys, callback) {
|
||||
var self = this,
|
||||
envelope, envelopes = [];
|
||||
|
||||
if (!receiverPubkeys || receiverPubkeys.length !== 1) {
|
||||
callback({
|
||||
errMsg: 'Encryption is currently implemented for only one receiver!'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var keypair = rsa.exportKeys();
|
||||
var senderPrivkey = {
|
||||
_id: keypair._id,
|
||||
privateKey: keypair.privkeyPem
|
||||
};
|
||||
|
||||
// package objects into batchable envelope format
|
||||
list.forEach(function(i) {
|
||||
envelope = {
|
||||
id: i.id,
|
||||
plaintext: i,
|
||||
key: util.random(self.keySize),
|
||||
iv: util.random(self.ivSize),
|
||||
receiverPk: receiverPubkeys[0]._id
|
||||
};
|
||||
envelopes.push(envelope);
|
||||
});
|
||||
|
||||
startWorker({
|
||||
script: BATCH_WORKER,
|
||||
args: {
|
||||
type: 'asymEncrypt',
|
||||
list: envelopes,
|
||||
senderPrivkey: senderPrivkey,
|
||||
receiverPubkeys: receiverPubkeys
|
||||
},
|
||||
callback: callback,
|
||||
noWorker: function() {
|
||||
return cryptoBatch.encryptListForUser(envelopes, receiverPubkeys, senderPrivkey);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Crypto.prototype.decryptListForUser = function(list, senderPubkeys, callback) {
|
||||
if (!senderPubkeys || senderPubkeys < 1) {
|
||||
callback({
|
||||
errMsg: 'Sender public keys must be set!'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var keypair = rsa.exportKeys();
|
||||
var receiverPrivkey = {
|
||||
_id: keypair._id,
|
||||
privateKey: keypair.privkeyPem
|
||||
};
|
||||
|
||||
startWorker({
|
||||
script: BATCH_WORKER,
|
||||
args: {
|
||||
type: 'asymDecrypt',
|
||||
list: list,
|
||||
receiverPrivkey: receiverPrivkey,
|
||||
senderPubkeys: senderPubkeys
|
||||
},
|
||||
callback: callback,
|
||||
noWorker: function() {
|
||||
return cryptoBatch.decryptListForUser(list, senderPubkeys, receiverPrivkey);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Re-encrypt keys item and items seperately
|
||||
//
|
||||
|
||||
Crypto.prototype.reencryptListKeysForUser = function(list, senderPubkeys, callback) {
|
||||
var keypair = rsa.exportKeys();
|
||||
var receiverPrivkey = {
|
||||
_id: keypair._id,
|
||||
privateKey: keypair.privkeyPem
|
||||
};
|
||||
|
||||
startWorker({
|
||||
script: BATCH_WORKER,
|
||||
args: {
|
||||
type: 'reencrypt',
|
||||
list: list,
|
||||
receiverPrivkey: receiverPrivkey,
|
||||
senderPubkeys: senderPubkeys,
|
||||
symKey: passBasedKey
|
||||
},
|
||||
callback: callback,
|
||||
noWorker: function() {
|
||||
return cryptoBatch.reencryptListKeysForUser(list, senderPubkeys, receiverPrivkey, passBasedKey);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Crypto.prototype.decryptKeysAndList = function(list, callback) {
|
||||
startWorker({
|
||||
script: BATCH_WORKER,
|
||||
args: {
|
||||
type: 'decryptItems',
|
||||
list: list,
|
||||
symKey: passBasedKey
|
||||
},
|
||||
callback: callback,
|
||||
noWorker: function() {
|
||||
return cryptoBatch.decryptKeysAndList(list, passBasedKey);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// helper functions
|
||||
//
|
||||
|
@ -1,23 +1,24 @@
|
||||
/**
|
||||
* A Wrapper for Forge's PBKDF2 function
|
||||
*/
|
||||
define(['node-forge'], function(forge) {
|
||||
define(['forge'], function(forge) {
|
||||
'use strict';
|
||||
|
||||
var self = {};
|
||||
|
||||
/**
|
||||
* PBKDF2-HMAC-SHA1 key derivation with a random salt and 1000 iterations
|
||||
* @param password [String] The password in UTF8
|
||||
* @param salt [String] The base64 encoded salt
|
||||
* @param keySize [Number] The key size in bits
|
||||
* @return [String] The base64 encoded key
|
||||
* @param {String} password The password in UTF8
|
||||
* @param {String} salt The base64 encoded salt
|
||||
* @param {String} keySize The key size in bits
|
||||
* @return {String} The base64 encoded key
|
||||
*/
|
||||
self.getKey = function(password, salt, keySize) {
|
||||
var key = forge.pkcs5.pbkdf2(password, forge.util.decode64(salt), 1000, keySize / 8);
|
||||
var keyBase64 = forge.util.encode64(key);
|
||||
var saltUtf8 = forge.util.decode64(salt);
|
||||
var md = forge.md.sha256.create();
|
||||
var key = forge.pkcs5.pbkdf2(password, saltUtf8, 10000, keySize / 8, md);
|
||||
|
||||
return keyBase64;
|
||||
return forge.util.encode64(key);
|
||||
};
|
||||
|
||||
return self;
|
||||
|
@ -1,7 +1,7 @@
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var util = require('cryptoLib/util'),
|
||||
var util = require('js/crypto/util'),
|
||||
_ = require('underscore'),
|
||||
config = require('js/app-config').config,
|
||||
str = require('js/app-config').string;
|
||||
|
@ -7,7 +7,6 @@
|
||||
paths: {
|
||||
js: '../js',
|
||||
test: '../../test',
|
||||
cryptoLib: '../js/crypto',
|
||||
underscore: 'underscore/underscore-min',
|
||||
lawnchair: 'lawnchair/lawnchair-git',
|
||||
lawnchairSQL: 'lawnchair/lawnchair-adapter-webkit-sqlite-git',
|
||||
|
56
test/new-unit/crypto-test.js
Normal file
56
test/new-unit/crypto-test.js
Normal file
@ -0,0 +1,56 @@
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var Crypto = require('js/crypto/crypto'),
|
||||
util = require('js/crypto/util'),
|
||||
expect = chai.expect;
|
||||
|
||||
describe('Crypto unit tests', function() {
|
||||
var crypto;
|
||||
|
||||
var password = 'password',
|
||||
keySize = 128,
|
||||
ivSize = 128;
|
||||
|
||||
beforeEach(function() {
|
||||
crypto = new Crypto();
|
||||
});
|
||||
|
||||
afterEach(function() {});
|
||||
|
||||
describe('AES encrypt/decrypt', function() {
|
||||
it('should work', function(done) {
|
||||
var plaintext = 'Hello, World!';
|
||||
var key = util.random(keySize);
|
||||
var iv = util.random(ivSize);
|
||||
|
||||
crypto.encrypt(plaintext, key, iv, function(err, ciphertext) {
|
||||
expect(err).to.not.exist;
|
||||
expect(ciphertext).to.exist;
|
||||
|
||||
crypto.decrypt(ciphertext, key, iv, function(err, decrypted) {
|
||||
expect(err).to.not.exist;
|
||||
expect(decrypted).to.equal(plaintext);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("PBKDF2 (Async/Worker)", function() {
|
||||
it('should work', function(done) {
|
||||
var salt = util.random(keySize);
|
||||
|
||||
crypto.deriveKey(password, salt, keySize, function(err, key) {
|
||||
expect(err).to.not.exist;
|
||||
expect(util.base642Str(key).length * 8).to.equal(keySize);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -33,6 +33,7 @@ function startTests() {
|
||||
'test/new-unit/email-dao-test',
|
||||
'test/new-unit/app-controller-test',
|
||||
'test/new-unit/pgp-test',
|
||||
'test/new-unit/crypto-test',
|
||||
'test/new-unit/rest-dao-test',
|
||||
'test/new-unit/publickey-dao-test',
|
||||
'test/new-unit/lawnchair-dao-test',
|
||||
|
Loading…
Reference in New Issue
Block a user