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;
+ });
+ };
+};