feat(offline): PIN lock primitives + Apple-style keypad
Groundwork for offline pass-and-play (hotseat) seat/host PIN locks: - lib/pin.ts: salted SHA-256 PinLock (newSalt/hashPin/newLock/verifyPin); a social lock for a shared device (documented, not cryptography). - components/PinPad.svelte: 4-digit keypad (set/verify/change), auto-submit on the 4th digit, shake on mismatch, hardware-keyboard support. - i18n: pin.* keys (en + ru). Engine/source/UI wiring follows.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
// Seat / host PIN locks for offline pass-and-play (hotseat) games.
|
||||
//
|
||||
// A PinLock is a salted SHA-256 digest of a 4-digit PIN. This is a SOCIAL lock for a
|
||||
// shared device — not cryptographic protection. A 4-digit PIN has only 10^4
|
||||
// combinations and the whole game runs on the client, so a determined peek reads the
|
||||
// racks straight from memory regardless. The salt only keeps the PIN itself out of a
|
||||
// casual devtools glance and defeats precomputed tables; it does not make the PIN
|
||||
// brute-force resistant.
|
||||
|
||||
/** PinLock is a stored, salted SHA-256 digest of a PIN (see the file header caveat). */
|
||||
export interface PinLock {
|
||||
hash: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
// b64url encodes bytes as base64url without padding (matches the idiom in vkid.ts).
|
||||
function b64url(bytes: Uint8Array): string {
|
||||
let s = '';
|
||||
for (const b of bytes) s += String.fromCharCode(b);
|
||||
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
/** newSalt returns a fresh random base64url salt for a PinLock. */
|
||||
export function newSalt(): string {
|
||||
const a = new Uint8Array(16);
|
||||
crypto.getRandomValues(a);
|
||||
return b64url(a);
|
||||
}
|
||||
|
||||
/** hashPin returns the base64url SHA-256 digest binding salt to pin. */
|
||||
export async function hashPin(pin: string, salt: string): Promise<string> {
|
||||
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${salt}:${pin}`));
|
||||
return b64url(new Uint8Array(digest));
|
||||
}
|
||||
|
||||
/** newLock builds a PinLock for pin with a fresh salt. */
|
||||
export async function newLock(pin: string): Promise<PinLock> {
|
||||
const salt = newSalt();
|
||||
return { salt, hash: await hashPin(pin, salt) };
|
||||
}
|
||||
|
||||
/** verifyPin reports whether pin matches the digest stored in lock. */
|
||||
export async function verifyPin(pin: string, lock: PinLock): Promise<boolean> {
|
||||
return (await hashPin(pin, lock.salt)) === lock.hash;
|
||||
}
|
||||
Reference in New Issue
Block a user