diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index f6661b1b1..b973d330e 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -233,7 +233,7 @@ const OTPDisplay: React.FC = ({ code, otp, nextOTP }) => { overflow: "hidden", }} > - +
= ({ code, otp, nextOTP }) => { ); }; -interface TimerProgressProps { - period: number; +interface CodeValidityBarProps { + code: Code; } -const TimerProgress: React.FC = ({ period }) => { - const [progress, setProgress] = useState(0); - const us = period * 1e6; +const CodeValidityBar: React.FC = ({ code }) => { + const [progress, setProgress] = useState(code.type == "hotp" ? 1 : 0); useEffect(() => { const advance = () => { + const us = code.period * 1e6; const timeRemaining = us - ((Date.now() * 1000) % us); setProgress(timeRemaining / us); }; - const ticker = setInterval(advance, 10); + const ticker = + code.type == "hotp" ? undefined : setInterval(advance, 10); - return () => clearInterval(ticker); - }, []); + return () => ticker && clearInterval(ticker); + }, [code]); const color = progress > 0.4 ? "green" : "orange"; diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index b5da0ffe5..dc1a6db24 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -29,6 +29,15 @@ export interface Code { * code remains valid. */ period: number; + /** The (HMAC) algorithm used by the OTP generator. */ + algorithm: "sha1" | "sha256" | "sha512"; + /** + * HOTP counter. + * + * Only valid for HOTP codes. It might be even missing for HOTP codes, in + * which case we should start from 0. + */ + counter?: number; /** * The secret that is used to drive the OTP generator. * @@ -36,8 +45,6 @@ export interface Code { * {@link type}-specific manner). */ secret: string; - /** The (HMAC) algorithm used by the OTP generator. */ - algorithm: "sha1" | "sha256" | "sha512"; /** The original string from which this code was generated. */ uriString: string; } @@ -53,6 +60,12 @@ export interface Code { * - (TOTP) * otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM * + * - (HOTP) + * otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0 + * + * - (Steam) + * otpauth://steam/Steam:SteamAccount?algorithm=SHA1&digits=5&issuer=Steam&period=30&secret=AAABBBCCCDDDEEEFFF + * * See also `auth/test/models/code_test.dart`. */ export const codeFromURIString = (id: string, uriString: string): Code => { @@ -94,8 +107,9 @@ const _codeFromURIString = (id: string, uriString: string): Code => { issuer: parseIssuer(url, path), length: parseLength(url, type), period: parsePeriod(url), - secret: parseSecret(url), algorithm: parseAlgorithm(url), + counter: parseCounter(url), + secret: parseSecret(url), uriString, }; }; @@ -164,6 +178,11 @@ const parseAlgorithm = (url: URL): Code["algorithm"] => { } }; +const parseCounter = (url: URL): number | undefined => { + const c = url.searchParams.get("counter"); + return c ? parseInt(c, 10) : undefined; +}; + const parseSecret = (url: URL): string => ensure(url.searchParams.get("secret")).replaceAll(" ", "").toUpperCase(); @@ -194,13 +213,14 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { } case "hotp": { + const counter = code.counter || 0; const hotp = new HOTP({ secret: code.secret, - counter: 0, + counter: counter, algorithm: code.algorithm, }); - otp = hotp.generate(); - nextOTP = hotp.generate({ counter: 1 }); + otp = hotp.generate({ counter }); + nextOTP = hotp.generate({ counter: counter + 1 }); break; }