mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-29 17:38:04 +01:00
totp generator directive
This commit is contained in:
parent
8a90f562ef
commit
ed13644a02
171
src/app/directives/totpDirective.js
Normal file
171
src/app/directives/totpDirective.js
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
angular
|
||||||
|
.module('bit.directives')
|
||||||
|
|
||||||
|
.directive('totp', function ($timeout, $q) {
|
||||||
|
return {
|
||||||
|
template: '<div class="totp{{(low ? \' low\' : \'\')}}" ng-if="code">' +
|
||||||
|
'<span class="totp-countdown"><span class="totp-sec">{{sec}}</span>' +
|
||||||
|
'<svg><g><circle class="totp-circle inner" r="12.6" cy="16" cx="16" style="stroke-dashoffset: {{dash}}px;"></circle>' +
|
||||||
|
'<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle></g></svg></span>' +
|
||||||
|
'<span class="totp-code" id="totp-code">{{code}}</span>' +
|
||||||
|
'<a href="#" stop-click class="btn btn-link" ngclipboard ngclipboard-error="clipboardError(e)" ' +
|
||||||
|
'data-clipboard-text="{{code}}" uib-tooltip="Copy Code" tooltip-placement="right">' +
|
||||||
|
'<i class="fa fa-clipboard"></i></a>' +
|
||||||
|
'</div>',
|
||||||
|
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.');
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
@ -2,7 +2,7 @@
|
|||||||
.module('bit.vault')
|
.module('bit.vault')
|
||||||
|
|
||||||
.controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
.controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||||
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope) {
|
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService) {
|
||||||
$analytics.eventTrack('vaultAddLoginController', { category: 'Modal' });
|
$analytics.eventTrack('vaultAddLoginController', { category: 'Modal' });
|
||||||
$scope.folders = $rootScope.vaultFolders;
|
$scope.folders = $rootScope.vaultFolders;
|
||||||
$scope.login = {
|
$scope.login = {
|
||||||
@ -10,6 +10,10 @@
|
|||||||
favorite: checkedFavorite === true
|
favorite: checkedFavorite === true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
authService.getUserProfile().then(function (profile) {
|
||||||
|
$scope.premium = profile.premium;
|
||||||
|
});
|
||||||
|
|
||||||
$scope.savePromise = null;
|
$scope.savePromise = null;
|
||||||
$scope.save = function (model) {
|
$scope.save = function (model) {
|
||||||
var login = cipherService.encryptLogin(model);
|
var login = cipherService.encryptLogin(model);
|
||||||
|
@ -2,12 +2,16 @@
|
|||||||
.module('bit.vault')
|
.module('bit.vault')
|
||||||
|
|
||||||
.controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
.controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||||
passwordService, loginId, $analytics, $rootScope) {
|
passwordService, loginId, $analytics, $rootScope, authService) {
|
||||||
$analytics.eventTrack('vaultEditLoginController', { category: 'Modal' });
|
$analytics.eventTrack('vaultEditLoginController', { category: 'Modal' });
|
||||||
$scope.folders = $rootScope.vaultFolders;
|
$scope.folders = $rootScope.vaultFolders;
|
||||||
$scope.login = {};
|
$scope.login = {};
|
||||||
$scope.readOnly = false;
|
$scope.readOnly = false;
|
||||||
|
|
||||||
|
authService.getUserProfile().then(function (profile) {
|
||||||
|
$scope.premium = profile.premium;
|
||||||
|
});
|
||||||
|
|
||||||
apiService.logins.get({ id: loginId }, function (login) {
|
apiService.logins.get({ id: loginId }, function (login) {
|
||||||
$scope.login = cipherService.decryptLogin(login);
|
$scope.login = cipherService.decryptLogin(login);
|
||||||
$scope.readOnly = !login.Edit;
|
$scope.readOnly = !login.Edit;
|
||||||
|
@ -88,7 +88,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
<label class="invisible hidden-sm" for="verification-code">Verification Code</label>
|
||||||
|
<div totp="login.totp" id="verification-code" ng-if="premium"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" show-errors>
|
<div class="form-group" show-errors>
|
||||||
|
@ -100,7 +100,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
<label class="invisible hidden-sm hidden-xs" for="verification-code">Verification Code</label>
|
||||||
|
<div totp="login.totp" id="verification-code" ng-if="premium"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" show-errors>
|
<div class="form-group" show-errors>
|
||||||
|
@ -123,6 +123,7 @@
|
|||||||
<script src="app/directives/letterAvatarDirective.js"></script>
|
<script src="app/directives/letterAvatarDirective.js"></script>
|
||||||
<script src="app/directives/stopClickDirective.js"></script>
|
<script src="app/directives/stopClickDirective.js"></script>
|
||||||
<script src="app/directives/stopPropDirective.js"></script>
|
<script src="app/directives/stopPropDirective.js"></script>
|
||||||
|
<script src="app/directives/totpDirective.js"></script>
|
||||||
|
|
||||||
<script src="app/filters/filtersModule.js"></script>
|
<script src="app/filters/filtersModule.js"></script>
|
||||||
<script src="app/filters/enumNameFilter.js"></script>
|
<script src="app/filters/enumNameFilter.js"></script>
|
||||||
|
@ -624,3 +624,60 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
font-weight: normal;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user