002 encryption type with ivs and auth hash
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
class ItemParams {
|
||||
|
||||
constructor(item, ek) {
|
||||
constructor(item, keys) {
|
||||
this.item = item;
|
||||
this.ek = ek;
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
paramsForExportFile() {
|
||||
@@ -32,8 +32,8 @@ class ItemParams {
|
||||
|
||||
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
|
||||
|
||||
if(this.ek) {
|
||||
EncryptionHelper.encryptItem(itemCopy, this.ek);
|
||||
if(this.keys) {
|
||||
EncryptionHelper.encryptItem(itemCopy, this.keys, "002");
|
||||
params.content = itemCopy.content;
|
||||
params.enc_item_key = itemCopy.enc_item_key;
|
||||
params.auth_hash = itemCopy.auth_hash;
|
||||
|
||||
@@ -32,6 +32,18 @@ angular.module('app.frontend')
|
||||
return JSON.parse(localStorage.getItem("auth_params"));
|
||||
}
|
||||
|
||||
this.keys = function() {
|
||||
var keys = {mk: localStorage.getItem("mk")};
|
||||
if(!localStorage.getItem("encryptionKey")) {
|
||||
keys = _.merge(keys, Neeto.crypto.generateKeysFromMasterKey(keys.mk));
|
||||
localStorage.setItem("encryptionKey", keys.encryptionKey);
|
||||
localStorage.setItem("authKey", keys.authKey);
|
||||
} else {
|
||||
keys = _.merge(keys, {encryptionKey: localStorage.getItem("encryptionKey"), authKey: localStorage.getItem("authKey")});
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
this.getAuthParamsForEmail = function(url, email, callback) {
|
||||
var requestUrl = url + "/auth/params";
|
||||
httpManager.getAbsolute(requestUrl, {email: email}, function(response){
|
||||
|
||||
@@ -16,7 +16,7 @@ class AccountMenu {
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
|
||||
$scope.encryptionKey = function() {
|
||||
return syncManager.masterKey;
|
||||
return authManager.keys().mk;
|
||||
}
|
||||
|
||||
$scope.serverPassword = function() {
|
||||
@@ -223,9 +223,8 @@ class AccountMenu {
|
||||
|
||||
if(data.auth_params) {
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
|
||||
var mk = keys.mk;
|
||||
try {
|
||||
EncryptionHelper.decryptMultipleItems(data.items, mk, true);
|
||||
EncryptionHelper.decryptMultipleItems(data.items, keys, true);
|
||||
// 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;
|
||||
@@ -319,26 +318,26 @@ class AccountMenu {
|
||||
|
||||
$scope.downloadDataArchive = function() {
|
||||
// download in Standard File format
|
||||
var ek = $scope.archiveFormData.encrypted ? syncManager.masterKey : null;
|
||||
var data = $scope.itemsData(ek);
|
||||
var keys = $scope.archiveFormData.encrypted ? authManager.keys() : null;
|
||||
var data = $scope.itemsData(keys);
|
||||
downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||
|
||||
// download as zipped plain text files
|
||||
if(!ek) {
|
||||
if(!keys) {
|
||||
var notes = modelManager.allItemsMatchingTypes(["Note"]);
|
||||
downloadZippedNotes(notes);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.itemsData = function(ek) {
|
||||
$scope.itemsData = function(keys) {
|
||||
var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){
|
||||
var itemParams = new ItemParams(item, ek);
|
||||
var itemParams = new ItemParams(item, keys);
|
||||
return itemParams.paramsForExportFile();
|
||||
}.bind(this));
|
||||
|
||||
var data = {items: items}
|
||||
|
||||
if(ek) {
|
||||
if(keys) {
|
||||
// auth params are only needed when encrypted with a standard file key
|
||||
data["auth_params"] = authManager.getAuthParams();
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ class ExtensionManager {
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
var items = response.items || [response.item];
|
||||
EncryptionHelper.decryptMultipleItems(items, localStorage.getItem("mk"));
|
||||
EncryptionHelper.decryptMultipleItems(items, this.authManager.keys());
|
||||
items = this.modelManager.mapResponseItemsToLocalModels(items);
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
@@ -172,7 +172,7 @@ class ExtensionManager {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
EncryptionHelper.decryptItem(response.item, localStorage.getItem("mk"));
|
||||
EncryptionHelper.decryptItem(response.item, this.authManager.keys());
|
||||
var item = this.modelManager.createItem(response.item);
|
||||
customCallback({item: item});
|
||||
|
||||
@@ -301,11 +301,11 @@ class ExtensionManager {
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension) {
|
||||
var ek = this.syncManager.masterKey;
|
||||
var keys = this.authManager.keys();
|
||||
if(!this.extensionUsesEncryptedData(extension)) {
|
||||
ek = null;
|
||||
keys = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, ek);
|
||||
var itemParams = new ItemParams(item, keys);
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class SNCrypto {
|
||||
|
||||
generateRandomKey() {
|
||||
return CryptoJS.lib.WordArray.random(512/8).toString();
|
||||
generateRandomKey(bits) {
|
||||
return CryptoJS.lib.WordArray.random(bits/8).toString();
|
||||
}
|
||||
|
||||
generateUUID() {
|
||||
@@ -30,24 +30,30 @@ class SNCrypto {
|
||||
}
|
||||
}
|
||||
|
||||
decryptText(encrypted_content, key, iv, auth) {
|
||||
var keyData = CryptoJS.enc.Hex.parse(key);
|
||||
decryptText({ciphertextToAuth, contentCiphertext, encryptionKey, iv, authHash, authKey} = {}) {
|
||||
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(encrypted_content, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
|
||||
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) {
|
||||
encryptText(text, key, iv) {
|
||||
var keyData = CryptoJS.enc.Hex.parse(key);
|
||||
// items are encrypted with random keys; no two items are encrypted with same key, thus IV is not needed
|
||||
var ivData = CryptoJS.enc.Hex.parse("");
|
||||
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();
|
||||
var passphrase = Neeto.crypto.generateRandomKey();
|
||||
var salt = Neeto.crypto.generateRandomKey(512);
|
||||
var passphrase = Neeto.crypto.generateRandomKey(512);
|
||||
return CryptoJS.PBKDF2(passphrase, salt, { keySize: 512/32 }).toString();
|
||||
}
|
||||
|
||||
@@ -83,28 +89,37 @@ class SNCrypto {
|
||||
return CryptoJS.HmacSHA256(messageData, keyData).toString();
|
||||
}
|
||||
|
||||
generateKeysFromMasterKey(mk) {
|
||||
var encryptionKey = Neeto.crypto.hmac256(mk, "e");
|
||||
var authKey = Neeto.crypto.hmac256(mk, "a");
|
||||
return {encryptionKey: encryptionKey, authKey: authKey};
|
||||
}
|
||||
|
||||
computeEncryptionKeysForUser({password, pw_salt, pw_func, pw_alg, pw_cost, pw_key_size} = {}, callback) {
|
||||
this.generateSymmetricKeyPair({password: password, pw_salt: pw_salt,
|
||||
pw_func: pw_func, pw_alg: pw_alg, pw_cost: pw_cost, pw_key_size: pw_key_size}, function(keys){
|
||||
var pw = keys[0];
|
||||
var mk = keys[1];
|
||||
|
||||
callback({pw: pw, mk: mk});
|
||||
});
|
||||
callback(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk)));
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
generateInitialEncryptionKeysForUser({email, password} = {}, callback) {
|
||||
var defaults = this.defaultPasswordGenerationParams();
|
||||
var {pw_func, pw_alg, pw_key_size, pw_cost} = defaults;
|
||||
var pw_nonce = this.generateRandomKey();
|
||||
var pw_nonce = this.generateRandomKey(512);
|
||||
var pw_salt = this.sha1(email + "SN" + pw_nonce);
|
||||
_.merge(defaults, {pw_salt: pw_salt, pw_nonce: pw_nonce})
|
||||
this.generateSymmetricKeyPair(_.merge({email: email, password: password, pw_salt: pw_salt}, defaults), function(keys){
|
||||
var pw = keys[0];
|
||||
var mk = keys[1];
|
||||
|
||||
callback({pw: pw, mk: mk}, defaults);
|
||||
});
|
||||
var encryptionKey = Neeto.crypto.hmac256(mk, "e");
|
||||
var authKey = Neeto.crypto.hmac256(mk, "a");
|
||||
|
||||
callback(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk)), defaults);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,93 @@
|
||||
class EncryptionHelper {
|
||||
|
||||
static encryptItem(item, key) {
|
||||
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||
item.enc_item_key = Neeto.crypto.encryptText(item_key, key);
|
||||
static _private_encryptString(string, encryptionKey, authKey, 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, iv, contentCiphertext].join(":");
|
||||
var authHash = Neeto.crypto.hmac256(ciphertextToAuth, authKey);
|
||||
fullCiphertext = [version, authHash, iv, contentCiphertext].join(":");
|
||||
}
|
||||
|
||||
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
||||
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
||||
var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek);
|
||||
var authHash = Neeto.crypto.hmac256(encryptedContent, ak);
|
||||
|
||||
item.content = encryptedContent;
|
||||
item.auth_hash = authHash;
|
||||
return fullCiphertext;
|
||||
}
|
||||
|
||||
static decryptItem(item, key) {
|
||||
var item_key = Neeto.crypto.decryptText(item.enc_item_key, key);
|
||||
static encryptItem(item, keys, version) {
|
||||
// encrypt item key
|
||||
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||
if(version === "001") {
|
||||
// legacy
|
||||
item.enc_item_key = Neeto.crypto.encryptText(item_key, keys.mk, null);
|
||||
} else {
|
||||
item.enc_item_key = this._private_encryptString(item_key, keys.encryptionKey, keys.authKey, version);
|
||||
}
|
||||
|
||||
// encrypt content
|
||||
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
||||
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
||||
var authHash = Neeto.crypto.hmac256(item.content, ak);
|
||||
if(authHash !== item.auth_hash || !item.auth_hash) {
|
||||
console.log("Authentication hash does not match.")
|
||||
var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, version);
|
||||
if(version === "001") {
|
||||
var authHash = Neeto.crypto.hmac256(ciphertext, ak);
|
||||
item.auth_hash = authHash;
|
||||
}
|
||||
|
||||
item.content = ciphertext;
|
||||
}
|
||||
|
||||
static encryptionComponentsFromString(string, baseKey, 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: baseKey
|
||||
}
|
||||
} else {
|
||||
let components = string.split(":");
|
||||
return {
|
||||
encryptionVersion: components[0],
|
||||
authHash: components[1],
|
||||
iv: components[2],
|
||||
contentCiphertext: components[3],
|
||||
ciphertextToAuth: [components[0], components[2], components[3]].join(":"),
|
||||
encryptionKey: encryptionKey,
|
||||
authKey: authKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static decryptItem(item, keys) {
|
||||
// decrypt encrypted key
|
||||
var encryptedItemKey = item.enc_item_key;
|
||||
if(!encryptedItemKey.startsWith("002")) {
|
||||
// legacy encryption type, has no prefix
|
||||
encryptedItemKey = "001" + encryptedItemKey;
|
||||
}
|
||||
var keyParams = this.encryptionComponentsFromString(encryptedItemKey, keys.mk, keys.encryptionKey, keys.authKey);
|
||||
var item_key = Neeto.crypto.decryptText(keyParams);
|
||||
|
||||
if(!item_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
var content = Neeto.crypto.decryptText(item.content.substring(3, item.content.length), ek);
|
||||
// decrypt content
|
||||
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
||||
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
||||
var itemParams = this.encryptionComponentsFromString(item.content, ek, ek, ak);
|
||||
var content = Neeto.crypto.decryptText(itemParams);
|
||||
|
||||
item.content = content;
|
||||
}
|
||||
|
||||
static decryptMultipleItems(items, key, throws) {
|
||||
for (var item of items) {
|
||||
static decryptMultipleItems(items, keys, throws) {
|
||||
for (var item of items) {
|
||||
if(item.deleted == true) {
|
||||
continue;
|
||||
}
|
||||
@@ -37,9 +95,9 @@ class EncryptionHelper {
|
||||
var isString = typeof item.content === 'string' || item.content instanceof String;
|
||||
if(isString) {
|
||||
try {
|
||||
if(item.content.substring(0, 3) == "001" && item.enc_item_key) {
|
||||
if((item.content.startsWith("001") || item.content.startsWith("002")) && item.enc_item_key) {
|
||||
// is encrypted
|
||||
this.decryptItem(item, key);
|
||||
this.decryptItem(item, keys);
|
||||
} else {
|
||||
// is base64 encoded
|
||||
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
||||
@@ -52,7 +110,7 @@ class EncryptionHelper {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class SyncManager {
|
||||
var params = {};
|
||||
params.limit = 150;
|
||||
params.items = _.map(subItems, function(item){
|
||||
var itemParams = new ItemParams(item, localStorage.getItem("mk"));
|
||||
var itemParams = new ItemParams(item, this.authManager.keys());
|
||||
itemParams.additionalFields = options.additionalFields;
|
||||
return itemParams.paramsForSync();
|
||||
}.bind(this));
|
||||
@@ -252,7 +252,7 @@ class SyncManager {
|
||||
}
|
||||
|
||||
handleItemsResponse(responseItems, omitFields) {
|
||||
EncryptionHelper.decryptMultipleItems(responseItems, localStorage.getItem("mk"));
|
||||
EncryptionHelper.decryptMultipleItems(responseItems, this.authManager.keys());
|
||||
return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user