Authentication

A second factor protects the path to your seal

A professional seal carries legal weight. We require MFA enrollment before any user can seal, and we re-verify a fresh TOTP (or backup code) at sealing time. The first seal in a session prompts the second factor; subsequent seals within a 10-minute sudo window pass through. This pattern aligns with 21 CFR Part 11 §11.200(a)(1)(i): the first signing in a continuous period uses all electronic signature components; subsequent signings use at least one. TOTP secrets are AES-256-GCM encrypted at rest; backup codes are bcrypt-hashed.

Executive summary

We use TOTP-based MFA (RFC 6238) and require it before any user can seal. Sealing additionally re-verifies the second factor: the first seal in a session prompts a TOTP code (or one-time backup code); subsequent seals within a 10-minute sudo window pass through. The TOTP secret is encrypted at rest with field-level AES-256-GCM, and the backup recovery codes are stored as Bcrypt hashes — even a database read alone cannot bypass the gate.

FIDO2 / WebAuthn passkeys are on the roadmap. Passkeys are phishing-resistant, device-bound, and have no shared secret — strictly stronger than TOTP. SMS is explicitly not on the path; NIST has been deprecating it as a second factor since 2016 for cause.

Our commitments

Five rules for the second factor

MFA enrollment is required to seal, and a step-up check runs at sealing time on a 10-minute sudo window.

01

MFA step-up at sealing time (10-minute sudo window)

MFA enrollment is required before any user can issue a seal. At sealing time, the first seal in a session prompts a fresh TOTP (or one-time backup code); subsequent seals within a 10-minute window pass through. This mirrors 21 CFR Part 11 §11.200(a)(1)(i): the first signing in a continuous period uses all signature components; subsequent signings use at least one.

02

The TOTP secret is encrypted at rest

The shared secret between user and authenticator is field-level AES-256-GCM encrypted with redact: true. A database read does not yield usable secrets.

03

Backup codes are Bcrypt-hashed

Recovery codes are stored as Bcrypt password hashes — slow to brute-force, single-use, marked used at the moment of redemption in a single transaction.

04

Verification is timing-safe

TOTP and backup-code verification paths take the same observable time. An attacker cannot tell whether they failed because the code was wrong or because the code was a different type.

05

No SMS, ever

SS7 attacks, SIM swapping, carrier-side inspection — SMS as a second factor has been deprecated by NIST since 2016. We do not offer it as an option.

Implementation — TOTP

How TOTP is hardened

Standard TOTP — RFC 6238 HMAC-based One-Time Password
Library NimbleTOTP Audited Elixir TOTP implementation
Secret length 20 bytes (160 bits) NimbleTOTP.secret(20); base32-encoded for QR/manual entry
Code length 6 digits Standard for compatibility with all authenticator apps
Step interval 30 seconds Default; matches Google Authenticator, 1Password, Authy
Secret storage AES-256-GCM at rest Field-level encryption with redact: true on schema
Decryption window Single verification call Plaintext exists in memory only for the duration of the verify_totp/2 call
Algorithm HMAC-SHA1 RFC 6238 default; broadly compatible with authenticator apps
Drift tolerance ±1 step (30s window) Conservative — prevents replay across multiple windows
Onboarding QR code + manual fallback otpauth:// URI generated; QR rendered server-side

Implementation — backup codes

How recovery codes are hardened

Generation Cryptographic random :crypto.strong_rand_bytes/1 per code
Code count 10 codes per regeneration Enough for prolonged authenticator unavailability without inviting reuse
Format Hyphenated for readability Normalized at verification: hyphens and whitespace stripped
Storage Bcrypt hash Slow hash; rate-limits offline brute-force; salt per code
Use semantics Single-use used_at marked atomically at redemption; replay rejected
Display Shown to user once at creation Plaintext returned exactly once; we cannot recover or re-display
Regeneration Invalidates all prior codes Single transaction: delete old + insert new
Verification timing Padded to match TOTP path Backup verification (DB hit) and TOTP verification (CPU only) take the same observable time

The full picture

What is built, what is being built, and what we chose not to build

Live today

TOTP-based MFA via NimbleTOTP

Live

RFC 6238 with 6-digit codes, 30-second steps, ±1-step drift tolerance. Compatible with every mainstream authenticator app.

Step-up MFA at sealing (10-minute sudo window)

Live

MFA enrollment is required to seal. The first seal in a session prompts a fresh TOTP (or one-time backup code); subsequent seals within a 10-minute window pass through without re-prompting. The same gate applies to single-doc seals and batch seals — one verification authorizes the batch. Aligns with 21 CFR Part 11 §11.200(a)(1)(i).

Field-level encrypted TOTP secret

Live

AES-256-GCM at rest with redact: true; the shared secret never reaches logs, telemetry, or Inspect output.

Bcrypt-hashed backup codes

Live

Slow-hash storage rate-limits offline brute-force. Single-use marking is atomic at redemption time.

Timing-safe verification

Live

TOTP and backup paths take the same observable time. Failures look identical regardless of code type.

QR-code onboarding with manual fallback

Live

Server-rendered SVG QR; manual base32 entry for environments without a camera.

Building now

WebAuthn / FIDO2 passkey enrollment

Building now

Device-bound public-key authentication. Phishing-resistant — the credential is bound to the relying party origin and cannot be replayed elsewhere. No shared secret to leak.

Target: enrollment in /settings/security this quarter. Sealing-gate integration follows.

Step-up authentication for sensitive non-sealing operations

Building now

Re-prompt MFA on credential changes, payment-method updates, member role escalation, and SSO config changes. Today's gate is sealing-only; this expands the surface.

Hardware security key enforcement (Enterprise policy)

Building now

Org admins can require a FIDO2 hardware key for sealing, optionally limiting accepted vendors (YubiKey, Feitian, etc.). Locks out TOTP and platform passkeys for the strictest compliance regimes.

Roadmap

Passkey-only mode

Roadmap

An org-level setting that disables TOTP entirely for the org's users, leaving passkeys as the only second factor. For customers running NIST AAL3 environments.

Device attestation at enrollment

Roadmap

Verify the FIDO2 authenticator's manufacturer attestation at enrollment time so org admins can constrain which physical devices are acceptable.

Risk-based step-up

Roadmap

A risk-scored login (new geo, new device, anomalous time-of-day) triggers a step-up challenge regardless of the operation. Today, sealing is the only step-up trigger.

Considered & rejected

SMS-based MFA

Considered & rejected

SS7 attacks, SIM swapping, carrier-side inspection. SMS has been deprecated by NIST as a second factor since SP 800-63B-2016.

Why we rejected it: every "we'll just SMS the code" is a way to ship a known-broken second factor for the convenience of users who do not own a smartphone with an authenticator app. The right answer is: TOTP works on every phone. Passkeys work on every modern device. SMS is a security regression we will not subsidize.

Email-based MFA

Considered & rejected

If the email is compromised, MFA is bypassed. Email is a recovery channel, not a second factor.

Why we rejected it: an attacker who has the password very often has the email. Email-as-second-factor collapses to single-factor in those cases. We use email for password recovery — explicitly named as such — and never as MFA.

Persistent "remember this device for 30 days" MFA bypass

Considered & rejected

The most common path to professional account takeover. We do not offer it, even though competitors do.

Why we rejected it: every "remember me" is a prefilled answer to "did you actually verify the human?" For sealing — a legally significant act — the friction of MFA every time is the feature. The cost is one 6-digit code; the benefit is that a stolen session cannot produce a forged seal.

Push notifications as a second factor

Considered & rejected

Push fatigue is a documented attack vector — users approve prompts they did not initiate.

Why we rejected it: the 2022 Uber breach is the canonical example. Push approvals are a usability win when paired with number-matching (where the user types a code from the prompt back into the app), and we may revisit number-matched push for non-sealing step-up. As a primary factor, push without number-matching is a regression we will not ship.

Storing the TOTP secret in plaintext for "operational ease"

Considered & rejected

Plaintext shared secrets in a database is the textbook example of a horizon-of-failure leak.

Why we rejected it: every "we keep it plaintext for backup recovery" is a way for one breach to compromise every user's second factor. Encrypting it is one extra symmetric operation per verification. We pay it.

Recovery via security questions

Considered & rejected

Security questions are public information and have been since LinkedIn data leaked in 2012.

Why we rejected it: mother's maiden name, first pet, high school — all of these are findable. Backup codes are unguessable cryptographic random. They are the right primitive for "I lost my authenticator." We do not invent a worse one.

Compliance mappings

Controls this surface satisfies

SOC 2 CC6.1

Logical Access — Authentication

Session MFA; encrypted secret; hashed backup codes

SOC 2 CC6.6

Logical Access — Network Segmentation

MFA verifies the human regardless of network origin

ISO 27001 A.9.4.2

Secure log-on procedures

Multi-factor with hardened secret storage and timing-safe verification

ISO 27001 A.9.4.3

Password management system

Backup codes stored as Bcrypt hashes; never displayed twice

NIST SP 800-63B AAL2

Authentication Assurance Level 2

TOTP today (multi-factor cryptographic); AAL3 with passkeys, in progress

HIPAA §164.312(d)

Person or Entity Authentication

Verifies the user before access to protected health information surfaces

21 CFR Part 11 11.200(a)

Electronic signature components

Two distinct identification components required for signing

PCI DSS 8.4

Multi-factor authentication

Standard MFA for all administrative access

For compliance teams

Questions you do not need to call to ask

Which authenticator apps are supported?
All RFC 6238-compatible apps: Google Authenticator, 1Password, Authy, Microsoft Authenticator, Bitwarden, Aegis, Raivo, FreeOTP, and any passkey-enabled mobile keychain. The QR code uses the standard otpauth:// URI; manual base32 entry works for apps that do not support QR.
What happens if a user loses their authenticator and their backup codes?
An organization admin with the manage_members permission can initiate identity-verified MFA reset. The user proves identity through an out-of-band channel (org-admin-initiated email, SSO re-auth in federated orgs, or video verification per org policy). The reset event is recorded in the hash-chained audit log with both actors named.
When do passkeys ship?
Enrollment in /settings/security is the active build target this quarter. Sealing-gate integration follows immediately. We will not silently swap TOTP for passkey for existing users — passkeys are added alongside TOTP, and an org-level policy can later require passkey-only.
Can MFA be required at the org level?
MFA is required by default for sealing. Org admins can additionally require MFA at every login (not just sealing) — a stricter setting than the default. Disabling MFA below the sealing-gate baseline is not configurable; the gate is fixed.
How are TOTP codes verified server-side?
NimbleTOTP.valid?/2 compares the user-submitted code against the HMAC-derived expected code for the current step, with a ±1-step drift tolerance. The TOTP secret is decrypted only inside the verification call, used once, and dropped from memory.
What if our org uses SSO — do users still need MFA on top?
If the IdP enforces MFA, sealing trusts that assertion's authentication context (acr) when the IdP includes one strong enough. If the IdP does not enforce MFA, EngineeringID enforces TOTP at sealing time as a second factor on top. The decision is per-org policy and audit-logged.
Are MFA failures rate-limited?
Yes. Per-user rate limits with exponential backoff on consecutive failures. After a threshold, the account is temporarily locked and the user is notified via email and in-app event. The audit log records every failed attempt with source IP and user agent.

The friction is the feature

Read the device & session page for the broader session-integrity story, or talk to our security team about org-specific MFA policies.