From ed13644a028959d13db29afb9491d5365bd6adb2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 7 Jul 2017 00:13:26 -0400 Subject: [PATCH] totp generator directive --- src/app/directives/totpDirective.js | 171 ++++++++++++++++++++++ src/app/vault/vaultAddLoginController.js | 6 +- src/app/vault/vaultEditLoginController.js | 6 +- src/app/vault/views/vaultAddLogin.html | 3 +- src/app/vault/views/vaultEditLogin.html | 3 +- src/index.html | 1 + src/less/vault.less | 57 ++++++++ 7 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 src/app/directives/totpDirective.js diff --git a/src/app/directives/totpDirective.js b/src/app/directives/totpDirective.js new file mode 100644 index 0000000000..8200bf2915 --- /dev/null +++ b/src/app/directives/totpDirective.js @@ -0,0 +1,171 @@ +angular + .module('bit.directives') + + .directive('totp', function ($timeout, $q) { + return { + template: '
' + + '{{sec}}' + + '' + + '' + + '{{code}}' + + '' + + '' + + '
', + restrict: 'A', + scope: { + key: '=totp' + }, + link: function (scope) { + var interval = null; + + var Totp = function () { + 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' }, key, timeBytes); + }).then(function (signature) { + return buff2hex(signature); + }).catch(function (err) { + return null; + }); + }; + + this.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(function (resolve, reject) { + resolve(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; + }); + }; + } + + var totp = new Totp(); + + var updateCode = function (scope) { + totp.getCode(scope.key).then(function (code) { + $timeout(function () { + if (code) { + scope.code = code.substring(0, 3) + ' ' + code.substring(3); + } + else { + scope.code = null; + if (interval) { + clearInterval(interval); + } + } + }); + }); + }; + + var tick = function (scope) { + $timeout(function () { + var epoch = Math.round(new Date().getTime() / 1000.0); + var mod = (epoch % 30); + var sec = 30 - mod; + + scope.sec = sec; + scope.dash = (2.62 * mod).toFixed(2); + scope.low = sec <= 7; + if (epoch % 30 == 0) { + updateCode(scope); + } + }); + }; + + scope.$watch('key', function () { + if (!scope.key) { + scope.code = null; + if (interval) { + clearInterval(interval); + } + return; + } + + updateCode(scope); + tick(scope); + + if (interval) { + clearInterval(interval); + } + + interval = setInterval(function () { + tick(scope); + }, 1000); + }); + + scope.clipboardError = function (e) { + alert('Your web browser does not support easy clipboard copying.'); + }; + }, + }; + }); diff --git a/src/app/vault/vaultAddLoginController.js b/src/app/vault/vaultAddLoginController.js index 183d48eb5d..b5b352b5aa 100644 --- a/src/app/vault/vaultAddLoginController.js +++ b/src/app/vault/vaultAddLoginController.js @@ -2,7 +2,7 @@ .module('bit.vault') .controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, - passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope) { + passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService) { $analytics.eventTrack('vaultAddLoginController', { category: 'Modal' }); $scope.folders = $rootScope.vaultFolders; $scope.login = { @@ -10,6 +10,10 @@ favorite: checkedFavorite === true }; + authService.getUserProfile().then(function (profile) { + $scope.premium = profile.premium; + }); + $scope.savePromise = null; $scope.save = function (model) { var login = cipherService.encryptLogin(model); diff --git a/src/app/vault/vaultEditLoginController.js b/src/app/vault/vaultEditLoginController.js index bb74d8c635..f7c6764746 100644 --- a/src/app/vault/vaultEditLoginController.js +++ b/src/app/vault/vaultEditLoginController.js @@ -2,12 +2,16 @@ .module('bit.vault') .controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, - passwordService, loginId, $analytics, $rootScope) { + passwordService, loginId, $analytics, $rootScope, authService) { $analytics.eventTrack('vaultEditLoginController', { category: 'Modal' }); $scope.folders = $rootScope.vaultFolders; $scope.login = {}; $scope.readOnly = false; + authService.getUserProfile().then(function (profile) { + $scope.premium = profile.premium; + }); + apiService.logins.get({ id: loginId }, function (login) { $scope.login = cipherService.decryptLogin(login); $scope.readOnly = !login.Edit; diff --git a/src/app/vault/views/vaultAddLogin.html b/src/app/vault/views/vaultAddLogin.html index 97e3dda6e7..faecfbc988 100644 --- a/src/app/vault/views/vaultAddLogin.html +++ b/src/app/vault/views/vaultAddLogin.html @@ -88,7 +88,8 @@
- + +
diff --git a/src/app/vault/views/vaultEditLogin.html b/src/app/vault/views/vaultEditLogin.html index 2d09f53b74..637b0fdad7 100644 --- a/src/app/vault/views/vaultEditLogin.html +++ b/src/app/vault/views/vaultEditLogin.html @@ -100,7 +100,8 @@
- + +
diff --git a/src/index.html b/src/index.html index 21b2aad84c..0b5368a201 100644 --- a/src/index.html +++ b/src/index.html @@ -123,6 +123,7 @@ + diff --git a/src/less/vault.less b/src/less/vault.less index 13123b2fec..256d9585e7 100644 --- a/src/less/vault.less +++ b/src/less/vault.less @@ -624,3 +624,60 @@ h1, h2, h3, h4, h5, h6 { font-size: 85%; font-weight: normal; } + +.totp { + margin-bottom: 10px; + + .totp-code { + font-family: @font-family-monospace; + font-size: 1.2em; + } + + .totp-countdown { + margin-right: 11px; + display: inline-block; + vertical-align: -50%; + user-select: none; + + .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: 20px; + } + + &.outer { + stroke-width: 2; + stroke-dasharray: 88; + stroke-dashoffset: 0px; + } + } + } + + &.low { + .totp-sec, .totp-code { + color: @red; + } + + .totp-circle { + stroke: @red; + } + } +}