diff --git a/src/js/crypto/aes-cbc.js b/src/js/crypto/aes-cbc.js index f69e8bb..9904a80 100644 --- a/src/js/crypto/aes-cbc.js +++ b/src/js/crypto/aes-cbc.js @@ -1,59 +1,55 @@ -(function() { +/** + * A Wrapper for Crypto.js's AES-CBC encryption + */ +app.crypto.AesCBC = function() { 'use strict'; + var mode = CryptoJS.mode.CBC; // use CBC mode for Crypto.js + var padding = CryptoJS.pad.Pkcs7; // use Pkcs7/Pkcs5 padding for Crypto.js + /** - * A Wrapper for Crypto.js's AES-CBC encryption + * 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 */ - app.crypto.AesCBC = function() { + 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 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; - }; + var encrypted = CryptoJS.AES.encrypt(plaintextWords, keyWords, { + iv: ivWords, + mode: mode, + padding: padding + }); + var ctBase64 = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); + return ctBase64; }; -}()); \ No newline at end of file + /** + * 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 diff --git a/src/js/crypto/aes-ccm.js b/src/js/crypto/aes-ccm.js index 3943d69..05cdc8f 100644 --- a/src/js/crypto/aes-ccm.js +++ b/src/js/crypto/aes-ccm.js @@ -1,54 +1,50 @@ -(function() { +/** + * A Wrapper for SJCL's authenticated AES-CCM encryption + */ +app.crypto.AesCCM = function() { 'use strict'; + var adata = []; // authenticated data (empty by default) + var tlen = 64; // The tag length in bits + /** - * A Wrapper for SJCL's authenticated AES-CCM encryption + * 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 */ - app.crypto.AesCCM = function() { + 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 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; - }; + 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; }; -}()); \ No newline at end of file + /** + * 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 diff --git a/src/js/crypto/aes-gcm.js b/src/js/crypto/aes-gcm.js index bd12611..8dbcacc 100644 --- a/src/js/crypto/aes-gcm.js +++ b/src/js/crypto/aes-gcm.js @@ -1,54 +1,50 @@ -(function() { +/** + * A Wrapper for SJCL's authenticated AES-GCM encryption + */ +app.crypto.AesGCM = function() { 'use strict'; + var adata = []; // authenticated data (empty by default) + var tlen = 128; // The tag length in bits + /** - * A Wrapper for SJCL's authenticated AES-GCM encryption + * 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 */ - app.crypto.AesGCM = function() { + 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 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; - }; + 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; }; -}()); \ No newline at end of file + /** + * 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 diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index 02b20cf..44fc804 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -1,249 +1,245 @@ -(function() { +/** + * High level crypto api that invokes native crypto (if available) and + * gracefully degrades to JS crypto (if unavailable) + */ +app.crypto.Crypto = function(window, util) { 'use strict'; + var symmetricUserKey, // the user's secret key used to encrypt item-keys + aes = new app.crypto.AesCCM(); // use authenticated AES-CCM mode by default + /** - * High level crypto api that invokes native crypto (if available) and - * gracefully degrades to JS crypto (if unavailable) + * Initializes the crypto modules by fetching the user's + * encrypted secret key from storage and storing it in memory. */ - app.crypto.Crypto = function(window, util) { + this.init = function(emailAddress, password, keySize, ivSize, callback) { + this.emailAddress = emailAddress; + this.keySize = keySize; + this.ivSize = ivSize; - var symmetricUserKey, // the user's secret key used to encrypt item-keys - aes = new app.crypto.AesCCM(); // use authenticated AES-CCM mode by default + // derive PBKDF2 from password in web worker thread + this.deriveKey(password, keySize, function(pbkdf2) { - /** - * 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; + // 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); - // 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 - }); - - } 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, + // 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 { - 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); + // decrypt key + symmetricUserKey = aes.decrypt(encryptedKey.key, pbkdf2, encryptedKey.iv); } - // 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++) { - // 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); - } - - callback(list); - }); - }; - + callback(); + }); }; -}()); \ No newline at end of file + /** + * 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) { + + // 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++) { + // 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); + } + + callback(list); + }); + }; + +}; \ No newline at end of file diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index 3f4c1d0..90764c6 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -1,28 +1,24 @@ -(function() { +/** + * A Wrapper for Crypto.js's PBKDF2 function + */ +app.crypto.PBKDF2 = function() { 'use strict'; /** - * A Wrapper for Crypto.js's 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 */ - 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; - }; + 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 9809733..89afa28 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -1,342 +1,338 @@ -(function() { +/** + * A wrapper for asymmetric OpenPGP encryption logic + */ +app.crypto.PGP = function(window, openpgp, util, server) { 'use strict'; + 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 + // + /** - * A wrapper for asymmetric OpenPGP encryption logic + * Check if user already has a public key on the server and if not, + * generate a new keypait for the user */ - app.crypto.PGP = function(window, openpgp, util, server) { + 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); - var self = this, - privateKey, // user's private key - publicKey, // user's public key - passphrase; // user's passphrase used for decryption + } 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(); - 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); + // display finish + finishCallback(keyId); // 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(); + /** + * 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!'; + } - // display finish - finishCallback(keyId); - // read the user's keys from local storage - callback(keyId); - }); - } + 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 }; - /** - * 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 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); } + }); - var userId = 'SafeWith.me User '; - var keys = openpgp.generate_key_pair(1, numBits, userId, passphrase); // keytype 1=RSA + // then upload private key - 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 + function uploadPrivateKeys() { server.xhr({ type: 'POST', - uri: '/ws/publicKeys', + uri: '/ws/privateKeys', contentType: 'application/json', expected: 201, - body: jsonPublicKey, + body: jsonPrivateKey, 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!'); + // read the user's keys from local storage callback(keyId); } }); + } + }; - // then upload private key + /** + * 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); - 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 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); } - }; + }); - /** - * 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 private key - // get public key + function getPrivateKey(pubKey) { server.xhr({ type: 'GET', - uri: '/ws/publicKeys?keyId=' + encodedKeyId, + uri: '/ws/privateKeys?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); + success: function(privKey) { + // import keys + self.importKeys(pubKey.asciiArmored, privKey.asciiArmored, email); + callback({ + privateKey: privKey, + publicKey: pubKey + }); } }); + } + }; - // get private key + /** + * Get the current user's private key + */ + self.getPrivateKey = function() { + if (!privateKey) { + return undefined; + } + return privateKey.armored; + }; - 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 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; } - }; - - /** - * 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) { + 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].privateKeyPacket + keymaterial: priv_key[0].subKeys[j] }; 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!"; + } + 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 diff --git a/src/js/crypto/util.js b/src/js/crypto/util.js index 3fd4a0b..7791ac2 100644 --- a/src/js/crypto/util.js +++ b/src/js/crypto/util.js @@ -1,168 +1,167 @@ -(function() { +/** + * Various utitity methods for crypto, encoding & decoding + */ +app.crypto.Util = function(window, uuid) { '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 = []; - - 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; - }; - + /** + * Generates a new RFC 4122 version 4 compliant random UUID + */ + this.UUID = function() { + return uuid.v4(); }; -}()); \ No newline at end of file + /** + * 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; + }; + +}; \ No newline at end of file diff --git a/src/js/dao/cloudstorage-dao.js b/src/js/dao/cloudstorage-dao.js index e2dca44..3394e45 100644 --- a/src/js/dao/cloudstorage-dao.js +++ b/src/js/dao/cloudstorage-dao.js @@ -1,131 +1,127 @@ -(function() { +/** + * High level storage api for handling syncing of data to + * and from the cloud. + */ +app.dao.CloudStorage = function(window, $) { 'use strict'; /** - * High level storage api for handling syncing of data to - * and from the cloud. + * 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) */ - app.dao.CloudStorage = function(window, $) { + this.listEncryptedItems = function(type, emailAddress, folderName, callback) { + var folder, uri, self = this; - /** - * 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 + }); + } + }); + }; - // 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 }; - /** - * 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 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 + }); + } + }); + }; - var payload = { - userId: emailAddress, - encryptedKey: encryptedKey.key, - keyIV: encryptedKey.iv - }; + /** + * 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: 'PUT', - data: JSON.stringify(payload), - contentType: 'application/json', - success: function() { - callback(); - }, - error: function(xhr, textStatus, err) { - callback({ - error: err, - status: textStatus + 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(); - /** - * 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 + } 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 (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(); + if (confirm('Swap cloud key?')) { + // upload local key to cloud + self.persistUserSecretKey(emailAddress, callback); + } else { + callback({ + error: 'err', + status: 'Key not synced!' + }); + } } - }, - error: function(xhr, textStatus, err) { - callback({ - error: err, - status: textStatus - }); - } - }); - }; + } 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 cd36f3f..4be1ffa 100644 --- a/src/js/dao/devicestorage.js +++ b/src/js/dao/devicestorage.js @@ -1,69 +1,65 @@ -(function() { +/** + * 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) { '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. + * 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' */ - app.dao.DeviceStorage = function(util, crypto, jsonDao, sqlcipherDao) { + this.storeEcryptedList = function(list, type, callback) { + var i, date, key, items = []; - /** - * 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++) { - // 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] - }); + // 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; } - jsonDao.batch(items, function() { - callback(); + items.push({ + key: key, + object: list[i] }); - }; - - /** - * 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); - }; + } + jsonDao.batch(items, function() { + callback(); + }); }; -}()); \ No newline at end of file + /** + * 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); + }; + +}; \ No newline at end of file diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index bcc5d77..a26b0f8 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -1,121 +1,117 @@ -(function() { +/** + * 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) { 'use strict'; /** - * A high-level Data-Access Api for handling Email synchronization - * between the cloud service and the device's local storage + * Inits all dependencies */ - app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage) { + this.init = function(account, password, callback) { + this.account = account; - /** - * 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(); - // 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 + }, function() { + // replaced local key with cloud key... whipe local storage + devicestorage.clear(function() { initCrypto(); - - }, function() { - // replaced local key with cloud key... whipe local storage - devicestorage.clear(function() { - 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 + ''; + function initCrypto() { + crypto.init(account.get('emailAddress'), password, account.get('symKeySize'), account.get('symIvSize'), function() { + callback(); }); - 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 + /** + * 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 diff --git a/src/js/dao/lawnchair-dao.js b/src/js/dao/lawnchair-dao.js index e6e3765..16d6a21 100644 --- a/src/js/dao/lawnchair-dao.js +++ b/src/js/dao/lawnchair-dao.js @@ -1,119 +1,115 @@ -(function() { +/** + * Handles generic caching of JSON objects in a lawnchair adapter + */ +app.dao.LawnchairDAO = function(window) { 'use strict'; + var db = new Lawnchair({ + name: 'data-store' + }, function() {}); + /** - * Handles generic caching of JSON objects in a lawnchair adapter + * Create or update an object */ - 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; - - // get all 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]; - time = parseInt(timeStr, 10); - return time; - }); - - // if num is null, list all items - num = (num !== null) ? num : matchingKeys.length; - - // set window of items to fetch - if (offset + num < matchingKeys.length) { - matchingKeys = matchingKeys.splice(matchingKeys.length - offset - num, num); - } else if (offset + num >= matchingKeys.length && offset < matchingKeys.length) { - 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 - 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); - }; - + this.persist = function(key, object, callback) { + db.save({ + key: key, + object: object + }, callback); }; -}()); \ No newline at end of file + /** + * 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; + + // get all 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]; + time = parseInt(timeStr, 10); + return time; + }); + + // if num is null, list all items + num = (num !== null) ? num : matchingKeys.length; + + // set window of items to fetch + if (offset + num < matchingKeys.length) { + matchingKeys = matchingKeys.splice(matchingKeys.length - offset - num, num); + } else if (offset + num >= matchingKeys.length && offset < matchingKeys.length) { + 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 + 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); + }; + +}; \ No newline at end of file diff --git a/src/js/dao/localstorage-dao.js b/src/js/dao/localstorage-dao.js index 189b306..d923d91 100644 --- a/src/js/dao/localstorage-dao.js +++ b/src/js/dao/localstorage-dao.js @@ -1,59 +1,55 @@ -(function() { +/** + * Handles generic caching of JSON objects in LocalStorage + */ +app.dao.LocalStorageDAO = function(window) { 'use strict'; /** - * Handles generic caching of JSON objects in LocalStorage + * Stringifies an object literal to JSON and perists it */ - 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; - }; - - /** - * 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(); - }; - + this.persist = function(key, object) { + var json = JSON.stringify(object); + window.localStorage.setItem(key, json); }; -}()); \ No newline at end of file + /** + * 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; + }; + + /** + * 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