mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
totp code management and countdown timer
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
<script type="text/javascript" src="services/autofillService.js"></script>
|
<script type="text/javascript" src="services/autofillService.js"></script>
|
||||||
<script type="text/javascript" src="services/appIdService.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/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="background.js"></script>
|
||||||
<script type="text/javascript" src="scripts/analytics.js"></script>
|
<script type="text/javascript" src="scripts/analytics.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var bg_syncService = new SyncService(bg_loginService, bg_folderService, bg_userS
|
|||||||
bg_cryptoService, logout);
|
bg_cryptoService, logout);
|
||||||
var bg_autofillService = new AutofillService();
|
var bg_autofillService = new AutofillService();
|
||||||
var bg_passwordGenerationService = new PasswordGenerationService();
|
var bg_passwordGenerationService = new PasswordGenerationService();
|
||||||
|
var bg_totpService = new TotpService();
|
||||||
|
|
||||||
if (chrome.commands) {
|
if (chrome.commands) {
|
||||||
chrome.commands.onCommand.addListener(function (command) {
|
chrome.commands.onCommand.addListener(function (command) {
|
||||||
|
|||||||
@@ -45,4 +45,7 @@
|
|||||||
})
|
})
|
||||||
.factory('lockService', function () {
|
.factory('lockService', function () {
|
||||||
return chrome.extension.getBackgroundPage().bg_lockService;
|
return chrome.extension.getBackgroundPage().bg_lockService;
|
||||||
|
})
|
||||||
|
.factory('totpService', function () {
|
||||||
|
return chrome.extension.getBackgroundPage().bg_totpService;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ angular
|
|||||||
.module('bit.vault')
|
.module('bit.vault')
|
||||||
|
|
||||||
.controller('vaultViewLoginController', function ($scope, $state, $stateParams, loginService, toastr, $q,
|
.controller('vaultViewLoginController', function ($scope, $state, $stateParams, loginService, toastr, $q,
|
||||||
$analytics, i18nService, utilsService) {
|
$analytics, i18nService, utilsService, totpService, $timeout) {
|
||||||
$scope.i18n = i18nService;
|
$scope.i18n = i18nService;
|
||||||
var from = $stateParams.from;
|
var from = $stateParams.from,
|
||||||
|
totpInterval = null;
|
||||||
|
|
||||||
$scope.login = null;
|
$scope.login = null;
|
||||||
loginService.get($stateParams.loginId, function (login) {
|
loginService.get($stateParams.loginId, function (login) {
|
||||||
@@ -37,6 +38,19 @@ angular
|
|||||||
else {
|
else {
|
||||||
$scope.login.showLaunch = false;
|
$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');
|
$analytics.eventTrack('Toggled Password');
|
||||||
$scope.showPassword = !$scope.showPassword;
|
$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}}
|
{{i18n.generatePassword}}
|
||||||
<i class="fa fa-chevron-right"></i>
|
<i class="fa fa-chevron-right"></i>
|
||||||
</a>
|
</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>
|
</div>
|
||||||
<div class="list-section">
|
<div class="list-section">
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
{{i18n.generatePassword}}
|
{{i18n.generatePassword}}
|
||||||
<i class="fa fa-chevron-right"></i>
|
<i class="fa fa-chevron-right"></i>
|
||||||
</a>
|
</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>
|
</div>
|
||||||
<div class="list-section">
|
<div class="list-section">
|
||||||
|
|||||||
@@ -47,6 +47,24 @@
|
|||||||
<span ng-show="!showPassword">{{login.maskedPassword}}</span>
|
<span ng-show="!showPassword">{{login.maskedPassword}}</span>
|
||||||
<span id="password" ng-show="showPassword" class="monospaced">{{login.password}}</span>
|
<span id="password" ng-show="showPassword" class="monospaced">{{login.password}}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="list-section" ng-if="login.notes">
|
<div class="list-section" ng-if="login.notes">
|
||||||
|
|||||||
@@ -506,3 +506,58 @@
|
|||||||
border: none;
|
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';
|
//this.identityBaseUrl = 'https://localhost:44392';
|
||||||
|
|
||||||
// Desktop external
|
// Desktop external
|
||||||
//this.baseUrl = 'http://192.168.1.6:4000';
|
//this.baseUrl = 'http://192.168.1.4:4000';
|
||||||
//this.identityBaseUrl = 'http://192.168.1.6:33656';
|
//this.identityBaseUrl = 'http://192.168.1.4:33656';
|
||||||
|
|
||||||
// Preview
|
// Preview
|
||||||
//this.baseUrl = 'https://preview-api.bitwarden.com';
|
//this.baseUrl = 'https://preview-api.bitwarden.com';
|
||||||
@@ -441,7 +441,7 @@ function initApiService() {
|
|||||||
deviceName: self.utilsService.getBrowser()
|
deviceName: self.utilsService.getBrowser()
|
||||||
}, function (token) {
|
}, function (token) {
|
||||||
self.tokenService.clearAuthBearer(function () {
|
self.tokenService.clearAuthBearer(function () {
|
||||||
tokenService.setTokens(token.accessToken, token.refreshToken, function () {
|
self.tokenService.setTokens(token.accessToken, token.refreshToken, function () {
|
||||||
resolveTokenQs(token.accessToken, self, deferred);
|
resolveTokenQs(token.accessToken, self, deferred);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -462,7 +462,7 @@ function initApiService() {
|
|||||||
client_id: 'browser',
|
client_id: 'browser',
|
||||||
refresh_token: refreshToken
|
refresh_token: refreshToken
|
||||||
}, function (token) {
|
}, function (token) {
|
||||||
tokenService.setTokens(token.accessToken, token.refreshToken, function () {
|
self.tokenService.setTokens(token.accessToken, token.refreshToken, function () {
|
||||||
resolveTokenQs(token.accessToken, self, deferred);
|
resolveTokenQs(token.accessToken, self, deferred);
|
||||||
});
|
});
|
||||||
}, function (jqXHR) {
|
}, function (jqXHR) {
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ function initLoginService() {
|
|||||||
return self.cryptoService.encrypt(login.notes, orgKey);
|
return self.cryptoService.encrypt(login.notes, orgKey);
|
||||||
}).then(function (cs) {
|
}).then(function (cs) {
|
||||||
model.notes = cs;
|
model.notes = cs;
|
||||||
|
return self.cryptoService.encrypt(login.totp, orgKey);
|
||||||
|
}).then(function (cs) {
|
||||||
|
model.totp = cs;
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -193,7 +196,7 @@ function initLoginService() {
|
|||||||
|
|
||||||
function apiSuccess(response) {
|
function apiSuccess(response) {
|
||||||
login.id = response.id;
|
login.id = response.id;
|
||||||
userService.getUserId(function (userId) {
|
self.userService.getUserId(function (userId) {
|
||||||
var data = new LoginData(response, userId);
|
var data = new LoginData(response, userId);
|
||||||
self.upsert(data, function () {
|
self.upsert(data, function () {
|
||||||
deferred.resolve(login);
|
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user