1
0
mirror of https://github.com/moparisthebest/mail synced 2024-12-22 15:28:49 -05:00

Implement and test crypto module

This commit is contained in:
Tankred Hase 2014-06-05 15:26:19 +02:00
parent a7a562bef6
commit 18d1c39b0a
12 changed files with 108 additions and 387 deletions

4
.gitignore vendored
View File

@ -8,7 +8,5 @@ dist/
release/ release/
test/integration/src/ test/integration/src/
src/lib/*.js src/lib/*.js
src/js/crypto/aes-cbc.js src/js/crypto/aes-gcm.js
src/js/crypto/crypto-batch.js
src/js/crypto/rsa.js
src/js/crypto/util.js src/js/crypto/util.js

View File

@ -10,7 +10,7 @@
"start": "grunt && grunt dev" "start": "grunt && grunt dev"
}, },
"dependencies": { "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", "imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.3",
"mailreader": "https://github.com/whiteout-io/mailreader/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", "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.4",

View File

@ -18,7 +18,7 @@ requirejs([
'js/controller/read', 'js/controller/read',
'js/controller/write', 'js/controller/write',
'js/controller/navigation', 'js/controller/navigation',
'cryptoLib/util', 'js/crypto/util',
'js/util/error', 'js/util/error',
'fastclick', 'fastclick',
'angularSanitize', 'angularSanitize',

View File

@ -2,7 +2,7 @@ define(function(require) {
'use strict'; 'use strict';
var _ = require('underscore'), var _ = require('underscore'),
util = require('cryptoLib/util'), util = require('js/crypto/util'),
config = require('js/app-config').config, config = require('js/app-config').config,
outboxDb = 'email_OUTBOX'; outboxDb = 'email_OUTBOX';

View File

@ -4,8 +4,8 @@ define(function(require) {
var angular = require('angular'), var angular = require('angular'),
_ = require('underscore'), _ = require('underscore'),
appController = require('js/app-controller'), appController = require('js/app-controller'),
aes = require('cryptoLib/aes-cbc'), aes = require('js/crypto/aes-gcm'),
util = require('cryptoLib/util'), util = require('js/crypto/util'),
str = require('js/app-config').string, str = require('js/app-config').string,
crypto, emailDao, outbox, keychainDao; crypto, emailDao, outbox, keychainDao;

View File

@ -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;
}
}());

View File

@ -5,120 +5,54 @@
define(function(require) { define(function(require) {
'use strict'; 'use strict';
var util = require('cryptoLib/util'), var aes = require('js/crypto/aes-gcm'),
aes = require('cryptoLib/aes-cbc'),
rsa = require('cryptoLib/rsa'),
cryptoBatch = require('cryptoLib/crypto-batch'),
pbkdf2 = require('js/crypto/pbkdf2'), pbkdf2 = require('js/crypto/pbkdf2'),
config = require('js/app-config').config; config = require('js/app-config').config;
var passBasedKey, var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js';
BATCH_WORKER = '/crypto/crypto-batch-worker.js',
PBKDF2_WORKER = '/crypto/pbkdf2-worker.js';
var Crypto = function() { var Crypto = function() {};
};
/** /**
* Initializes the crypto modules by fetching the user's * Encrypt plaintext using AES-GCM.
* encrypted secret key from storage and storing it in memory. * @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) { Crypto.prototype.encrypt = function(plaintext, key, iv, callback) {
var self = this; var ct;
// valdiate input try {
if (!args.emailAddress || !args.keySize || !args.rsaKeySize || typeof args.password !== 'string' || !args.salt) { ct = aes.encrypt(plaintext, key, iv);
callback({ } catch (err) {
errMsg: 'Crypto init failed. Not all args set!' callback(err);
});
return; return;
} }
self.emailAddress = args.emailAddress; callback(null, ct);
self.keySize = args.keySize; };
self.ivSize = args.keySize;
self.rsaKeySize = args.rsaKeySize;
// derive PBKDF2 from password in web worker thread /**
self.deriveKey(args.password, args.salt, self.keySize, function(err, derivedKey) { * Decrypt ciphertext suing AES-GCM
if (err) { * @param {String} ciphertext The base64 encoded ciphertext
callback(err); * @param {String} key The base64 encoded key
return; * @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 try {
passBasedKey = derivedKey; pt = aes.decrypt(ciphertext, key, iv);
} catch (err) {
// check if key exists callback(err);
if (!args.storedKeypair) { return;
// 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);
});
} }
function decryptKeypair(storedKeypair, derivedKey) { callback(null, pt);
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();
}
}; };
/** /**
@ -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 // helper functions
// //

View File

@ -1,23 +1,24 @@
/** /**
* A Wrapper for Forge's PBKDF2 function * A Wrapper for Forge's PBKDF2 function
*/ */
define(['node-forge'], function(forge) { define(['forge'], function(forge) {
'use strict'; 'use strict';
var self = {}; var self = {};
/** /**
* PBKDF2-HMAC-SHA1 key derivation with a random salt and 1000 iterations * PBKDF2-HMAC-SHA1 key derivation with a random salt and 1000 iterations
* @param password [String] The password in UTF8 * @param {String} password The password in UTF8
* @param salt [String] The base64 encoded salt * @param {String} salt The base64 encoded salt
* @param keySize [Number] The key size in bits * @param {String} keySize The key size in bits
* @return [String] The base64 encoded key * @return {String} The base64 encoded key
*/ */
self.getKey = function(password, salt, keySize) { self.getKey = function(password, salt, keySize) {
var key = forge.pkcs5.pbkdf2(password, forge.util.decode64(salt), 1000, keySize / 8); var saltUtf8 = forge.util.decode64(salt);
var keyBase64 = forge.util.encode64(key); 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; return self;

View File

@ -1,7 +1,7 @@
define(function(require) { define(function(require) {
'use strict'; 'use strict';
var util = require('cryptoLib/util'), var util = require('js/crypto/util'),
_ = require('underscore'), _ = require('underscore'),
config = require('js/app-config').config, config = require('js/app-config').config,
str = require('js/app-config').string; str = require('js/app-config').string;

View File

@ -7,7 +7,6 @@
paths: { paths: {
js: '../js', js: '../js',
test: '../../test', test: '../../test',
cryptoLib: '../js/crypto',
underscore: 'underscore/underscore-min', underscore: 'underscore/underscore-min',
lawnchair: 'lawnchair/lawnchair-git', lawnchair: 'lawnchair/lawnchair-git',
lawnchairSQL: 'lawnchair/lawnchair-adapter-webkit-sqlite-git', lawnchairSQL: 'lawnchair/lawnchair-adapter-webkit-sqlite-git',

View 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();
});
});
});
});
});

View File

@ -33,6 +33,7 @@ function startTests() {
'test/new-unit/email-dao-test', 'test/new-unit/email-dao-test',
'test/new-unit/app-controller-test', 'test/new-unit/app-controller-test',
'test/new-unit/pgp-test', 'test/new-unit/pgp-test',
'test/new-unit/crypto-test',
'test/new-unit/rest-dao-test', 'test/new-unit/rest-dao-test',
'test/new-unit/publickey-dao-test', 'test/new-unit/publickey-dao-test',
'test/new-unit/lawnchair-dao-test', 'test/new-unit/lawnchair-dao-test',