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:
parent
fccaa5f8de
commit
21feb653cb
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -45,4 +45,7 @@
|
||||
})
|
||||
.factory('lockService', function () {
|
||||
return chrome.extension.getBackgroundPage().bg_lockService;
|
||||
})
|
||||
.factory('totpService', function () {
|
||||
return chrome.extension.getBackgroundPage().bg_totpService;
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
93
src/services/totpService.js
Normal file
93
src/services/totpService.js
Normal 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;
|
||||
});
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user