diff --git a/app/js/controllers.js b/app/js/controllers.js index edee7fa8..b30bb006 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -30,7 +30,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) LayoutSwitchService.start(); }) - .controller('AppLoginController', function ($scope, $rootScope, $location, $timeout, $modal, $modalStack, MtpApiManager, ErrorService, NotificationsManager, ChangelogNotifyService, IdleManager, LayoutSwitchService, TelegramMeWebService, _) { + .controller('AppLoginController', function ($scope, $rootScope, $location, $timeout, $modal, $modalStack, MtpApiManager, ErrorService, NotificationsManager, PasswordManager, ChangelogNotifyService, IdleManager, LayoutSwitchService, TelegramMeWebService, _) { $modalStack.dismissAll(); IdleManager.start(); @@ -156,6 +156,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) var callTimeout; + var updatePasswordTimeout = false; function saveAuth (result) { MtpApiManager.setUserAuth(options.dcID, { @@ -204,7 +205,8 @@ angular.module('myApp.controllers', ['myApp.i18n']) phone_number: $scope.credentials.phone_full, // sms_type: 5, api_id: Config.App.id, - api_hash: Config.App.hash + api_hash: Config.App.hash, + lang_code: navigator.language || 'en' }, options).then(function (sentCode) { $scope.progress.enabled = false; @@ -290,6 +292,16 @@ angular.module('myApp.controllers', ['myApp.i18n']) } else if (error.code == 400 && error.type == 'PHONE_NUMBER_OCCUPIED') { error.handled = true; return $scope.logIn(false); + } else if (error.code == 401 && error.type == 'SESSION_PASSWORD_NEEDED') { + $scope.progress.enabled = true; + updatePasswordState().then(function () { + $scope.progress.enabled = false; + $scope.credentials.phone_code_valid = true; + $scope.credentials.password_needed = true; + $scope.about = {}; + }); + error.handled = true; + return; } @@ -312,6 +324,82 @@ angular.module('myApp.controllers', ['myApp.i18n']) }; + $scope.checkPassword = function () { + return PasswordManager.check($scope.password, $scope.credentials.password, options).then(saveAuth, function (error) { + switch (error.type) { + case 'PASSWORD_HASH_INVALID': + $scope.error = {field: 'password'}; + error.handled = true; + break; + } + }); + }; + + $scope.forgotPassword = function (event) { + PasswordManager.requestRecovery($scope.password, options).then(function (emailRecovery) { + + var scope = $rootScope.$new(); + scope.recovery = emailRecovery; + scope.options = options; + var modal = $modal.open({ + scope: scope, + templateUrl: templateUrl('password_recovery_modal'), + controller: 'PasswordRecoveryModalController', + windowClass: 'md_simple_modal_window mobile_modal' + }); + + modal.result.then(function (result) { + if (result && result.user) { + saveAuth(result); + } else { + $scope.canReset = true; + } + }); + + }, function (error) { + switch (error.type) { + case 'PASSWORD_EMPTY': + $scope.logIn(); + break; + case 'PASSWORD_RECOVERY_NA': + $timeout(function () { + $scope.canReset = true; + }, 1000); + break; + } + }) + + return cancelEvent(event); + }; + + $scope.resetAccount = function () { + ErrorService.confirm({ + type: 'RESET_ACCOUNT' + }).then(function () { + $scope.progress.enabled = true; + MtpApiManager.invokeApi('account.deleteAccount', { + reason: 'Forgot password' + }, options).then(function () { + delete $scope.progress.enabled; + delete $scope.credentials.password_needed; + $scope.credentials.phone_unoccupied = true; + }, function () { + delete $scope.progress.enabled; + }) + }); + }; + + function updatePasswordState () { + // $timeout.cancel(updatePasswordTimeout); + // updatePasswordTimeout = false; + return PasswordManager.getState(options).then(function (result) { + return $scope.password = result; + // if (result._ == 'account.noPassword' && result.email_unconfirmed_pattern) { + // updatePasswordTimeout = $timeout(updatePasswordState, 5000); + // } + }); + } + ChangelogNotifyService.checkUpdate(); LayoutSwitchService.start(); }) @@ -364,6 +452,8 @@ angular.module('myApp.controllers', ['myApp.i18n']) }); }; + // setTimeout($scope.openSettings, 1000); + $scope.openFaq = function () { var url = 'https://telegram.org/faq'; switch (Config.I18n.locale) { @@ -2535,10 +2625,17 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.password = {_: 'account.noPassword'}; updatePasswordState(); + var updatePasswordTimeout = false; $scope.changePassword = function (options) { options = options || {}; + if (options.action == 'cancel_email') { + return ErrorService.confirm({type: 'PASSWORD_ABORT_SETUP'}).then(function () { + PasswordManager.updateSettings($scope.password, {email: ''}).then(updatePasswordState); + }); + } var scope = $rootScope.$new(); + scope.password = $scope.password; angular.extend(scope, options); var modal = $modal.open({ scope: scope, @@ -2550,9 +2647,14 @@ angular.module('myApp.controllers', ['myApp.i18n']) modal.result['finally'](updatePasswordState); }; - function updatePasswordState (argument) { - PasswordManager.getPasswordState().then(function (result) { + function updatePasswordState () { + $timeout.cancel(updatePasswordTimeout); + updatePasswordTimeout = false; + PasswordManager.getState().then(function (result) { $scope.password = result; + if (result._ == 'account.noPassword' && result.email_unconfirmed_pattern) { + updatePasswordTimeout = $timeout(updatePasswordState, 5000); + } }); } @@ -2882,7 +2984,141 @@ angular.module('myApp.controllers', ['myApp.i18n']) }) }) - .controller('PasswordUpdateModalController', function ($scope, PasswordManager, MtpApiManager) { + .controller('PasswordUpdateModalController', function ($scope, $q, _, PasswordManager, MtpApiManager, ErrorService, $modalInstance) { + + $scope.passwordSettings = {}; + + $scope.updatePassword = function () { + delete $scope.passwordSettings.error_field; + + var confirmPromise; + if ($scope.action == 'disable') { + confirmPromise = $q.when(); + } + else { + if (!$scope.passwordSettings.new_password) { + $scope.passwordSettings.error_field = 'new_password'; + $scope.$broadcast('new_password_focus'); + return false; + } + if ($scope.passwordSettings.new_password != $scope.passwordSettings.confirm_password) { + $scope.passwordSettings.error_field = 'confirm_password'; + $scope.$broadcast('confirm_password_focus'); + return false; + } + confirmPromise = $scope.passwordSettings.email + ? $q.when() + : ErrorService.confirm({type: 'RECOVERY_EMAIL_EMPTY'}); + } + + $scope.passwordSettings.loading = true; + + confirmPromise.then(function () { + PasswordManager.updateSettings($scope.password, { + cur_password: $scope.passwordSettings.cur_password || '', + new_password: $scope.passwordSettings.new_password, + email: $scope.passwordSettings.email, + hint: $scope.passwordSettings.hint + }).then(function (result) { + delete $scope.passwordSettings.loading; + $modalInstance.close(true); + if ($scope.action == 'disable') { + ErrorService.alert( + _('error_modal_password_disabled_title'), + _('error_modal_password_disabled_descripion') + ); + } else { + ErrorService.alert( + _('error_modal_password_success_title'), + _('error_modal_password_success_descripion') + ); + } + }, function (error) { + switch (error.type) { + case 'PASSWORD_HASH_INVALID': + case 'NEW_PASSWORD_BAD': + $scope.passwordSettings.error_field = 'cur_password'; + error.handled = true; + $scope.$broadcast('cur_password_focus'); + break; + case 'NEW_PASSWORD_BAD': + $scope.passwordSettings.error_field = 'new_password'; + error.handled = true; + break; + case 'EMAIL_INVALID': + $scope.passwordSettings.error_field = 'email'; + error.handled = true; + break; + case 'EMAIL_UNCONFIRMED': + ErrorService.alert( + _('error_modal_email_unconfirmed_title'), + _('error_modal_email_unconfirmed_descripion') + ); + $modalInstance.close(true); + error.handled = true; + break; + } + delete $scope.passwordSettings.loading; + }); + }) + } + + switch ($scope.action) { + case 'disable': + $scope.passwordSettings.new_password = ''; + break; + case 'create': + onContentLoaded(function () { + $scope.$broadcast('new_password_focus'); + }); + break; + + } + + $scope.$watch('passwordSettings.new_password', function (newValue) { + var len = newValue && newValue.length || 0; + if (!len) { + $scope.passwordSettings.hint = ''; + } + else if (len <= 3) { + $scope.passwordSettings.hint = '***'; + } + else { + $scope.passwordSettings.hint = newValue.charAt(0) + (new Array(len - 1)).join('*') + newValue.charAt(len - 1); + } + $scope.$broadcast('value_updated'); + }) + }) + + .controller('PasswordRecoveryModalController', function ($scope, $q, _, PasswordManager, MtpApiManager, ErrorService, $modalInstance) { + + $scope.checkCode = function () { + $scope.recovery.updating = true; + + PasswordManager.recover($scope.recovery.code, $scope.options).then(function (result) { + ErrorService.alert( + _('error_modal_password_disabled_title'), + _('error_modal_password_disabled_descripion') + ); + $modalInstance.close(result); + }, function (error) { + delete $scope.recovery.updating; + switch (error.type) { + case 'CODE_EMPTY': + case 'CODE_INVALID': + $scope.recovery.error_field = 'code'; + error.handled = true; + break; + + case 'PASSWORD_EMPTY': + case 'PASSWORD_RECOVERY_NA': + case 'PASSWORD_RECOVERY_EXPIRED': + $modalInstance.dismiss(); + error.handled = true; + break; + } + }); + }; }) diff --git a/app/js/directives.js b/app/js/directives.js index 13b6c596..8139bee8 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -2840,7 +2840,7 @@ angular.module('myApp.directives', ['myApp.filters']) }); } - $scope.$on('value_updated', function (event, args) { + $scope.$on('value_updated', function () { setZeroTimeout(function () { updateHasValueClass(); }); @@ -2858,6 +2858,7 @@ angular.module('myApp.directives', ['myApp.filters']) element.on('keydown', function (event) { if (event.keyCode == 13) { element.trigger('submit'); + return cancelEvent(event); } }); }; diff --git a/app/js/lib/ng_utils.js b/app/js/lib/ng_utils.js index 5cda1646..1199e87e 100644 --- a/app/js/lib/ng_utils.js +++ b/app/js/lib/ng_utils.js @@ -651,7 +651,7 @@ angular.module('izhukov.utils', []) awaiting = {}, webCrypto = Config.Modes.webcrypto && window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle)/* || window.msCrypto && window.msCrypto.subtle*/, useSha1Crypto = webCrypto && webCrypto.digest !== undefined, - useSha2Crypto = webCrypto && webCrypto.digest !== undefined, + useSha256Crypto = webCrypto && webCrypto.digest !== undefined, finalizeTask = function (taskID, result) { var deferred = awaiting[taskID]; if (deferred !== undefined) { @@ -729,8 +729,8 @@ angular.module('izhukov.utils', []) return sha1HashSync(bytes); }); }, - sha2Hash: function (bytes) { - if (useSha2Crypto) { + sha256Hash: function (bytes) { + if (useSha256Crypto) { var deferred = $q.defer(), bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes; // console.log(dT(), 'Native sha1 start'); @@ -739,7 +739,7 @@ angular.module('izhukov.utils', []) deferred.resolve(digest); }, function (e) { console.error('Crypto digest error', e); - useSha2Crypto = false; + useSha256Crypto = false; deferred.resolve(sha256HashSync(bytes)); }); diff --git a/app/js/lib/tl_utils.js b/app/js/lib/tl_utils.js index 79147668..c9e4c0af 100644 --- a/app/js/lib/tl_utils.js +++ b/app/js/lib/tl_utils.js @@ -122,6 +122,9 @@ TLSerialization.prototype.storeDouble = function (f) { TLSerialization.prototype.storeString = function (s, field) { this.debug && console.log('>>>', s, (field || '') + ':string'); + if (s === undefined) { + s = ''; + } var sUTF8 = unescape(encodeURIComponent(s)); this.checkLength(sUTF8.length + 8); @@ -151,6 +154,9 @@ TLSerialization.prototype.storeBytes = function (bytes, field) { if (bytes instanceof ArrayBuffer) { bytes = new Uint8Array(bytes); } + else if (bytes === undefined) { + bytes = []; + } this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':bytes'); var len = bytes.byteLength || bytes.length; diff --git a/app/js/locales/en-us.json b/app/js/locales/en-us.json index a9bc90da..01363a9d 100644 --- a/app/js/locales/en-us.json +++ b/app/js/locales/en-us.json @@ -54,6 +54,26 @@ "settings_modal_follow_us_twitter": "Follow us on Twitter!", "settings_modal_recent_updates": "Recent updates (ver. {version})", + "settings_modal_set_password": "Set Additional Password", + "settings_modal_change_password": "Change password", + "settings_modal_disable_password": "Turn off", + "settings_modal_password_email_pending": "Click the link in {email} to complete Two-Step Verification setup.", + "settings_modal_password_email_pending_cancel": "Abort", + + "password_delete_title": "Turn Password Off", + "password_change_title": "Two-Step Verification", + "password_current_placeholder": "Enter current password", + "password_create_placeholder": "Enter a password", + "password_new_placeholder": "Enter new password", + "password_confirm_placeholder": "Re-enter new password", + "password_hint_placeholder": "Enter password hint", + "password_email_placeholder": "Enter recovery e-mail", + "password_create_description": "This password will be required when you log in on a new device in addition to the pin code.", + "password_create_active": "Saving...", + "password_create_submit": "Save", + "password_delete_active": "Deleting...", + "password_delete_submit": "Delete password", + "page_title_pluralize_notifications": "{'0': 'No notifications', 'one': '1 notification', 'other': '{} notifications'}", "profile_edit_modal_title": "Edit profile", @@ -147,6 +167,9 @@ "confirm_modal_migrate_to_https_md": "Telegram Web now supports additional SSL encryption. Would you like to switch to HTTPS?\nThe HTTP version will be disabled soon.", "confirm_modal_resize_desktop_md": "Would you like to switch to desktop version?", "confirm_modal_resize_mobile_md": "Would you like to switch to mobile version?", + "confirm_modal_recovery_email_empty_md": "Warning! Are you sure you don't want to add a password recovery e-mail?\n\nIf you forget your password, you will lose access to your Telegram account", + "confirm_modal_abort_password_setup": "Abort two-step verification setup?", + "confirm_modal_reset_account_md": "Are you sure?\nThis action can not be undone.\n\nYou will lose all your chats and messages, along with any media and files you shared, if you proceed with resetting your account.", "confirm_modal_are_u_sure": "Are you sure?", "confirm_modal_logout_submit": "Log Out", @@ -161,6 +184,7 @@ "confirm_modal_share_video_submit": "Forward video", "confirm_modal_share_contact_submit": "Send contact", "confirm_modal_share_file_submit": "Share file", + "confirm_modal_reset_account_submit": "Reset my account", "contacts_modal_edit_list": "Edit", "contacts_modal_edit_cancel": "Cancel", @@ -232,7 +256,12 @@ "error_modal_flood_title": "Too fast", "error_modal_internal_title": "Server error", "error_modal_alert": "Alert", + "error_modal_email_unconfirmed_title": "Almost there!", + "error_modal_email_unconfirmed_descripion": "Please check your e-mail (don't forget the spam folder) to complete Two-Step Verification setup.", + "error_modal_password_success_title": "Success!", + "error_modal_password_disabled_title": "Password deactivated", "error_modal_media_not_supported_title": "Unsupported media", + "error_modal_recovery_na_title": "Sorry", "error_modal_network_description": "Please check your internet connection.", "error_modal_firstname_invali_description": "The first name you entered is invalid.", @@ -258,6 +287,9 @@ "error_modal_internal_description": "Internal server error occured. Please try again later.", "error_modal_tech_details": "Technical details here", "error_modal_multiple_open_tabs": "Please close other Telegram app tabs.", + "error_modal_recovery_na_description": "Since you haven't provided a recovery e-mail when setting up your password, your remaining options are either to remember your password or to reset your account.", + "error_modal_password_success_descripion": "Your password for Two-Step Verification is now active.", + "error_modal_password_disabled_descripion": "You have disabled Two-Step Verification.", "head_telegram": "Telegram", @@ -367,6 +399,21 @@ "login_about_desc3_md": "Our {source-link: source code} is open, so everyone can make a contribution.", "login_about_intro": "Welcome to the official Telegram web-client.", "login_about_learn": "Learn more", + "login_password_title": "Password", + "login_password_label": "You have enabled Two-Step Verification, so your account is protected with an additional password.", + "login_password_forgot_link": "Forgot password?", + "login_account_reset": "Reset account", + "login_password": "Your Password", + "login_incorrect_password": "Incorrect password", + "login_checking_password": "Checking", + "login_recovery_title": "Forgot password?", + "login_code_placeholder": "Code", + "login_code_incorrect": "Incorrect code", + "login_recovery_description_md": "We have sent a recovery code to the e-mail you provided:\n\n{email}\n\nPlease check your e-mail and enter the 6-digit code we have sent here.", + + "password_recover_active": "Checking...", + "password_recover_submit": "Submit", + "login_controller_unknown_country": "Unknown", diff --git a/app/js/services.js b/app/js/services.js index d71b4005..1d875f1e 100755 --- a/app/js/services.js +++ b/app/js/services.js @@ -4537,18 +4537,104 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) }) -.service('PasswordManager', function ($timeout, $rootScope, MtpApiManager) { +.service('PasswordManager', function ($timeout, $q, $rootScope, MtpApiManager, CryptoWorker, MtpSecureRandom) { return { - getPasswordState: getPasswordState + check: check, + getState: getState, + requestRecovery: requestRecovery, + recover: recover, + updateSettings: updateSettings }; - function getPasswordState () { - return MtpApiManager.invokeApi('account.getPassword').then(function (result) { + function getState (options) { + return MtpApiManager.invokeApi('account.getPassword', {}, options).then(function (result) { return result; }); } + function updateSettings (state, settings) { + var currentHashPromise; + var newHashPromise; + var params = { + new_settings: { + _: 'account.passwordInputSettings', + flags: 0, + hint: settings.hint || '' + } + }; + + if (typeof settings.cur_password === 'string' && + settings.cur_password.length > 0) { + currentHashPromise = makePasswordHash(state.current_salt, settings.cur_password); + } else { + currentHashPromise = $q.when([]); + } + + if (typeof settings.new_password === 'string' && + settings.new_password.length > 0) { + var saltRandom = new Array(8); + var newSalt = bufferConcat(state.new_salt, saltRandom); + MtpSecureRandom.nextBytes(saltRandom); + newHashPromise = makePasswordHash(newSalt, settings.new_password); + params.new_settings.new_salt = newSalt; + params.new_settings.flags |= 1; + } else { + if (typeof settings.new_password === 'string') { + params.new_settings.flags |= 1; + params.new_settings.new_salt = []; + } + newHashPromise = $q.when([]); + } + + if (typeof settings.email === 'string') { + params.new_settings.flags |= 2; + params.new_settings.email = settings.email || ''; + } + + return $q.all([currentHashPromise, newHashPromise]).then(function (hashes) { + params.current_password_hash = hashes[0]; + params.new_settings.new_password_hash = hashes[1]; + + return MtpApiManager.invokeApi('account.updatePasswordSettings', params); + }); + + } + + function check (state, password, options) { + return makePasswordHash(state.current_salt, password).then(function (passwordHash) { + return MtpApiManager.invokeApi('auth.checkPassword', { + password_hash: passwordHash + }, options); + }); + } + + function requestRecovery (state, options) { + return MtpApiManager.invokeApi('auth.requestPasswordRecovery', {}, options); + } + + function recover (code, options) { + return MtpApiManager.invokeApi('auth.recoverPassword', { + code: code + }, options); + } + + + + function makePasswordHash (salt, password) { + var passwordUTF8 = unescape(encodeURIComponent(password)); + + var buffer = new ArrayBuffer(passwordUTF8.length); + var byteView = new Uint8Array(buffer); + for (var i = 0, len = passwordUTF8.length; i < len; i++) { + byteView[i] = passwordUTF8.charCodeAt(i); + } + + buffer = bufferConcat(bufferConcat(salt, byteView), salt); + + return CryptoWorker.sha256Hash(buffer); + } + }) diff --git a/app/less/app.less b/app/less/app.less index ba96eae5..c2d7e217 100644 --- a/app/less/app.less +++ b/app/less/app.less @@ -242,6 +242,9 @@ input[type="number"] { padding: 0; margin: 0 0 22px; } +.md-input-grouped { + margin-bottom: 12px; +} .md-input-label { font-weight: normal; @@ -974,6 +977,12 @@ a.tg_radio_on:hover i.icon-radio { font-size: 13px; line-height: 160%; } +.login_form_hint { + color: #999; + margin: 0 0 20px; + font-size: 13px; + line-height: 160%; +} .login_form_messaging { color: #999; font-size: 13px; @@ -1002,6 +1011,15 @@ a.tg_radio_on:hover i.icon-radio { } } +.login_forgot_button { + text-align: center; + margin: 30px 0 10px; +} +.login_reset_button { + text-align: center; + margin: 10px 0 0; +} + /* IM page start */ /* Dialogs list */ @@ -3363,6 +3381,15 @@ a.countries_modal_search_clear { &:hover { text-decoration: none; } + &.pull-right { + color: #3a6d99; + } + } + + &_text { + display: block; + padding: 4px 0; + color: #777; } &_version { diff --git a/app/partials/desktop/confirm_modal.html b/app/partials/desktop/confirm_modal.html index 1db20e77..6d9f866d 100644 --- a/app/partials/desktop/confirm_modal.html +++ b/app/partials/desktop/confirm_modal.html @@ -43,6 +43,9 @@
+ + + @@ -55,7 +58,7 @@ -