diff --git a/src/abstractions/totp.service.ts b/src/abstractions/totp.service.ts index 1155d3c2f7..608c7d1b3d 100644 --- a/src/abstractions/totp.service.ts +++ b/src/abstractions/totp.service.ts @@ -1,4 +1,5 @@ export abstract class TotpService { - getCode: (keyb32: string) => Promise; + getCode: (key: string) => Promise; + getTimeInterval: (key: string) => number; isAutoCopyEnabled: () => Promise; } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index a7325893dc..b05fd6a76d 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -63,10 +63,11 @@ export class ViewComponent implements OnDestroy { if (this.cipher.type === CipherType.Login && this.cipher.login.totp && (cipher.organizationUseTotp || this.isPremium)) { await this.totpUpdateCode(); - await this.totpTick(); + const interval = this.totpService.getTimeInterval(this.cipher.login.totp); + await this.totpTick(interval); this.totpInterval = setInterval(async () => { - await this.totpTick(); + await this.totpTick(interval); }, 1000); } } @@ -178,7 +179,12 @@ export class ViewComponent implements OnDestroy { this.totpCode = await this.totpService.getCode(this.cipher.login.totp); 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 { this.totpCodeFormatted = null; 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 mod = epoch % 30; + const mod = epoch % intervalSeconds; - this.totpSec = 30 - mod; - this.totpDash = +(Math.round(((2.62 * mod) + 'e+2') as any) + 'e-2'); + this.totpSec = intervalSeconds - mod; + this.totpDash = +(Math.round((((78.6 / intervalSeconds) * mod) + 'e+2') as any) + 'e-2'); this.totpLow = this.totpSec <= 7; if (mod === 0) { await this.totpUpdateCode(); diff --git a/src/misc/utils.ts b/src/misc/utils.ts index b507f2aba1..67627cb4b0 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -151,6 +151,23 @@ export class Utils { return url != null ? url.host : null; } + static getQueryParams(uriString: string): Map { + const url = Utils.getUrl(uriString); + if (url == null || url.search == null || url.search === '') { + return null; + } + const map = new Map(); + 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) { return (a: any, b: any) => { if (a[prop] == null && b[prop] != null) { @@ -178,23 +195,24 @@ export class Utils { return null; } - if (uriString.indexOf('://') === -1 && uriString.indexOf('.') > -1) { + const hasProtocol = uriString.indexOf('://') > -1; + if (!hasProtocol && uriString.indexOf('.') > -1) { uriString = 'http://' + uriString; + } else if (!hasProtocol) { + return null; } - if (uriString.startsWith('http://') || uriString.startsWith('https://')) { - try { - if (nodeURL != null) { - return new nodeURL(uriString); - } else if (typeof URL === 'function') { - return new URL(uriString); - } else if (window != null) { - const anchor = window.document.createElement('a'); - anchor.href = uriString; - return anchor as any; - } - } catch (e) { } - } + try { + if (nodeURL != null) { + return new nodeURL(uriString); + } else if (typeof URL === 'function') { + return new URL(uriString); + } else if (window != null) { + const anchor = window.document.createElement('a'); + anchor.href = uriString; + return anchor as any; + } + } catch (e) { } return null; } diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 3a8fb3b896..233ddde406 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -9,30 +9,78 @@ import { Utils } from '../misc/utils'; const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; export class TotpService implements TotpServiceAbstraction { - constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) {} + constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) { } + + async getCode(key: string): Promise { + 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 { 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 keyBytes = this.b32tobytes(keyb32); + const keyBytes = this.b32tobytes(keyB32); if (!keyBytes.length || !timeBytes.length) { return null; } - const hashHex = await this.sign(keyBytes, timeBytes); - if (!hashHex) { + const hash = await this.sign(keyBytes, timeBytes, alg); + if (hash.length === 0) { return null; } - const offset = this.hex2dec(hashHex.substring(hashHex.length - 1)); - // tslint:disable-next-line - let otp = (this.hex2dec(hashHex.substr(offset * 2, 8)) & this.hex2dec('7fffffff')) + ''; - otp = (otp).substr(otp.length - 6, 6); + /* tslint:disable */ + const offset = (hash[hash.length - 1] & 0xf); + const binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | + ((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; } + 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 { return !(await this.storageService.get(ConstantsService.disableAutoTotpCopyKey)); } @@ -50,10 +98,6 @@ export class TotpService implements TotpServiceAbstraction { return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); } - private hex2dec(s: string): number { - return parseInt(s, 16); - } - private b32tohex(s: string): string { s = s.toUpperCase(); let cleanedInput = ''; @@ -87,8 +131,8 @@ export class TotpService implements TotpServiceAbstraction { return Utils.fromHexToArray(this.b32tohex(s)); } - private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) { - const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, 'sha1'); - return Utils.fromBufferToHex(signature); + private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array, alg: 'sha1' | 'sha256' | 'sha512') { + const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg); + return new Uint8Array(signature); } }