1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-22 08:52:15 -05:00

refactored all files to use functional strict scope

This commit is contained in:
Tankred Hase 2013-04-02 00:12:15 +02:00
parent 5ee864fe01
commit 5c0e04cc31
26 changed files with 1669 additions and 1505 deletions

View File

@ -1,4 +1,4 @@
var app; var app; // container for the application namespace
(function() { (function() {
'use strict'; 'use strict';

View File

@ -1,43 +1,42 @@
'use strict'; (function() {
'use strict';
// import web worker dependencies // import web worker dependencies
importScripts('../../lib/sjcl/sjcl.js'); importScripts('../../lib/sjcl/sjcl.js');
importScripts('../../lib/sjcl/bitArray.js'); importScripts('../../lib/sjcl/bitArray.js');
importScripts('../../lib/sjcl/codecBase64.js'); importScripts('../../lib/sjcl/codecBase64.js');
importScripts('../../lib/sjcl/codecString.js'); importScripts('../../lib/sjcl/codecString.js');
importScripts('../../lib/sjcl/aes.js'); importScripts('../../lib/sjcl/aes.js');
importScripts('../../lib/sjcl/ccm.js'); importScripts('../../lib/sjcl/ccm.js');
importScripts('../app-config.js'); importScripts('../app-config.js');
importScripts('./aes-ccm.js'); importScripts('./aes-ccm.js');
importScripts('./util.js'); importScripts('./util.js');
var AESBATCHWORKER = (function () {
/** /**
* In the web worker thread context, 'this' and 'self' can be used as a global * 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 * variable namespace similar to the 'window' object in the main thread
*/ */
self.addEventListener('message', function(e) { self.addEventListener('message', function(e) {
var args = e.data, var args = e.data,
output = null, output = null,
aes = new app.crypto.AesCCM(), aes = new app.crypto.AesCCM(),
util = new app.crypto.Util(null, null); util = new app.crypto.Util(null, null);
if (args.type === 'encrypt' && args.list) { if (args.type === 'encrypt' && args.list) {
// start encryption // start encryption
output = util.encryptList(aes, args.list); output = util.encryptList(aes, args.list);
} else if (args.type === 'decrypt' && args.list) { } else if (args.type === 'decrypt' && args.list) {
// start decryption // start decryption
output = util.decryptList(aes, args.list); output = util.decryptList(aes, args.list);
} else { } else {
throw 'Not all arguments for web worker crypto are defined!'; throw 'Not all arguments for web worker crypto are defined!';
} }
// pass output back to main thread // pass output back to main thread
self.postMessage(output); self.postMessage(output);
}, false); }, false);
}()); }());

View File

@ -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) * A Wrapper for Crypto.js's AES-CBC encryption
* @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) { app.crypto.AesCBC = function() {
// parse base64 input to crypto.js WordArrays
var keyWords = CryptoJS.enc.Base64.parse(key); var mode = CryptoJS.mode.CBC; // use CBC mode for Crypto.js
var ivWords = CryptoJS.enc.Base64.parse(iv); var padding = CryptoJS.pad.Pkcs7; // use Pkcs7/Pkcs5 padding for Crypto.js
var plaintextWords = CryptoJS.enc.Utf8.parse(plaintext);
/**
var encrypted = CryptoJS.AES.encrypt(plaintextWords, keyWords, { iv: ivWords, mode: mode, padding: padding }); * Encrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256)
var ctBase64 = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); * @param plaintext [String] The input string in UTF8
* @param key [String] The base64 encoded key
return ctBase64; * @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;
};
};

View File

@ -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) * A Wrapper for SJCL's authenticated AES-CCM encryption
* @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) { app.crypto.AesCCM = function() {
// convert parameters to WordArrays
var keyWords = sjcl.codec.base64.toBits(key); var adata = []; // authenticated data (empty by default)
var ivWords = sjcl.codec.base64.toBits(iv); var tlen = 64; // The tag length in bits
var plaintextWords = sjcl.codec.utf8String.toBits(plaintext);
/**
var blockCipher = new sjcl.cipher.aes(keyWords); * Encrypt a String using AES-CCM using the provided keysize (e.g. 128, 256)
var ciphertext = sjcl.mode.ccm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen); * @param plaintext [String] The input string in UTF8
var ctBase64 = sjcl.codec.base64.fromBits(ciphertext); * @param key [String] The base64 encoded key
* @param iv [String] The base64 encoded IV
return ctBase64; * @return [String] The base64 encoded ciphertext
}; */
this.encrypt = function(plaintext, key, iv) {
/** // convert parameters to WordArrays
* Decrypt a String using AES-CCM using the provided keysize (e.g. 128, 256) var keyWords = sjcl.codec.base64.toBits(key);
* @param ciphertext [String] The base64 encoded ciphertext var ivWords = sjcl.codec.base64.toBits(iv);
* @param key [String] The base64 encoded key var plaintextWords = sjcl.codec.utf8String.toBits(plaintext);
* @param iv [String] The base64 encoded IV
* @return [String] The decrypted plaintext in UTF8 var blockCipher = new sjcl.cipher.aes(keyWords);
*/ var ciphertext = sjcl.mode.ccm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen);
this.decrypt = function(ciphertext, key, iv) { var ctBase64 = sjcl.codec.base64.fromBits(ciphertext);
// convert parameters to WordArrays
var keyWords = sjcl.codec.base64.toBits(key); return ctBase64;
var ivWords = sjcl.codec.base64.toBits(iv); };
var ciphertextWords = sjcl.codec.base64.toBits(ciphertext);
/**
var blockCipher = new sjcl.cipher.aes(keyWords); * Decrypt a String using AES-CCM using the provided keysize (e.g. 128, 256)
var decrypted = sjcl.mode.ccm.decrypt(blockCipher, ciphertextWords, ivWords, adata, tlen); * @param ciphertext [String] The base64 encoded ciphertext
var pt = sjcl.codec.utf8String.fromBits(decrypted); * @param key [String] The base64 encoded key
* @param iv [String] The base64 encoded IV
return pt; * @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;
};
}; };
}; }());

View File

@ -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) * A Wrapper for SJCL's authenticated AES-GCM encryption
* @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) { app.crypto.AesGCM = function() {
// convert parameters to WordArrays
var keyWords = sjcl.codec.base64.toBits(key); var adata = []; // authenticated data (empty by default)
var ivWords = sjcl.codec.base64.toBits(iv); var tlen = 128; // The tag length in bits
var plaintextWords = sjcl.codec.utf8String.toBits(plaintext);
/**
var blockCipher = new sjcl.cipher.aes(keyWords); * Encrypt a String using AES-GCM using the provided keysize (e.g. 128, 256)
var ciphertext = sjcl.mode.gcm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen); * @param plaintext [String] The input string in UTF8
var ctBase64 = sjcl.codec.base64.fromBits(ciphertext); * @param key [String] The base64 encoded key
* @param iv [String] The base64 encoded IV
return ctBase64; * @return [String] The base64 encoded ciphertext
}; */
this.encrypt = function(plaintext, key, iv) {
/** // convert parameters to WordArrays
* Decrypt a String using AES-GCM using the provided keysize (e.g. 128, 256) var keyWords = sjcl.codec.base64.toBits(key);
* @param ciphertext [String] The base64 encoded ciphertext var ivWords = sjcl.codec.base64.toBits(iv);
* @param key [String] The base64 encoded key var plaintextWords = sjcl.codec.utf8String.toBits(plaintext);
* @param iv [String] The base64 encoded IV
* @return [String] The decrypted plaintext in UTF8 var blockCipher = new sjcl.cipher.aes(keyWords);
*/ var ciphertext = sjcl.mode.gcm.encrypt(blockCipher, plaintextWords, ivWords, adata, tlen);
this.decrypt = function(ciphertext, key, iv) { var ctBase64 = sjcl.codec.base64.fromBits(ciphertext);
// convert parameters to WordArrays
var keyWords = sjcl.codec.base64.toBits(key); return ctBase64;
var ivWords = sjcl.codec.base64.toBits(iv); };
var ciphertextWords = sjcl.codec.base64.toBits(ciphertext);
/**
var blockCipher = new sjcl.cipher.aes(keyWords); * Decrypt a String using AES-GCM using the provided keysize (e.g. 128, 256)
var decrypted = sjcl.mode.gcm.decrypt(blockCipher, ciphertextWords, ivWords, adata, tlen); * @param ciphertext [String] The base64 encoded ciphertext
var pt = sjcl.codec.utf8String.fromBits(decrypted); * @param key [String] The base64 encoded key
* @param iv [String] The base64 encoded IV
return pt; * @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;
};
}; };
}; }());

View File

@ -1,41 +1,40 @@
'use strict'; (function() {
'use strict';
// import web worker dependencies // import web worker dependencies
importScripts('../../lib/sjcl/sjcl.js'); importScripts('../../lib/sjcl/sjcl.js');
importScripts('../../lib/sjcl/bitArray.js'); importScripts('../../lib/sjcl/bitArray.js');
importScripts('../../lib/sjcl/codecBase64.js'); importScripts('../../lib/sjcl/codecBase64.js');
importScripts('../../lib/sjcl/codecString.js'); importScripts('../../lib/sjcl/codecString.js');
importScripts('../../lib/sjcl/aes.js'); importScripts('../../lib/sjcl/aes.js');
importScripts('../../lib/sjcl/ccm.js'); importScripts('../../lib/sjcl/ccm.js');
importScripts('../app-config.js'); importScripts('../app-config.js');
importScripts('./aes-ccm.js'); importScripts('./aes-ccm.js');
var AESWORKER = (function () {
/** /**
* In the web worker thread context, 'this' and 'self' can be used as a global * 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 * variable namespace similar to the 'window' object in the main thread
*/ */
self.addEventListener('message', function(e) { self.addEventListener('message', function(e) {
var args = e.data, var args = e.data,
output = null, output = null,
aes = new app.crypto.AesCCM(); aes = new app.crypto.AesCCM();
if (args.type === 'encrypt' && args.plaintext && args.key && args.iv) { if (args.type === 'encrypt' && args.plaintext && args.key && args.iv) {
// start encryption // start encryption
output = aes.encrypt(args.plaintext, args.key, args.iv); output = aes.encrypt(args.plaintext, args.key, args.iv);
} else if (args.type === 'decrypt' && args.ciphertext && args.key && args.iv) { } else if (args.type === 'decrypt' && args.ciphertext && args.key && args.iv) {
// start decryption // start decryption
output = aes.decrypt(args.ciphertext, args.key, args.iv); output = aes.decrypt(args.ciphertext, args.key, args.iv);
} else { } else {
throw 'Not all arguments for web worker crypto are defined!'; throw 'Not all arguments for web worker crypto are defined!';
} }
// pass output back to main thread // pass output back to main thread
self.postMessage(output); self.postMessage(output);
}, false); }, false);
}()); }());

View File

@ -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 * High level crypto api that invokes native crypto (if available) and
* encrypted secret key from storage and storing it in memory. * gracefully degrades to JS crypto (if unavailable)
*/ */
this.init = function(emailAddress, password, keySize, ivSize, callback) { app.crypto.Crypto = function(window, util) {
this.emailAddress = emailAddress;
this.keySize = keySize; var symmetricUserKey, // the user's secret key used to encrypt item-keys
this.ivSize = ivSize; 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.
// fetch user's encrypted secret key from keychain/storage */
var keyStore = new app.dao.LocalStorageDAO(window); this.init = function(emailAddress, password, keySize, ivSize, callback) {
var storageId = emailAddress + '_encryptedSymmetricKey'; this.emailAddress = emailAddress;
var encryptedKey = keyStore.read(storageId); 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 { } else {
// decrypt key // no WebWorker support... do synchronous call
symmetricUserKey = aes.decrypt(encryptedKey.key, pbkdf2, encryptedKey.iv); var pbkdf2 = new app.crypto.PBKDF2();
} var key = pbkdf2.getKey(password, keySize);
callback(key);
}
};
callback(); //
}); // En/Decrypts single item
}; //
/**
* 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 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++) { for (i = 0; i < encryptedList.length; i++) {
// process new values // decrypt item key
encryptedList[i].itemIV = encryptedList[i].iv; encryptedList[i].key = this.aesDecryptForUserSync(encryptedList[i].encryptedKey, encryptedList[i].keyIV);
encryptedList[i].keyIV = util.random(self.ivSize); encryptedList[i].iv = encryptedList[i].itemIV;
encryptedList[i].encryptedKey = self.aesEncryptForUserSync(encryptedList[i].key, encryptedList[i].keyIV); // delete old values
// delete old ones delete encryptedList[i].keyIV;
delete encryptedList[i].iv; delete encryptedList[i].itemIV;
delete encryptedList[i].key; delete encryptedList[i].encryptedKey;
}
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); // 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);
});
};
}; };
}; }());

View File

@ -1,36 +1,35 @@
'use strict'; (function() {
'use strict';
// import web worker dependencies // import web worker dependencies
importScripts('../../lib/crypto-js/core.js'); importScripts('../../lib/crypto-js/core.js');
importScripts('../../lib/crypto-js/enc-base64.js'); importScripts('../../lib/crypto-js/enc-base64.js');
importScripts('../../lib/crypto-js/sha1.js'); importScripts('../../lib/crypto-js/sha1.js');
importScripts('../../lib/crypto-js/hmac.js'); importScripts('../../lib/crypto-js/hmac.js');
importScripts('../../lib/crypto-js/pbkdf2.js'); importScripts('../../lib/crypto-js/pbkdf2.js');
importScripts('../app-config.js'); importScripts('../app-config.js');
importScripts('./pbkdf2.js'); importScripts('./pbkdf2.js');
var PBKDF2WORKER = (function () {
/** /**
* In the web worker thread context, 'this' and 'self' can be used as a global * 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 * variable namespace similar to the 'window' object in the main thread
*/ */
self.addEventListener('message', function(e) { self.addEventListener('message', function(e) {
var args = e.data, var args = e.data,
key = null; key = null;
if (e.data.password && e.data.keySize) { if (e.data.password && e.data.keySize) {
// start deriving key // start deriving key
var pbkdf2 = new app.crypto.PBKDF2(); var pbkdf2 = new app.crypto.PBKDF2();
key = pbkdf2.getKey(e.data.password, e.data.keySize); key = pbkdf2.getKey(e.data.password, e.data.keySize);
} else { } else {
throw 'Not all arguments for web worker crypto are defined!'; throw 'Not all arguments for web worker crypto are defined!';
} }
// pass output back to main thread // pass output back to main thread
self.postMessage(key); self.postMessage(key);
}, false); }, false);
}()); }());

View File

@ -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 * A Wrapper for Crypto.js's PBKDF2 function
* @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) { app.crypto.PBKDF2 = function() {
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); * PBKDF2-HMAC-SHA1 key derivation with a constant salt and 1000 iterations
* @param password [String] The password in UTF8
return keyBase64; * @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;
};
}; };
}; }());

View File

@ -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, * A wrapper for asymmetric OpenPGP encryption logic
* generate a new keypait for the user
*/ */
self.initKeyPair = function(loginInfo, callback, displayCallback, finishCallback) { app.crypto.PGP = function(window, openpgp, util, server) {
// check if user already has a keypair in local storage
if (loginInfo.publicKeyId) { var self = this,
// decode base 64 key ID privateKey, // user's private key
var keyId = window.atob(loginInfo.publicKeyId); publicKey, // user's public key
// read the user's keys from local storage passphrase; // user's passphrase used for decryption
callback(keyId);
openpgp.init(); // initialize OpenPGP.js
} else {
// user has no key pair yet //
displayCallback(function() { // Key management
// generate new key pair with 2048 bit RSA keys //
var keys = self.generateKeys(2048);
var keyId = keys.privateKey.getKeyId(); /**
* Check if user already has a public key on the server and if not,
// display finish * generate a new keypait for the user
finishCallback(keyId); */
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 // read the user's keys from local storage
callback(keyId); callback(keyId);
});
}
};
/** } else {
* Generate a key pair for the user // user has no key pair yet
* @param numBits [int] number of bits for the key creation. (should be 1024+, generally) displayCallback(function() {
* @email [string] user's email address // generate new key pair with 2048 bit RSA keys
* @pass [string] a passphrase used to protect the private key var keys = self.generateKeys(2048);
*/ var keyId = keys.privateKey.getKeyId();
self.generateKeys = function(numBits) {
// check passphrase
if (!passphrase && passphrase !== '') { throw 'No passphrase set!'; }
var userId = 'SafeWith.me User <anonymous@dunno.com>';
var keys = openpgp.generate_key_pair(1, numBits, userId, passphrase); // keytype 1=RSA
self.importKeys(keys.publicKeyArmored, keys.privateKeyArmored); // display finish
finishCallback(keyId);
return keys; // read the user's keys from local storage
}; callback(keyId);
});
/**
* 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);
} }
}); };
// 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 <anonymous@dunno.com>';
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({ server.xhr({
type: 'POST', type: 'POST',
uri: '/ws/privateKeys', uri: '/ws/publicKeys',
contentType: 'application/json', contentType: 'application/json',
expected: 201, expected: 201,
body: jsonPrivateKey, body: jsonPublicKey,
success: function(resp) { 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); callback(keyId);
} }
}); });
}
}; // then upload private key
/** function uploadPrivateKeys() {
* Get the keypair from the server and import them into localstorage server.xhr({
*/ type: 'POST',
self.fetchKeys = function(email, keyId, callback, errCallback) { uri: '/ws/privateKeys',
var base64Key = window.btoa(keyId); contentType: 'application/json',
var encodedKeyId = encodeURIComponent(base64Key); expected: 201,
body: jsonPrivateKey,
// get public key success: function(resp) {
server.xhr({ // read the user's keys from local storage
type: 'GET', callback(keyId);
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 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({ server.xhr({
type: 'GET', type: 'GET',
uri: '/ws/privateKeys?keyId=' + encodedKeyId, uri: '/ws/publicKeys?keyId=' + encodedKeyId,
expected: 200, expected: 200,
success: function(privKey) { success: function(pubKey) {
// import keys getPrivateKey(pubKey);
self.importKeys(pubKey.asciiArmored, privKey.asciiArmored, email); },
callback({ privateKey:privKey, publicKey: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 private key
/** function getPrivateKey(pubKey) {
* Get the current user's private key server.xhr({
*/ type: 'GET',
self.getPrivateKey = function() { uri: '/ws/privateKeys?keyId=' + encodedKeyId,
if (!privateKey) { return undefined; } expected: 200,
return privateKey.armored; 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 * This function needs to be implemented, since it is used by the openpgp utils
*/ */
function showMessages(str) {} function showMessages(str) {}

View File

@ -1,153 +1,168 @@
'use strict'; (function() {
'use strict';
app.crypto.Util = function(window, uuid) { 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 * Generates a new RFC 4122 version 4 compliant random UUID
json = aes.decrypt(list[i].ciphertext, list[i].key, list[i].iv); */
pt = JSON.parse(json); this.UUID = function() {
outList.push({ id:list[i].id, plaintext:pt, key:list[i].key, iv:list[i].iv }); return uuid.v4();
} };
return outList; /**
}; * 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
* Parse a date string with the following format "1900-01-31 18:17:53" */
*/ this.random = function(keySize) {
this.parseDate = function(str) { var keyBase64, keyBuf;
var parts = str.match(/(\d+)/g);
return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]); if (window.crypto && window.crypto.getRandomValues) {
}; keyBuf = new Uint8Array(keySize / 8);
window.crypto.getRandomValues(keyBuf);
/** keyBase64 = window.btoa(this.uint8Arr2BinStr(keyBuf));
* Converts a binary String (e.g. from the FileReader Api) to an ArrayBuffer } else {
* @param str [String] a binary string with integer values (0..255) per character sjcl.random.addEntropy((new Date()).valueOf(), 2, "calltime");
* @return [ArrayBuffer] keyBuf = sjcl.random.randomWords(keySize / 32, 0);
*/ keyBase64 = sjcl.codec.base64.fromBits(keyBuf);
this.binStr2ArrBuf = function(str) { }
var b = new ArrayBuffer(str.length);
var buf = new Uint8Array(b); return keyBase64;
};
for(var i = 0; i < b.byteLength; i++){
buf[i] = str.charCodeAt(i); /**
} * Encrypt a list of items
* @param aes [Object] The object implementing the aes mode
return b; * @list list [Array] The list of items to encrypt
}; */
this.encryptList = function(aes, list) {
/** var i, json, ct, outList = [];
* Creates a Blob from an ArrayBuffer using the BlobBuilder Api
* @param str [String] a binary string with integer values (0..255) per character for (i = 0; i < list.length; i++) {
* @return [ArrayBuffer] either a data url or a filesystem url // stringify to JSON before encryption
*/ json = JSON.stringify(list[i].plaintext);
this.arrBuf2Blob = function(buf, mimeType) { ct = aes.encrypt(json, list[i].key, list[i].iv);
var b = new Uint8Array(buf); outList.push({
var blob = new Blob([b], {type: mimeType}); id: list[i].id,
ciphertext: ct,
return blob; key: list[i].key,
}; iv: list[i].iv
});
/** }
* Creates a binary String from a Blob using the FileReader Api
* @param blob [Blob/File] a blob containing the the binary data return outList;
* @return [String] a binary string with integer values (0..255) per character };
*/
this.blob2BinStr = function(blob, callback) { /**
var reader = new FileReader(); * Decrypt a list of items
* @param aes [Object] The object implementing the aes mode
reader.onload = function(event) { * @list list [Array] The list of items to decrypt
callback(event.target.result); */
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;
};
};

View File

@ -1,110 +1,131 @@
'use strict'; (function() {
'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});
}
});
};
/** /**
* 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) { app.dao.CloudStorage = function(window, $) {
// 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({ * Lists the encrypted items
url: uri, * @param type [String] The type of item e.g. 'email'
type: 'GET', * @param offset [Number] The offset of items to fetch (0 is the last stored item)
dataType: 'json', * @param num [Number] The number of items to fetch (null means fetch all)
success: function(fetchedKey) { */
if ((!storedKey || !storedKey.key) && fetchedKey && fetchedKey.encryptedKey && fetchedKey.keyIV) { this.listEncryptedItems = function(type, emailAddress, folderName, callback) {
// no local key... persist fetched key var folder, uri, self = this;
keyStore.persist(storageId, { key: fetchedKey.encryptedKey, iv: fetchedKey.keyIV });
replaceCallback(); // fetch encrypted json objects from cloud service
uri = app.config.cloudUrl + '/' + type + '/user/' + emailAddress + '/folder/' + folderName;
} else if (storedKey && fetchedKey && (storedKey.key !== fetchedKey.encryptedKey || storedKey.iv !== fetchedKey.keyIV)){ $.ajax({
// local and fetched keys are not equal url: uri,
if (confirm('Swap local key?')) { type: 'GET',
// replace local key with fetched key dataType: 'json',
keyStore.persist(storageId, { key: fetchedKey.encryptedKey, iv: fetchedKey.keyIV }); success: function(list) {
replaceCallback(); callback(list);
} else { },
if (confirm('Swap cloud key?')) { error: function(xhr, textStatus, err) {
// upload local key to cloud callback({
self.persistUserSecretKey(emailAddress, callback); error: err,
} else { status: textStatus
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});
} /**
}); * 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
});
}
});
};
}; };
}; }());

View File

@ -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 * High level storage api that handles all persistence on the device. If
* @param list [Array] The list of items to be persisted * SQLcipher/SQLite is available, all data is securely persisted there,
* @param type [String] The type of item to be persisted e.g. 'email' * 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) { app.dao.DeviceStorage = function(util, crypto, jsonDao, sqlcipherDao) {
var i, date, key, items = [];
/**
// format items for batch storing in dao * Stores a list of encrypted items in the object store
for (i = 0; i < list.length; i++) { * @param list [Array] The list of items to be persisted
* @param type [String] The type of item to be persisted e.g. 'email'
// put date in key if available... for easy querying */
if (list[i].sentDate) { this.storeEcryptedList = function(list, type, callback) {
date = util.parseDate(list[i].sentDate); var i, date, key, items = [];
key = crypto.emailAddress + '_' + type + '_' + date.getTime() + '_' + list[i].id;
} else { // format items for batch storing in dao
key = crypto.emailAddress + '_' + type + '_' + list[i].id; for (i = 0; i < list.length; i++) {
}
// put date in key if available... for easy querying
items.push({ key:key, object:list[i] }); if (list[i].sentDate) {
} date = util.parseDate(list[i].sentDate);
key = crypto.emailAddress + '_' + type + '_' + date.getTime() + '_' + list[i].id;
jsonDao.batch(items, function() { } else {
callback(); key = crypto.emailAddress + '_' + type + '_' + list[i].id;
}); }
};
items.push({
/** key: key,
* Decrypts the stored items of a given type and returns them object: list[i]
* @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)
*/ jsonDao.batch(items, function() {
this.listItems = function(type, offset, num, callback) { callback();
});
// fetch all items of a certain type from the data-store };
jsonDao.list(crypto.emailAddress + '_' + type, offset, num, function(encryptedList) {
/**
* 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);
};
};

View File

@ -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) { app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage) {
this.account = account;
/**
// sync user's cloud key with local storage * Inits all dependencies
cloudstorage.getUserSecretKey(account.get('emailAddress'), function(err) { */
if (err) { this.init = function(account, password, callback) {
console.log('Could not sync user key from server: ' + JSON.stringify(err)); this.account = account;
}
// init crypto // sync user's cloud key with local storage
initCrypto(); cloudstorage.getUserSecretKey(account.get('emailAddress'), function(err) {
if (err) {
}, function() { console.log('Could not sync user key from server: ' + JSON.stringify(err));
// replaced local key with cloud key... whipe local storage }
devicestorage.clear(function() { // init crypto
initCrypto(); 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 }, function() {
if (decryptedList.length > 0) { // replaced local key with cloud key... whipe local storage
folder = new app.model.Folder({name: folderName}); devicestorage.clear(function() {
folder.set('items', collection); initCrypto();
self.account.get('folders').add(folder); });
}
callback(collection);
}); });
} else { function initCrypto() {
// read items from memory crypto.init(account.get('emailAddress'), password, account.get('symKeySize'), account.get('symIvSize'), function() {
collection = folder.get('items'); callback();
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 * Fetch an email with the following id
devicestorage.storeEcryptedList(res, 'email_' + folderName, function() { */
// remove cached folder in account model this.getItem = function(folderName, itemId) {
folder = self.account.get('folders').where({name: folderName})[0]; var folder = this.account.get('folders').where({
if (folder) { name: folderName
self.account.get('folders').remove(folder); })[0];
} var mail = _.find(folder.get('items').models, function(email) {
callback(); 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();
});
});
};
}; };
}; }());

View File

@ -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) { app.dao.LawnchairDAO = function(window) {
Lawnchair(function() {
this.save({ key:key, object:object }, callback); var db = new Lawnchair({
}); name: 'data-store'
}; }, function() {});
/** /**
* Persist a bunch of items at once * Create or update an object
*/ */
this.batch = function(list, callback) { this.persist = function(key, object, callback) {
Lawnchair(function() { db.save({
this.batch(list, callback); key: key,
}); object: object
}; }, callback);
};
/**
* Read a single item by its key /**
*/ * Persist a bunch of items at once
this.read = function(key, callback) { */
Lawnchair(function() { this.batch = function(list, callback) {
this.get(key, function(o) { db.batch(list, callback);
};
/**
* Read a single item by its key
*/
this.read = function(key, callback) {
db.get(key, function(o) {
if (o) { if (o) {
callback(o.object); callback(o.object);
} else { } else {
callback(null); callback(null);
} }
}); });
}); };
};
/**
/** * List all the items of a certain type
* List all the items of a certain type * @param type [String] The type of item e.g. 'email'
* @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 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)
* @param num [Number] The number of items to fetch (null means fetch all) */
*/ this.list = function(type, offset, num, callback) {
this.list = function(type, offset, num, callback) { var i, list = [],
var i, list = [], matchingKeys = [], parts, timeStr, time; matchingKeys = [],
parts, timeStr, time;
Lawnchair(function() {
var self = this;
// get all keys // get all keys
this.keys(function(keys) { db.keys(function(keys) {
// check if key begins with type // check if key begins with type
for (i = 0; i < keys.length; i++) { for (i = 0; i < keys.length; i++) {
if (keys[i].indexOf(type) === 0) { if (keys[i].indexOf(type) === 0) {
matchingKeys.push(keys[i]); matchingKeys.push(keys[i]);
} }
} }
// sort keys by type and date // sort keys by type and date
matchingKeys = _.sortBy(matchingKeys, function(key) { matchingKeys = _.sortBy(matchingKeys, function(key) {
parts = key.split('_'); parts = key.split('_');
timeStr = parts[parts.length-2]; timeStr = parts[parts.length - 2];
time = parseInt(timeStr, 10); time = parseInt(timeStr, 10);
return time; return time;
}); });
// if num is null, list all items // if num is null, list all items
num = (num !== null) ? num : matchingKeys.length; num = (num !== null) ? num : matchingKeys.length;
@ -78,44 +79,41 @@ app.dao.LawnchairDAO = function(window) {
matchingKeys = matchingKeys.splice(0, matchingKeys.length - offset); matchingKeys = matchingKeys.splice(0, matchingKeys.length - offset);
} else { } else {
matchingKeys = []; matchingKeys = [];
} }
// return if there are no matching keys // return if there are no matching keys
if (matchingKeys.length === 0) { if (matchingKeys.length === 0) {
callback(list); callback(list);
return; return;
} }
// fetch all items from data-store with matching key // 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++) { for (i = 0; i < matchingList.length; i++) {
list.push(matchingList[i].object); list.push(matchingList[i].object);
} }
// return only the interval between offset and num // return only the interval between offset and num
callback(list); 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);
});
};
};

View File

@ -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) { app.dao.LocalStorageDAO = function(window) {
var json = JSON.stringify(object);
window.localStorage.setItem(key, json); /**
}; * Stringifies an object literal to JSON and perists it
*/
/** this.persist = function(key, object) {
* Fetches a json string from local storage by its key and parses it to an object literal var json = JSON.stringify(object);
*/ window.localStorage.setItem(key, json);
this.read = function(key) { };
var json = window.localStorage.getItem(key);
return JSON.parse(json); /**
}; * Fetches a json string from local storage by its key and parses it to an object literal
*/
/** this.read = function(key) {
* List all the items of a certain type in local storage var json = window.localStorage.getItem(key);
* @param type [String] The type of item e.g. 'email' return JSON.parse(json);
*/ };
this.list = function(type) {
var i, key, json, list = []; /**
* List all the items of a certain type in local storage
for (i = 0; i < window.localStorage.length; i++) { * @param type [String] The type of item e.g. 'email'
key = window.localStorage.key(i); */
if (key.indexOf(type) === 0) { this.list = function(type) {
json = window.localStorage.getItem(key); var i, key, json, list = [];
list.push(JSON.parse(json));
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();
};
};

View File

@ -1,25 +1,27 @@
'use strict'; (function() {
'use strict';
app.model.Account = Backbone.Model.extend({ app.model.Account = Backbone.Model.extend({
defaults: {
emailAddress: null,
symKeySize: null,
symIvSize: null,
folders: null
},
initialize: function () { defaults: {
this.set('folders', new app.model.FolderCollection()); 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,
}
}); findByName: function(key) {}
});
}());

View File

@ -1,35 +1,37 @@
'use strict'; (function() {
'use strict';
app.model.Email = Backbone.Model.extend({ app.model.Email = Backbone.Model.extend({
defaults: {
id: null,
from: null,
to: [],
cc: [],
bcc: [],
subject: null,
body: null,
sentDate: null
},
initialize: function () { defaults: {
// decode body id: null,
try { from: null,
var decodedBody = window.atob(this.get('body')); to: [],
this.set('body', decodedBody); cc: [],
} catch (ex) { bcc: [],
console.log(ex); 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) {}
}
}); });
}());

View File

@ -1,22 +1,23 @@
'use strict'; (function() {
'use strict';
app.model.Folder = Backbone.Model.extend({ app.model.Folder = Backbone.Model.extend({
defaults: {
name: null,
items: null
},
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,
}
}); findByName: function(key) {}
});
}());

View File

@ -1,13 +1,16 @@
'use strict'; (function() {
'use strict';
app.view.AccountsView = Backbone.View.extend({ app.view.AccountsView = Backbone.View.extend({
initialize:function () { initialize: function() {
this.template = _.template(app.util.tpl.get('accounts')); this.template = _.template(app.util.tpl.get('accounts'));
}, },
render:function (eventName) { render: function(eventName) {
$(this.el).html(this.template()); $(this.el).html(this.template());
return this; return this;
} }
}); });
}());

View File

@ -1,13 +1,16 @@
'use strict'; (function() {
'use strict';
app.view.ComposeView = Backbone.View.extend({ app.view.ComposeView = Backbone.View.extend({
initialize:function () { initialize: function() {
this.template = _.template(app.util.tpl.get('compose')); this.template = _.template(app.util.tpl.get('compose'));
}, },
render:function (eventName) { render: function(eventName) {
$(this.el).html(this.template()); $(this.el).html(this.template());
return this; return this;
} }
}); });
}());

View File

@ -1,23 +1,26 @@
'use strict'; (function() {
'use strict';
app.view.FolderListView = Backbone.View.extend({ app.view.FolderListView = Backbone.View.extend({
initialize:function () { initialize: function() {
this.template = _.template(app.util.tpl.get('folderlist')); this.template = _.template(app.util.tpl.get('folderlist'));
}, },
render:function (eventName) { render: function(eventName) {
var page = $(this.el); 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;
});
return this; 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;
}
});
}());

View File

@ -1,41 +1,49 @@
'use strict'; (function() {
'use strict';
app.view.LoginView = Backbone.View.extend({ app.view.LoginView = Backbone.View.extend({
initialize: function(args) { initialize: function(args) {
this.template = _.template(app.util.tpl.get('login')); this.template = _.template(app.util.tpl.get('login'));
this.dao = args.dao; this.dao = args.dao;
}, },
render: function(eventName) { render: function(eventName) {
var self = this, page = $(this.el); 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;
},
login: function() { page.html(this.template());
var page = $(this.el), page.attr('data-theme', 'a');
userId = page.find('#userId').val(),
password = page.find('#password').val(); page.find('#loginBtn').on('vmousedown', function() {
self.login();
var account = new app.model.Account({ });
emailAddress: userId,
symKeySize: app.config.symKeySize, return this;
symIvSize: app.config.symIvSize },
});
login: function() {
// show loading msg during init var page = $(this.el),
$.mobile.loading('show', { text: 'Unlocking...', textVisible: true, theme: 'c' }); userId = page.find('#userId').val(),
this.dao.init(account, password, function() { password = page.find('#password').val();
$.mobile.loading('hide');
window.location = '#accounts/' + account.get('emailAddress') + '/folders'; 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';
});
}
});
}());

View File

@ -1,73 +1,86 @@
'use strict'; (function() {
'use strict';
app.view.MessageListView = Backbone.View.extend({ app.view.MessageListView = Backbone.View.extend({
initialize: function(args) { initialize: function(args) {
this.template = _.template(app.util.tpl.get('messagelist')); this.template = _.template(app.util.tpl.get('messagelist'));
this.folder = args.folder; this.folder = args.folder;
this.dao = args.dao; this.dao = args.dao;
}, },
render: function(eventName) { render: function(eventName) {
var self = this, var self = this,
page = $(this.el); page = $(this.el);
page.html(this.template(this.options)); page.html(this.template(this.options));
page.find('#refreshBtn').on('vmousedown', function() { page.find('#refreshBtn').on('vmousedown', function() {
self.syncFolder(); self.syncFolder();
}); });
return this; return this;
}, },
/** /**
* Synchronize emails from the cloud * Synchronize emails from the cloud
*/ */
syncFolder: function() { syncFolder: function() {
var self = this; var self = this;
$.mobile.loading('show', { text: 'Syncing...', textVisible: true }); $.mobile.loading('show', {
// sync from cloud text: 'Syncing...',
this.dao.syncFromCloud(this.folder, function(res) { textVisible: true
$.mobile.loading('hide'); });
// sync from cloud
// check for error this.dao.syncFromCloud(this.folder, function(res) {
if (res && res.status) { $.mobile.loading('hide');
alert('Syncing failed!');
return; // check for error
} if (res && res.status) {
alert('Syncing failed!');
// read local storage and add to list view return;
self.loadItems(); }
});
}, // read local storage and add to list view
self.loadItems();
/** });
* Load items from local storage },
*/
loadItems: function() { /**
var self = this, * Load items from local storage
page = $(this.el), */
list = page.find('#message-list'), loadItems: function() {
listItemArgs, i, email; var self = this,
page = $(this.el),
$.mobile.loading('show', { text: 'decrypting...', textVisible: true }); list = page.find('#message-list'),
this.dao.listItems(this.folder, 0, 10, function(collection) { listItemArgs, i, email;
// clear list
list.html(''); $.mobile.loading('show', {
text: 'decrypting...',
// append items to list in reverse order so mails with the most recent date will be displayed first textVisible: true
for (i = collection.models.length - 1; i >= 0; i--) { });
email = collection.at(i); this.dao.listItems(this.folder, 0, 10, function(collection) {
listItemArgs = {account: self.options.account, folder: self.folder, model: email}; // clear list
list.append(new app.view.MessageListItemView(listItemArgs).render().el); list.html('');
}
// append items to list in reverse order so mails with the most recent date will be displayed first
// refresh list view for (i = collection.models.length - 1; i >= 0; i--) {
list.listview('refresh'); email = collection.at(i);
$.mobile.loading('hide'); 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');
});
}
});
}());

View File

@ -1,24 +1,27 @@
'use strict'; (function() {
'use strict';
app.view.MessageListItemView = Backbone.View.extend({ app.view.MessageListItemView = Backbone.View.extend({
tagName:"li",
initialize:function () { tagName: "li",
this.template = _.template(app.util.tpl.get('messagelistitem'));
},
render:function (eventName) { initialize: function() {
var params = this.model.toJSON(); this.template = _.template(app.util.tpl.get('messagelistitem'));
params.account = this.options.account; },
params.folder = this.options.folder;
params.id = encodeURIComponent(params.id); render: function(eventName) {
var params = this.model.toJSON();
var util = new app.crypto.Util(window, null); params.account = this.options.account;
var date = util.parseDate(params.sentDate); params.folder = this.options.folder;
params.displayDate = date.getDate() + '.' + (date.getMonth() + 1) + '. ' + date.getHours() + ':' + date.getMinutes(); params.id = encodeURIComponent(params.id);
$(this.el).html(this.template(params)); var util = new app.crypto.Util(window, null);
return this; 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;
}
});
}());

View File

@ -1,32 +1,35 @@
'use strict'; (function() {
'use strict';
app.view.ReadView = Backbone.View.extend({ app.view.ReadView = Backbone.View.extend({
initialize: function(args) { initialize: function(args) {
this.template = _.template(app.util.tpl.get('read')); this.template = _.template(app.util.tpl.get('read'));
this.model = args.dao.getItem(args.folder, args.messageId); this.model = args.dao.getItem(args.folder, args.messageId);
}, },
render: function(eventName) { render: function(eventName) {
$(this.el).html(this.template(this.model.toJSON())); $(this.el).html(this.template(this.model.toJSON()));
return this; return this;
}, },
renderBody: function() { renderBody: function() {
var emailBody = this.model.get('body'), var emailBody = this.model.get('body'),
iframe = $('#idMailContent'), iframe = $('#idMailContent'),
iframeDoc = iframe[0].contentDocument || iframe[0].contentWindow.document; iframeDoc = iframe[0].contentDocument || iframe[0].contentWindow.document;
iframe.load(function() { iframe.load(function() {
// resize // resize
var newheight = iframeDoc.body.scrollHeight; var newheight = iframeDoc.body.scrollHeight;
var newwidth = iframeDoc.body.scrollWidth; var newwidth = iframeDoc.body.scrollWidth;
iframe[0].height = (newheight) + 'px'; iframe[0].height = (newheight) + 'px';
iframe[0].width = (newwidth) + 'px'; iframe[0].width = (newwidth) + 'px';
}); });
iframeDoc.write(emailBody); iframeDoc.write(emailBody);
iframeDoc.close(); iframeDoc.close();
} }
}); });
}());