This commit is contained in:
Manav Rathi
2025-05-30 12:49:17 +05:30
parent 2b35677227
commit 1acd1f81f4
3 changed files with 34 additions and 13 deletions

View File

@@ -180,13 +180,13 @@ const CodeDisplay: React.FC<CodeDisplayProps> = ({ code, timeOffset }) => {
const regen = useCallback(() => {
try {
const [m, n] = generateOTPs(code);
const [m, n] = generateOTPs(code, timeOffset);
setOTP(m);
setNextOTP(n);
} catch (e) {
setErrorMessage(e instanceof Error ? e.message : String(e));
}
}, [code]);
}, [code, timeOffset]);
const copyCode = () =>
void navigator.clipboard.writeText(otp).then(() => {
@@ -327,7 +327,7 @@ const CodeValidityBar: React.FC<CodeValidityBarProps> = ({
code.type == "hotp" ? undefined : setInterval(advance, 10);
return () => ticker && clearInterval(ticker);
}, [code]);
}, [code, timeOffset]);
const progressColor =
progress > 0.4

View File

@@ -256,12 +256,19 @@ const parseCodeDisplay = (url: URL): CodeDisplay | undefined => {
*
* @param code The parsed code data, including the secret and code type.
*
* @param timeOffset A millisecond delta that should be applied to Date.now when
* deriving the OTP.
*
* @returns a pair of OTPs, the current one and the next one, using the given
* {@link code}.
*/
export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
export const generateOTPs = (
code: Code,
timeOffset: number,
): [otp: string, nextOTP: string] => {
let otp: string;
let nextOTP: string;
const timestamp = Date.now() + timeOffset;
switch (code.type) {
case "totp": {
const totp = new TOTP({
@@ -270,9 +277,9 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
period: code.period,
digits: code.length,
});
otp = totp.generate();
otp = totp.generate({ timestamp });
nextOTP = totp.generate({
timestamp: Date.now() + code.period * 1000,
timestamp: timestamp + code.period * 1000,
});
break;
}
@@ -291,9 +298,9 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
case "steam": {
const steam = new Steam({ secret: code.secret });
otp = steam.generate();
otp = steam.generate({ timestamp });
nextOTP = steam.generate({
timestamp: Date.now() + code.period * 1000,
timestamp: timestamp + code.period * 1000,
});
break;
}

View File

@@ -14,8 +14,8 @@ import { z } from "zod";
export interface AuthCodesAndTimeOffset {
codes: Code[];
/**
* An optional and approximate correction which should be applied to the
* current client's time when deriving TOTPs.
* An optional and approximate correction (milliseconds) which should be
* applied to the current client's time when deriving TOTPs.
*/
timeOffset?: number;
}
@@ -112,7 +112,14 @@ const RemoteAuthenticatorEntityChange = z.object({
});
const AuthenticatorEntityDiffResponse = z.object({
/**
* Changes to entities.
*/
diff: z.array(RemoteAuthenticatorEntityChange),
/**
* An optional epoch microseconds indicating the remote time when it
* generated the response.
*/
timestamp: z.number().nullish().transform(nullToUndefined),
});
@@ -122,8 +129,8 @@ export interface AuthenticatorEntityDiffResult {
*/
entities: AuthenticatorEntity[];
/**
* An optional and approximate offset by which the time on the current client
* is out of sync.
* An optional and approximate offset (in milliseconds) by which the time on
* the current client is out of sync.
*
* This offset is computed by calculated by comparing the timestamp when the
* remote generated the response to the time we received it. As such
@@ -174,10 +181,17 @@ export const authenticatorEntityDiff = async (
headers: await authenticatedRequestHeaders(),
});
ensureOk(res);
const { diff, timestamp } = AuthenticatorEntityDiffResponse.parse(
await res.json(),
);
if (timestamp) timeOffset = Date.now() - timestamp;
if (timestamp) {
// - timestamp is in epoch microseconds.
// - Date.now and timeOffset are in epoch milliseconds.
timeOffset = Date.now() - Math.floor(timestamp / 1e3);
}
if (diff.length == 0) break;
for (const change of diff) {