2018-01-09 23:45:21 +01:00
|
|
|
import { ConstantsService } from './constants.service';
|
|
|
|
|
2018-04-22 05:55:21 +02:00
|
|
|
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
|
2018-02-19 19:07:19 +01:00
|
|
|
import { StorageService } from '../abstractions/storage.service';
|
|
|
|
import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service';
|
2018-01-09 23:45:21 +01:00
|
|
|
|
2018-04-22 05:55:21 +02:00
|
|
|
import { Utils } from '../misc/utils';
|
2018-01-09 23:45:21 +01:00
|
|
|
|
2018-04-22 05:55:21 +02:00
|
|
|
const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
2018-01-09 23:45:21 +01:00
|
|
|
|
2018-01-25 05:27:04 +01:00
|
|
|
export class TotpService implements TotpServiceAbstraction {
|
2018-07-31 17:25:50 +02:00
|
|
|
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 {
|
2018-07-31 17:35:04 +02:00
|
|
|
const periodParam = parseInt(params.get('period').trim(), null);
|
|
|
|
if (periodParam > 0) {
|
|
|
|
period = periodParam;
|
|
|
|
}
|
2018-07-31 17:25:50 +02:00
|
|
|
} 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-01-09 23:45:21 +01:00
|
|
|
|
|
|
|
const epoch = Math.round(new Date().getTime() / 1000.0);
|
2018-07-31 17:25:50 +02:00
|
|
|
const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / period)), 16, '0');
|
2018-04-22 05:55:21 +02:00
|
|
|
const timeBytes = Utils.fromHexToArray(timeHex);
|
2018-07-31 17:25:50 +02:00
|
|
|
const keyBytes = this.b32tobytes(keyB32);
|
2018-01-09 23:45:21 +01:00
|
|
|
|
|
|
|
if (!keyBytes.length || !timeBytes.length) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-07-31 17:25:50 +02:00
|
|
|
const hash = await this.sign(keyBytes, timeBytes, alg);
|
|
|
|
if (hash.length === 0) {
|
2018-01-09 23:45:21 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-07-31 17:25:50 +02:00
|
|
|
/* 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');
|
2018-01-09 23:45:21 +01:00
|
|
|
return otp;
|
|
|
|
}
|
|
|
|
|
2018-07-31 17:25:50 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-01-09 23:45:21 +01:00
|
|
|
async isAutoCopyEnabled(): Promise<boolean> {
|
|
|
|
return !(await this.storageService.get<boolean>(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 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 {
|
2018-04-22 05:55:21 +02:00
|
|
|
return Utils.fromHexToArray(this.b32tohex(s));
|
2018-01-09 23:45:21 +01:00
|
|
|
}
|
|
|
|
|
2018-07-31 17:25:50 +02:00
|
|
|
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);
|
2018-01-09 23:45:21 +01:00
|
|
|
}
|
|
|
|
}
|