From 21feb653cb1256728c066d1bb2e8adfc8acb2458 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 11 Jul 2017 14:05:04 -0400 Subject: [PATCH] totp code management and countdown timer --- src/background.html | 1 + src/background.js | 1 + src/popup/app/services/backgroundService.js | 3 + .../app/vault/vaultViewLoginController.js | 60 +++++++++++- src/popup/app/vault/views/vaultAddLogin.html | 4 + src/popup/app/vault/views/vaultEditLogin.html | 4 + src/popup/app/vault/views/vaultViewLogin.html | 18 ++++ src/popup/less/components.less | 55 +++++++++++ src/services/apiService.js | 8 +- src/services/loginService.js | 5 +- src/services/totpService.js | 93 +++++++++++++++++++ 11 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 src/services/totpService.js diff --git a/src/background.html b/src/background.html index eb0d8dd15d..6f9e86d4cb 100644 --- a/src/background.html +++ b/src/background.html @@ -23,6 +23,7 @@ + diff --git a/src/background.js b/src/background.js index ed31b4d1b5..5915ceb575 100644 --- a/src/background.js +++ b/src/background.js @@ -17,6 +17,7 @@ var bg_syncService = new SyncService(bg_loginService, bg_folderService, bg_userS bg_cryptoService, logout); var bg_autofillService = new AutofillService(); var bg_passwordGenerationService = new PasswordGenerationService(); +var bg_totpService = new TotpService(); if (chrome.commands) { chrome.commands.onCommand.addListener(function (command) { diff --git a/src/popup/app/services/backgroundService.js b/src/popup/app/services/backgroundService.js index 844be60d3e..95ea4a7e43 100644 --- a/src/popup/app/services/backgroundService.js +++ b/src/popup/app/services/backgroundService.js @@ -45,4 +45,7 @@ }) .factory('lockService', function () { return chrome.extension.getBackgroundPage().bg_lockService; + }) + .factory('totpService', function () { + return chrome.extension.getBackgroundPage().bg_totpService; }); diff --git a/src/popup/app/vault/vaultViewLoginController.js b/src/popup/app/vault/vaultViewLoginController.js index 0da099a5de..e60587b21d 100644 --- a/src/popup/app/vault/vaultViewLoginController.js +++ b/src/popup/app/vault/vaultViewLoginController.js @@ -2,9 +2,10 @@ angular .module('bit.vault') .controller('vaultViewLoginController', function ($scope, $state, $stateParams, loginService, toastr, $q, - $analytics, i18nService, utilsService) { + $analytics, i18nService, utilsService, totpService, $timeout) { $scope.i18n = i18nService; - var from = $stateParams.from; + var from = $stateParams.from, + totpInterval = null; $scope.login = null; loginService.get($stateParams.loginId, function (login) { @@ -37,6 +38,19 @@ angular else { $scope.login.showLaunch = false; } + + if (model.totp) { + totpUpdateCode(); + totpTick(); + + if (totpInterval) { + clearInterval(totpInterval); + } + + totpInterval = setInterval(function () { + totpTick(); + }, 1000); + } }); }); @@ -89,4 +103,46 @@ angular $analytics.eventTrack('Toggled Password'); $scope.showPassword = !$scope.showPassword; }; + + $scope.$on("$destroy", function () { + if (totpInterval) { + clearInterval(totpInterval); + } + }); + + function totpUpdateCode() { + if (!$scope.login.totp) { + return; + } + + totpService.getCode($scope.login.totp).then(function (code) { + $timeout(function () { + if (code) { + $scope.totpCodeFormatted = code.substring(0, 3) + ' ' + code.substring(3); + $scope.totpCode = code; + } + else { + $scope.totpCode = $scope.totpCodeFormatted = null; + if (totpInterval) { + clearInterval(totpInterval); + } + } + }); + }); + }; + + function totpTick() { + $timeout(function () { + var epoch = Math.round(new Date().getTime() / 1000.0); + var mod = (epoch % 30); + var sec = 30 - mod; + + $scope.totpSec = sec; + $scope.totpDash = (2.62 * mod).toFixed(2); + $scope.totpLow = sec <= 7; + if (epoch % 30 == 0) { + totpUpdateCode(); + } + }); + }; }); diff --git a/src/popup/app/vault/views/vaultAddLogin.html b/src/popup/app/vault/views/vaultAddLogin.html index 20e663e041..a86f031075 100644 --- a/src/popup/app/vault/views/vaultAddLogin.html +++ b/src/popup/app/vault/views/vaultAddLogin.html @@ -36,6 +36,10 @@ {{i18n.generatePassword}} +
+ + +
diff --git a/src/popup/app/vault/views/vaultEditLogin.html b/src/popup/app/vault/views/vaultEditLogin.html index cb7023efeb..e14c576dfb 100644 --- a/src/popup/app/vault/views/vaultEditLogin.html +++ b/src/popup/app/vault/views/vaultEditLogin.html @@ -37,6 +37,10 @@ {{i18n.generatePassword}} +
+ + +
diff --git a/src/popup/app/vault/views/vaultViewLogin.html b/src/popup/app/vault/views/vaultViewLogin.html index 0e4b20fa97..a1b875c851 100644 --- a/src/popup/app/vault/views/vaultViewLogin.html +++ b/src/popup/app/vault/views/vaultViewLogin.html @@ -47,6 +47,24 @@ {{login.maskedPassword}} {{login.password}}
+
+ + + + + {{totpSec}} + + + + + + + + Verification Code (TOTP) + {{totpCodeFormatted}} +
diff --git a/src/popup/less/components.less b/src/popup/less/components.less index 9caa0e9cd5..c6fcb335fa 100644 --- a/src/popup/less/components.less +++ b/src/popup/less/components.less @@ -506,3 +506,58 @@ border: none; } } + +.totp { + .totp-code { + font-family: @font-family-monospace; + font-size: 1.1em; + } + + .totp-countdown { + margin: 3px 3px 0 0; + display: block; + user-select: none; + float: right; + + .totp-sec { + font-size: 0.85em; + position: absolute; + line-height: 32px; + width: 32px; + text-align: center; + } + + svg { + width: 32px; + height: 32px; + transform: rotate(-90deg); + } + + .totp-circle { + stroke: @brand-primary; + fill: none; + + &.inner { + stroke-width: 3; + stroke-dasharray: 78.6; + stroke-dashoffset: 0px; + } + + &.outer { + stroke-width: 2; + stroke-dasharray: 88; + stroke-dashoffset: 0px; + } + } + } + + &.low { + .totp-sec, .totp-code { + color: @brand-danger; + } + + .totp-circle { + stroke: @brand-danger; + } + } +} diff --git a/src/services/apiService.js b/src/services/apiService.js index a1db49b789..0c5591c8d4 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -8,8 +8,8 @@ function ApiService(tokenService, appIdService, utilsService, logoutCallback) { //this.identityBaseUrl = 'https://localhost:44392'; // Desktop external - //this.baseUrl = 'http://192.168.1.6:4000'; - //this.identityBaseUrl = 'http://192.168.1.6:33656'; + //this.baseUrl = 'http://192.168.1.4:4000'; + //this.identityBaseUrl = 'http://192.168.1.4:33656'; // Preview //this.baseUrl = 'https://preview-api.bitwarden.com'; @@ -441,7 +441,7 @@ function initApiService() { deviceName: self.utilsService.getBrowser() }, function (token) { self.tokenService.clearAuthBearer(function () { - tokenService.setTokens(token.accessToken, token.refreshToken, function () { + self.tokenService.setTokens(token.accessToken, token.refreshToken, function () { resolveTokenQs(token.accessToken, self, deferred); }); }); @@ -462,7 +462,7 @@ function initApiService() { client_id: 'browser', refresh_token: refreshToken }, function (token) { - tokenService.setTokens(token.accessToken, token.refreshToken, function () { + self.tokenService.setTokens(token.accessToken, token.refreshToken, function () { resolveTokenQs(token.accessToken, self, deferred); }); }, function (jqXHR) { diff --git a/src/services/loginService.js b/src/services/loginService.js index 0124f9f153..b2ae2c9397 100644 --- a/src/services/loginService.js +++ b/src/services/loginService.js @@ -41,6 +41,9 @@ function initLoginService() { return self.cryptoService.encrypt(login.notes, orgKey); }).then(function (cs) { model.notes = cs; + return self.cryptoService.encrypt(login.totp, orgKey); + }).then(function (cs) { + model.totp = cs; return model; }); }; @@ -193,7 +196,7 @@ function initLoginService() { function apiSuccess(response) { login.id = response.id; - userService.getUserId(function (userId) { + self.userService.getUserId(function (userId) { var data = new LoginData(response, userId); self.upsert(data, function () { deferred.resolve(login); diff --git a/src/services/totpService.js b/src/services/totpService.js new file mode 100644 index 0000000000..892bba33de --- /dev/null +++ b/src/services/totpService.js @@ -0,0 +1,93 @@ +function TotpService() { + initTotpService(); +}; + +function initTotpService() { + var b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + + var leftpad = function (s, l, p) { + if (l + 1 >= s.length) { + s = Array(l + 1 - s.length).join(p) + s; + } + return s; + }; + + var dec2hex = function (d) { + return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); + }; + + var hex2dec = function (s) { + return parseInt(s, 16); + }; + + var hex2bytes = function (s) { + var bytes = new Uint8Array(s.length / 2); + for (var i = 0; i < s.length; i += 2) { + bytes[i / 2] = parseInt(s.substr(i, 2), 16); + } + return bytes; + } + + var buff2hex = function (buff) { + var bytes = new Uint8Array(buff); + var hex = []; + for (var i = 0; i < bytes.length; i++) { + hex.push((bytes[i] >>> 4).toString(16)); + hex.push((bytes[i] & 0xF).toString(16)); + } + return hex.join(''); + } + + var b32tohex = function (s) { + var bits = ''; + var hex = ''; + for (var i = 0; i < s.length; i++) { + var val = b32Chars.indexOf(s.charAt(i).toUpperCase()); + bits += leftpad(val.toString(2), 5, '0'); + } + for (var i = 0; i + 4 <= bits.length; i += 4) { + var chunk = bits.substr(i, 4); + hex = hex + parseInt(chunk, 2).toString(16); + } + return hex; + }; + + var b32tobytes = function (s) { + return hex2bytes(b32tohex(s)); + }; + + var sign = function (keyBytes, timeBytes) { + return window.crypto.subtle.importKey('raw', keyBytes, + { name: 'HMAC', hash: { name: 'SHA-1' } }, false, ['sign']).then(function (key) { + return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-1' } }, key, timeBytes); + }).then(function (signature) { + return buff2hex(signature); + }).catch(function (err) { + return null; + }); + }; + + TotpService.prototype.getCode = function (keyb32) { + var epoch = Math.round(new Date().getTime() / 1000.0); + var timeHex = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0'); + var timeBytes = hex2bytes(timeHex); + var keyBytes = b32tobytes(keyb32); + + if (!keyBytes.length || !timeBytes.length) { + return Q.fcall(function () { + return null; + }); + } + + return sign(keyBytes, timeBytes).then(function (hashHex) { + if (!hashHex) { + return null; + } + + var offset = hex2dec(hashHex.substring(hashHex.length - 1)); + var otp = (hex2dec(hashHex.substr(offset * 2, 8)) & hex2dec('7fffffff')) + ''; + otp = (otp).substr(otp.length - 6, 6); + return otp; + }); + }; +};