diff --git a/.gitignore b/.gitignore index 2e20e0d..da695ab 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/package.json b/package.json index 4718407..b7b6663 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/js/app.js b/src/js/app.js index 5f92e05..af72bca 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -18,7 +18,7 @@ requirejs([ 'js/controller/read', 'js/controller/write', 'js/controller/navigation', - 'cryptoLib/util', + 'js/crypto/util', 'js/util/error', 'fastclick', 'angularSanitize', diff --git a/src/js/bo/outbox.js b/src/js/bo/outbox.js index a9dd582..8a09cfb 100644 --- a/src/js/bo/outbox.js +++ b/src/js/bo/outbox.js @@ -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. */ diff --git a/src/js/controller/write.js b/src/js/controller/write.js index d1c6593..4325da7 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -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; diff --git a/src/js/crypto/crypto-batch-worker.js b/src/js/crypto/crypto-batch-worker.js deleted file mode 100644 index 59aaa63..0000000 --- a/src/js/crypto/crypto-batch-worker.js +++ /dev/null @@ -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; - } - -}()); \ No newline at end of file diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index 95ec3ee..7150966 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -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 // diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index ec01b69..a6ece33 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -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; -}); +}); \ No newline at end of file diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 3452e1a..57d2eb7 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -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; diff --git a/src/require-config.js b/src/require-config.js index 7439c8b..52d064d 100644 --- a/src/require-config.js +++ b/src/require-config.js @@ -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', diff --git a/test/new-unit/crypto-test.js b/test/new-unit/crypto-test.js new file mode 100644 index 0000000..7cd75af --- /dev/null +++ b/test/new-unit/crypto-test.js @@ -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(); + }); + }); + + }); + + }); +}); \ No newline at end of file diff --git a/test/new-unit/main.js b/test/new-unit/main.js index f151929..abdb328 100644 --- a/test/new-unit/main.js +++ b/test/new-unit/main.js @@ -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',