auth/totp.ts per 08 §2.4: RFC 6238 (HMAC-SHA1, 30 s, 6 digits, ±1
step) with node:crypto only, checked against the RFC's test vectors.
Migration 0003: users.totp_recovery_hashes and totp_last_step.
Admins turn it on from My account (the header's "Account"): scan the
QR or type the key, confirm a code, get 10 recovery codes once. Login
asks for the code only after the password is right (401 totp_required,
no session); a wrong code is a failed login (rate limit, audit). A
step signs in once (totp_last_step updated atomically) and a recovery
code once (array_remove guarded by any()). Off and new codes need the
password; another admin can turn it off for a lost phone, which signs
that user out. Users list shows it (twoStep).
Also, found by the full e2e chain on one shared database:
- Linphone tokens: only the exact token issued is accepted. The last
base64 character can carry ignored bits, so another spelling decoded
to the same signed token.
- Test scripts no longer assume a fresh system: m3-t3 picks Dan in the
test box, m8-check dials whatever slot the park took (findslot =
next), m10-t4 uses the admin's existing extension, m11-t1 clears the
public IP on its own first (local nets are in the transports while
one is set, and transports change only on a restart).
e2e m11-t1-totp; the full chain M1-M11 (33 scripts) passes.
Task: M11-T1
Headers (08 §2.4) on every response: CSP (connect-src also allows
WEBRTC_WS_URL's origin when set), nosniff, Referrer-Policy,
Permissions-Policy. The dark-mode bootstrap moved from an inline
<script> to /theme.js so script-src stays 'self'.
Phone ACL (08 §2.2): with sip.restrictPhonesToLocalNets, every desk
extension endpoint gets deny 0.0.0.0/0 and permit for each local and
extra net. The spec's snippet was wrong: contact_acl names acl.conf
sections, a pjsip type=acl is the global ACL (it would filter trunks),
and Contact-header rules let a NATed remote phone through. Browser
endpoints come through the reverse proxy and aren't filtered. Golden
case phone-acl. Settings → Security holds it, with the provisioning
switches.
PIN rules for PINs changed from the phone's voicemail menu, which went
around the api: externpasscheck = sp-vm-pincheck (in the Asterisk
image, same rules as isTrivialPin, a unit test keeps them in step) and
minpassword = 4. Accepted PINs reach the api through AMI
VoicemailPasswordChange and are stored; before, the next config write
silently put the old PIN back.
e2e m11-t1: no CSP violations across admin pages; Asterisk refuses a
REGISTER ("Not match Endpoint ACL") until the network is listed; from
the phone, 1111 is refused by the check script and 7391 is stored and
survives a later config write.
Task: M11-T1