import { Secret, TOTP } from 'otpauth' import encryption from '@adonisjs/core/services/encryption' import type User from '#models/user' const ISSUER = 'Big Brother' const PERIOD = 30 function build(secret: Secret, username: string) { return new TOTP({ issuer: ISSUER, label: username, secret, period: PERIOD, digits: 6 }) } /** * Two-factor authentication with a 6-digit code (Google Authenticator, * Aegis, 2FAS…). The secret is stored encrypted with the APP_KEY. */ const totpService = { /** * New secret, its encrypted form for the database and the otpauth:// * URI shown as a QR code. */ generate(username: string) { const secret = new Secret({ size: 20 }) return { encrypted: encryption.encrypt(secret.base32), base32: secret.base32, uri: build(secret, username).toString(), } }, /** * Checks a code against the given encrypted secret, accepting the * previous and next periods (clock drift). Returns the time step used. */ check(encryptedSecret: string, username: string, code: string): number | null { const base32 = encryption.decrypt(encryptedSecret) if (!base32 || !/^\d{6}$/.test(code)) return null const totp = build(Secret.fromBase32(base32), username) const delta = totp.validate({ token: code, window: 1 }) if (delta === null) return null return Math.floor(Date.now() / 1000 / PERIOD) + delta }, /** * Checks the code of a user and refuses a code already used (replay). */ async verify(user: User, code: string) { const step = this.check(user.totpSecret, user.username, code) if (step === null) return false if (user.totpLastStep !== null && step <= user.totpLastStep) return false user.totpLastStep = step await user.save() return true }, } export default totpService