45 lines
1.6 KiB
TypeScript
45 lines
1.6 KiB
TypeScript
|
|
import { randomInt } from "node:crypto";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The number-matching challenge.
|
||
|
|
*
|
||
|
|
* The laptop shows ONE number; the phone asks for it. This defeats blind
|
||
|
|
* approval — an attacker who can trigger a prompt but cannot see the victim's
|
||
|
|
* screen has to guess.
|
||
|
|
*
|
||
|
|
* How much it buys is arithmetic, and worth stating because "number matching"
|
||
|
|
* sounds stronger than the weak variant is:
|
||
|
|
*
|
||
|
|
* mode "type" two digits, entered on a keypad log2(90) = 6.5 bits
|
||
|
|
* mode "choice" one of three buttons log2(3) = 1.6 bits
|
||
|
|
*
|
||
|
|
* With a single attempt and a cap of 3 sessions per source, "choice" leaves an
|
||
|
|
* attacker at 1 - (2/3)^3 = 70%, which is not a control. "type" leaves them at
|
||
|
|
* 1 - (89/90)^3 = 3.3%. Hence `type` is the default; `choice` exists because it
|
||
|
|
* is the friendlier UX and some deployments will want it with eyes open.
|
||
|
|
*/
|
||
|
|
export type NumberMode = "type" | "choice";
|
||
|
|
|
||
|
|
/** Two digits. Never 0-9: a leading zero reads ambiguously across fonts. */
|
||
|
|
export function pickNumber(): number {
|
||
|
|
return randomInt(10, 100);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Two decoys for `choice` mode, distinct from each other and from the answer.
|
||
|
|
* Returned already shuffled, so the correct one is not positionally biased.
|
||
|
|
*/
|
||
|
|
export function pickDecoys(answer: number): number[] {
|
||
|
|
const chosen = new Set<number>([answer]);
|
||
|
|
while (chosen.size < 3) chosen.add(pickNumber());
|
||
|
|
|
||
|
|
const all = [...chosen];
|
||
|
|
// Fisher-Yates with a CSPRNG. Math.random would be fine for display order,
|
||
|
|
// but using it here invites someone to reuse it where it is not.
|
||
|
|
for (let i = all.length - 1; i > 0; i--) {
|
||
|
|
const j = randomInt(0, i + 1);
|
||
|
|
[all[i], all[j]] = [all[j], all[i]];
|
||
|
|
}
|
||
|
|
return all;
|
||
|
|
}
|