002 + verification updates
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
class ItemParams {
|
||||
|
||||
constructor(item, keys) {
|
||||
constructor(item, keys, version) {
|
||||
this.item = item;
|
||||
this.keys = keys;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
paramsForExportFile() {
|
||||
@@ -16,7 +17,7 @@ class ItemParams {
|
||||
}
|
||||
|
||||
paramsForLocalStorage() {
|
||||
this.additionalFields = ["updated_at", "dirty"];
|
||||
this.additionalFields = ["updated_at", "dirty", "errorDecrypting"];
|
||||
this.forExportFile = true;
|
||||
return this.__params();
|
||||
}
|
||||
@@ -26,17 +27,17 @@ class ItemParams {
|
||||
}
|
||||
|
||||
__params() {
|
||||
let encryptionVersion = "001";
|
||||
|
||||
console.log("Encrypting with version: ", this.version);
|
||||
|
||||
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
|
||||
|
||||
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
|
||||
|
||||
if(this.keys && !this.item.doNotEncrypt()) {
|
||||
var encryptedParams = EncryptionHelper.encryptItem(this.item, this.keys, encryptionVersion);
|
||||
var encryptedParams = EncryptionHelper.encryptItem(this.item, this.keys, this.version);
|
||||
_.merge(params, encryptedParams);
|
||||
|
||||
if(encryptionVersion !== "001") {
|
||||
if(this.version !== "001") {
|
||||
params.auth_hash = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ angular.module('app.frontend')
|
||||
return domain;
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, httpManager, modelManager, dbManager) {
|
||||
return new AuthManager($rootScope, httpManager, modelManager, dbManager);
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager);
|
||||
}
|
||||
|
||||
function AuthManager($rootScope, httpManager, modelManager, dbManager) {
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager) {
|
||||
|
||||
var userData = localStorage.getItem("user");
|
||||
if(userData) {
|
||||
@@ -45,17 +45,19 @@ angular.module('app.frontend')
|
||||
if(!mk) {
|
||||
return null;
|
||||
}
|
||||
var keys = {mk: mk};
|
||||
if(!localStorage.getItem("encryptionKey")) {
|
||||
_.merge(keys, Neeto.crypto.generateKeysFromMasterKey(keys.mk));
|
||||
localStorage.setItem("encryptionKey", keys.encryptionKey);
|
||||
localStorage.setItem("authKey", keys.authKey);
|
||||
} else {
|
||||
_.merge(keys, {encryptionKey: localStorage.getItem("encryptionKey"), authKey: localStorage.getItem("authKey")});
|
||||
}
|
||||
var keys = {mk: mk, ak: localStorage.getItem("ak")};
|
||||
return keys;
|
||||
}
|
||||
|
||||
this.encryptionVersion = function() {
|
||||
var keys = this.keys();
|
||||
if(keys && keys.ak) {
|
||||
return "002";
|
||||
} else {
|
||||
return "001";
|
||||
}
|
||||
}
|
||||
|
||||
this.getAuthParamsForEmail = function(url, email, callback) {
|
||||
var requestUrl = url + "/auth/params";
|
||||
httpManager.getAbsolute(requestUrl, {email: email}, function(response){
|
||||
@@ -80,7 +82,7 @@ angular.module('app.frontend')
|
||||
|
||||
this.login = function(url, email, password, callback) {
|
||||
this.getAuthParamsForEmail(url, email, function(authParams){
|
||||
if(!authParams) {
|
||||
if(!authParams.pw_cost) {
|
||||
callback({error : {message: "Unable to get authentication parameters."}});
|
||||
return;
|
||||
}
|
||||
@@ -94,12 +96,41 @@ angular.module('app.frontend')
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
|
||||
|
||||
var uploadVTagOnCompletion = false;
|
||||
var localVTag = Neeto.crypto.calculateVerificationTag(authParams.pw_cost, authParams.pw_salt, keys.ak);
|
||||
|
||||
if(authParams.pw_auth) {
|
||||
// verify auth params
|
||||
if(localVTag !== authParams.pw_auth) {
|
||||
alert("Invalid server verification tag; aborting login. Learn more at standardnotes.org/verification.");
|
||||
$timeout(function(){
|
||||
callback({error: true, didDisplayAlert: true});
|
||||
})
|
||||
return;
|
||||
} else {
|
||||
console.log("Verification tag success.");
|
||||
}
|
||||
} else {
|
||||
// either user has not uploaded pw_auth, or server is attempting to bypass authentication
|
||||
if(confirm("Unable to locate verification tag for server. If this is your first time seeing this message and your account was created before July 2017, press OK to upload verification tag. If your account was created after July 2017, or if you've already seen this message, press cancel to abort login. Learn more at standardnotes.org/verification.")) {
|
||||
// upload verification tag on completion
|
||||
uploadVTagOnCompletion = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var params = {password: keys.pw, email: email};
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
callback(response);
|
||||
if(uploadVTagOnCompletion) {
|
||||
this.uploadVerificationTag(localVTag, authParams);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
console.error("Error logging in", response);
|
||||
callback(response);
|
||||
@@ -109,28 +140,46 @@ angular.module('app.frontend')
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.handleAuthResponse = function(response, email, url, authParams, mk, pw) {
|
||||
this.uploadVerificationTag = function(tag, authParams) {
|
||||
var requestUrl = localStorage.getItem("server") + "/auth/update";
|
||||
var params = {pw_auth: tag};
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
_.merge(authParams, params);
|
||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
||||
alert("Your verification tag was successfully uploaded.");
|
||||
}.bind(this), function(response){
|
||||
alert("There was an error uploading your verification tag.");
|
||||
})
|
||||
}
|
||||
|
||||
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
||||
try {
|
||||
if(url) {
|
||||
localStorage.setItem("server", url);
|
||||
}
|
||||
localStorage.setItem("user", JSON.stringify(response.user));
|
||||
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
|
||||
localStorage.setItem("mk", mk);
|
||||
localStorage.setItem("pw", pw);
|
||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
||||
localStorage.setItem("jwt", response.token);
|
||||
this.saveKeys(keys);
|
||||
} catch(e) {
|
||||
dbManager.displayOfflineAlert();
|
||||
}
|
||||
}
|
||||
|
||||
this.saveKeys = function(keys) {
|
||||
localStorage.setItem("pw", keys.pw);
|
||||
localStorage.setItem("mk", keys.mk);
|
||||
localStorage.setItem("ak", keys.ak);
|
||||
}
|
||||
|
||||
this.register = function(url, email, password, callback) {
|
||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
|
||||
var requestUrl = url + "/auth";
|
||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
console.error("Registration error", response);
|
||||
@@ -144,8 +193,8 @@ angular.module('app.frontend')
|
||||
var requestUrl = localStorage.getItem("server") + "/auth/change_pw";
|
||||
var params = _.merge({new_password: keys.pw}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.handleAuthResponse(response, email, null, authParams, keys.mk, keys.pw);
|
||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||
this.handleAuthResponse(response, email, null, authParams, keys);
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
var error = response;
|
||||
|
||||
@@ -20,6 +20,10 @@ class AccountMenu {
|
||||
return authManager.keys().mk;
|
||||
}
|
||||
|
||||
$scope.authKey = function() {
|
||||
return authManager.keys().ak;
|
||||
}
|
||||
|
||||
$scope.serverPassword = function() {
|
||||
return syncManager.serverPassword;
|
||||
}
|
||||
@@ -341,7 +345,7 @@ class AccountMenu {
|
||||
|
||||
$scope.itemsData = function(keys) {
|
||||
var items = _.map(modelManager.allItems, function(item){
|
||||
var itemParams = new ItemParams(item, keys);
|
||||
var itemParams = new ItemParams(item, keys, authManager.encryptionVersion());
|
||||
return itemParams.paramsForExportFile();
|
||||
}.bind(this));
|
||||
|
||||
@@ -356,6 +360,69 @@ class AccountMenu {
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Advanced
|
||||
|
||||
$scope.reencryptPressed = function() {
|
||||
if(!confirm("Are you sure you want to re-encrypt and sync all your items? This is useful when updates are made to our encryption specification. You should have been instructed to come here from our website.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!confirm("It is highly recommended that you download a backup of your data before proceeding. Press cancel to go back. Note that this procedure can take some time, depending on the number of items you have. Do not close the app during process.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
|
||||
$timeout(function(){
|
||||
alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 002 Update
|
||||
|
||||
$scope.securityUpdateAvailable = function() {
|
||||
// whether user needs to upload pw_auth
|
||||
return !authManager.getAuthParams().pw_auth;
|
||||
}
|
||||
|
||||
$scope.clickedSecurityUpdate = function() {
|
||||
if(!$scope.securityUpdateData) {
|
||||
$scope.securityUpdateData = {};
|
||||
}
|
||||
$scope.securityUpdateData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitSecurityUpdateForm = function() {
|
||||
$scope.securityUpdateData.processing = true;
|
||||
var authParams = authManager.getAuthParams();
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: $scope.securityUpdateData.password}, authParams), function(keys){
|
||||
if(keys.mk !== authManager.keys().mk) {
|
||||
alert("Invalid password. Please try again.");
|
||||
$timeout(function(){
|
||||
$scope.securityUpdateData.processing = false;
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
var tag = Neeto.crypto.calculateVerificationTag(authParams.pw_cost, authParams.pw_salt, keys.ak);
|
||||
authManager.uploadVerificationTag(tag, authParams);
|
||||
authManager.saveKeys(keys);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ class ExtensionManager {
|
||||
if(!this.extensionUsesEncryptedData(extension)) {
|
||||
keys = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, keys);
|
||||
var itemParams = new ItemParams(item, keys, this.authManager.encryptionVersion());
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
|
||||
@@ -93,35 +93,26 @@ class SNCrypto {
|
||||
return result;
|
||||
}
|
||||
|
||||
generateKeysFromMasterKey(mk) {
|
||||
var encryptionKey = Neeto.crypto.hmac256(mk, CryptoJS.enc.Utf8.parse("e").toString(CryptoJS.enc.Hex));
|
||||
var authKey = Neeto.crypto.hmac256(mk, CryptoJS.enc.Utf8.parse("a").toString(CryptoJS.enc.Hex));
|
||||
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(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk)));
|
||||
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 defaults = this.defaultPasswordGenerationParams();
|
||||
var {pw_func, pw_alg, pw_key_size, pw_cost} = defaults;
|
||||
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];
|
||||
calculateVerificationTag(cost, salt, ak) {
|
||||
return Neeto.crypto.hmac256([cost, salt].join(":"), ak);
|
||||
}
|
||||
|
||||
callback(_.merge({pw: pw, mk: mk}, this.generateKeysFromMasterKey(mk)), defaults);
|
||||
}.bind(this));
|
||||
}
|
||||
generateInitialEncryptionKeysForUser({email, password} = {}, callback) {
|
||||
var pw_cost = this.defaultPasswordGenerationCost();
|
||||
var pw_nonce = this.generateRandomKey(512);
|
||||
var pw_salt = this.sha1([email, pw_nonce].join(":"));
|
||||
this.generateSymmetricKeyPair({email: email, password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){
|
||||
var ak = keys[2];
|
||||
var pw_auth = this.calculateVerificationTag(pw_cost, pw_salt, ak);
|
||||
callback({pw: keys[0], mk: keys[1], ak: ak}, {pw_auth: pw_auth, pw_salt: pw_salt, pw_cost: pw_cost});
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
export { SNCrypto }
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
class SNCryptoJS extends SNCrypto {
|
||||
|
||||
/** Generates two deterministic keys based on one input */
|
||||
generateSymmetricKeyPair({password, pw_salt, pw_func, pw_alg, pw_cost, pw_key_size} = {}, callback) {
|
||||
var algMapping = {
|
||||
"sha256" : CryptoJS.algo.SHA256,
|
||||
"sha512" : CryptoJS.algo.SHA512
|
||||
}
|
||||
var fnMapping = {
|
||||
"pbkdf2" : CryptoJS.PBKDF2
|
||||
}
|
||||
|
||||
var alg = algMapping[pw_alg];
|
||||
var kdf = fnMapping[pw_func];
|
||||
var output = kdf(password, pw_salt, { keySize: pw_key_size/32, hasher: alg, iterations: pw_cost }).toString();
|
||||
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 firstHalf = output.slice(0, outputLength/2);
|
||||
var secondHalf = output.slice(outputLength/2, outputLength);
|
||||
callback([firstHalf, secondHalf])
|
||||
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])
|
||||
}
|
||||
|
||||
defaultPasswordGenerationParams() {
|
||||
return {pw_func: "pbkdf2", pw_alg: "sha512", pw_key_size: 512, pw_cost: 3000};
|
||||
defaultPasswordGenerationCost() {
|
||||
return 3000;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class EncryptionHelper {
|
||||
|
||||
static _private_encryptString(string, encryptionKey, authKey, version) {
|
||||
static _private_encryptString(string, encryptionKey, authKey, uuid, version) {
|
||||
var fullCiphertext, contentCiphertext;
|
||||
if(version === "001") {
|
||||
contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, null);
|
||||
@@ -8,9 +8,9 @@ class EncryptionHelper {
|
||||
} else {
|
||||
var iv = Neeto.crypto.generateRandomKey(128);
|
||||
contentCiphertext = Neeto.crypto.encryptText(string, encryptionKey, iv);
|
||||
var ciphertextToAuth = [version, iv, contentCiphertext].join(":");
|
||||
var ciphertextToAuth = [version, uuid, iv, contentCiphertext].join(":");
|
||||
var authHash = Neeto.crypto.hmac256(ciphertextToAuth, authKey);
|
||||
fullCiphertext = [version, authHash, iv, contentCiphertext].join(":");
|
||||
fullCiphertext = [version, authHash, uuid, iv, contentCiphertext].join(":");
|
||||
}
|
||||
|
||||
return fullCiphertext;
|
||||
@@ -24,13 +24,13 @@ class EncryptionHelper {
|
||||
// legacy
|
||||
params.enc_item_key = Neeto.crypto.encryptText(item_key, keys.mk, null);
|
||||
} else {
|
||||
params.enc_item_key = this._private_encryptString(item_key, keys.encryptionKey, keys.authKey, version);
|
||||
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, version);
|
||||
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;
|
||||
@@ -40,7 +40,7 @@ class EncryptionHelper {
|
||||
return params;
|
||||
}
|
||||
|
||||
static encryptionComponentsFromString(string, baseKey, encryptionKey, authKey) {
|
||||
static encryptionComponentsFromString(string, encryptionKey, authKey) {
|
||||
var encryptionVersion = string.substring(0, 3);
|
||||
if(encryptionVersion === "001") {
|
||||
return {
|
||||
@@ -49,7 +49,7 @@ class EncryptionHelper {
|
||||
ciphertextToAuth: string,
|
||||
iv: null,
|
||||
authHash: null,
|
||||
encryptionKey: baseKey,
|
||||
encryptionKey: encryptionKey,
|
||||
authKey: authKey
|
||||
}
|
||||
} else {
|
||||
@@ -57,9 +57,10 @@ class EncryptionHelper {
|
||||
return {
|
||||
encryptionVersion: components[0],
|
||||
authHash: components[1],
|
||||
iv: components[2],
|
||||
contentCiphertext: components[3],
|
||||
ciphertextToAuth: [components[0], components[2], components[3]].join(":"),
|
||||
uuid: components[2],
|
||||
iv: components[3],
|
||||
contentCiphertext: components[4],
|
||||
ciphertextToAuth: [components[0], components[2], components[3], components[4]].join(":"),
|
||||
encryptionKey: encryptionKey,
|
||||
authKey: authKey
|
||||
}
|
||||
@@ -75,21 +76,41 @@ class EncryptionHelper {
|
||||
encryptedItemKey = "001" + encryptedItemKey;
|
||||
requiresAuth = false;
|
||||
}
|
||||
var keyParams = this.encryptionComponentsFromString(encryptedItemKey, keys.mk, keys.encryptionKey, keys.authKey);
|
||||
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) {
|
||||
item.errorDecrypting = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var item_key = Neeto.crypto.decryptText(keyParams, requiresAuth);
|
||||
|
||||
if(!item_key) {
|
||||
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, ek, ak);
|
||||
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) {
|
||||
item.errorDecrypting = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!itemParams.authHash) {
|
||||
// legacy 001
|
||||
itemParams.authHash = item.auth_hash;
|
||||
}
|
||||
|
||||
var content = Neeto.crypto.decryptText(itemParams, true);
|
||||
if(!content) {
|
||||
item.errorDecrypting = true;
|
||||
}
|
||||
item.content = content;
|
||||
}
|
||||
|
||||
@@ -110,6 +131,7 @@ class EncryptionHelper {
|
||||
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
||||
}
|
||||
} catch (e) {
|
||||
item.errorDecrypting = true;
|
||||
if(throws) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ class SNCryptoWeb extends SNCrypto {
|
||||
/**
|
||||
Overrides
|
||||
*/
|
||||
defaultPasswordGenerationParams() {
|
||||
return {pw_func: "pbkdf2", pw_alg: "sha512", pw_key_size: 512, pw_cost: 5000};
|
||||
defaultPasswordGenerationCost() {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
/** Generates two deterministic keys based on one input */
|
||||
generateSymmetricKeyPair({password, pw_salt, pw_func, pw_alg, pw_cost, pw_key_size} = {}, callback) {
|
||||
this.stretchPassword({password: password, pw_func: pw_func, pw_alg: pw_alg, pw_salt: pw_salt, pw_cost: pw_cost, pw_key_size: pw_key_size}, function(output){
|
||||
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 firstHalf = output.slice(0, outputLength/2);
|
||||
var secondHalf = output.slice(outputLength/2, outputLength);
|
||||
callback([firstHalf, secondHalf]);
|
||||
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])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,9 +25,9 @@ class SNCryptoWeb extends SNCrypto {
|
||||
Internal
|
||||
*/
|
||||
|
||||
stretchPassword({password, pw_salt, pw_cost, pw_func, pw_alg, pw_key_size} = {}, callback) {
|
||||
stretchPassword({password, pw_salt, pw_cost} = {}, callback) {
|
||||
|
||||
this.webCryptoImportKey(password, pw_func, function(key){
|
||||
this.webCryptoImportKey(password, function(key){
|
||||
|
||||
if(!key) {
|
||||
console.log("Key is null, unable to continue");
|
||||
@@ -33,7 +35,7 @@ class SNCryptoWeb extends SNCrypto {
|
||||
return;
|
||||
}
|
||||
|
||||
this.webCryptoDeriveBits({key: key, pw_func: pw_func, pw_alg: pw_alg, pw_salt: pw_salt, pw_cost: pw_cost, pw_key_size: pw_key_size}, function(key){
|
||||
this.webCryptoDeriveBits({key: key, pw_salt: pw_salt, pw_cost: pw_cost}, function(key){
|
||||
if(!key) {
|
||||
callback(null);
|
||||
return;
|
||||
@@ -45,11 +47,11 @@ class SNCryptoWeb extends SNCrypto {
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
webCryptoImportKey(input, pw_func, callback) {
|
||||
webCryptoImportKey(input, callback) {
|
||||
subtleCrypto.importKey(
|
||||
"raw",
|
||||
this.stringToArrayBuffer(input),
|
||||
{name: pw_func.toUpperCase()},
|
||||
{name: "PBKDF2"},
|
||||
false,
|
||||
["deriveBits"]
|
||||
)
|
||||
@@ -62,21 +64,16 @@ class SNCryptoWeb extends SNCrypto {
|
||||
});
|
||||
}
|
||||
|
||||
webCryptoDeriveBits({key, pw_func, pw_alg, pw_salt, pw_cost, pw_key_size} = {}, callback) {
|
||||
var algMapping = {
|
||||
"sha256" : "SHA-256",
|
||||
"sha512" : "SHA-512",
|
||||
}
|
||||
var alg = algMapping[pw_alg];
|
||||
webCryptoDeriveBits({key, pw_salt, pw_cost} = {}, callback) {
|
||||
subtleCrypto.deriveBits(
|
||||
{
|
||||
"name": pw_func.toUpperCase(),
|
||||
"name": "PBKDF2",
|
||||
salt: this.stringToArrayBuffer(pw_salt),
|
||||
iterations: pw_cost,
|
||||
hash: {name: alg},
|
||||
hash: {name: "SHA-512"},
|
||||
},
|
||||
key,
|
||||
pw_key_size
|
||||
768
|
||||
)
|
||||
.then(function(bits){
|
||||
var key = this.arrayBufferToHexString(new Uint8Array(bits));
|
||||
|
||||
@@ -16,6 +16,10 @@ class HttpManager {
|
||||
this.httpRequest("post", url, params, onsuccess, onerror);
|
||||
}
|
||||
|
||||
patchAbsolute(url, params, onsuccess, onerror) {
|
||||
this.httpRequest("patch", url, params, onsuccess, onerror);
|
||||
}
|
||||
|
||||
getAbsolute(url, params, onsuccess, onerror) {
|
||||
this.httpRequest("get", url, params, onsuccess, onerror);
|
||||
}
|
||||
@@ -54,7 +58,7 @@ class HttpManager {
|
||||
this.setAuthHeadersForRequest(xmlhttp);
|
||||
xmlhttp.setRequestHeader('Content-type', 'application/json');
|
||||
|
||||
if(verb == "post") {
|
||||
if(verb == "post" || verb == "patch") {
|
||||
xmlhttp.send(JSON.stringify(params));
|
||||
} else {
|
||||
xmlhttp.send();
|
||||
|
||||
@@ -24,7 +24,7 @@ class SyncManager {
|
||||
|
||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||
var params = items.map(function(item) {
|
||||
var itemParams = new ItemParams(item, null);
|
||||
var itemParams = new ItemParams(item, null, this.authManager.encryptionVersion());
|
||||
itemParams = itemParams.paramsForLocalStorage();
|
||||
if(offlineOnly) {
|
||||
delete itemParams.dirty;
|
||||
@@ -142,7 +142,7 @@ class SyncManager {
|
||||
sync(callback, options = {}) {
|
||||
|
||||
var allDirtyItems = this.modelManager.getDirtyItems();
|
||||
|
||||
|
||||
if(this.syncStatus.syncOpInProgress) {
|
||||
this.repeatOnCompletion = true;
|
||||
if(callback) {
|
||||
@@ -189,7 +189,7 @@ class SyncManager {
|
||||
var params = {};
|
||||
params.limit = 150;
|
||||
params.items = _.map(subItems, function(item){
|
||||
var itemParams = new ItemParams(item, this.authManager.keys());
|
||||
var itemParams = new ItemParams(item, this.authManager.keys(), this.authManager.encryptionVersion());
|
||||
itemParams.additionalFields = options.additionalFields;
|
||||
return itemParams.paramsForSync();
|
||||
}.bind(this));
|
||||
|
||||
@@ -54,15 +54,7 @@
|
||||
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
|
||||
%p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
|
||||
|
||||
%a.block.mt-15{"href" => "{{dashboardURL()}}", "target" => "_blank"} Data Dashboard
|
||||
%a.block.mt-5{"ng-click" => "showCredentials = !showCredentials"} Show Credentials
|
||||
%section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"}
|
||||
%label.block
|
||||
Encryption key:
|
||||
.wrap.normal.mt-1.selectable {{encryptionKey()}}
|
||||
%label.block.mt-5.mb-0
|
||||
Server password:
|
||||
.wrap.normal.mt-1.selectable {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}}
|
||||
|
||||
|
||||
%a.block.mt-5{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password
|
||||
%section.gray-bg.mt-10.medium-padding{"ng-if" => "newPasswordData.changePassword"}
|
||||
@@ -81,6 +73,45 @@
|
||||
%button.ui-button.block{"ng-click" => "submitPasswordChange()"} Submit
|
||||
%p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}}
|
||||
|
||||
|
||||
|
||||
|
||||
%a.block.mt-5{"ng-click" => "showAdvanced = !showAdvanced"} Advanced
|
||||
%div{"ng-if" => "showAdvanced"}
|
||||
%a.block.mt-15{"href" => "{{dashboardURL()}}", "target" => "_blank"} Data Dashboard
|
||||
%a.block.mt-5{"ng-click" => "reencryptPressed()"} Re-encrypt All Items
|
||||
%a.block.mt-5{"ng-click" => "showCredentials = !showCredentials"} Show Credentials
|
||||
%section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"}
|
||||
%label.block
|
||||
Encryption key:
|
||||
.wrap.normal.mt-1.selectable {{encryptionKey()}}
|
||||
%label.block.mt-5.mb-0
|
||||
Server password:
|
||||
.wrap.normal.mt-1.selectable {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}}
|
||||
%label.block.mt-5.mb-0
|
||||
Authentication key:
|
||||
.wrap.normal.mt-1.selectable {{authKey() ? authKey() : 'Not available. Sign out then sign back in to compute.'}}
|
||||
|
||||
|
||||
%div{"ng-if" => "securityUpdateAvailable()"}
|
||||
%a.block.mt-5{"ng-click" => "clickedSecurityUpdate()"} Security Update Available
|
||||
%section.gray-bg.mt-10.medium-padding{"ng-if" => "securityUpdateData.showForm"}
|
||||
%p
|
||||
A new security feature is available that adds an additional level of verification to your sign-ins.
|
||||
This feature assures that when you login, the server cannot tamper or modify your authentication parameters.
|
||||
%a{"href" => "https://standardnotes.org/verification", "target" => "_blank"} Learn more.
|
||||
%div.mt-10{"ng-if" => "!securityUpdateData.processing"}
|
||||
%p.bold Enter your password to update:
|
||||
%form.mt-5
|
||||
%input.form-control{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"}
|
||||
%button.ui-button.block{"ng-click" => "submitSecurityUpdateForm()"} Update
|
||||
%div.mt-5{"ng-if" => "securityUpdateData.processing"}
|
||||
%p.blue Processing...
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.medium-v-space
|
||||
|
||||
%h4 Local Encryption
|
||||
@@ -106,9 +137,9 @@
|
||||
.fake-link Import Data from Archive
|
||||
|
||||
%div{"ng-if" => "importData.requestPassword"}
|
||||
Enter the account password associated with the import file.
|
||||
%input{:type => 'password', "ng-model" => "importData.password"}
|
||||
%button{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
||||
%p Enter the account password associated with the import file.
|
||||
%input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password"}
|
||||
%button.standard.ui-button.block.blue.mt-5{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
||||
|
||||
%p.mt-5{"ng-if" => "user"} Notes are downloaded in the Standard File format, which allows you to re-import back into this app easily. To download as plain text files, choose "Decrypted".
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.section.editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen}"}
|
||||
#editor-title-bar.section-title-bar{"ng-if" => "ctrl.note", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"}
|
||||
#editor-title-bar.section-title-bar{"ng-if" => "ctrl.note && !ctrl.note.errorDecrypting", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"}
|
||||
.title
|
||||
%input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
|
||||
"ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()",
|
||||
@@ -32,12 +32,16 @@
|
||||
%label{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false; ctrl.showEditorMenu = false;"} Actions
|
||||
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
|
||||
|
||||
.editor-content{"ng-if" => "ctrl.noteReady", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"}
|
||||
.editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"}
|
||||
%iframe#editor-iframe{"ng-if" => "ctrl.editor && !ctrl.editor.systemEditor", "ng-src" => "{{ctrl.editor.url | trusted}}", "frameBorder" => "0", "style" => "width: 100%;"}
|
||||
Loading
|
||||
%textarea.editable#note-text-editor{"ng-if" => "!ctrl.editor || ctrl.editor.systemEditor", "ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-model" => "ctrl.note.text",
|
||||
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"}
|
||||
|
||||
|
||||
%section.section{"ng-if" => "ctrl.note.errorDecrypting"}
|
||||
%p.medium-padding{"style" => "padding-top: 0 !important;"} There was an error decrypting this item. Ensure you are running the latest version of this app, then sign out and sign back in to try again.
|
||||
|
||||
#editor-pane-component-stack
|
||||
.component.component-stack-border{"ng-repeat" => "component in ctrl.componentStack", "ng-if" => "component.active", "ng-show" => "!component.ignoreEvents", "id" => "{{'component-' + component.uuid}}", "ng-mouseover" => "component.showExit = true", "ng-mouseleave" => "component.showExit = false"}
|
||||
.exit-button.body-text-color{"ng-if" => "component.showExit", "ng-click" => "ctrl.disableComponent(component)"} ×
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | orderBy: ctrl.sortBy:true | limitTo:ctrl.notesToDisplay))",
|
||||
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
|
||||
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy
|
||||
%strong.red.medium{"ng-if" => "note.errorDecrypting"} Error decrypting
|
||||
.name{"ng-if" => "note.title"}
|
||||
{{note.title}}
|
||||
.note-preview
|
||||
|
||||
Reference in New Issue
Block a user