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/
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

View File

@ -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",

View File

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

View File

@ -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';
@ -27,7 +27,7 @@ define(function(require) {
this._outboxBusy = false;
};
/**
/**
* This function activates the periodic checking of the local device storage for pending mails.
* @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails.
*/

View File

@ -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;

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) {
'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
//

View File

@ -1,24 +1,25 @@
/**
* 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;
});
});

View File

@ -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;

View File

@ -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',

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/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',