mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-13 01:58:44 +02:00
support for otpauth:// urls for totp codes
This commit is contained in:
parent
2045e7047a
commit
41ab22a82f
@ -1,4 +1,5 @@
|
|||||||
export abstract class TotpService {
|
export abstract class TotpService {
|
||||||
getCode: (keyb32: string) => Promise<string>;
|
getCode: (key: string) => Promise<string>;
|
||||||
|
getTimeInterval: (key: string) => number;
|
||||||
isAutoCopyEnabled: () => Promise<boolean>;
|
isAutoCopyEnabled: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -63,10 +63,11 @@ export class ViewComponent implements OnDestroy {
|
|||||||
if (this.cipher.type === CipherType.Login && this.cipher.login.totp &&
|
if (this.cipher.type === CipherType.Login && this.cipher.login.totp &&
|
||||||
(cipher.organizationUseTotp || this.isPremium)) {
|
(cipher.organizationUseTotp || this.isPremium)) {
|
||||||
await this.totpUpdateCode();
|
await this.totpUpdateCode();
|
||||||
await this.totpTick();
|
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||||
|
await this.totpTick(interval);
|
||||||
|
|
||||||
this.totpInterval = setInterval(async () => {
|
this.totpInterval = setInterval(async () => {
|
||||||
await this.totpTick();
|
await this.totpTick(interval);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,7 +179,12 @@ export class ViewComponent implements OnDestroy {
|
|||||||
|
|
||||||
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
|
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
|
||||||
if (this.totpCode != null) {
|
if (this.totpCode != null) {
|
||||||
this.totpCodeFormatted = this.totpCode.substring(0, 3) + ' ' + this.totpCode.substring(3);
|
if (this.totpCode.length > 4) {
|
||||||
|
const half = Math.floor(this.totpCode.length / 2);
|
||||||
|
this.totpCodeFormatted = this.totpCode.substring(0, half) + ' ' + this.totpCode.substring(half);
|
||||||
|
} else {
|
||||||
|
this.totpCodeFormatted = this.totpCode;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.totpCodeFormatted = null;
|
this.totpCodeFormatted = null;
|
||||||
if (this.totpInterval) {
|
if (this.totpInterval) {
|
||||||
@ -187,12 +193,12 @@ export class ViewComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async totpTick() {
|
private async totpTick(intervalSeconds: number) {
|
||||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||||
const mod = epoch % 30;
|
const mod = epoch % intervalSeconds;
|
||||||
|
|
||||||
this.totpSec = 30 - mod;
|
this.totpSec = intervalSeconds - mod;
|
||||||
this.totpDash = +(Math.round(((2.62 * mod) + 'e+2') as any) + 'e-2');
|
this.totpDash = +(Math.round((((78.6 / intervalSeconds) * mod) + 'e+2') as any) + 'e-2');
|
||||||
this.totpLow = this.totpSec <= 7;
|
this.totpLow = this.totpSec <= 7;
|
||||||
if (mod === 0) {
|
if (mod === 0) {
|
||||||
await this.totpUpdateCode();
|
await this.totpUpdateCode();
|
||||||
|
@ -151,6 +151,23 @@ export class Utils {
|
|||||||
return url != null ? url.host : null;
|
return url != null ? url.host : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getQueryParams(uriString: string): Map<string, string> {
|
||||||
|
const url = Utils.getUrl(uriString);
|
||||||
|
if (url == null || url.search == null || url.search === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
const pairs = (url.search[0] === '?' ? url.search.substr(1) : url.search).split('&');
|
||||||
|
pairs.forEach((pair) => {
|
||||||
|
const parts = pair.split('=');
|
||||||
|
if (parts.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(decodeURIComponent(parts[0]).toLowerCase(), parts[1] == null ? '' : decodeURIComponent(parts[1]));
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
static getSortFunction(i18nService: I18nService, prop: string) {
|
static getSortFunction(i18nService: I18nService, prop: string) {
|
||||||
return (a: any, b: any) => {
|
return (a: any, b: any) => {
|
||||||
if (a[prop] == null && b[prop] != null) {
|
if (a[prop] == null && b[prop] != null) {
|
||||||
@ -178,23 +195,24 @@ export class Utils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uriString.indexOf('://') === -1 && uriString.indexOf('.') > -1) {
|
const hasProtocol = uriString.indexOf('://') > -1;
|
||||||
|
if (!hasProtocol && uriString.indexOf('.') > -1) {
|
||||||
uriString = 'http://' + uriString;
|
uriString = 'http://' + uriString;
|
||||||
|
} else if (!hasProtocol) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uriString.startsWith('http://') || uriString.startsWith('https://')) {
|
try {
|
||||||
try {
|
if (nodeURL != null) {
|
||||||
if (nodeURL != null) {
|
return new nodeURL(uriString);
|
||||||
return new nodeURL(uriString);
|
} else if (typeof URL === 'function') {
|
||||||
} else if (typeof URL === 'function') {
|
return new URL(uriString);
|
||||||
return new URL(uriString);
|
} else if (window != null) {
|
||||||
} else if (window != null) {
|
const anchor = window.document.createElement('a');
|
||||||
const anchor = window.document.createElement('a');
|
anchor.href = uriString;
|
||||||
anchor.href = uriString;
|
return anchor as any;
|
||||||
return anchor as any;
|
}
|
||||||
}
|
} catch (e) { }
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -9,30 +9,78 @@ import { Utils } from '../misc/utils';
|
|||||||
const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
|
||||||
export class TotpService implements TotpServiceAbstraction {
|
export class TotpService implements TotpServiceAbstraction {
|
||||||
constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) {}
|
constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) { }
|
||||||
|
|
||||||
|
async getCode(key: string): Promise<string> {
|
||||||
|
let period = 30;
|
||||||
|
let alg: 'sha1' | 'sha256' | 'sha512' = 'sha1';
|
||||||
|
let digits = 6;
|
||||||
|
let keyB32 = key;
|
||||||
|
if (key.indexOf('otpauth://') === 0) {
|
||||||
|
const params = Utils.getQueryParams(key);
|
||||||
|
if (params.has('digits') && params.get('digits') != null) {
|
||||||
|
try {
|
||||||
|
const digitParams = parseInt(params.get('digits').trim(), null);
|
||||||
|
if (digitParams > 10) {
|
||||||
|
digits = 10;
|
||||||
|
} else if (digitParams > 0) {
|
||||||
|
digits = digitParams;
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if (params.has('period') && params.get('period') != null) {
|
||||||
|
try {
|
||||||
|
period = parseInt(params.get('period').trim(), null);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if (params.has('secret') && params.get('secret') != null) {
|
||||||
|
keyB32 = params.get('secret');
|
||||||
|
}
|
||||||
|
if (params.has('algorithm') && params.get('algorithm') != null) {
|
||||||
|
const algParam = params.get('algorithm').toLowerCase();
|
||||||
|
if (algParam === 'sha1' || algParam === 'sha256' || algParam === 'sha512') {
|
||||||
|
alg = algParam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getCode(keyb32: string): Promise<string> {
|
|
||||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||||
const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / 30)), 16, '0');
|
const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / period)), 16, '0');
|
||||||
const timeBytes = Utils.fromHexToArray(timeHex);
|
const timeBytes = Utils.fromHexToArray(timeHex);
|
||||||
const keyBytes = this.b32tobytes(keyb32);
|
const keyBytes = this.b32tobytes(keyB32);
|
||||||
|
|
||||||
if (!keyBytes.length || !timeBytes.length) {
|
if (!keyBytes.length || !timeBytes.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashHex = await this.sign(keyBytes, timeBytes);
|
const hash = await this.sign(keyBytes, timeBytes, alg);
|
||||||
if (!hashHex) {
|
if (hash.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const offset = this.hex2dec(hashHex.substring(hashHex.length - 1));
|
/* tslint:disable */
|
||||||
// tslint:disable-next-line
|
const offset = (hash[hash.length - 1] & 0xf);
|
||||||
let otp = (this.hex2dec(hashHex.substr(offset * 2, 8)) & this.hex2dec('7fffffff')) + '';
|
const binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) |
|
||||||
otp = (otp).substr(otp.length - 6, 6);
|
((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
|
||||||
|
/* tslint:enable */
|
||||||
|
let otp = (binary % Math.pow(10, digits)).toString();
|
||||||
|
otp = this.leftpad(otp, digits, '0');
|
||||||
return otp;
|
return otp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimeInterval(key: string): number {
|
||||||
|
let period = 30;
|
||||||
|
if (key.indexOf('otpauth://') === 0) {
|
||||||
|
const params = Utils.getQueryParams(key);
|
||||||
|
if (params.has('period') && params.get('period') != null) {
|
||||||
|
try {
|
||||||
|
period = parseInt(params.get('period').trim(), null);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return period;
|
||||||
|
}
|
||||||
|
|
||||||
async isAutoCopyEnabled(): Promise<boolean> {
|
async isAutoCopyEnabled(): Promise<boolean> {
|
||||||
return !(await this.storageService.get<boolean>(ConstantsService.disableAutoTotpCopyKey));
|
return !(await this.storageService.get<boolean>(ConstantsService.disableAutoTotpCopyKey));
|
||||||
}
|
}
|
||||||
@ -50,10 +98,6 @@ export class TotpService implements TotpServiceAbstraction {
|
|||||||
return (d < 15.5 ? '0' : '') + Math.round(d).toString(16);
|
return (d < 15.5 ? '0' : '') + Math.round(d).toString(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hex2dec(s: string): number {
|
|
||||||
return parseInt(s, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
private b32tohex(s: string): string {
|
private b32tohex(s: string): string {
|
||||||
s = s.toUpperCase();
|
s = s.toUpperCase();
|
||||||
let cleanedInput = '';
|
let cleanedInput = '';
|
||||||
@ -87,8 +131,8 @@ export class TotpService implements TotpServiceAbstraction {
|
|||||||
return Utils.fromHexToArray(this.b32tohex(s));
|
return Utils.fromHexToArray(this.b32tohex(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) {
|
private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array, alg: 'sha1' | 'sha256' | 'sha512') {
|
||||||
const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, 'sha1');
|
const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg);
|
||||||
return Utils.fromBufferToHex(signature);
|
return new Uint8Array(signature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user