diff --git a/src/js/app-config.js b/src/js/app-config.js index d808919..20f9340 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -1,4 +1,4 @@ -var app; +var app; // container for the application namespace (function() { 'use strict'; diff --git a/src/js/crypto/aes-batch-worker.js b/src/js/crypto/aes-batch-worker.js index b08b075..7b67937 100644 --- a/src/js/crypto/aes-batch-worker.js +++ b/src/js/crypto/aes-batch-worker.js @@ -1,43 +1,42 @@ -'use strict'; +(function() { + 'use strict'; -// import web worker dependencies -importScripts('../../lib/sjcl/sjcl.js'); -importScripts('../../lib/sjcl/bitArray.js'); -importScripts('../../lib/sjcl/codecBase64.js'); -importScripts('../../lib/sjcl/codecString.js'); -importScripts('../../lib/sjcl/aes.js'); -importScripts('../../lib/sjcl/ccm.js'); -importScripts('../app-config.js'); -importScripts('./aes-ccm.js'); -importScripts('./util.js'); + // import web worker dependencies + importScripts('../../lib/sjcl/sjcl.js'); + importScripts('../../lib/sjcl/bitArray.js'); + importScripts('../../lib/sjcl/codecBase64.js'); + importScripts('../../lib/sjcl/codecString.js'); + importScripts('../../lib/sjcl/aes.js'); + importScripts('../../lib/sjcl/ccm.js'); + importScripts('../app-config.js'); + importScripts('./aes-ccm.js'); + importScripts('./util.js'); -var AESBATCHWORKER = (function () { - /** * 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.addEventListener('message', function(e) { - + var args = e.data, output = null, aes = new app.crypto.AesCCM(), util = new app.crypto.Util(null, null); - + if (args.type === 'encrypt' && args.list) { // start encryption output = util.encryptList(aes, args.list); - + } else if (args.type === 'decrypt' && args.list) { // start decryption output = util.decryptList(aes, args.list); - + } else { throw 'Not all arguments for web worker crypto are defined!'; } - + // pass output back to main thread self.postMessage(output); }, false); - + }()); \ No newline at end of file diff --git a/src/js/crypto/aes-cbc.js b/src/js/crypto/aes-cbc.js index 1e3033b..f69e8bb 100644 --- a/src/js/crypto/aes-cbc.js +++ b/src/js/crypto/aes-cbc.js @@ -1,48 +1,59 @@ -'use strict'; +(function() { + 'use strict'; -/** - * A Wrapper for Crypto.js's AES-CBC encryption - */ -app.crypto.AesCBC = function() { - - var mode = CryptoJS.mode.CBC; // use CBC mode for Crypto.js - var padding = CryptoJS.pad.Pkcs7; // use Pkcs7/Pkcs5 padding for Crypto.js - /** - * Encrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256) - * @param plaintext [String] The input string in UTF8 - * @param key [String] The base64 encoded key - * @param iv [String] The base64 encoded IV - * @return [String] The base64 encoded ciphertext + * A Wrapper for Crypto.js's AES-CBC encryption */ - this.encrypt = function(plaintext, key, iv) { - // parse base64 input to crypto.js WordArrays - var keyWords = CryptoJS.enc.Base64.parse(key); - var ivWords = CryptoJS.enc.Base64.parse(iv); - var plaintextWords = CryptoJS.enc.Utf8.parse(plaintext); - - var encrypted = CryptoJS.AES.encrypt(plaintextWords, keyWords, { iv: ivWords, mode: mode, padding: padding }); - var ctBase64 = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); - - return ctBase64; + app.crypto.AesCBC = function() { + + var mode = CryptoJS.mode.CBC; // use CBC mode for Crypto.js + var padding = CryptoJS.pad.Pkcs7; // use Pkcs7/Pkcs5 padding for Crypto.js + + /** + * Encrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256) + * @param plaintext [String] The input string in UTF8 + * @param key [String] The base64 encoded key + * @param iv [String] The base64 encoded IV + * @return [String] The base64 encoded ciphertext + */ + this.encrypt = function(plaintext, key, iv) { + // parse base64 input to crypto.js WordArrays + var keyWords = CryptoJS.enc.Base64.parse(key); + var ivWords = CryptoJS.enc.Base64.parse(iv); + var plaintextWords = CryptoJS.enc.Utf8.parse(plaintext); + + var encrypted = CryptoJS.AES.encrypt(plaintextWords, keyWords, { + iv: ivWords, + mode: mode, + padding: padding + }); + var ctBase64 = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + + return ctBase64; + }; + + /** + * Decrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256) + * @param ciphertext [String] The base64 encoded ciphertext + * @param key [String] The base64 encoded key + * @param iv [String] The base64 encoded IV + * @return [String] The decrypted plaintext in UTF8 + */ + this.decrypt = function(ciphertext, key, iv) { + // parse base64 input to crypto.js WordArrays + var keyWords = CryptoJS.enc.Base64.parse(key); + var ivWords = CryptoJS.enc.Base64.parse(iv); + + var decrypted = CryptoJS.AES.decrypt(ciphertext, keyWords, { + iv: ivWords, + mode: mode, + padding: padding + }); + var pt = decrypted.toString(CryptoJS.enc.Utf8); + + return pt; + }; + }; - - /** - * Decrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256) - * @param ciphertext [String] The base64 encoded ciphertext - * @param key [String] The base64 encoded key - * @param iv [String] The base64 encoded IV - * @return [String] The decrypted plaintext in UTF8 - */ - this.decrypt = function(ciphertext, key, iv) { - // parse base64 input to crypto.js WordArrays - var keyWords = CryptoJS.enc.Base64.parse(key); - var ivWords = CryptoJS.enc.Base64.parse(iv); - - var decrypted = CryptoJS.AES.decrypt(ciphertext, keyWords, { iv: ivWords, mode: mode, padding: padding }); - var pt = decrypted.toString(CryptoJS.enc.Utf8); - - return pt; - }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/crypto/aes-ccm.js b/src/js/crypto/aes-ccm.js index bddf08c..3943d69 100644 --- a/src/js/crypto/aes-ccm.js +++ b/src/js/crypto/aes-ccm.js @@ -1,51 +1,54 @@ -'use strict'; +(function() { + 'use strict'; -/** - * A Wrapper for SJCL's authenticated AES-CCM encryption - */ -app.crypto.AesCCM = function() { - - var adata = []; // authenticated data (empty by default) - var tlen = 64; // The tag length in bits - /** - * Encrypt a String using AES-CCM using the provided keysize (e.g. 128, 256) - * @param plaintext [String] The input string in UTF8 - * @param key [String] The base64 encoded key - * @param iv [String] The base64 encoded IV - * @return [String] The base64 encoded ciphertext + * A Wrapper for SJCL's authenticated AES-CCM encryption */ - this.encrypt = function(plaintext, key, iv) { - // convert parameters to WordArrays - var keyWords = sjcl.codec.base64.toBits(key); - var ivWords = sjcl.codec.base64.toBits(iv); - var plaintextWords = sjcl.codec.utf8String.toBits(plaintext); - - var blockCipher = new sjcl.cipher.aes(keyWords); - var ciphertext = sjcl.mode.ccm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen); - var ctBase64 = sjcl.codec.base64.fromBits(ciphertext); - - return ctBase64; - }; - - /** - * Decrypt a String using AES-CCM using the provided keysize (e.g. 128, 256) - * @param ciphertext [String] The base64 encoded ciphertext - * @param key [String] The base64 encoded key - * @param iv [String] The base64 encoded IV - * @return [String] The decrypted plaintext in UTF8 - */ - this.decrypt = function(ciphertext, key, iv) { - // convert parameters to WordArrays - var keyWords = sjcl.codec.base64.toBits(key); - var ivWords = sjcl.codec.base64.toBits(iv); - var ciphertextWords = sjcl.codec.base64.toBits(ciphertext); - - var blockCipher = new sjcl.cipher.aes(keyWords); - var decrypted = sjcl.mode.ccm.decrypt(blockCipher, ciphertextWords, ivWords, adata, tlen); - var pt = sjcl.codec.utf8String.fromBits(decrypted); - - return pt; + app.crypto.AesCCM = function() { + + var adata = []; // authenticated data (empty by default) + var tlen = 64; // The tag length in bits + + /** + * Encrypt a String using AES-CCM using the provided keysize (e.g. 128, 256) + * @param plaintext [String] The input string in UTF8 + * @param key [String] The base64 encoded key + * @param iv [String] The base64 encoded IV + * @return [String] The base64 encoded ciphertext + */ + this.encrypt = function(plaintext, key, iv) { + // convert parameters to WordArrays + var keyWords = sjcl.codec.base64.toBits(key); + var ivWords = sjcl.codec.base64.toBits(iv); + var plaintextWords = sjcl.codec.utf8String.toBits(plaintext); + + var blockCipher = new sjcl.cipher.aes(keyWords); + var ciphertext = sjcl.mode.ccm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen); + var ctBase64 = sjcl.codec.base64.fromBits(ciphertext); + + return ctBase64; + }; + + /** + * Decrypt a String using AES-CCM using the provided keysize (e.g. 128, 256) + * @param ciphertext [String] The base64 encoded ciphertext + * @param key [String] The base64 encoded key + * @param iv [String] The base64 encoded IV + * @return [String] The decrypted plaintext in UTF8 + */ + this.decrypt = function(ciphertext, key, iv) { + // convert parameters to WordArrays + var keyWords = sjcl.codec.base64.toBits(key); + var ivWords = sjcl.codec.base64.toBits(iv); + var ciphertextWords = sjcl.codec.base64.toBits(ciphertext); + + var blockCipher = new sjcl.cipher.aes(keyWords); + var decrypted = sjcl.mode.ccm.decrypt(blockCipher, ciphertextWords, ivWords, adata, tlen); + var pt = sjcl.codec.utf8String.fromBits(decrypted); + + return pt; + }; + }; -}; \ No newline at end of file +}()); \ No newline at end of file diff --git a/src/js/crypto/aes-gcm.js b/src/js/crypto/aes-gcm.js index 15b4e9e..bd12611 100644 --- a/src/js/crypto/aes-gcm.js +++ b/src/js/crypto/aes-gcm.js @@ -1,51 +1,54 @@ -'use strict'; +(function() { + 'use strict'; -/** - * A Wrapper for SJCL's authenticated AES-GCM encryption - */ -app.crypto.AesGCM = function() { - - var adata = []; // authenticated data (empty by default) - var tlen = 128; // The tag length in bits - /** - * Encrypt a String using AES-GCM using the provided keysize (e.g. 128, 256) - * @param plaintext [String] The input string in UTF8 - * @param key [String] The base64 encoded key - * @param iv [String] The base64 encoded IV - * @return [String] The base64 encoded ciphertext + * A Wrapper for SJCL's authenticated AES-GCM encryption */ - this.encrypt = function(plaintext, key, iv) { - // convert parameters to WordArrays - var keyWords = sjcl.codec.base64.toBits(key); - var ivWords = sjcl.codec.base64.toBits(iv); - var plaintextWords = sjcl.codec.utf8String.toBits(plaintext); - - var blockCipher = new sjcl.cipher.aes(keyWords); - var ciphertext = sjcl.mode.gcm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen); - var ctBase64 = sjcl.codec.base64.fromBits(ciphertext); - - return ctBase64; - }; - - /** - * Decrypt a String using AES-GCM using the provided keysize (e.g. 128, 256) - * @param ciphertext [String] The base64 encoded ciphertext - * @param key [String] The base64 encoded key - * @param iv [String] The base64 encoded IV - * @return [String] The decrypted plaintext in UTF8 - */ - this.decrypt = function(ciphertext, key, iv) { - // convert parameters to WordArrays - var keyWords = sjcl.codec.base64.toBits(key); - var ivWords = sjcl.codec.base64.toBits(iv); - var ciphertextWords = sjcl.codec.base64.toBits(ciphertext); - - var blockCipher = new sjcl.cipher.aes(keyWords); - var decrypted = sjcl.mode.gcm.decrypt(blockCipher, ciphertextWords, ivWords, adata, tlen); - var pt = sjcl.codec.utf8String.fromBits(decrypted); - - return pt; + app.crypto.AesGCM = function() { + + var adata = []; // authenticated data (empty by default) + var tlen = 128; // The tag length in bits + + /** + * Encrypt a String using AES-GCM using the provided keysize (e.g. 128, 256) + * @param plaintext [String] The input string in UTF8 + * @param key [String] The base64 encoded key + * @param iv [String] The base64 encoded IV + * @return [String] The base64 encoded ciphertext + */ + this.encrypt = function(plaintext, key, iv) { + // convert parameters to WordArrays + var keyWords = sjcl.codec.base64.toBits(key); + var ivWords = sjcl.codec.base64.toBits(iv); + var plaintextWords = sjcl.codec.utf8String.toBits(plaintext); + + var blockCipher = new sjcl.cipher.aes(keyWords); + var ciphertext = sjcl.mode.gcm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen); + var ctBase64 = sjcl.codec.base64.fromBits(ciphertext); + + return ctBase64; + }; + + /** + * Decrypt a String using AES-GCM using the provided keysize (e.g. 128, 256) + * @param ciphertext [String] The base64 encoded ciphertext + * @param key [String] The base64 encoded key + * @param iv [String] The base64 encoded IV + * @return [String] The decrypted plaintext in UTF8 + */ + this.decrypt = function(ciphertext, key, iv) { + // convert parameters to WordArrays + var keyWords = sjcl.codec.base64.toBits(key); + var ivWords = sjcl.codec.base64.toBits(iv); + var ciphertextWords = sjcl.codec.base64.toBits(ciphertext); + + var blockCipher = new sjcl.cipher.aes(keyWords); + var decrypted = sjcl.mode.gcm.decrypt(blockCipher, ciphertextWords, ivWords, adata, tlen); + var pt = sjcl.codec.utf8String.fromBits(decrypted); + + return pt; + }; + }; -}; \ No newline at end of file +}()); \ No newline at end of file diff --git a/src/js/crypto/aes-worker.js b/src/js/crypto/aes-worker.js index 697da27..2dacea0 100644 --- a/src/js/crypto/aes-worker.js +++ b/src/js/crypto/aes-worker.js @@ -1,41 +1,40 @@ -'use strict'; +(function() { + 'use strict'; -// import web worker dependencies -importScripts('../../lib/sjcl/sjcl.js'); -importScripts('../../lib/sjcl/bitArray.js'); -importScripts('../../lib/sjcl/codecBase64.js'); -importScripts('../../lib/sjcl/codecString.js'); -importScripts('../../lib/sjcl/aes.js'); -importScripts('../../lib/sjcl/ccm.js'); -importScripts('../app-config.js'); -importScripts('./aes-ccm.js'); + // import web worker dependencies + importScripts('../../lib/sjcl/sjcl.js'); + importScripts('../../lib/sjcl/bitArray.js'); + importScripts('../../lib/sjcl/codecBase64.js'); + importScripts('../../lib/sjcl/codecString.js'); + importScripts('../../lib/sjcl/aes.js'); + importScripts('../../lib/sjcl/ccm.js'); + importScripts('../app-config.js'); + importScripts('./aes-ccm.js'); -var AESWORKER = (function () { - /** * 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.addEventListener('message', function(e) { - + var args = e.data, output = null, aes = new app.crypto.AesCCM(); - + if (args.type === 'encrypt' && args.plaintext && args.key && args.iv) { // start encryption output = aes.encrypt(args.plaintext, args.key, args.iv); - + } else if (args.type === 'decrypt' && args.ciphertext && args.key && args.iv) { // start decryption output = aes.decrypt(args.ciphertext, args.key, args.iv); - + } else { throw 'Not all arguments for web worker crypto are defined!'; } - + // pass output back to main thread self.postMessage(output); }, false); - + }()); \ No newline at end of file diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index 03f9644..02b20cf 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -1,223 +1,249 @@ -'use strict'; +(function() { + 'use strict'; -/** - * High level crypto api that invokes native crypto (if available) and - * gracefully degrades to JS crypto (if unavailable) - */ -app.crypto.Crypto = function(window, util) { - - var symmetricUserKey, // the user's secret key used to encrypt item-keys - aes = new app.crypto.AesCCM(); // use authenticated AES-CCM mode by default - /** - * Initializes the crypto modules by fetching the user's - * encrypted secret key from storage and storing it in memory. + * High level crypto api that invokes native crypto (if available) and + * gracefully degrades to JS crypto (if unavailable) */ - this.init = function(emailAddress, password, keySize, ivSize, callback) { - this.emailAddress = emailAddress; - this.keySize = keySize; - this.ivSize = ivSize; - - // derive PBKDF2 from password in web worker thread - this.deriveKey(password, keySize, function(pbkdf2) { - - // fetch user's encrypted secret key from keychain/storage - var keyStore = new app.dao.LocalStorageDAO(window); - var storageId = emailAddress + '_encryptedSymmetricKey'; - var encryptedKey = keyStore.read(storageId); + app.crypto.Crypto = function(window, util) { + + var symmetricUserKey, // the user's secret key used to encrypt item-keys + aes = new app.crypto.AesCCM(); // use authenticated AES-CCM mode by default + + /** + * Initializes the crypto modules by fetching the user's + * encrypted secret key from storage and storing it in memory. + */ + this.init = function(emailAddress, password, keySize, ivSize, callback) { + this.emailAddress = emailAddress; + this.keySize = keySize; + this.ivSize = ivSize; + + // derive PBKDF2 from password in web worker thread + this.deriveKey(password, keySize, function(pbkdf2) { + + // fetch user's encrypted secret key from keychain/storage + var keyStore = new app.dao.LocalStorageDAO(window); + var storageId = emailAddress + '_encryptedSymmetricKey'; + var encryptedKey = keyStore.read(storageId); + + // check if key exists + if (!encryptedKey) { + // generate key, encrypt and persist if none exists + symmetricUserKey = util.random(keySize); + var iv = util.random(ivSize); + var key = aes.encrypt(symmetricUserKey, pbkdf2, iv); + keyStore.persist(storageId, { + key: key, + iv: iv + }); + } else { + // decrypt key + symmetricUserKey = aes.decrypt(encryptedKey.key, pbkdf2, encryptedKey.iv); + } + + callback(); + }); + }; + + /** + * Do PBKDF2 key derivation in a WebWorker thread + */ + this.deriveKey = function(password, keySize, callback) { + // check for WebWorker support + if (window.Worker) { + + // init webworker thread + var worker = new Worker(app.config.workerPath + '/crypto/pbkdf2-worker.js'); + + worker.addEventListener('message', function(e) { + // return derived key from the worker + callback(e.data); + }, false); + + // send plaintext data to the worker + worker.postMessage({ + password: password, + keySize: keySize + }); - // check if key exists - if(!encryptedKey) { - // generate key, encrypt and persist if none exists - symmetricUserKey = util.random(keySize); - var iv = util.random(ivSize); - var key = aes.encrypt(symmetricUserKey, pbkdf2, iv); - keyStore.persist(storageId, { key: key, iv: iv }); } else { - // decrypt key - symmetricUserKey = aes.decrypt(encryptedKey.key, pbkdf2, encryptedKey.iv); - } + // no WebWorker support... do synchronous call + var pbkdf2 = new app.crypto.PBKDF2(); + var key = pbkdf2.getKey(password, keySize); + callback(key); + } + }; - callback(); - }); - }; - - /** - * Do PBKDF2 key derivation in a WebWorker thread - */ - this.deriveKey = function(password, keySize, callback) { - // check for WebWorker support - if (window.Worker) { - - // init webworker thread - var worker = new Worker(app.config.workerPath + '/crypto/pbkdf2-worker.js'); - - worker.addEventListener('message', function(e) { - // return derived key from the worker - callback(e.data); - }, false); - - // send plaintext data to the worker - worker.postMessage({ password:password, keySize:keySize }); - - } else { - // no WebWorker support... do synchronous call - var pbkdf2 = new app.crypto.PBKDF2(); - var key = pbkdf2.getKey(password, keySize); - callback(key); - } - }; - - // - // En/Decrypts single item - // - - this.aesEncrypt = function(plaintext, key, iv, callback) { - if (window.Worker) { - - var worker = new Worker(app.config.workerPath + '/crypto/aes-worker.js'); - worker.addEventListener('message', function(e) { - callback(e.data); - }, false); - worker.postMessage({ type:'encrypt', plaintext:plaintext, key:key, iv:iv }); - - } else { - var ct = this.aesEncryptSync(plaintext, key, iv); - callback(ct); - } - }; - - this.aesDecrypt = function(ciphertext, key, iv, callback) { - if (window.Worker) { - - var worker = new Worker(app.config.workerPath + '/crypto/aes-worker.js'); - worker.addEventListener('message', function(e) { - callback(e.data); - }, false); - worker.postMessage({ type:'decrypt', ciphertext:ciphertext, key:key, iv:iv }); - - } else { - var pt = this.aesDecryptSync(ciphertext, key, iv); - callback(pt); - } - }; - - this.aesEncryptSync = function(plaintext, key, iv) { - return aes.encrypt(plaintext, key, iv); - }; - - this.aesDecryptSync = function(ciphertext, key, iv) { - return aes.decrypt(ciphertext, key, iv); - }; - - // - // En/Decrypt a list of items with AES in a WebWorker thread - // - - this.aesEncryptList = function(list, callback) { - if (window.Worker) { - - var worker = new Worker(app.config.workerPath + '/crypto/aes-batch-worker.js'); - worker.addEventListener('message', function(e) { - callback(e.data); - }, false); - worker.postMessage({ type:'encrypt', list:list }); - - } else { - var encryptedList = util.encryptList(aes, list); - callback(encryptedList); - } - }; - - this.aesDecryptList = function(list, callback) { - if (window.Worker) { - - var worker = new Worker(app.config.workerPath + '/crypto/aes-batch-worker.js'); - worker.addEventListener('message', function(e) { - callback(e.data); - }, false); - worker.postMessage({ type:'decrypt', list:list }); - - } else { - var decryptedList = util.decryptList(aes, list); - callback(decryptedList); - } - }; - - // - // En/Decrypt something speficially using the user's secret key - // - - this.aesEncryptForUser = function(plaintext, iv, callback) { - var ciphertext = aes.encrypt(plaintext, symmetricUserKey, iv); - callback(ciphertext); - }; - this.aesDecryptForUser = function(ciphertext, iv, callback) { - var decrypted = aes.decrypt(ciphertext, symmetricUserKey, iv); - callback(decrypted); - }; - this.aesEncryptForUserSync = function(plaintext, iv) { - return aes.encrypt(plaintext, symmetricUserKey, iv); - }; - this.aesDecryptForUserSync = function(ciphertext, iv) { - return aes.decrypt(ciphertext, symmetricUserKey, iv); - }; - - this.aesEncryptListForUser = function(list, callback) { - var i, envelope, envelopes = [], self = this; - - // package objects into batchable envelope format - for (i = 0; i < list.length; i++) { - envelope = { - id: list[i].id, - plaintext: list[i], - key: util.random(self.keySize), - iv: util.random(self.ivSize) - }; - envelopes.push(envelope); - } - - // encrypt list - this.aesEncryptList(envelopes, function(encryptedList) { + // + // En/Decrypts single item + // - // encrypt keys for user + this.aesEncrypt = function(plaintext, key, iv, callback) { + if (window.Worker) { + + var worker = new Worker(app.config.workerPath + '/crypto/aes-worker.js'); + worker.addEventListener('message', function(e) { + callback(e.data); + }, false); + worker.postMessage({ + type: 'encrypt', + plaintext: plaintext, + key: key, + iv: iv + }); + + } else { + var ct = this.aesEncryptSync(plaintext, key, iv); + callback(ct); + } + }; + + this.aesDecrypt = function(ciphertext, key, iv, callback) { + if (window.Worker) { + + var worker = new Worker(app.config.workerPath + '/crypto/aes-worker.js'); + worker.addEventListener('message', function(e) { + callback(e.data); + }, false); + worker.postMessage({ + type: 'decrypt', + ciphertext: ciphertext, + key: key, + iv: iv + }); + + } else { + var pt = this.aesDecryptSync(ciphertext, key, iv); + callback(pt); + } + }; + + this.aesEncryptSync = function(plaintext, key, iv) { + return aes.encrypt(plaintext, key, iv); + }; + + this.aesDecryptSync = function(ciphertext, key, iv) { + return aes.decrypt(ciphertext, key, iv); + }; + + // + // En/Decrypt a list of items with AES in a WebWorker thread + // + + this.aesEncryptList = function(list, callback) { + if (window.Worker) { + + var worker = new Worker(app.config.workerPath + '/crypto/aes-batch-worker.js'); + worker.addEventListener('message', function(e) { + callback(e.data); + }, false); + worker.postMessage({ + type: 'encrypt', + list: list + }); + + } else { + var encryptedList = util.encryptList(aes, list); + callback(encryptedList); + } + }; + + this.aesDecryptList = function(list, callback) { + if (window.Worker) { + + var worker = new Worker(app.config.workerPath + '/crypto/aes-batch-worker.js'); + worker.addEventListener('message', function(e) { + callback(e.data); + }, false); + worker.postMessage({ + type: 'decrypt', + list: list + }); + + } else { + var decryptedList = util.decryptList(aes, list); + callback(decryptedList); + } + }; + + // + // En/Decrypt something speficially using the user's secret key + // + + this.aesEncryptForUser = function(plaintext, iv, callback) { + var ciphertext = aes.encrypt(plaintext, symmetricUserKey, iv); + callback(ciphertext); + }; + this.aesDecryptForUser = function(ciphertext, iv, callback) { + var decrypted = aes.decrypt(ciphertext, symmetricUserKey, iv); + callback(decrypted); + }; + this.aesEncryptForUserSync = function(plaintext, iv) { + return aes.encrypt(plaintext, symmetricUserKey, iv); + }; + this.aesDecryptForUserSync = function(ciphertext, iv) { + return aes.decrypt(ciphertext, symmetricUserKey, iv); + }; + + this.aesEncryptListForUser = function(list, callback) { + var i, envelope, envelopes = [], + self = this; + + // package objects into batchable envelope format + for (i = 0; i < list.length; i++) { + envelope = { + id: list[i].id, + plaintext: list[i], + key: util.random(self.keySize), + iv: util.random(self.ivSize) + }; + envelopes.push(envelope); + } + + // encrypt list + this.aesEncryptList(envelopes, function(encryptedList) { + + // encrypt keys for user + for (i = 0; i < encryptedList.length; i++) { + // process new values + encryptedList[i].itemIV = encryptedList[i].iv; + encryptedList[i].keyIV = util.random(self.ivSize); + encryptedList[i].encryptedKey = self.aesEncryptForUserSync(encryptedList[i].key, encryptedList[i].keyIV); + // delete old ones + delete encryptedList[i].iv; + delete encryptedList[i].key; + } + + callback(encryptedList); + }); + }; + + this.aesDecryptListForUser = function(encryptedList, callback) { + var i, list = []; + + // decrypt keys for user for (i = 0; i < encryptedList.length; i++) { - // process new values - encryptedList[i].itemIV = encryptedList[i].iv; - encryptedList[i].keyIV = util.random(self.ivSize); - encryptedList[i].encryptedKey = self.aesEncryptForUserSync(encryptedList[i].key, encryptedList[i].keyIV); - // delete old ones - delete encryptedList[i].iv; - delete encryptedList[i].key; - } - - callback(encryptedList); - }); - }; - - this.aesDecryptListForUser = function(encryptedList, callback) { - var i, list = []; - - // decrypt keys for user - for (i = 0; i < encryptedList.length; i++) { - // decrypt item key - encryptedList[i].key = this.aesDecryptForUserSync(encryptedList[i].encryptedKey, encryptedList[i].keyIV); - encryptedList[i].iv = encryptedList[i].itemIV; - // delete old values - delete encryptedList[i].keyIV; - delete encryptedList[i].itemIV; - delete encryptedList[i].encryptedKey; - } - - // decrypt list - this.aesDecryptList(encryptedList, function(decryptedList) { - // add plaintext to list - for (i = 0; i < decryptedList.length; i++) { - list.push(decryptedList[i].plaintext); + // decrypt item key + encryptedList[i].key = this.aesDecryptForUserSync(encryptedList[i].encryptedKey, encryptedList[i].keyIV); + encryptedList[i].iv = encryptedList[i].itemIV; + // delete old values + delete encryptedList[i].keyIV; + delete encryptedList[i].itemIV; + delete encryptedList[i].encryptedKey; } - callback(list); - }); + // decrypt list + this.aesDecryptList(encryptedList, function(decryptedList) { + // add plaintext to list + for (i = 0; i < decryptedList.length; i++) { + list.push(decryptedList[i].plaintext); + } + + callback(list); + }); + }; + }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/crypto/pbkdf2-worker.js b/src/js/crypto/pbkdf2-worker.js index 3351072..8d43d65 100644 --- a/src/js/crypto/pbkdf2-worker.js +++ b/src/js/crypto/pbkdf2-worker.js @@ -1,36 +1,35 @@ -'use strict'; +(function() { + 'use strict'; -// import web worker dependencies -importScripts('../../lib/crypto-js/core.js'); -importScripts('../../lib/crypto-js/enc-base64.js'); -importScripts('../../lib/crypto-js/sha1.js'); -importScripts('../../lib/crypto-js/hmac.js'); -importScripts('../../lib/crypto-js/pbkdf2.js'); -importScripts('../app-config.js'); -importScripts('./pbkdf2.js'); + // import web worker dependencies + importScripts('../../lib/crypto-js/core.js'); + importScripts('../../lib/crypto-js/enc-base64.js'); + importScripts('../../lib/crypto-js/sha1.js'); + importScripts('../../lib/crypto-js/hmac.js'); + importScripts('../../lib/crypto-js/pbkdf2.js'); + importScripts('../app-config.js'); + importScripts('./pbkdf2.js'); -var PBKDF2WORKER = (function () { - /** * 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.addEventListener('message', function(e) { - + var args = e.data, key = null; - + if (e.data.password && e.data.keySize) { // start deriving key var pbkdf2 = new app.crypto.PBKDF2(); key = pbkdf2.getKey(e.data.password, e.data.keySize); - + } else { throw 'Not all arguments for web worker crypto are defined!'; } - + // pass output back to main thread self.postMessage(key); }, false); - + }()); \ No newline at end of file diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index 3c8ceb9..3f4c1d0 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -1,22 +1,28 @@ -'use strict'; +(function() { + 'use strict'; -/** - * A Wrapper for Crypto.js's PBKDF2 function - */ -app.crypto.PBKDF2 = function() { - /** - * PBKDF2-HMAC-SHA1 key derivation with a constant salt and 1000 iterations - * @param password [String] The password in UTF8 - * @param keySize [Number] The key size in bits - * @return [String] The base64 encoded key + * A Wrapper for Crypto.js's PBKDF2 function */ - this.getKey = function(password, keySize) { - var salt = CryptoJS.enc.Base64.parse("vbhmLjC+Ub6MSbhS6/CkOwxB25wvwRkSLP2DzDtYb+4="); // from random 256 bit value - var key = CryptoJS.PBKDF2(password, salt, { keySize: keySize/32, iterations: 1000 }); - var keyBase64 = CryptoJS.enc.Base64.stringify(key); - - return keyBase64; + app.crypto.PBKDF2 = function() { + + /** + * PBKDF2-HMAC-SHA1 key derivation with a constant salt and 1000 iterations + * @param password [String] The password in UTF8 + * @param keySize [Number] The key size in bits + * @return [String] The base64 encoded key + */ + this.getKey = function(password, keySize) { + var salt = CryptoJS.enc.Base64.parse("vbhmLjC+Ub6MSbhS6/CkOwxB25wvwRkSLP2DzDtYb+4="); // from random 256 bit value + var key = CryptoJS.PBKDF2(password, salt, { + keySize: keySize / 32, + iterations: 1000 + }); + var keyBase64 = CryptoJS.enc.Base64.stringify(key); + + return keyBase64; + }; + }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index b84884c..9809733 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -1,322 +1,345 @@ -'use strict'; +(function() { + 'use strict'; -/** - * A wrapper for asymmetric OpenPGP encryption logic - */ -app.crypto.PGP = function(window, openpgp, util, server) { - - var self = this, - privateKey, // user's private key - publicKey, // user's public key - passphrase; // user's passphrase used for decryption - - openpgp.init(); // initialize OpenPGP.js - - // - // Key management - // - /** - * Check if user already has a public key on the server and if not, - * generate a new keypait for the user + * A wrapper for asymmetric OpenPGP encryption logic */ - self.initKeyPair = function(loginInfo, callback, displayCallback, finishCallback) { - // check if user already has a keypair in local storage - if (loginInfo.publicKeyId) { - // decode base 64 key ID - var keyId = window.atob(loginInfo.publicKeyId); - // read the user's keys from local storage - callback(keyId); - - } else { - // user has no key pair yet - displayCallback(function() { - // generate new key pair with 2048 bit RSA keys - var keys = self.generateKeys(2048); - var keyId = keys.privateKey.getKeyId(); - - // display finish - finishCallback(keyId); + app.crypto.PGP = function(window, openpgp, util, server) { + + var self = this, + privateKey, // user's private key + publicKey, // user's public key + passphrase; // user's passphrase used for decryption + + openpgp.init(); // initialize OpenPGP.js + + // + // Key management + // + + /** + * Check if user already has a public key on the server and if not, + * generate a new keypait for the user + */ + self.initKeyPair = function(loginInfo, callback, displayCallback, finishCallback) { + // check if user already has a keypair in local storage + if (loginInfo.publicKeyId) { + // decode base 64 key ID + var keyId = window.atob(loginInfo.publicKeyId); // read the user's keys from local storage callback(keyId); - }); - } - }; - /** - * Generate a key pair for the user - * @param numBits [int] number of bits for the key creation. (should be 1024+, generally) - * @email [string] user's email address - * @pass [string] a passphrase used to protect the private key - */ - self.generateKeys = function(numBits) { - // check passphrase - if (!passphrase && passphrase !== '') { throw 'No passphrase set!'; } - - var userId = 'SafeWith.me User '; - var keys = openpgp.generate_key_pair(1, numBits, userId, passphrase); // keytype 1=RSA + } else { + // user has no key pair yet + displayCallback(function() { + // generate new key pair with 2048 bit RSA keys + var keys = self.generateKeys(2048); + var keyId = keys.privateKey.getKeyId(); - self.importKeys(keys.publicKeyArmored, keys.privateKeyArmored); - - return keys; - }; - - /** - * Import the users key into the HTML5 local storage - */ - self.importKeys = function(publicKeyArmored, privateKeyArmored) { - // check passphrase - if (!passphrase && passphrase !== '') { throw 'No passphrase set!'; } - - // store keys in html5 local storage - openpgp.keyring.importPrivateKey(privateKeyArmored, passphrase); - openpgp.keyring.importPublicKey(publicKeyArmored); - openpgp.keyring.store(); - }; - - /** - * Export the keys by using the HTML5 FileWriter - */ - self.exportKeys = function(callback) { - // build blob - var buf = util.binStr2ArrBuf(publicKey.armored + privateKey.armored); - var blob = util.arrBuf2Blob(buf, 'text/plain'); - // create url - util.createUrl(undefined, blob, callback); - }; - - /** - * Read the users keys from the browser's HTML5 local storage - * @email [string] user's email address - * @keyId [string] the public key ID in unicode (not base 64) - */ - self.readKeys = function(keyId, callback, errorCallback) { - // read keys from keyring (local storage) - var privKeyQuery = openpgp.keyring.getPrivateKeyForKeyId(keyId)[0]; - if (privKeyQuery) { - privateKey = privKeyQuery.key; - } - publicKey = openpgp.keyring.getPublicKeysForKeyId(keyId)[0]; - - // check keys - if (!publicKey || !privateKey || (publicKey.keyId !== privateKey.keyId)) { - // no amtching keys found in the key store - return false; - } - - // read passphrase from local storage if no passphrase is specified - if(!passphrase && passphrase !== '') { - passphrase = window.sessionStorage.getItem(window.btoa(keyId) + 'Passphrase'); - } - - // check passphrase - if (!passphrase && passphrase !== '') { - return false; - } - // do test encrypt/decrypt to verify passphrase - try { - var testCt = self.asymmetricEncrypt('test'); - self.asymmetricDecrypt(testCt); - } catch (e) { - return false; - } - - return true; - }; - - /** - * Generate a new key pair for the user and persist the public key on the server - */ - self.syncKeysToServer = function(email, callback) { - // base64 encode key ID - var keyId = publicKey.keyId; - var encodedKeyId = window.btoa(keyId); - var pubKey = { - keyId : encodedKeyId, - ownerEmail : email, - asciiArmored : publicKey.armored - }; - var privKey = { - keyId : encodedKeyId, - ownerEmail : email, - asciiArmored : privateKey.armored - }; - - var jsonPublicKey = JSON.stringify(pubKey); - var jsonPrivateKey = JSON.stringify(privKey); - - // first upload public key - server.xhr({ - type: 'POST', - uri: '/ws/publicKeys', - contentType: 'application/json', - expected: 201, - body: jsonPublicKey, - success: function(resp) { - uploadPrivateKeys(); - }, - error: function(e) { - // if server is not available, just continue - // and read the user's keys from local storage - console.log('Server unavailable: keys were not synced to server!'); - callback(keyId); + // display finish + finishCallback(keyId); + // read the user's keys from local storage + callback(keyId); + }); } - }); - - // then upload private key - function uploadPrivateKeys() { + }; + + /** + * Generate a key pair for the user + * @param numBits [int] number of bits for the key creation. (should be 1024+, generally) + * @email [string] user's email address + * @pass [string] a passphrase used to protect the private key + */ + self.generateKeys = function(numBits) { + // check passphrase + if (!passphrase && passphrase !== '') { + throw 'No passphrase set!'; + } + + var userId = 'SafeWith.me User '; + var keys = openpgp.generate_key_pair(1, numBits, userId, passphrase); // keytype 1=RSA + + self.importKeys(keys.publicKeyArmored, keys.privateKeyArmored); + + return keys; + }; + + /** + * Import the users key into the HTML5 local storage + */ + self.importKeys = function(publicKeyArmored, privateKeyArmored) { + // check passphrase + if (!passphrase && passphrase !== '') { + throw 'No passphrase set!'; + } + + // store keys in html5 local storage + openpgp.keyring.importPrivateKey(privateKeyArmored, passphrase); + openpgp.keyring.importPublicKey(publicKeyArmored); + openpgp.keyring.store(); + }; + + /** + * Export the keys by using the HTML5 FileWriter + */ + self.exportKeys = function(callback) { + // build blob + var buf = util.binStr2ArrBuf(publicKey.armored + privateKey.armored); + var blob = util.arrBuf2Blob(buf, 'text/plain'); + // create url + util.createUrl(undefined, blob, callback); + }; + + /** + * Read the users keys from the browser's HTML5 local storage + * @email [string] user's email address + * @keyId [string] the public key ID in unicode (not base 64) + */ + self.readKeys = function(keyId, callback, errorCallback) { + // read keys from keyring (local storage) + var privKeyQuery = openpgp.keyring.getPrivateKeyForKeyId(keyId)[0]; + if (privKeyQuery) { + privateKey = privKeyQuery.key; + } + publicKey = openpgp.keyring.getPublicKeysForKeyId(keyId)[0]; + + // check keys + if (!publicKey || !privateKey || (publicKey.keyId !== privateKey.keyId)) { + // no amtching keys found in the key store + return false; + } + + // read passphrase from local storage if no passphrase is specified + if (!passphrase && passphrase !== '') { + passphrase = window.sessionStorage.getItem(window.btoa(keyId) + 'Passphrase'); + } + + // check passphrase + if (!passphrase && passphrase !== '') { + return false; + } + // do test encrypt/decrypt to verify passphrase + try { + var testCt = self.asymmetricEncrypt('test'); + self.asymmetricDecrypt(testCt); + } catch (e) { + return false; + } + + return true; + }; + + /** + * Generate a new key pair for the user and persist the public key on the server + */ + self.syncKeysToServer = function(email, callback) { + // base64 encode key ID + var keyId = publicKey.keyId; + var encodedKeyId = window.btoa(keyId); + var pubKey = { + keyId: encodedKeyId, + ownerEmail: email, + asciiArmored: publicKey.armored + }; + var privKey = { + keyId: encodedKeyId, + ownerEmail: email, + asciiArmored: privateKey.armored + }; + + var jsonPublicKey = JSON.stringify(pubKey); + var jsonPrivateKey = JSON.stringify(privKey); + + // first upload public key server.xhr({ type: 'POST', - uri: '/ws/privateKeys', + uri: '/ws/publicKeys', contentType: 'application/json', expected: 201, - body: jsonPrivateKey, + body: jsonPublicKey, success: function(resp) { - // read the user's keys from local storage + uploadPrivateKeys(); + }, + error: function(e) { + // if server is not available, just continue + // and read the user's keys from local storage + console.log('Server unavailable: keys were not synced to server!'); callback(keyId); } }); - } - }; - - /** - * Get the keypair from the server and import them into localstorage - */ - self.fetchKeys = function(email, keyId, callback, errCallback) { - var base64Key = window.btoa(keyId); - var encodedKeyId = encodeURIComponent(base64Key); - - // get public key - server.xhr({ - type: 'GET', - uri: '/ws/publicKeys?keyId=' + encodedKeyId, - expected: 200, - success: function(pubKey) { - getPrivateKey(pubKey); - }, - error: function(e) { - // if server is not available, just continue - console.log('Server unavailable: keys could not be fetched from server!'); - errCallback(e); + + // then upload private key + + function uploadPrivateKeys() { + server.xhr({ + type: 'POST', + uri: '/ws/privateKeys', + contentType: 'application/json', + expected: 201, + body: jsonPrivateKey, + success: function(resp) { + // read the user's keys from local storage + callback(keyId); + } + }); } - }); - - // get private key - function getPrivateKey(pubKey) { + }; + + /** + * Get the keypair from the server and import them into localstorage + */ + self.fetchKeys = function(email, keyId, callback, errCallback) { + var base64Key = window.btoa(keyId); + var encodedKeyId = encodeURIComponent(base64Key); + + // get public key server.xhr({ type: 'GET', - uri: '/ws/privateKeys?keyId=' + encodedKeyId, + uri: '/ws/publicKeys?keyId=' + encodedKeyId, expected: 200, - success: function(privKey) { - // import keys - self.importKeys(pubKey.asciiArmored, privKey.asciiArmored, email); - callback({ privateKey:privKey, publicKey:pubKey }); + success: function(pubKey) { + getPrivateKey(pubKey); + }, + error: function(e) { + // if server is not available, just continue + console.log('Server unavailable: keys could not be fetched from server!'); + errCallback(e); } }); - } - }; - - /** - * Get the current user's private key - */ - self.getPrivateKey = function() { - if (!privateKey) { return undefined; } - return privateKey.armored; + + // get private key + + function getPrivateKey(pubKey) { + server.xhr({ + type: 'GET', + uri: '/ws/privateKeys?keyId=' + encodedKeyId, + expected: 200, + success: function(privKey) { + // import keys + self.importKeys(pubKey.asciiArmored, privKey.asciiArmored, email); + callback({ + privateKey: privKey, + publicKey: pubKey + }); + } + }); + } + }; + + /** + * Get the current user's private key + */ + self.getPrivateKey = function() { + if (!privateKey) { + return undefined; + } + return privateKey.armored; + }; + + /** + * Get the current user's public key + */ + self.getPublicKey = function() { + if (!publicKey) { + return undefined; + } + return publicKey.armored; + }; + + /** + * Get the current user's base64 encoded public key ID + */ + self.getPublicKeyIdBase64 = function() { + return window.btoa(publicKey.keyId); + }; + + /** + * Get the user's passphrase for decrypting their private key + */ + self.setPassphrase = function(pass) { + passphrase = pass; + }; + + /** + * Store the passphrase for the current session + */ + self.rememberPassphrase = function(keyId) { + var base64KeyId = window.btoa(keyId); + window.sessionStorage.setItem(base64KeyId + 'Passphrase', passphrase); + }; + + // + // Asymmetric crypto + // + + /** + * Encrypt a string + * @param customPubKey [PublicKey] (optional) another user's public key for sharing + */ + self.asymmetricEncrypt = function(plaintext, customPubKey) { + var pub_key = null; + if (customPubKey) { + // use a custom set public for e.g. or sharing + pub_key = openpgp.read_publicKey(customPubKey); + } else { + // use the user's local public key + pub_key = openpgp.read_publicKey(publicKey.armored); + } + + var ciphertext = openpgp.write_encrypted_message(pub_key, window.btoa(plaintext)); + return ciphertext; + }; + + /** + * Decrypt a string + */ + self.asymmetricDecrypt = function(ciphertext) { + var priv_key = openpgp.read_privateKey(privateKey.armored); + + var msg = openpgp.read_message(ciphertext); + var keymat = null; + var sesskey = null; + + // Find the private (sub)key for the session key of the message + for (var i = 0; i < msg[0].sessionKeys.length; i++) { + if (priv_key[0].privateKeyPacket.publicKey.getKeyId() == msg[0].sessionKeys[i].keyId.bytes) { + keymat = { + key: priv_key[0], + keymaterial: priv_key[0].privateKeyPacket + }; + sesskey = msg[0].sessionKeys[i]; + break; + } + for (var j = 0; j < priv_key[0].subKeys.length; j++) { + if (priv_key[0].subKeys[j].publicKey.getKeyId() == msg[0].sessionKeys[i].keyId.bytes) { + keymat = { + key: priv_key[0], + keymaterial: priv_key[0].subKeys[j] + }; + sesskey = msg[0].sessionKeys[i]; + break; + } + } + } + if (keymat !== null) { + if (!keymat.keymaterial.decryptSecretMPIs(passphrase)) { + throw "Passphrase for secrect key was incorrect!"; + } + + var decrypted = msg[0].decrypt(keymat, sesskey); + return window.atob(decrypted); + + } else { + throw "No private key found!"; + } + }; + }; - /** - * Get the current user's public key - */ - self.getPublicKey = function() { - if (!publicKey) { return undefined; } - return publicKey.armored; - }; - - /** - * Get the current user's base64 encoded public key ID - */ - self.getPublicKeyIdBase64 = function() { - return window.btoa(publicKey.keyId); - }; - - /** - * Get the user's passphrase for decrypting their private key - */ - self.setPassphrase = function(pass) { - passphrase = pass; - }; - - /** - * Store the passphrase for the current session - */ - self.rememberPassphrase = function(keyId) { - var base64KeyId = window.btoa(keyId); - window.sessionStorage.setItem(base64KeyId + 'Passphrase', passphrase); - }; - - // - // Asymmetric crypto - // - - /** - * Encrypt a string - * @param customPubKey [PublicKey] (optional) another user's public key for sharing - */ - self.asymmetricEncrypt = function(plaintext, customPubKey) { - var pub_key = null; - if (customPubKey) { - // use a custom set public for e.g. or sharing - pub_key = openpgp.read_publicKey(customPubKey); - } else { - // use the user's local public key - pub_key = openpgp.read_publicKey(publicKey.armored); - } - - var ciphertext = openpgp.write_encrypted_message(pub_key, window.btoa(plaintext)); - return ciphertext; - }; - - /** - * Decrypt a string - */ - self.asymmetricDecrypt = function(ciphertext) { - var priv_key = openpgp.read_privateKey(privateKey.armored); - - var msg = openpgp.read_message(ciphertext); - var keymat = null; - var sesskey = null; - - // Find the private (sub)key for the session key of the message - for (var i = 0; i< msg[0].sessionKeys.length; i++) { - if (priv_key[0].privateKeyPacket.publicKey.getKeyId() == msg[0].sessionKeys[i].keyId.bytes) { - keymat = { key: priv_key[0], keymaterial: priv_key[0].privateKeyPacket}; - sesskey = msg[0].sessionKeys[i]; - break; - } - for (var j = 0; j < priv_key[0].subKeys.length; j++) { - if (priv_key[0].subKeys[j].publicKey.getKeyId() == msg[0].sessionKeys[i].keyId.bytes) { - keymat = { key: priv_key[0], keymaterial: priv_key[0].subKeys[j]}; - sesskey = msg[0].sessionKeys[i]; - break; - } - } - } - if (keymat != null) { - if (!keymat.keymaterial.decryptSecretMPIs(passphrase)) { - throw "Passphrase for secrect key was incorrect!"; - } - - var decrypted = msg[0].decrypt(keymat, sesskey); - return window.atob(decrypted); - - } else { - throw "No private key found!"; - } - }; - -}; +}()); /** * This function needs to be implemented, since it is used by the openpgp utils */ + function showMessages(str) {} \ No newline at end of file diff --git a/src/js/crypto/util.js b/src/js/crypto/util.js index 5edd0dc..3fd4a0b 100644 --- a/src/js/crypto/util.js +++ b/src/js/crypto/util.js @@ -1,153 +1,168 @@ -'use strict'; +(function() { + 'use strict'; -app.crypto.Util = function(window, uuid) { - - /** - * Generates a new RFC 4122 version 4 compliant random UUID - */ - this.UUID = function() { - return uuid.v4(); - }; - - /** - * Generates a cryptographically secure random base64-encoded key or IV - * @param keySize [Number] The size of the key in bits (e.g. 128, 256) - * @return [String] The base64 encoded key/IV - */ - this.random = function(keySize) { - var keyBase64, keyBuf; - - if (window.crypto && window.crypto.getRandomValues) { - keyBuf = new Uint8Array(keySize / 8); - window.crypto.getRandomValues(keyBuf); - keyBase64 = window.btoa(this.uint8Arr2BinStr(keyBuf)); - } else { - sjcl.random.addEntropy((new Date()).valueOf(), 2, "calltime"); - keyBuf = sjcl.random.randomWords(keySize / 32, 0); - keyBase64 = sjcl.codec.base64.fromBits(keyBuf); - } - - return keyBase64; - }; - - /** - * Encrypt a list of items - * @param aes [Object] The object implementing the aes mode - * @list list [Array] The list of items to encrypt - */ - this.encryptList = function(aes, list) { - var i, json, ct, outList = []; - - for (i = 0; i < list.length; i++) { - // stringify to JSON before encryption - json = JSON.stringify(list[i].plaintext); - ct = aes.encrypt(json, list[i].key, list[i].iv); - outList.push({ id:list[i].id, ciphertext:ct, key:list[i].key, iv:list[i].iv }); - } - - return outList; - }; - - /** - * Decrypt a list of items - * @param aes [Object] The object implementing the aes mode - * @list list [Array] The list of items to decrypt - */ - this.decryptList = function(aes, list) { - var i, json, pt, outList = []; + app.crypto.Util = function(window, uuid) { - for (i = 0; i < list.length; i++) { - // decrypt JSON and parse to object literal - json = aes.decrypt(list[i].ciphertext, list[i].key, list[i].iv); - pt = JSON.parse(json); - outList.push({ id:list[i].id, plaintext:pt, key:list[i].key, iv:list[i].iv }); - } - - return outList; - }; - - /** - * Parse a date string with the following format "1900-01-31 18:17:53" - */ - this.parseDate = function(str) { - var parts = str.match(/(\d+)/g); - return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]); - }; - - /** - * Converts a binary String (e.g. from the FileReader Api) to an ArrayBuffer - * @param str [String] a binary string with integer values (0..255) per character - * @return [ArrayBuffer] - */ - this.binStr2ArrBuf = function(str) { - var b = new ArrayBuffer(str.length); - var buf = new Uint8Array(b); - - for(var i = 0; i < b.byteLength; i++){ - buf[i] = str.charCodeAt(i); - } - - return b; - }; - - /** - * Creates a Blob from an ArrayBuffer using the BlobBuilder Api - * @param str [String] a binary string with integer values (0..255) per character - * @return [ArrayBuffer] either a data url or a filesystem url - */ - this.arrBuf2Blob = function(buf, mimeType) { - var b = new Uint8Array(buf); - var blob = new Blob([b], {type: mimeType}); - - return blob; - }; - - /** - * Creates a binary String from a Blob using the FileReader Api - * @param blob [Blob/File] a blob containing the the binary data - * @return [String] a binary string with integer values (0..255) per character - */ - this.blob2BinStr = function(blob, callback) { - var reader = new FileReader(); - - reader.onload = function(event) { - callback(event.target.result); + /** + * Generates a new RFC 4122 version 4 compliant random UUID + */ + this.UUID = function() { + return uuid.v4(); + }; + + /** + * Generates a cryptographically secure random base64-encoded key or IV + * @param keySize [Number] The size of the key in bits (e.g. 128, 256) + * @return [String] The base64 encoded key/IV + */ + this.random = function(keySize) { + var keyBase64, keyBuf; + + if (window.crypto && window.crypto.getRandomValues) { + keyBuf = new Uint8Array(keySize / 8); + window.crypto.getRandomValues(keyBuf); + keyBase64 = window.btoa(this.uint8Arr2BinStr(keyBuf)); + } else { + sjcl.random.addEntropy((new Date()).valueOf(), 2, "calltime"); + keyBuf = sjcl.random.randomWords(keySize / 32, 0); + keyBase64 = sjcl.codec.base64.fromBits(keyBuf); + } + + return keyBase64; + }; + + /** + * Encrypt a list of items + * @param aes [Object] The object implementing the aes mode + * @list list [Array] The list of items to encrypt + */ + this.encryptList = function(aes, list) { + var i, json, ct, outList = []; + + for (i = 0; i < list.length; i++) { + // stringify to JSON before encryption + json = JSON.stringify(list[i].plaintext); + ct = aes.encrypt(json, list[i].key, list[i].iv); + outList.push({ + id: list[i].id, + ciphertext: ct, + key: list[i].key, + iv: list[i].iv + }); + } + + return outList; + }; + + /** + * Decrypt a list of items + * @param aes [Object] The object implementing the aes mode + * @list list [Array] The list of items to decrypt + */ + this.decryptList = function(aes, list) { + var i, json, pt, outList = []; + + for (i = 0; i < list.length; i++) { + // decrypt JSON and parse to object literal + json = aes.decrypt(list[i].ciphertext, list[i].key, list[i].iv); + pt = JSON.parse(json); + outList.push({ + id: list[i].id, + plaintext: pt, + key: list[i].key, + iv: list[i].iv + }); + } + + return outList; + }; + + /** + * Parse a date string with the following format "1900-01-31 18:17:53" + */ + this.parseDate = function(str) { + var parts = str.match(/(\d+)/g); + return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]); + }; + + /** + * Converts a binary String (e.g. from the FileReader Api) to an ArrayBuffer + * @param str [String] a binary string with integer values (0..255) per character + * @return [ArrayBuffer] + */ + this.binStr2ArrBuf = function(str) { + var b = new ArrayBuffer(str.length); + var buf = new Uint8Array(b); + + for (var i = 0; i < b.byteLength; i++) { + buf[i] = str.charCodeAt(i); + } + + return b; + }; + + /** + * Creates a Blob from an ArrayBuffer using the BlobBuilder Api + * @param str [String] a binary string with integer values (0..255) per character + * @return [ArrayBuffer] either a data url or a filesystem url + */ + this.arrBuf2Blob = function(buf, mimeType) { + var b = new Uint8Array(buf); + var blob = new Blob([b], { + type: mimeType + }); + + return blob; + }; + + /** + * Creates a binary String from a Blob using the FileReader Api + * @param blob [Blob/File] a blob containing the the binary data + * @return [String] a binary string with integer values (0..255) per character + */ + this.blob2BinStr = function(blob, callback) { + var reader = new FileReader(); + + reader.onload = function(event) { + callback(event.target.result); + }; + + reader.readAsBinaryString(blob); + }; + + /** + * Converts an ArrayBuffer to a binary String. This is a slower alternative to + * conversion with arrBuf2Blob -> blob2BinStr, since these use native apis, + * but it can be used on browsers without the BlodBuilder Api + * @param buf [ArrayBuffer] + * @return [String] a binary string with integer values (0..255) per character + */ + this.arrBuf2BinStr = function(buf) { + var b = new Uint8Array(buf); + var str = ''; + + for (var i = 0; i < b.byteLength; i++) { + str += String.fromCharCode(b[i]); + } + + return str; + }; + + /** + * Converts a UInt8Array to a binary String. + * @param buf [UInt8Array] + * @return [String] a binary string with integer values (0..255) per character + */ + this.uint8Arr2BinStr = function(buf) { + var str = ''; + + for (var i = 0; i < buf.byteLength; i++) { + str += String.fromCharCode(buf[i]); + } + + return str; }; - reader.readAsBinaryString(blob); }; - - /** - * Converts an ArrayBuffer to a binary String. This is a slower alternative to - * conversion with arrBuf2Blob -> blob2BinStr, since these use native apis, - * but it can be used on browsers without the BlodBuilder Api - * @param buf [ArrayBuffer] - * @return [String] a binary string with integer values (0..255) per character - */ - this.arrBuf2BinStr = function(buf) { - var b = new Uint8Array(buf); - var str = ''; - - for(var i = 0; i < b.byteLength; i++){ - str += String.fromCharCode(b[i]); - } - - return str; - }; - - /** - * Converts a UInt8Array to a binary String. - * @param buf [UInt8Array] - * @return [String] a binary string with integer values (0..255) per character - */ - this.uint8Arr2BinStr = function(buf) { - var str = ''; - - for(var i = 0; i < buf.byteLength; i++){ - str += String.fromCharCode(buf[i]); - } - - return str; - }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/dao/cloudstorage-dao.js b/src/js/dao/cloudstorage-dao.js index c53bbe2..e2dca44 100644 --- a/src/js/dao/cloudstorage-dao.js +++ b/src/js/dao/cloudstorage-dao.js @@ -1,110 +1,131 @@ -'use strict'; - -/** - * High level storage api for handling syncing of data to - * and from the cloud. - */ -app.dao.CloudStorage = function(window, $) { - - /** - * Lists the encrypted items - * @param type [String] The type of item e.g. 'email' - * @param offset [Number] The offset of items to fetch (0 is the last stored item) - * @param num [Number] The number of items to fetch (null means fetch all) - */ - this.listEncryptedItems = function(type, emailAddress, folderName, callback) { - var folder, uri, self = this; - - // fetch encrypted json objects from cloud service - uri = app.config.cloudUrl + '/' + type + '/user/' + emailAddress + '/folder/' + folderName; - $.ajax({ - url: uri, - type: 'GET', - dataType: 'json', - success: function(list) { - callback(list); - }, - error: function(xhr, textStatus, err) { - callback({error: err, status: textStatus}); - } - }); - }; - - /** - * Persist encrypted user key to cloud service - */ - this.persistUserSecretKey = function(emailAddress, callback) { - // fetch user's encrypted secret key from keychain/storage - var keyStore = new app.dao.LocalStorageDAO(window); - var storageId = emailAddress + '_encryptedSymmetricKey'; - var encryptedKey = keyStore.read(storageId); - - var payload = { - userId: emailAddress, - encryptedKey: encryptedKey.key, - keyIV: encryptedKey.iv - }; - - var uri = app.config.cloudUrl + '/keys/user/' + emailAddress; - $.ajax({ - url: uri, - type: 'PUT', - data: JSON.stringify(payload), - contentType: 'application/json', - success: function() { - callback(); - }, - error: function(xhr, textStatus, err) { - callback({error: err, status: textStatus}); - } - }); - }; +(function() { + 'use strict'; /** - * Get encrypted user key from cloud service + * High level storage api for handling syncing of data to + * and from the cloud. */ - this.getUserSecretKey = function(emailAddress, callback, replaceCallback) { - // fetch user's encrypted secret key from keychain/storage - var self = this; - var keyStore = new app.dao.LocalStorageDAO(window); - var storageId = emailAddress + '_encryptedSymmetricKey'; - var storedKey = keyStore.read(storageId); + app.dao.CloudStorage = function(window, $) { - var uri = app.config.cloudUrl + '/keys/user/' + emailAddress; - $.ajax({ - url: uri, - type: 'GET', - dataType: 'json', - success: function(fetchedKey) { - if ((!storedKey || !storedKey.key) && fetchedKey && fetchedKey.encryptedKey && fetchedKey.keyIV) { - // no local key... persist fetched key - keyStore.persist(storageId, { key: fetchedKey.encryptedKey, iv: fetchedKey.keyIV }); - replaceCallback(); - - } else if (storedKey && fetchedKey && (storedKey.key !== fetchedKey.encryptedKey || storedKey.iv !== fetchedKey.keyIV)){ - // local and fetched keys are not equal - if (confirm('Swap local key?')) { - // replace local key with fetched key - keyStore.persist(storageId, { key: fetchedKey.encryptedKey, iv: fetchedKey.keyIV }); - replaceCallback(); - } else { - if (confirm('Swap cloud key?')) { - // upload local key to cloud - self.persistUserSecretKey(emailAddress, callback); - } else { - callback({error: 'err', status: 'Key not synced!'}); - } - } - - } else { - // local and cloud keys are equal or cloud key is null - callback(); + /** + * Lists the encrypted items + * @param type [String] The type of item e.g. 'email' + * @param offset [Number] The offset of items to fetch (0 is the last stored item) + * @param num [Number] The number of items to fetch (null means fetch all) + */ + this.listEncryptedItems = function(type, emailAddress, folderName, callback) { + var folder, uri, self = this; + + // fetch encrypted json objects from cloud service + uri = app.config.cloudUrl + '/' + type + '/user/' + emailAddress + '/folder/' + folderName; + $.ajax({ + url: uri, + type: 'GET', + dataType: 'json', + success: function(list) { + callback(list); + }, + error: function(xhr, textStatus, err) { + callback({ + error: err, + status: textStatus + }); } - }, - error: function(xhr, textStatus, err) { - callback({error: err, status: textStatus}); - } - }); + }); + }; + + /** + * Persist encrypted user key to cloud service + */ + this.persistUserSecretKey = function(emailAddress, callback) { + // fetch user's encrypted secret key from keychain/storage + var keyStore = new app.dao.LocalStorageDAO(window); + var storageId = emailAddress + '_encryptedSymmetricKey'; + var encryptedKey = keyStore.read(storageId); + + var payload = { + userId: emailAddress, + encryptedKey: encryptedKey.key, + keyIV: encryptedKey.iv + }; + + var uri = app.config.cloudUrl + '/keys/user/' + emailAddress; + $.ajax({ + url: uri, + type: 'PUT', + data: JSON.stringify(payload), + contentType: 'application/json', + success: function() { + callback(); + }, + error: function(xhr, textStatus, err) { + callback({ + error: err, + status: textStatus + }); + } + }); + }; + + /** + * Get encrypted user key from cloud service + */ + this.getUserSecretKey = function(emailAddress, callback, replaceCallback) { + // fetch user's encrypted secret key from keychain/storage + var self = this; + var keyStore = new app.dao.LocalStorageDAO(window); + var storageId = emailAddress + '_encryptedSymmetricKey'; + var storedKey = keyStore.read(storageId); + + var uri = app.config.cloudUrl + '/keys/user/' + emailAddress; + $.ajax({ + url: uri, + type: 'GET', + dataType: 'json', + success: function(fetchedKey) { + if ((!storedKey || !storedKey.key) && fetchedKey && fetchedKey.encryptedKey && fetchedKey.keyIV) { + // no local key... persist fetched key + keyStore.persist(storageId, { + key: fetchedKey.encryptedKey, + iv: fetchedKey.keyIV + }); + replaceCallback(); + + } else if (storedKey && fetchedKey && (storedKey.key !== fetchedKey.encryptedKey || storedKey.iv !== fetchedKey.keyIV)) { + // local and fetched keys are not equal + if (confirm('Swap local key?')) { + // replace local key with fetched key + keyStore.persist(storageId, { + key: fetchedKey.encryptedKey, + iv: fetchedKey.keyIV + }); + replaceCallback(); + } else { + if (confirm('Swap cloud key?')) { + // upload local key to cloud + self.persistUserSecretKey(emailAddress, callback); + } else { + callback({ + error: 'err', + status: 'Key not synced!' + }); + } + } + + } else { + // local and cloud keys are equal or cloud key is null + callback(); + } + }, + error: function(xhr, textStatus, err) { + callback({ + error: err, + status: textStatus + }); + } + }); + }; + }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/dao/devicestorage.js b/src/js/dao/devicestorage.js index 3b131af..cd36f3f 100644 --- a/src/js/dao/devicestorage.js +++ b/src/js/dao/devicestorage.js @@ -1,63 +1,69 @@ -'use strict'; +(function() { + 'use strict'; -/** - * High level storage api that handles all persistence on the device. If - * SQLcipher/SQLite is available, all data is securely persisted there, - * through transparent encryption. If not, the crypto API is - * used to encrypt data on the fly before persisting via a JSON store. - */ -app.dao.DeviceStorage = function(util, crypto, jsonDao, sqlcipherDao) { - /** - * Stores a list of encrypted items in the object store - * @param list [Array] The list of items to be persisted - * @param type [String] The type of item to be persisted e.g. 'email' + * High level storage api that handles all persistence on the device. If + * SQLcipher/SQLite is available, all data is securely persisted there, + * through transparent encryption. If not, the crypto API is + * used to encrypt data on the fly before persisting via a JSON store. */ - this.storeEcryptedList = function(list, type, callback) { - var i, date, key, items = []; - - // format items for batch storing in dao - for (i = 0; i < list.length; i++) { - - // put date in key if available... for easy querying - if (list[i].sentDate) { - date = util.parseDate(list[i].sentDate); - key = crypto.emailAddress + '_' + type + '_' + date.getTime() + '_' + list[i].id; - } else { - key = crypto.emailAddress + '_' + type + '_' + list[i].id; - } - - items.push({ key:key, object:list[i] }); - } - - jsonDao.batch(items, function() { - callback(); - }); - }; - - /** - * Decrypts the stored items of a given type and returns them - * @param type [String] The type of item e.g. 'email' - * @param offset [Number] The offset of items to fetch (0 is the last stored item) - * @param num [Number] The number of items to fetch (null means fetch all) - */ - this.listItems = function(type, offset, num, callback) { - - // fetch all items of a certain type from the data-store - jsonDao.list(crypto.emailAddress + '_' + type, offset, num, function(encryptedList) { + app.dao.DeviceStorage = function(util, crypto, jsonDao, sqlcipherDao) { + + /** + * Stores a list of encrypted items in the object store + * @param list [Array] The list of items to be persisted + * @param type [String] The type of item to be persisted e.g. 'email' + */ + this.storeEcryptedList = function(list, type, callback) { + var i, date, key, items = []; + + // format items for batch storing in dao + for (i = 0; i < list.length; i++) { + + // put date in key if available... for easy querying + if (list[i].sentDate) { + date = util.parseDate(list[i].sentDate); + key = crypto.emailAddress + '_' + type + '_' + date.getTime() + '_' + list[i].id; + } else { + key = crypto.emailAddress + '_' + type + '_' + list[i].id; + } + + items.push({ + key: key, + object: list[i] + }); + } + + jsonDao.batch(items, function() { + callback(); + }); + }; + + /** + * Decrypts the stored items of a given type and returns them + * @param type [String] The type of item e.g. 'email' + * @param offset [Number] The offset of items to fetch (0 is the last stored item) + * @param num [Number] The number of items to fetch (null means fetch all) + */ + this.listItems = function(type, offset, num, callback) { + + // fetch all items of a certain type from the data-store + jsonDao.list(crypto.emailAddress + '_' + type, offset, num, function(encryptedList) { + + // decrypt list + crypto.aesDecryptListForUser(encryptedList, function(decryptedList) { + callback(decryptedList); + }); + }); + }; + + /** + * Clear the whole device data-store + */ + this.clear = function(callback) { + jsonDao.clear(callback); + }; - // decrypt list - crypto.aesDecryptListForUser(encryptedList, function(decryptedList) { - callback(decryptedList); - }); - }); }; - - /** - * Clear the whole device data-store - */ - this.clear = function(callback) { - jsonDao.clear(callback); - }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 0b803f0..bcc5d77 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -1,110 +1,121 @@ -'use strict'; +(function() { + 'use strict'; -/** - * A high-level Data-Access Api for handling Email synchronization - * between the cloud service and the device's local storage - */ -app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage) { - /** - * Inits all dependencies + * A high-level Data-Access Api for handling Email synchronization + * between the cloud service and the device's local storage */ - this.init = function(account, password, callback) { - this.account = account; - - // sync user's cloud key with local storage - cloudstorage.getUserSecretKey(account.get('emailAddress'), function(err) { - if (err) { - console.log('Could not sync user key from server: ' + JSON.stringify(err)); - } - // init crypto - initCrypto(); - - }, function() { - // replaced local key with cloud key... whipe local storage - devicestorage.clear(function() { + app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage) { + + /** + * Inits all dependencies + */ + this.init = function(account, password, callback) { + this.account = account; + + // sync user's cloud key with local storage + cloudstorage.getUserSecretKey(account.get('emailAddress'), function(err) { + if (err) { + console.log('Could not sync user key from server: ' + JSON.stringify(err)); + } + // init crypto initCrypto(); - }); - }); - - function initCrypto() { - crypto.init(account.get('emailAddress'), password, account.get('symKeySize'), account.get('symIvSize'), function() { - callback(); - }); - } - }; - - /** - * Fetch an email with the following id - */ - this.getItem = function(folderName, itemId) { - var folder = this.account.get('folders').where({name: folderName})[0]; - var mail = _.find(folder.get('items').models, function(email) { - return email.id+'' === itemId+''; - }); - return mail; - }; - - /** - * Fetch a list of emails from the device's local storage - * @param offset [Number] The offset of items to fetch (0 is the last stored item) - * @param num [Number] The number of items to fetch (null means fetch all) - */ - this.listItems = function(folderName, offset, num, callback) { - var collection, folder, self = this; - - // check if items are in memory already (account.folders model) - folder = this.account.get('folders').where({name: folderName})[0]; - - if (!folder) { - // get items from storage - devicestorage.listItems('email_' + folderName ,offset ,num, function(decryptedList) { - // parse to backbone model collection - collection = new app.model.EmailCollection(decryptedList); - // cache collection in folder memory - if (decryptedList.length > 0) { - folder = new app.model.Folder({name: folderName}); - folder.set('items', collection); - self.account.get('folders').add(folder); - } - - callback(collection); + }, function() { + // replaced local key with cloud key... whipe local storage + devicestorage.clear(function() { + initCrypto(); + }); }); - - } else { - // read items from memory - collection = folder.get('items'); - callback(collection); - } - }; - - /** - * Synchronize a folder's items from the cloud to the device-storage - * @param folderName [String] The name of the folder e.g. 'inbox' - */ - this.syncFromCloud = function(folderName, callback) { - var folder, self = this; - - cloudstorage.listEncryptedItems('email', this.account.get('emailAddress'), folderName, function(res) { - // return if an error occured or if fetched list from cloud storage is empty - if (!res || res.status || res.length === 0) { - callback(res); // error - return; + + function initCrypto() { + crypto.init(account.get('emailAddress'), password, account.get('symKeySize'), account.get('symIvSize'), function() { + callback(); + }); } - - // TODO: remove old folder items from devicestorage - - // persist encrypted list in device storage - devicestorage.storeEcryptedList(res, 'email_' + folderName, function() { - // remove cached folder in account model - folder = self.account.get('folders').where({name: folderName})[0]; - if (folder) { - self.account.get('folders').remove(folder); - } - callback(); - }); - }); + }; + + /** + * Fetch an email with the following id + */ + this.getItem = function(folderName, itemId) { + var folder = this.account.get('folders').where({ + name: folderName + })[0]; + var mail = _.find(folder.get('items').models, function(email) { + return email.id + '' === itemId + ''; + }); + return mail; + }; + + /** + * Fetch a list of emails from the device's local storage + * @param offset [Number] The offset of items to fetch (0 is the last stored item) + * @param num [Number] The number of items to fetch (null means fetch all) + */ + this.listItems = function(folderName, offset, num, callback) { + var collection, folder, self = this; + + // check if items are in memory already (account.folders model) + folder = this.account.get('folders').where({ + name: folderName + })[0]; + + if (!folder) { + // get items from storage + devicestorage.listItems('email_' + folderName, offset, num, function(decryptedList) { + // parse to backbone model collection + collection = new app.model.EmailCollection(decryptedList); + + // cache collection in folder memory + if (decryptedList.length > 0) { + folder = new app.model.Folder({ + name: folderName + }); + folder.set('items', collection); + self.account.get('folders').add(folder); + } + + callback(collection); + }); + + } else { + // read items from memory + collection = folder.get('items'); + callback(collection); + } + }; + + /** + * Synchronize a folder's items from the cloud to the device-storage + * @param folderName [String] The name of the folder e.g. 'inbox' + */ + this.syncFromCloud = function(folderName, callback) { + var folder, self = this; + + cloudstorage.listEncryptedItems('email', this.account.get('emailAddress'), folderName, function(res) { + // return if an error occured or if fetched list from cloud storage is empty + if (!res || res.status || res.length === 0) { + callback(res); // error + return; + } + + // TODO: remove old folder items from devicestorage + + // persist encrypted list in device storage + devicestorage.storeEcryptedList(res, 'email_' + folderName, function() { + // remove cached folder in account model + folder = self.account.get('folders').where({ + name: folderName + })[0]; + if (folder) { + self.account.get('folders').remove(folder); + } + callback(); + }); + }); + }; + }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/dao/lawnchair-dao.js b/src/js/dao/lawnchair-dao.js index 23ca52c..e6e3765 100644 --- a/src/js/dao/lawnchair-dao.js +++ b/src/js/dao/lawnchair-dao.js @@ -1,73 +1,74 @@ -'use strict'; +(function() { + 'use strict'; -/** - * Handles generic caching of JSON objects in a lawnchair adapter - */ -app.dao.LawnchairDAO = function(window) { - /** - * Create or update an object + * Handles generic caching of JSON objects in a lawnchair adapter */ - this.persist = function(key, object, callback) { - Lawnchair(function() { - this.save({ key:key, object:object }, callback); - }); - }; - - /** - * Persist a bunch of items at once - */ - this.batch = function(list, callback) { - Lawnchair(function() { - this.batch(list, callback); - }); - }; - - /** - * Read a single item by its key - */ - this.read = function(key, callback) { - Lawnchair(function() { - this.get(key, function(o) { + app.dao.LawnchairDAO = function(window) { + + var db = new Lawnchair({ + name: 'data-store' + }, function() {}); + + /** + * Create or update an object + */ + this.persist = function(key, object, callback) { + db.save({ + key: key, + object: object + }, callback); + }; + + /** + * Persist a bunch of items at once + */ + this.batch = function(list, callback) { + db.batch(list, callback); + }; + + /** + * Read a single item by its key + */ + this.read = function(key, callback) { + db.get(key, function(o) { if (o) { callback(o.object); } else { callback(null); } }); - }); - }; - - /** - * List all the items of a certain type - * @param type [String] The type of item e.g. 'email' - * @param offset [Number] The offset of items to fetch (0 is the last stored item) - * @param num [Number] The number of items to fetch (null means fetch all) - */ - this.list = function(type, offset, num, callback) { - var i, list = [], matchingKeys = [], parts, timeStr, time; - - Lawnchair(function() { - var self = this; - + }; + + /** + * List all the items of a certain type + * @param type [String] The type of item e.g. 'email' + * @param offset [Number] The offset of items to fetch (0 is the last stored item) + * @param num [Number] The number of items to fetch (null means fetch all) + */ + this.list = function(type, offset, num, callback) { + var i, list = [], + matchingKeys = [], + parts, timeStr, time; + // get all keys - this.keys(function(keys) { - + db.keys(function(keys) { + // check if key begins with type for (i = 0; i < keys.length; i++) { if (keys[i].indexOf(type) === 0) { matchingKeys.push(keys[i]); } - } - + } + // sort keys by type and date matchingKeys = _.sortBy(matchingKeys, function(key) { parts = key.split('_'); - timeStr = parts[parts.length-2]; + timeStr = parts[parts.length - 2]; time = parseInt(timeStr, 10); return time; - }); - + }); + // if num is null, list all items num = (num !== null) ? num : matchingKeys.length; @@ -78,44 +79,41 @@ app.dao.LawnchairDAO = function(window) { matchingKeys = matchingKeys.splice(0, matchingKeys.length - offset); } else { matchingKeys = []; - } - + } + // return if there are no matching keys if (matchingKeys.length === 0) { callback(list); return; } - + // fetch all items from data-store with matching key - self.get(matchingKeys, function(matchingList) { + db.get(matchingKeys, function(matchingList) { for (i = 0; i < matchingList.length; i++) { list.push(matchingList[i].object); - } - + } + // return only the interval between offset and num callback(list); - }); - + }); + }); - }); + }; + + /** + * Removes an object liter from local storage by its key (delete) + */ + this.remove = function(key, callback) { + db.remove(key, callback); + }; + + /** + * Clears the whole local storage cache + */ + this.clear = function(callback) { + db.nuke(callback); + }; + }; - - /** - * Removes an object liter from local storage by its key (delete) - */ - this.remove = function(key, callback) { - Lawnchair(function() { - this.remove(key, callback); - }); - }; - - /** - * Clears the whole local storage cache - */ - this.clear = function(callback) { - Lawnchair(function() { - this.nuke(callback); - }); - }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/dao/localstorage-dao.js b/src/js/dao/localstorage-dao.js index 52ffe8b..189b306 100644 --- a/src/js/dao/localstorage-dao.js +++ b/src/js/dao/localstorage-dao.js @@ -1,56 +1,59 @@ -'use strict'; +(function() { + 'use strict'; -/** - * Handles generic caching of JSON objects in LocalStorage - */ -app.dao.LocalStorageDAO = function(window) { - /** - * Stringifies an object literal to JSON and perists it + * Handles generic caching of JSON objects in LocalStorage */ - this.persist = function(key, object) { - var json = JSON.stringify(object); - window.localStorage.setItem(key, json); - }; - - /** - * Fetches a json string from local storage by its key and parses it to an object literal - */ - this.read = function(key) { - var json = window.localStorage.getItem(key); - return JSON.parse(json); - }; - - /** - * List all the items of a certain type in local storage - * @param type [String] The type of item e.g. 'email' - */ - this.list = function(type) { - var i, key, json, list = []; - - for (i = 0; i < window.localStorage.length; i++) { - key = window.localStorage.key(i); - if (key.indexOf(type) === 0) { - json = window.localStorage.getItem(key); - list.push(JSON.parse(json)); + app.dao.LocalStorageDAO = function(window) { + + /** + * Stringifies an object literal to JSON and perists it + */ + this.persist = function(key, object) { + var json = JSON.stringify(object); + window.localStorage.setItem(key, json); + }; + + /** + * Fetches a json string from local storage by its key and parses it to an object literal + */ + this.read = function(key) { + var json = window.localStorage.getItem(key); + return JSON.parse(json); + }; + + /** + * List all the items of a certain type in local storage + * @param type [String] The type of item e.g. 'email' + */ + this.list = function(type) { + var i, key, json, list = []; + + for (i = 0; i < window.localStorage.length; i++) { + key = window.localStorage.key(i); + if (key.indexOf(type) === 0) { + json = window.localStorage.getItem(key); + list.push(JSON.parse(json)); + } } - } - - return list; + + return list; + }; + + /** + * Removes an object liter from local storage by its key (delete) + */ + this.remove = function(key) { + window.localStorage.removeItem(key); + }; + + /** + * Clears the whole local storage cache + */ + this.clear = function() { + window.localStorage.clear(); + }; + }; - - /** - * Removes an object liter from local storage by its key (delete) - */ - this.remove = function(key) { - window.localStorage.removeItem(key); - }; - - /** - * Clears the whole local storage cache - */ - this.clear = function() { - window.localStorage.clear(); - }; - -}; \ No newline at end of file + +}()); \ No newline at end of file diff --git a/src/js/model/account-model.js b/src/js/model/account-model.js index ae55404..a0a639c 100644 --- a/src/js/model/account-model.js +++ b/src/js/model/account-model.js @@ -1,25 +1,27 @@ -'use strict'; +(function() { + 'use strict'; -app.model.Account = Backbone.Model.extend({ - - defaults: { - emailAddress: null, - symKeySize: null, - symIvSize: null, - folders: null - }, + app.model.Account = Backbone.Model.extend({ - initialize: function () { - this.set('folders', new app.model.FolderCollection()); - } + defaults: { + emailAddress: null, + symKeySize: null, + symIvSize: null, + folders: null + }, -}); + initialize: function() { + this.set('folders', new app.model.FolderCollection()); + } -app.model.AccountCollection = Backbone.Collection.extend({ + }); - model: app.model.Account, + app.model.AccountCollection = Backbone.Collection.extend({ - findByName: function (key) { - } + model: app.model.Account, -}); \ No newline at end of file + findByName: function(key) {} + + }); + +}()); \ No newline at end of file diff --git a/src/js/model/email-model.js b/src/js/model/email-model.js index 20f4cd5..80d8de1 100644 --- a/src/js/model/email-model.js +++ b/src/js/model/email-model.js @@ -1,35 +1,37 @@ -'use strict'; +(function() { + 'use strict'; -app.model.Email = Backbone.Model.extend({ - - defaults: { - id: null, - from: null, - to: [], - cc: [], - bcc: [], - subject: null, - body: null, - sentDate: null - }, + app.model.Email = Backbone.Model.extend({ - initialize: function () { - // decode body - try { - var decodedBody = window.atob(this.get('body')); - this.set('body', decodedBody); - } catch (ex) { - console.log(ex); + defaults: { + id: null, + from: null, + to: [], + cc: [], + bcc: [], + subject: null, + body: null, + sentDate: null + }, + + initialize: function() { + // decode body + try { + var decodedBody = window.atob(this.get('body')); + this.set('body', decodedBody); + } catch (ex) { + console.log(ex); + } } - } -}); + }); -app.model.EmailCollection = Backbone.Collection.extend({ + app.model.EmailCollection = Backbone.Collection.extend({ - model: app.model.Email, + model: app.model.Email, - findByName: function (key) { - } + findByName: function(key) {} -}); \ No newline at end of file + }); + +}()); \ No newline at end of file diff --git a/src/js/model/folder-model.js b/src/js/model/folder-model.js index 097adaf..d8c490e 100644 --- a/src/js/model/folder-model.js +++ b/src/js/model/folder-model.js @@ -1,22 +1,23 @@ -'use strict'; +(function() { + 'use strict'; -app.model.Folder = Backbone.Model.extend({ - - defaults: { - name: null, - items: null - }, + app.model.Folder = Backbone.Model.extend({ - initialize: function () { - } + defaults: { + name: null, + items: null + }, -}); + initialize: function() {} -app.model.FolderCollection = Backbone.Collection.extend({ + }); - model: app.model.Folder, + app.model.FolderCollection = Backbone.Collection.extend({ - findByName: function (key) { - } + model: app.model.Folder, -}); \ No newline at end of file + findByName: function(key) {} + + }); + +}()); \ No newline at end of file diff --git a/src/js/view/accounts-view.js b/src/js/view/accounts-view.js index abb1b17..1acd00b 100644 --- a/src/js/view/accounts-view.js +++ b/src/js/view/accounts-view.js @@ -1,13 +1,16 @@ -'use strict'; +(function() { + 'use strict'; -app.view.AccountsView = Backbone.View.extend({ + app.view.AccountsView = Backbone.View.extend({ - initialize:function () { - this.template = _.template(app.util.tpl.get('accounts')); - }, + initialize: function() { + this.template = _.template(app.util.tpl.get('accounts')); + }, - render:function (eventName) { - $(this.el).html(this.template()); - return this; - } -}); \ No newline at end of file + render: function(eventName) { + $(this.el).html(this.template()); + return this; + } + }); + +}()); \ No newline at end of file diff --git a/src/js/view/compose-view.js b/src/js/view/compose-view.js index b22359d..b274bb1 100644 --- a/src/js/view/compose-view.js +++ b/src/js/view/compose-view.js @@ -1,13 +1,16 @@ -'use strict'; +(function() { + 'use strict'; -app.view.ComposeView = Backbone.View.extend({ + app.view.ComposeView = Backbone.View.extend({ - initialize:function () { - this.template = _.template(app.util.tpl.get('compose')); - }, + initialize: function() { + this.template = _.template(app.util.tpl.get('compose')); + }, - render:function (eventName) { - $(this.el).html(this.template()); - return this; - } -}); \ No newline at end of file + render: function(eventName) { + $(this.el).html(this.template()); + return this; + } + }); + +}()); \ No newline at end of file diff --git a/src/js/view/folderlist-view.js b/src/js/view/folderlist-view.js index a82e2ed..93f20b5 100644 --- a/src/js/view/folderlist-view.js +++ b/src/js/view/folderlist-view.js @@ -1,23 +1,26 @@ -'use strict'; +(function() { + 'use strict'; -app.view.FolderListView = Backbone.View.extend({ + app.view.FolderListView = Backbone.View.extend({ - initialize:function () { - this.template = _.template(app.util.tpl.get('folderlist')); - }, + initialize: function() { + this.template = _.template(app.util.tpl.get('folderlist')); + }, - render:function (eventName) { - var page = $(this.el); - - page.html(this.template(this.options)); - - // change page for folder links on vmousedown instead of waiting on vmouseup - page.on('vmousedown', 'li a', function(e) { - e.preventDefault(); - var href = $(e.currentTarget).attr('href'); - window.location = href; - }); + render: function(eventName) { + var page = $(this.el); - return this; - } -}); \ No newline at end of file + page.html(this.template(this.options)); + + // change page for folder links on vmousedown instead of waiting on vmouseup + page.on('vmousedown', 'li a', function(e) { + e.preventDefault(); + var href = $(e.currentTarget).attr('href'); + window.location = href; + }); + + return this; + } + }); + +}()); \ No newline at end of file diff --git a/src/js/view/login-view.js b/src/js/view/login-view.js index 551ce4d..86e0a5c 100644 --- a/src/js/view/login-view.js +++ b/src/js/view/login-view.js @@ -1,41 +1,49 @@ -'use strict'; +(function() { + 'use strict'; -app.view.LoginView = Backbone.View.extend({ + app.view.LoginView = Backbone.View.extend({ - initialize: function(args) { - this.template = _.template(app.util.tpl.get('login')); - this.dao = args.dao; - }, + initialize: function(args) { + this.template = _.template(app.util.tpl.get('login')); + this.dao = args.dao; + }, - render: function(eventName) { - var self = this, page = $(this.el); - - page.html(this.template()); - page.attr('data-theme', 'a'); - - page.find('#loginBtn').on('vmousedown', function() { - self.login(); - }); - - return this; - }, + render: function(eventName) { + var self = this, + page = $(this.el); - login: function() { - var page = $(this.el), - userId = page.find('#userId').val(), - password = page.find('#password').val(); - - var account = new app.model.Account({ - emailAddress: userId, - symKeySize: app.config.symKeySize, - symIvSize: app.config.symIvSize - }); - - // show loading msg during init - $.mobile.loading('show', { text: 'Unlocking...', textVisible: true, theme: 'c' }); - this.dao.init(account, password, function() { - $.mobile.loading('hide'); - window.location = '#accounts/' + account.get('emailAddress') + '/folders'; - }); - } -}); \ No newline at end of file + page.html(this.template()); + page.attr('data-theme', 'a'); + + page.find('#loginBtn').on('vmousedown', function() { + self.login(); + }); + + return this; + }, + + login: function() { + var page = $(this.el), + userId = page.find('#userId').val(), + password = page.find('#password').val(); + + var account = new app.model.Account({ + emailAddress: userId, + symKeySize: app.config.symKeySize, + symIvSize: app.config.symIvSize + }); + + // show loading msg during init + $.mobile.loading('show', { + text: 'Unlocking...', + textVisible: true, + theme: 'c' + }); + this.dao.init(account, password, function() { + $.mobile.loading('hide'); + window.location = '#accounts/' + account.get('emailAddress') + '/folders'; + }); + } + }); + +}()); \ No newline at end of file diff --git a/src/js/view/messagelist-view.js b/src/js/view/messagelist-view.js index d570d76..a5f7d53 100644 --- a/src/js/view/messagelist-view.js +++ b/src/js/view/messagelist-view.js @@ -1,73 +1,86 @@ -'use strict'; +(function() { + 'use strict'; -app.view.MessageListView = Backbone.View.extend({ + app.view.MessageListView = Backbone.View.extend({ - initialize: function(args) { - this.template = _.template(app.util.tpl.get('messagelist')); - this.folder = args.folder; - this.dao = args.dao; - }, + initialize: function(args) { + this.template = _.template(app.util.tpl.get('messagelist')); + this.folder = args.folder; + this.dao = args.dao; + }, - render: function(eventName) { - var self = this, - page = $(this.el); + render: function(eventName) { + var self = this, + page = $(this.el); - page.html(this.template(this.options)); + page.html(this.template(this.options)); - page.find('#refreshBtn').on('vmousedown', function() { - self.syncFolder(); - }); + page.find('#refreshBtn').on('vmousedown', function() { + self.syncFolder(); + }); - return this; - }, + return this; + }, - /** - * Synchronize emails from the cloud - */ - syncFolder: function() { - var self = this; - - $.mobile.loading('show', { text: 'Syncing...', textVisible: true }); - // sync from cloud - this.dao.syncFromCloud(this.folder, function(res) { - $.mobile.loading('hide'); - - // check for error - if (res && res.status) { - alert('Syncing failed!'); - return; - } - - // read local storage and add to list view - self.loadItems(); - }); - }, - - /** - * Load items from local storage - */ - loadItems: function() { - var self = this, - page = $(this.el), - list = page.find('#message-list'), - listItemArgs, i, email; - - $.mobile.loading('show', { text: 'decrypting...', textVisible: true }); - this.dao.listItems(this.folder, 0, 10, function(collection) { - // clear list - list.html(''); - - // append items to list in reverse order so mails with the most recent date will be displayed first - for (i = collection.models.length - 1; i >= 0; i--) { - email = collection.at(i); - listItemArgs = {account: self.options.account, folder: self.folder, model: email}; - list.append(new app.view.MessageListItemView(listItemArgs).render().el); - } - - // refresh list view - list.listview('refresh'); - $.mobile.loading('hide'); - }); - } - -}); \ No newline at end of file + /** + * Synchronize emails from the cloud + */ + syncFolder: function() { + var self = this; + + $.mobile.loading('show', { + text: 'Syncing...', + textVisible: true + }); + // sync from cloud + this.dao.syncFromCloud(this.folder, function(res) { + $.mobile.loading('hide'); + + // check for error + if (res && res.status) { + alert('Syncing failed!'); + return; + } + + // read local storage and add to list view + self.loadItems(); + }); + }, + + /** + * Load items from local storage + */ + loadItems: function() { + var self = this, + page = $(this.el), + list = page.find('#message-list'), + listItemArgs, i, email; + + $.mobile.loading('show', { + text: 'decrypting...', + textVisible: true + }); + this.dao.listItems(this.folder, 0, 10, function(collection) { + // clear list + list.html(''); + + // append items to list in reverse order so mails with the most recent date will be displayed first + for (i = collection.models.length - 1; i >= 0; i--) { + email = collection.at(i); + listItemArgs = { + account: self.options.account, + folder: self.folder, + model: email + }; + list.append(new app.view.MessageListItemView(listItemArgs).render().el); + } + + // refresh list view + list.listview('refresh'); + $.mobile.loading('hide'); + }); + } + + }); + +}()); \ No newline at end of file diff --git a/src/js/view/messagelistitem-view.js b/src/js/view/messagelistitem-view.js index e5b142e..46f744d 100644 --- a/src/js/view/messagelistitem-view.js +++ b/src/js/view/messagelistitem-view.js @@ -1,24 +1,27 @@ -'use strict'; +(function() { + 'use strict'; -app.view.MessageListItemView = Backbone.View.extend({ - - tagName:"li", + app.view.MessageListItemView = Backbone.View.extend({ - initialize:function () { - this.template = _.template(app.util.tpl.get('messagelistitem')); - }, + tagName: "li", - render:function (eventName) { - var params = this.model.toJSON(); - params.account = this.options.account; - params.folder = this.options.folder; - params.id = encodeURIComponent(params.id); - - var util = new app.crypto.Util(window, null); - var date = util.parseDate(params.sentDate); - params.displayDate = date.getDate() + '.' + (date.getMonth() + 1) + '. ' + date.getHours() + ':' + date.getMinutes(); - - $(this.el).html(this.template(params)); - return this; - } -}); \ No newline at end of file + initialize: function() { + this.template = _.template(app.util.tpl.get('messagelistitem')); + }, + + render: function(eventName) { + var params = this.model.toJSON(); + params.account = this.options.account; + params.folder = this.options.folder; + params.id = encodeURIComponent(params.id); + + var util = new app.crypto.Util(window, null); + var date = util.parseDate(params.sentDate); + params.displayDate = date.getDate() + '.' + (date.getMonth() + 1) + '. ' + date.getHours() + ':' + date.getMinutes(); + + $(this.el).html(this.template(params)); + return this; + } + }); + +}()); \ No newline at end of file diff --git a/src/js/view/read-view.js b/src/js/view/read-view.js index b48bbca..9b77989 100644 --- a/src/js/view/read-view.js +++ b/src/js/view/read-view.js @@ -1,32 +1,35 @@ -'use strict'; +(function() { + 'use strict'; -app.view.ReadView = Backbone.View.extend({ + app.view.ReadView = Backbone.View.extend({ - initialize: function(args) { - this.template = _.template(app.util.tpl.get('read')); - this.model = args.dao.getItem(args.folder, args.messageId); - }, + initialize: function(args) { + this.template = _.template(app.util.tpl.get('read')); + this.model = args.dao.getItem(args.folder, args.messageId); + }, - render: function(eventName) { - $(this.el).html(this.template(this.model.toJSON())); - return this; - }, + render: function(eventName) { + $(this.el).html(this.template(this.model.toJSON())); + return this; + }, - renderBody: function() { - var emailBody = this.model.get('body'), - iframe = $('#idMailContent'), - iframeDoc = iframe[0].contentDocument || iframe[0].contentWindow.document; + renderBody: function() { + var emailBody = this.model.get('body'), + iframe = $('#idMailContent'), + iframeDoc = iframe[0].contentDocument || iframe[0].contentWindow.document; - iframe.load(function() { - // resize - var newheight = iframeDoc.body.scrollHeight; - var newwidth = iframeDoc.body.scrollWidth; - iframe[0].height = (newheight) + 'px'; - iframe[0].width = (newwidth) + 'px'; - }); - - iframeDoc.write(emailBody); - iframeDoc.close(); - } - -}); \ No newline at end of file + iframe.load(function() { + // resize + var newheight = iframeDoc.body.scrollHeight; + var newwidth = iframeDoc.body.scrollWidth; + iframe[0].height = (newheight) + 'px'; + iframe[0].width = (newwidth) + 'px'; + }); + + iframeDoc.write(emailBody); + iframeDoc.close(); + } + + }); + +}()); \ No newline at end of file