From 9b327683b4c0b8504724aca213e08d484c290804 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 17:45:21 -0500 Subject: [PATCH] convert totp service to jslib --- src/abstractions/index.ts | 1 + src/abstractions/totp.service.ts | 4 ++ src/services/index.ts | 1 + src/services/totp.service.ts | 118 +++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 src/abstractions/totp.service.ts create mode 100644 src/services/totp.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index b2348f7326..d026e4c372 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -5,5 +5,6 @@ export { MessagingService } from './messaging.service'; export { PlatformUtilsService } from './platformUtils.service'; export { StorageService } from './storage.service'; export { TokenService } from './token.service'; +export { TotpService } from './totp.service'; export { UserService } from './user.service'; export { UtilsService } from './utils.service'; diff --git a/src/abstractions/totp.service.ts b/src/abstractions/totp.service.ts new file mode 100644 index 0000000000..2e8fa79aa7 --- /dev/null +++ b/src/abstractions/totp.service.ts @@ -0,0 +1,4 @@ +export interface TotpService { + getCode(keyb32: string): Promise; + isAutoCopyEnabled(): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index 1ef71d40bb..67e7b6aa59 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -3,5 +3,6 @@ export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; export { TokenService } from './token.service'; +export { TotpService } from './totp.service'; export { UserService } from './user.service'; export { UtilsService } from './utils.service'; diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts new file mode 100644 index 0000000000..ac6cc4cb78 --- /dev/null +++ b/src/services/totp.service.ts @@ -0,0 +1,118 @@ +import { ConstantsService } from './constants.service'; + +import { StorageService } from '../abstractions/storage.service'; +import { TotpService as TotpServiceInterface } from '../abstractions/totp.service'; + +const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + +const TotpAlgorithm = { + name: 'HMAC', + hash: { name: 'SHA-1' }, +}; + +export class TotpService implements TotpServiceInterface { + constructor(private storageService: StorageService) { + } + + 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 timeBytes = this.hex2bytes(timeHex); + const keyBytes = this.b32tobytes(keyb32); + + if (!keyBytes.length || !timeBytes.length) { + return null; + } + + const hashHex = await this.sign(keyBytes, timeBytes); + if (!hashHex) { + 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); + return otp; + } + + async isAutoCopyEnabled(): Promise { + return !(await this.storageService.get(ConstantsService.disableAutoTotpCopyKey)); + } + + // Helpers + + private leftpad(s: string, l: number, p: string): string { + if (l + 1 >= s.length) { + s = Array(l + 1 - s.length).join(p) + s; + } + return s; + } + + private dec2hex(d: number): string { + return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); + } + + private hex2dec(s: string): number { + return parseInt(s, 16); + } + + private hex2bytes(s: string): Uint8Array { + const bytes = new Uint8Array(s.length / 2); + for (let i = 0; i < s.length; i += 2) { + bytes[i / 2] = parseInt(s.substr(i, 2), 16); + } + return bytes; + } + + private buff2hex(buff: ArrayBuffer): string { + const bytes = new Uint8Array(buff); + const hex: string[] = []; + bytes.forEach((b) => { + // tslint:disable-next-line + hex.push((b >>> 4).toString(16)); + // tslint:disable-next-line + hex.push((b & 0xF).toString(16)); + }); + return hex.join(''); + } + + private b32tohex(s: string): string { + s = s.toUpperCase(); + let cleanedInput = ''; + + for (let i = 0; i < s.length; i++) { + if (b32Chars.indexOf(s[i]) < 0) { + continue; + } + + cleanedInput += s[i]; + } + s = cleanedInput; + + let bits = ''; + let hex = ''; + for (let i = 0; i < s.length; i++) { + const byteIndex = b32Chars.indexOf(s.charAt(i)); + if (byteIndex < 0) { + continue; + } + bits += this.leftpad(byteIndex.toString(2), 5, '0'); + } + for (let i = 0; i + 4 <= bits.length; i += 4) { + const chunk = bits.substr(i, 4); + hex = hex + parseInt(chunk, 2).toString(16); + } + return hex; + } + + private b32tobytes(s: string): Uint8Array { + return this.hex2bytes(this.b32tohex(s)); + } + + private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) { + const key = await window.crypto.subtle.importKey('raw', keyBytes, TotpAlgorithm, false, ['sign']); + const signature = await window.crypto.subtle.sign(TotpAlgorithm, key, timeBytes); + return this.buff2hex(signature); + } +}