1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-27 12:36:14 +01:00

totp code management and countdown timer

This commit is contained in:
Kyle Spearrin 2017-07-11 14:05:04 -04:00
parent fccaa5f8de
commit 21feb653cb
11 changed files with 245 additions and 7 deletions

View File

@ -23,6 +23,7 @@
<script type="text/javascript" src="services/autofillService.js"></script>
<script type="text/javascript" src="services/appIdService.js"></script>
<script type="text/javascript" src="services/passwordGenerationService.js"></script>
<script type="text/javascript" src="services/totpService.js"></script>
<script type="text/javascript" src="background.js"></script>
<script type="text/javascript" src="scripts/analytics.js"></script>
</head>

View File

@ -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) {

View File

@ -45,4 +45,7 @@
})
.factory('lockService', function () {
return chrome.extension.getBackgroundPage().bg_lockService;
})
.factory('totpService', function () {
return chrome.extension.getBackgroundPage().bg_totpService;
});

View File

@ -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();
}
});
};
});

View File

@ -36,6 +36,10 @@
{{i18n.generatePassword}}
<i class="fa fa-chevron-right"></i>
</a>
<div class="list-section-item">
<label for="totp" class="item-label">Authenticator Key (TOTP)</label>
<input id="totp" type="text" name="Totp" ng-model="login.totp">
</div>
</div>
</div>
<div class="list-section">

View File

@ -37,6 +37,10 @@
{{i18n.generatePassword}}
<i class="fa fa-chevron-right"></i>
</a>
<div class="list-section-item">
<label for="totp" class="item-label">Authenticator Key (TOTP)</label>
<input id="totp" type="text" name="Totp" ng-model="login.totp">
</div>
</div>
</div>
<div class="list-section">

View File

@ -47,6 +47,24 @@
<span ng-show="!showPassword">{{login.maskedPassword}}</span>
<span id="password" ng-show="showPassword" class="monospaced">{{login.password}}</span>
</div>
<div class="list-section-item totp" ng-class="{'low': totpLow}" ng-if="login.totp && totpCode">
<a class="btn-list" href="" title="Copy TOTP" ngclipboard ngclipboard-error="clipboardError(e)"
ngclipboard-success="clipboardSuccess(e, 'Totp')" data-clipboard-text="{{totpCode}}">
<i class="fa fa-lg fa-clipboard"></i>
</a>
<span class="totp-countdown">
<span class="totp-sec">{{totpSec}}</span>
<svg>
<g>
<circle class="totp-circle inner" r="12.6" cy="16" cx="16"
style="stroke-dashoffset: {{totpDash}}px;"></circle>
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
</g>
</svg>
</span>
<span class="item-label">Verification Code (TOTP)</span>
<span id="totp" class="totp-code" ng-class="{'text-danger': totpLow}">{{totpCodeFormatted}}</span>
</div>
</div>
</div>
<div class="list-section" ng-if="login.notes">

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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);

View File

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