diff --git a/.jshintrc b/.jshintrc index 57512cb3..9cb6ee00 100644 --- a/.jshintrc +++ b/.jshintrc @@ -20,7 +20,8 @@ "jquery": true, "predef": [ "_", - "angular", + "angular", + "bs58", "BigNumber", "iso4217", "Modernizr", diff --git a/app/index.html b/app/index.html index 93ed45e2..48844d01 100644 --- a/app/index.html +++ b/app/index.html @@ -133,6 +133,7 @@

Loading...

+ diff --git a/app/scripts/controllers/recovery-v2-controller.js b/app/scripts/controllers/recovery-v2-controller.js index ef14a5df..408ac11b 100644 --- a/app/scripts/controllers/recovery-v2-controller.js +++ b/app/scripts/controllers/recovery-v2-controller.js @@ -36,6 +36,7 @@ angular.module('stellarClient').controller('RecoveryV2Ctrl', function($scope, $s return $q.when(params) .then(validate) + .then(getServerRecoveryCode) .then(recover) .then(function (params) { $state.go('change_password_v2', { @@ -61,19 +62,63 @@ angular.module('stellarClient').controller('RecoveryV2Ctrl', function($scope, $s return $q.reject(); } - // Append suffix - params.username = params.username+'@stellar.org'; - return params; } + function getServerRecoveryCode(params) { + var deferred = $q.defer(); + + var data = { + username: params.username, + userRecoveryCode: params.recoveryCode + }; + + $http.post(Options.API_SERVER + '/user/recover', data) + .success(function(body) { + if (body.data && body.data.serverRecoveryCode) { + params.serverRecoveryCode = body.data.serverRecoveryCode; + deferred.resolve(params); + } else { + $scope.recoveryError = 'An error occurred.'; + deferred.reject(); + } + }) + .error(function(body, status) { + switch(status) { + case 400: + if (body.code === 'invalid') { + $scope.recoveryError = 'Invalid username or recovery code.'; + } else if (body.code === 'disabled') { + $scope.recoveryError = 'Recovery has been disabled for this account.'; + } + break; + case 0: + $scope.recoveryError = 'Unable to contact the server.'; + break; + default: + $scope.recoveryError = 'An error occurred.'; + } + deferred.reject(); + }); + + return deferred.promise; + } + function recover(params) { var deferred = $q.defer(); + // Append domain + params.username += '@stellar.org'; + + var userPartBytes = bs58.decode(params.recoveryCode); + var serverPartBytes = bs58.decode(params.serverRecoveryCode); + var fullRecoveryCodeBytes = userPartBytes.concat(serverPartBytes); + var fullRecoveryCode = bs58.encode(fullRecoveryCodeBytes); + var data = { server: Options.WALLET_SERVER+'/v2', username: params.username, - recoveryCode: params.recoveryCode + recoveryCode: fullRecoveryCode }; if ($scope.totpRequired) { diff --git a/app/scripts/controllers/settings-controller.js b/app/scripts/controllers/settings-controller.js index 10c4bdf5..4f0f4ab4 100644 --- a/app/scripts/controllers/settings-controller.js +++ b/app/scripts/controllers/settings-controller.js @@ -14,7 +14,7 @@ angular.module('stellarClient').controller('SettingsCtrl', function($scope, $htt $scope.handleServerError = function (element) { return function (error) { - var message = error.status === 'fail' ? error.message : 'Server error'; + var message = error.data.status === 'fail' ? error.data.message : 'Server error'; Util.showTooltip(element, message, 'error', 'top'); }; }; diff --git a/app/scripts/controllers/settings-email-controller.js b/app/scripts/controllers/settings-email-controller.js index b90886d9..860cc127 100644 --- a/app/scripts/controllers/settings-email-controller.js +++ b/app/scripts/controllers/settings-email-controller.js @@ -1,8 +1,9 @@ -angular.module('stellarClient').controller('SettingsEmailCtrl', function($scope, $http, session, singletonPromise) { +angular.module('stellarClient').controller('SettingsEmailCtrl', function($scope, $http, $q, session, singletonPromise) { $scope.$on('settings-refresh', function () { $scope.email = session.getUser().getEmailAddress(); $scope.emailVerified = session.getUser().isEmailVerified(); + $scope.verifyToken = null; $scope.resetEmailState(); }); @@ -27,36 +28,52 @@ angular.module('stellarClient').controller('SettingsEmailCtrl', function($scope, return changeEmail(); } else if ($scope.emailState === 'verify') { return verifyEmail(); - } else { - return; } }); function verifyEmail () { var verifyToken = $scope.verifyToken; return session.getUser().verifyEmail(verifyToken) - .then(function (response) { - if (response.data.data && response.data.data.serverRecoveryCode) { - return session.get('wallet').storeRecoveryData(verifyToken, response.data.data.serverRecoveryCode); - } - }) - .then(function () { - return $scope.refreshAndInitialize(); - }) - .then(function () { - $scope.verifyToken = null; - }) - .catch($scope.handleServerError($('#email-input'))); + .then(function (response) { + if (response.data.data && response.data.data.serverRecoveryCode) { + var userPartBytes = bs58.decode(verifyToken); + var serverPartBytes = bs58.decode(response.data.data.serverRecoveryCode); + var fullRecoveryCodeBytes = userPartBytes.concat(serverPartBytes); + return bs58.encode(fullRecoveryCodeBytes); + } else { + return $q.reject(); + } + }) + .then(function(fullRecoveryCode) { + var wallet = session.get('wallet'); + return wallet.walletV2.enableRecovery({ + recoveryCode: fullRecoveryCode, + secretKey: wallet.keychainData.signingKeys.secretKey + }); + }) + .then(function () { + return $scope.$parent.refreshAndInitialize(); + }) + .then(function () { + $scope.verifyToken = null; + }) + .catch(StellarWallet.errors.ConnectionError, function(e) { + Util.showTooltip($('#verify-input'), 'Error connecting wallet server.', 'error', 'top'); + }) + .catch($scope.$parent.handleServerError($('#verify-input'))) + .finally(function() { + $scope.$apply(); + }); } function changeEmail () { return session.getUser().changeEmail($scope.newEmail) - .then(function () { - return $scope.refreshAndInitialize(); - }) - .then(function () { - $scope.newEmail = null; - }) - .catch($scope.handleServerError($('#verify-input'))); + .then(function () { + return $scope.$parent.refreshAndInitialize(); + }) + .then(function () { + $scope.newEmail = null; + }) + .catch($scope.$parent.handleServerError($('#email-input'))); } }); diff --git a/app/scripts/controllers/settings-recovery-controller.js b/app/scripts/controllers/settings-recovery-controller.js index ed003972..81f38810 100644 --- a/app/scripts/controllers/settings-recovery-controller.js +++ b/app/scripts/controllers/settings-recovery-controller.js @@ -1,58 +1,133 @@ 'use strict'; -angular.module('stellarClient').controller('SettingsRecoveryCtrl', function($scope, session) { - var wallet = session.get('wallet').walletV2; +angular.module('stellarClient').controller('SettingsRecoveryCtrl', function($scope, $http, session, stellarApi, UserPrivateInfo, FlashMessages) { + var wallet = session.get('wallet'); + var params = { + username: session.get('username'), + updateToken: wallet.keychainData.updateToken + }; + + var userRecoveryCode = null; + var serverRecoveryCode = null; $scope.reset = function () { $scope.error = null; - $scope.enabling = false; + $scope.resetting = false; + $scope.sendingRecoveryCode = false; $scope.code = null; + + stellarApi.User.getNewRecoveryCode(params) + .then(function(response) { + if (response.data.status === 'success') { + $scope.resetting = true; + userRecoveryCode = response.data.userRecoveryCode; + serverRecoveryCode = response.data.serverRecoveryCode; + } else { + $scope.resetting = false; + } + }); }; $scope.reset(); $scope.$on('settings-refresh', $scope.reset); $scope.$on('settings-recovery-clicked', function($event, toggle) { - if (toggle.on) { - disableRecovery(); - } else { - enableRecovery(); - } + toggleRecovery(!toggle.on); }); - function disableRecovery() { - $scope.enabling = false; - $scope.$emit('settings-recovery-toggled', false); + function toggleRecovery(value) { + $http.post(Options.API_SERVER+'/user/setrecover', _.extend(params, { + recover: value + })).success(function () { + $scope.$emit('settings-recovery-toggled', value); + }).error(function () { + FlashMessages.add({ + title: 'Server error', + info: 'There was an error contacting server. Please try again later.', + type: 'error' + }); + }); } - var recoveryCode = null; - $scope.recoveryCode = null; // Temp + $scope.resetRecovery = function($event) { + $event.preventDefault(); + $scope.sendingRecoveryCode = true; - function enableRecovery() { - $scope.enabling = true; - recoveryCode = StellarWallet.util.generateRandomRecoveryCode(); - $scope.recoveryCode = recoveryCode; // temp - } + stellarApi.User.changeRecoveryToken(params) + .success(function() { + stellarApi.User.getNewRecoveryCode(params) + .then(function(response) { + if (response.data.status === 'success') { + $scope.resetting = true; + userRecoveryCode = response.data.userRecoveryCode; + serverRecoveryCode = response.data.serverRecoveryCode; + } + }); + }).error(function (response){ + $scope.resetting = false; + if (response.code === 'no_email') { + FlashMessages.add({ + title: 'No email added', + info: 'Add and verify your email first. Recovery token will be sent to your email inbox.' + }); + } else { + FlashMessages.add({ + title: 'Recovery Error', + info: 'There was error trying to generate recovery token. Please try again later.', + type: 'error' + }); + } + }).finally(function() { + $scope.sendingRecoveryCode = false; + }); + }; - $scope.confirmEnableRecovery = function($event) { + $scope.confirmResetRecovery = function($event) { $event.preventDefault(); - if ($scope.code === recoveryCode) { - wallet.enableRecovery({ - secretKey: session.get('wallet').keychainData.signingKeys.secretKey, - recoveryCode: $scope.code - }).then(function() { - recoveryCode = null; - $scope.code = null; - $scope.recoveryCode = null; // Temp - $scope.enabling = false; - $scope.$emit('settings-recovery-toggled', true); - if (session.isPersistent()) { - session.get('wallet').saveLocal(); // We need to rewrite wallet object because lockVersion has changed - } - }).finally(function() { - $scope.$apply(); - }); // TODO handle errors - } else { + + if ($scope.code !== userRecoveryCode) { $scope.error = 'Incorrect recovery code. Please try again.'; + return; } + + var userPartBytes = bs58.decode($scope.code); + var serverPartBytes = bs58.decode(serverRecoveryCode); + var fullRecoveryCodeBytes = userPartBytes.concat(serverPartBytes); + var fullRecoveryCode = bs58.encode(fullRecoveryCodeBytes); + wallet.walletV2.enableRecovery({ + recoveryCode: fullRecoveryCode, + secretKey: wallet.keychainData.signingKeys.secretKey + }).then(function() { + return stellarApi.User.finishChangeRecoveryToken(_.extend(params, { + userRecoveryCode: $scope.code + })); + }).then(function() { + $scope.code = null; + $scope.resetting = false; + FlashMessages.add({ + title: 'Success', + info: 'Your recovery token has been reset!' + }); + if (session.isPersistent()) { + session.get('wallet').saveLocal(); // We need to rewrite wallet object because lockVersion has changed + } + }).catch(StellarWallet.errors.ConnectionError, function(e) { + $scope.error = 'Connection error. Please try again.'; + }).catch(function(e) { + $scope.error = 'Unknown error. Please try again.'; + // TODO add logging + }).finally(function() { + $scope.$apply(); + }); + }; + + $scope.cancelResetRecovery = function($event) { + $event.preventDefault(); + + stellarApi.User.cancelChangingRecoveryCode(params) + .success(function() { + $scope.resetting = false; + }).error(function() { + $scope.error = 'Cannot cancel changing recovery code now. Please try again.'; + }); }; }); \ No newline at end of file diff --git a/app/scripts/libraries/bs58.js b/app/scripts/libraries/bs58.js new file mode 100644 index 00000000..1bc0505f --- /dev/null +++ b/app/scripts/libraries/bs58.js @@ -0,0 +1,82 @@ +// Base58 encoding/decoding +// Originally written by Mike Hearn for BitcoinJ +// Copyright (c) 2011 Google Inc +// Ported to JavaScript by Stefan Thomas +// Merged Buffer refactorings from base58-native by Stephen Pair +// Copyright (c) 2013 BitPay Inc + +var bs58 = function() { +var ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +var ALPHABET_MAP = {} +for(var i = 0; i < ALPHABET.length; i++) { + ALPHABET_MAP[ALPHABET.charAt(i)] = i +} +var BASE = 58 + +function encode(buffer) { + if (buffer.length === 0) return '' + + var i, j, digits = [0] + for (i = 0; i < buffer.length; i++) { + for (j = 0; j < digits.length; j++) digits[j] <<= 8 + + digits[0] += buffer[i] + + var carry = 0 + for (j = 0; j < digits.length; ++j) { + digits[j] += carry + + carry = (digits[j] / BASE) | 0 + digits[j] %= BASE + } + + while (carry) { + digits.push(carry % BASE) + + carry = (carry / BASE) | 0 + } + } + + // deal with leading zeros + for (i = 0; buffer[i] === 0 && i < buffer.length - 1; i++) digits.push(0) + + return digits.reverse().map(function(digit) { return ALPHABET[digit] }).join('') +} + +function decode(string) { + if (string.length === 0) return [] + + var i, j, bytes = [0] + for (i = 0; i < string.length; i++) { + var c = string[i] + if (!(c in ALPHABET_MAP)) throw new Error('Non-base58 character') + + for (j = 0; j < bytes.length; j++) bytes[j] *= BASE + bytes[0] += ALPHABET_MAP[c] + + var carry = 0 + for (j = 0; j < bytes.length; ++j) { + bytes[j] += carry + + carry = bytes[j] >> 8 + bytes[j] &= 0xff + } + + while (carry) { + bytes.push(carry & 0xff) + + carry >>= 8 + } + } + + // deal with leading zeros + for (i = 0; string[i] === '1' && i < string.length - 1; i++) bytes.push(0) + + return bytes.reverse() +} + +return { + encode: encode, + decode: decode +}; +}(); diff --git a/app/scripts/modules/stellar-api/user.js b/app/scripts/modules/stellar-api/user.js index f9cb9a3e..43a68489 100644 --- a/app/scripts/modules/stellar-api/user.js +++ b/app/scripts/modules/stellar-api/user.js @@ -2,7 +2,23 @@ var api = angular.module('stellarApi'); api.service('User', function(http) { var User = {}; - + + User.changeRecoveryToken = function(params) { + return http.post('/user/changeRecoveryToken', {username: params.username, updateToken: params.updateToken}); + }; + + User.getNewRecoveryCode = function(params) { + return http.get('/user/getNewRecoveryCode', {params: {username: params.username, updateToken: params.updateToken}}); + }; + + User.cancelChangingRecoveryCode = function(params) { + return http.post('/user/cancelChangingRecoveryToken', {username: params.username, updateToken: params.updateToken}); + }; + + User.finishChangeRecoveryToken = function(params) { + return http.post('/user/finishChangeRecoveryToken', {username: params.username, updateToken: params.updateToken, userRecoveryCode: params.userRecoveryCode}); + }; + User.validateUsername = function(username) { return http.post('/user/validname', {username: username}); }; diff --git a/app/scripts/utilities/wallet.js b/app/scripts/utilities/wallet.js index c120e257..75825b30 100644 --- a/app/scripts/utilities/wallet.js +++ b/app/scripts/utilities/wallet.js @@ -226,35 +226,6 @@ angular.module('stellarClient').factory('Wallet', function($q, $http, ipCookie) return this; }; - Wallet.prototype.storeRecoveryData = function (userRecoveryCode, serverRecoveryCode) { - var recoveryId = Wallet.deriveId(userRecoveryCode, serverRecoveryCode); - var recoveryKey = Wallet.deriveKey(recoveryId, userRecoveryCode, serverRecoveryCode); - - var data = this.createRecoveryData(recoveryId, recoveryKey); - - return $http.post(Options.WALLET_SERVER + '/wallets/create_recovery_data', data); - }; - - /** - * Encrypts the wallet's id and key into the recoveryData and sets its the recoveryId. - * - * @param {string} recoveryId - * @param {string} recoveryKey - * @memberOf Wallet - */ - Wallet.prototype.createRecoveryData = function(recoveryId, recoveryKey){ - var rawRecoveryKey = sjcl.codec.hex.toBits(recoveryKey); - var recoveryData = Wallet.encryptData({id: this.id, key: this.key}, rawRecoveryKey); - - return { - id: this.id, - authToken: this.keychainData.authToken, - recoveryId: recoveryId, - recoveryData: recoveryData, - recoveryDataHash: sjcl.codec.hex.fromBits(sjcl.hash.sha1.hash(recoveryData)) - }; - }; - /** * Encrypts the wallet data into a generic object. * diff --git a/app/templates/settings-recovery.html b/app/templates/settings-recovery.html index b37b331a..8bf4b77f 100644 --- a/app/templates/settings-recovery.html +++ b/app/templates/settings-recovery.html @@ -1,20 +1,33 @@ -
+
+ Sending recovery code to your email... +
+ +
+ reset +
+ +
- We've just sent a new code to your email. Enter your code below: ({{ recoveryCode }}) + We've sent a code to your email. Enter your code below:
{{ error }}
-
+
-
+
-
+
+
+
+
+ Cancel +