This commit is contained in:
Mo Bitar
2018-05-06 00:39:09 -05:00
parent fd01df6e01
commit e8aa7791a3
23 changed files with 31 additions and 566 deletions

View File

@@ -1,18 +1,7 @@
'use strict';
var Neeto = window.Neeto = Neeto || {};
var SN = SN || {};
// detect IE8 and above, and edge.
// IE and Edge do not support pbkdf2 in WebCrypto, therefore we need to use CryptoJS
var IEOrEdge = document.documentMode || /Edge/.test(navigator.userAgent);
if(!IEOrEdge && (window.crypto && window.crypto.subtle)) {
Neeto.crypto = new SNCryptoWeb();
} else {
Neeto.crypto = new SNCryptoJS();
}
angular.module('app', [])
function getParameterByName(name, url) {

View File

@@ -296,9 +296,9 @@ class AccountMenu {
}.bind(this)
if(data.auth_params) {
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
try {
EncryptionHelper.decryptMultipleItems(data.items, keys, false); /* throws = false as we don't want to interrupt all decryption if just one fails */
SFItemTransformer.decryptMultipleItems(data.items, keys, false); /* throws = false as we don't want to interrupt all decryption if just one fails */
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
data.items.forEach(function(item){
item.enc_item_key = null;
@@ -479,7 +479,7 @@ class AccountMenu {
$scope.securityUpdateData.processing = true;
var authParams = authManager.getAuthParams();
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: $scope.securityUpdateData.password}, authParams), function(keys){
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: $scope.securityUpdateData.password}, authParams), function(keys){
if(keys.mk !== authManager.keys().mk) {
alert("Invalid password. Please try again.");
$timeout(function(){

View File

@@ -9,7 +9,7 @@ class Item {
this.observers = [];
if(!this.uuid) {
this.uuid = Neeto.crypto.generateUUID();
this.uuid = SFJS.crypto.generateUUID();
}
}

View File

@@ -39,7 +39,7 @@ class ItemParams {
// Items should always be encrypted for export files. Only respect item.doNotEncrypt for remote sync params;
var doNotEncrypt = this.item.doNotEncrypt() && !this.forExportFile;
if(this.keys && !doNotEncrypt) {
var encryptedParams = EncryptionHelper.encryptItem(this.item, this.keys, this.version);
var encryptedParams = SFItemTransformer.encryptItem(this.item, this.keys, this.version);
_.merge(params, encryptedParams);
if(this.version !== "001") {
@@ -47,7 +47,7 @@ class ItemParams {
}
}
else {
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + SFJS.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
if(!this.forExportFile) {
params.enc_item_key = null;
params.auth_hash = null;

View File

@@ -63,7 +63,7 @@ class ActionsManager {
this.httpManager.getAbsolute(action.url, {}, function(response){
action.error = false;
var items = response.items || [response.item];
EncryptionHelper.decryptMultipleItems(items, this.authManager.keys());
SFItemTransformer.decryptMultipleItems(items, this.authManager.keys());
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
for(var item of items) {
item.setDirty(true);
@@ -82,7 +82,7 @@ class ActionsManager {
this.httpManager.getAbsolute(action.url, {}, function(response){
action.error = false;
EncryptionHelper.decryptItem(response.item, this.authManager.keys());
SFItemTransformer.decryptItem(response.item, this.authManager.keys());
var item = this.modelManager.createItem(response.item, true /* Dont notify observers */);
customCallback({item: item});

View File

@@ -112,11 +112,7 @@ angular.module('app')
// which accidentally used 60,000 iterations (now adjusted), which CryptoJS can't handle here (WebCrypto can however).
// if user has high password cost and is using browser that doesn't support WebCrypto,
// we want to tell them that they can't login with this browser.
if(cost > 5000) {
return Neeto.crypto instanceof SNCryptoWeb ? true : false;
} else {
return true;
}
return SFJS.crypto.supportsPasswordDerivationCost(cost);
}
this.login = function(url, email, password, ephemeral, extraParams, callback) {
@@ -153,7 +149,7 @@ angular.module('app')
return;
}
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
var requestUrl = url + "/auth/sign_in";
var params = _.merge({password: keys.pw, email: email}, extraParams);
@@ -202,7 +198,7 @@ angular.module('app')
}
this.register = function(url, email, password, ephemeral, callback) {
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
SFJS.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
var requestUrl = url + "/auth";
var params = _.merge({password: keys.pw, email: email}, authParams);
@@ -221,7 +217,7 @@ angular.module('app')
}
this.changePassword = function(email, new_password, callback) {
Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
SFJS.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
var requestUrl = storageManager.getItem("server") + "/auth/change_pw";
var params = _.merge({new_password: keys.pw}, authParams);

View File

@@ -716,7 +716,7 @@ class ComponentManager {
console.log("Web|componentManager|registerComponentWindow", component);
}
component.window = componentWindow;
component.sessionKey = Neeto.crypto.generateUUID();
component.sessionKey = SFJS.crypto.generateUUID();
this.sendMessageToComponent(component, {
action: "component-registered",
sessionKey: component.sessionKey,

View File

@@ -1,108 +0,0 @@
class SNCrypto {
generateRandomKey(bits) {
return CryptoJS.lib.WordArray.random(bits/8).toString();
}
generateUUID() {
var crypto = window.crypto || window.msCrypto;
if(crypto) {
var buf = new Uint32Array(4);
crypto.getRandomValues(buf);
var idx = -1;
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
idx++;
var r = (buf[idx>>3] >> ((idx%8)*4))&15;
var v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
} else {
var d = new Date().getTime();
if(window.performance && typeof window.performance.now === "function"){
d += performance.now(); //use high-precision timer if available
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=='x' ? r : (r&0x3|0x8)).toString(16);
});
return uuid;
}
}
decryptText({ciphertextToAuth, contentCiphertext, encryptionKey, iv, authHash, authKey} = {}, requiresAuth) {
if(requiresAuth && !authHash) {
console.error("Auth hash is required.");
return;
}
if(authHash) {
var localAuthHash = Neeto.crypto.hmac256(ciphertextToAuth, authKey);
if(authHash !== localAuthHash) {
console.error("Auth hash does not match, returning null.");
return null;
}
}
var keyData = CryptoJS.enc.Hex.parse(encryptionKey);
var ivData = CryptoJS.enc.Hex.parse(iv || "");
var decrypted = CryptoJS.AES.decrypt(contentCiphertext, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return decrypted.toString(CryptoJS.enc.Utf8);
}
encryptText(text, key, iv) {
var keyData = CryptoJS.enc.Hex.parse(key);
var ivData = CryptoJS.enc.Hex.parse(iv || "");
var encrypted = CryptoJS.AES.encrypt(text, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return encrypted.toString();
}
generateRandomEncryptionKey() {
var salt = Neeto.crypto.generateRandomKey(512);
var passphrase = Neeto.crypto.generateRandomKey(512);
return CryptoJS.PBKDF2(passphrase, salt, { keySize: 512/32 }).toString();
}
firstHalfOfKey(key) {
return key.substring(0, key.length/2);
}
secondHalfOfKey(key) {
return key.substring(key.length/2, key.length);
}
base64(text) {
return window.btoa(text);
}
base64Decode(base64String) {
return window.atob(base64String);
}
sha256(text) {
return CryptoJS.SHA256(text).toString();
}
hmac256(message, key) {
var keyData = CryptoJS.enc.Hex.parse(key);
var messageData = CryptoJS.enc.Utf8.parse(message);
var result = CryptoJS.HmacSHA256(messageData, keyData).toString();
return result;
}
computeEncryptionKeysForUser({password, pw_salt, pw_cost} = {}, callback) {
this.generateSymmetricKeyPair({password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){
callback({pw: keys[0], mk: keys[1], ak: keys[2]});
}.bind(this));
}
generateInitialEncryptionKeysForUser({email, password} = {}, callback) {
var pw_cost = this.defaultPasswordGenerationCost();
var pw_nonce = this.generateRandomKey(512);
var pw_salt = this.sha256([email, pw_nonce].join(":"));
this.generateSymmetricKeyPair({password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){
callback({pw: keys[0], mk: keys[1], ak: keys[2]}, {pw_salt: pw_salt, pw_cost: pw_cost, version: "002"});
}.bind(this));
}
}
export { SNCrypto }

View File

@@ -1,21 +0,0 @@
class SNCryptoJS extends SNCrypto {
/** Generates two deterministic keys based on one input */
generateSymmetricKeyPair({password, pw_salt, pw_cost} = {}, callback) {
var output = CryptoJS.PBKDF2(password, pw_salt, { keySize: 768/32, hasher: CryptoJS.algo.SHA512, iterations: pw_cost }).toString();
var outputLength = output.length;
var splitLength = outputLength/3;
var firstThird = output.slice(0, splitLength);
var secondThird = output.slice(splitLength, splitLength * 2);
var thirdThird = output.slice(splitLength * 2, splitLength * 3);
callback([firstThird, secondThird, thirdThird])
}
defaultPasswordGenerationCost() {
return 3000;
}
}
export { SNCryptoJS }

View File

@@ -1,163 +0,0 @@
class EncryptionHelper {
static _private_encryptString(string, encryptionKey, authKey, uuid, version) {
var fullCiphertext, contentCiphertext;
if(version === "001") {
contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, null);
fullCiphertext = version + contentCiphertext;
} else {
var iv = Neeto.crypto.generateRandomKey(128);
contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, iv);
var ciphertextToAuth = [version, uuid, iv, contentCiphertext].join(":");
var authHash = Neeto.crypto.hmac256(ciphertextToAuth, authKey);
fullCiphertext = [version, authHash, uuid, iv, contentCiphertext].join(":");
}
return fullCiphertext;
}
static encryptItem(item, keys, version = "002") {
var params = {};
// encrypt item key
var item_key = Neeto.crypto.generateRandomEncryptionKey();
if(version === "001") {
// legacy
params.enc_item_key = Neeto.crypto.encryptText(item_key, keys.mk, null);
} else {
params.enc_item_key = this._private_encryptString(item_key, keys.mk, keys.ak, item.uuid, version);
}
// encrypt content
var ek = Neeto.crypto.firstHalfOfKey(item_key);
var ak = Neeto.crypto.secondHalfOfKey(item_key);
var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, item.uuid, version);
if(version === "001") {
var authHash = Neeto.crypto.hmac256(ciphertext, ak);
params.auth_hash = authHash;
}
params.content = ciphertext;
return params;
}
static encryptionComponentsFromString(string, encryptionKey, authKey) {
var encryptionVersion = string.substring(0, 3);
if(encryptionVersion === "001") {
return {
contentCiphertext: string.substring(3, string.length),
encryptionVersion: encryptionVersion,
ciphertextToAuth: string,
iv: null,
authHash: null,
encryptionKey: encryptionKey,
authKey: authKey
}
} else {
let components = string.split(":");
return {
encryptionVersion: components[0],
authHash: components[1],
uuid: components[2],
iv: components[3],
contentCiphertext: components[4],
ciphertextToAuth: [components[0], components[2], components[3], components[4]].join(":"),
encryptionKey: encryptionKey,
authKey: authKey
}
}
}
static decryptItem(item, keys) {
if((item.content.startsWith("001") || item.content.startsWith("002")) && item.enc_item_key) {
// is encrypted, continue to below
} else {
// is base64 encoded
try {
item.content = JSON.parse(Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)));
} catch (e) {}
return;
}
// decrypt encrypted key
var encryptedItemKey = item.enc_item_key;
var requiresAuth = true;
if(encryptedItemKey.startsWith("002") === false) {
// legacy encryption type, has no prefix
encryptedItemKey = "001" + encryptedItemKey;
requiresAuth = false;
}
var keyParams = this.encryptionComponentsFromString(encryptedItemKey, keys.mk, keys.ak);
// return if uuid in auth hash does not match item uuid. Signs of tampering.
if(keyParams.uuid && keyParams.uuid !== item.uuid) {
if(!item.errorDecrypting) { item.errorDecryptingValueChanged = true;}
item.errorDecrypting = true;
return;
}
var item_key = Neeto.crypto.decryptText(keyParams, requiresAuth);
if(!item_key) {
if(!item.errorDecrypting) { item.errorDecryptingValueChanged = true;}
item.errorDecrypting = true;
return;
}
// decrypt content
var ek = Neeto.crypto.firstHalfOfKey(item_key);
var ak = Neeto.crypto.secondHalfOfKey(item_key);
var itemParams = this.encryptionComponentsFromString(item.content, ek, ak);
// return if uuid in auth hash does not match item uuid. Signs of tampering.
if(itemParams.uuid && itemParams.uuid !== item.uuid) {
if(!item.errorDecrypting) { item.errorDecryptingValueChanged = true;}
item.errorDecrypting = true;
return;
}
if(!itemParams.authHash) {
// legacy 001
itemParams.authHash = item.auth_hash;
}
var content = Neeto.crypto.decryptText(itemParams, true);
if(!content) {
if(!item.errorDecrypting) { item.errorDecryptingValueChanged = true;}
item.errorDecrypting = true;
} else {
if(item.errorDecrypting == true) { item.errorDecryptingValueChanged = true;}
// Content should only be set if it was successfully decrypted, and should otherwise remain unchanged.
item.errorDecrypting = false;
item.content = content;
}
}
static decryptMultipleItems(items, keys, throws) {
for (var item of items) {
// 4/15/18: Adding item.content == null clause. We still want to decrypt deleted items incase
// they were marked as dirty but not yet synced. Not yet sure why we had this requirement.
if(item.deleted == true && item.content == null) {
continue;
}
var isString = typeof item.content === 'string' || item.content instanceof String;
if(isString) {
try {
this.decryptItem(item, keys);
} catch (e) {
if(!item.errorDecrypting) { item.errorDecryptingValueChanged = true;}
item.errorDecrypting = true;
if(throws) {
throw e;
}
console.error("Error decrypting item", item, e);
continue;
}
}
}
}
}

View File

@@ -1,122 +0,0 @@
var subtleCrypto = window.crypto ? window.crypto.subtle : null;
class SNCryptoWeb extends SNCrypto {
/**
Overrides
*/
defaultPasswordGenerationCost() {
return 101000;
}
/** Generates two deterministic keys based on one input */
generateSymmetricKeyPair({password, pw_salt, pw_cost} = {}, callback) {
this.stretchPassword({password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(output){
var outputLength = output.length;
var splitLength = outputLength/3;
var firstThird = output.slice(0, splitLength);
var secondThird = output.slice(splitLength, splitLength * 2);
var thirdThird = output.slice(splitLength * 2, splitLength * 3);
callback([firstThird, secondThird, thirdThird])
})
}
/**
Internal
*/
stretchPassword({password, pw_salt, pw_cost} = {}, callback) {
this.webCryptoImportKey(password, function(key){
if(!key) {
console.log("Key is null, unable to continue");
callback(null);
return;
}
this.webCryptoDeriveBits({key: key, pw_salt: pw_salt, pw_cost: pw_cost}, function(key){
if(!key) {
callback(null);
return;
}
callback(key);
}.bind(this))
}.bind(this))
}
webCryptoImportKey(input, callback) {
subtleCrypto.importKey(
"raw",
this.stringToArrayBuffer(input),
{name: "PBKDF2"},
false,
["deriveBits"]
)
.then(function(key){
callback(key);
})
.catch(function(err){
console.error(err);
callback(null);
});
}
webCryptoDeriveBits({key, pw_salt, pw_cost} = {}, callback) {
subtleCrypto.deriveBits(
{
"name": "PBKDF2",
salt: this.stringToArrayBuffer(pw_salt),
iterations: pw_cost,
hash: {name: "SHA-512"},
},
key,
768
)
.then(function(bits){
var key = this.arrayBufferToHexString(new Uint8Array(bits));
callback(key);
}.bind(this))
.catch(function(err){
console.error(err);
callback(null);
});
}
stringToArrayBuffer(string) {
// not available on Edge/IE
if(window.TextEncoder) {
var encoder = new TextEncoder("utf-8");
var result = encoder.encode(string);
return result;
} else {
string = unescape(encodeURIComponent(string));
var buf = new ArrayBuffer(string.length);
var bufView = new Uint8Array(buf);
for (var i=0, strLen=string.length; i<strLen; i++) {
bufView[i] = string.charCodeAt(i);
}
return buf;
}
}
arrayBufferToHexString(arrayBuffer) {
var byteArray = new Uint8Array(arrayBuffer);
var hexString = "";
var nextHexByte;
for (var i=0; i<byteArray.byteLength; i++) {
nextHexByte = byteArray[i].toString(16);
if (nextHexByte.length < 2) {
nextHexByte = "0" + nextHexByte;
}
hexString += nextHexByte;
}
return hexString;
}
}
export { SNCryptoWeb }

View File

@@ -47,7 +47,7 @@ class ModelManager {
// we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup)
var newItem = this.createItem(item);
newItem.uuid = Neeto.crypto.generateUUID();
newItem.uuid = SFJS.crypto.generateUUID();
// Update uuids of relationships
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);

View File

@@ -28,7 +28,7 @@ angular.module('app')
this.unlock = function(passcode, callback) {
var params = this.passcodeAuthParams();
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, params), function(keys){
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, params), function(keys){
if(keys.pw !== params.hash) {
callback(false);
return;
@@ -42,11 +42,11 @@ angular.module('app')
}
this.setPasscode = (passcode, callback) => {
var cost = Neeto.crypto.defaultPasswordGenerationCost();
var salt = Neeto.crypto.generateRandomKey(512);
var cost = SFJS.crypto.defaultPasswordGenerationCost();
var salt = SFJS.crypto.generateRandomKey(512);
var defaultParams = {pw_cost: cost, pw_salt: salt, version: "002"};
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, defaultParams), function(keys) {
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, defaultParams), function(keys) {
defaultParams.hash = keys.pw;
this._keys = keys;
this._hasPasscode = true;

View File

@@ -158,7 +158,7 @@ class StorageManager {
decryptStorage() {
var stored = JSON.parse(this.getItem("encryptedStorage", StorageManager.Fixed));
EncryptionHelper.decryptItem(stored, this.encryptedStorageKeys);
SFItemTransformer.decryptItem(stored, this.encryptedStorageKeys);
var encryptedStorage = new EncryptedStorage(stored);
for(var key of Object.keys(encryptedStorage.storage)) {

View File

@@ -407,7 +407,7 @@ class SyncManager {
handleItemsResponse(responseItems, omitFields, source) {
var keys = this.authManager.keys() || this.passcodeManager.keys();
EncryptionHelper.decryptMultipleItems(responseItems, keys);
SFItemTransformer.decryptMultipleItems(responseItems, keys);
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields, source);
// During the decryption process, items may be marked as "errorDecrypting". If so, we want to be sure
@@ -450,7 +450,7 @@ class SyncManager {
var mapping = unsaved[i];
var itemResponse = mapping.item;
EncryptionHelper.decryptMultipleItems([itemResponse], this.authManager.keys());
SFItemTransformer.decryptMultipleItems([itemResponse], this.authManager.keys());
var item = this.modelManager.findItem(itemResponse.uuid);
if(!item) {