screentinker/server/lib/numeric-code.js
ScreenTinker dce0bc6f54 fix(devices): generate access-gating six-digit codes with a CSPRNG
The on-device settings PIN (devices.settings_pin, minted at pairing) and the pairing code
assigned to imported devices both came from
`Math.floor(100000 + Math.random() * 900000)`.

Math.random is not a CSPRNG. V8 implements it as xorshift128+, whose internal state is
recoverable from a handful of consecutive outputs, and every call in a process draws from
that one shared stream. Both values are also observable by ordinary users — settings_pin
is returned in device API responses today — so a user who collects a few outputs could
predict the values minted around them, including for other tenants.

lib/numeric-code.sixDigitCode() uses crypto.randomInt, which is CSPRNG-backed and
rejection-samples so the distribution stays uniform. Range is 100000..999999 inclusive,
identical to the old expression, so codes are still exactly six digits with no leading
zero — the on-device keypad and pairing UI are unchanged.

Deliberately NOT converted, because neither gates access: the image-generation seed in
lib/image-gen.js, and the anti-burn-in pixel jitter inside generated widget HTML.
Also unchanged: the settings_pin backfill in db/database.js, which uses SQLite's random()
— that is ChaCha20 seeded from OS entropy, not a weak PRNG.

This is the generator half of the finding only. The separate half — that settings_pin is
returned to every workspace member, including read-only roles — is a response-shape change
and waits on the consumer enumeration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:24:03 -05:00

27 lines
1.2 KiB
JavaScript

'use strict';
// Six-digit codes that gate access: the on-device settings PIN (devices.settings_pin) and
// the provisioning pairing code (devices.pairing_code).
//
// Both were generated with `Math.floor(100000 + Math.random() * 900000)`. Math.random is
// not a CSPRNG — V8 implements it as xorshift128+, whose internal state is recoverable
// from a handful of consecutive outputs, and every call in a process draws from that one
// shared stream. These two values are also OBSERVABLE by ordinary users (the PIN is
// returned in device API responses today), so an attacker who can collect a few can
// predict the ones minted around them for other tenants.
//
// crypto.randomInt is CSPRNG-backed and rejection-samples, so the distribution stays
// uniform across the range rather than skewing the way a modulo would.
//
// Range is 100000..999999 inclusive — identical to what the old expression produced, so
// the code is always exactly six digits with no leading zero, which is what the on-device
// keypad and the pairing UI expect.
const crypto = require('crypto');
function sixDigitCode() {
return String(crypto.randomInt(100000, 1000000));
}
module.exports = { sixDigitCode };