Skip to content

Developer notes

Overview · Installation · Configuration · Usage · FAQ

  • TOTP: RFC 6238 on top of HOTP (RFC 4226): HMAC-SHA-1, 6 digits, 30-second steps, the previous, current and next step accepted. Codes are compared with hash_equals for every candidate step (no early exit). Replay protection: the accepted step is stored per method and a code of the same or an older step is refused (atomic UPDATE ... WHERE last_step < step). Secrets: 160 random bits, base32, encrypted with AES-256-CBC + HMAC-SHA-256 (encrypt-then-MAC) under keys derived from the store’s crypt key; during setup the secret lives encrypted in the session until the first code is confirmed. The QR code is drawn as inline SVG by a small encoder in the add-on (byte mode, level M), so the otpauth:// URI never leaves the server.
  • WebAuthn Level 2 (W3C), pure PHP + OpenSSL: own CBOR decoder, COSE keys ES256 (-7, P-256) and RS256 (-257, RSA ≥ 2048 bits) converted to SubjectPublicKeyInfo and checked with openssl_verify.
    • Registration: clientDataJSON type webauthn.create, challenge, origin, no cross-origin; rpIdHash; user present; user verified when required; attested credential data; allowed algorithm; credential id unique. Attestation conveyance none: format none accepted, packed verified (self attestation and x5c), other formats accepted without trust. The add-on makes no trust decision from attestation.
    • Assertion: type webauthn.get, challenge, origin, rpIdHash, user present, user verified (always for passwordless sign-in), signature over authenticatorData || SHA-256(clientDataJSON), signature counter (refused when a non-zero counter does not grow; setting), backup flags consistent. Passwordless sign-in also requires the user handle and checks it against the account.
    • Challenges: 32 random bytes, stored as SHA-256 in ?:kv_ls2fa_challenges with purpose (reg, auth, login), user, a keyed hash of the session id and the relying party id; valid 5 minutes; taken by an atomic DELETE (single use).
    • Relying party id = request host without port; expected origin = scheme://host[:port] of the request.
  • Recovery codes: 10 × 10 characters of the Crockford base32 alphabet (50 bits each), stored as HMAC-SHA-256, used by an atomic update. O, I and L typed by the user are read as 0, 1, 1.
  • Trusted devices: HttpOnly, SameSite=Lax cookie per area (kv_ls2fa_td_c|a|v) holding a selector and a 256-bit validator; only the HMAC of the validator is stored.

Every CS-Cart sign-in ends in fn_login_user(). Its login_user_pre hook calls Gate::intercept():

  1. Skipped for: no user id, console, the Storefront REST API (API), the remember-me cookie re-login (LOGGED_VIA_COOKIE; that cookie is only written after the second step), inactive users, policy off, the emergency switch (admin area only), a valid trusted-device cookie.
  2. Enrolled user: the sign-in is parked in the session (kv_ls2fa: user, area, panel, mode, time, remember me, return URL, guest order ids), the session id is regenerated, remember_me is removed from the request and the browser is redirected to the second step (force_redirection for AJAX requests). The user is not signed in.
  3. Not enrolled and required: the same with mode setup (skipped for accounts created in the last 2 minutes and while the license is not valid).
  4. After the second step Gate::complete() runs fn_login_user($id, true) with a bypass flag, assigns guest orders, applies remember me, issues the trusted-device cookie, logs and redirects to the original return URL (own hosts only).

Second-step failures count per account; at the limit the account’s second step is locked and the parked sign-in dropped. Passwordless passkey failures are rate-limited per IP (20 per 15 minutes).

The storefront sign-in form only gets a static passkey button (settings and license; controllers/frontend/init.post.php never reads the session). All per-visitor work runs in the non-cacheable kv_login_security_2fa.* dispatches (pages and POST/XHR endpoints). No ?:storage_data writes; own tables only.

Dispatch Area Purpose
kv_login_security_2fa.challenge / auth.kv_ls2fa storefront / panel Second-step page (challenge, setup, recovery codes)
…verify, …pk_options, …pk_verify, …setup_totp, …setup_confirm, …setup_pk_options, …setup_pk_add, …finish, …cancel both (auth.kv_ls2fa_* in the panel) Second-step actions (POST, JSON for the passkey calls)
…login_options, …login both Passwordless passkey sign-in (JSON; storefront CSRF-exempt, protected by the session-bound single-use challenge and the signature)
kv_login_security_2fa.manage / .security storefront / panel Own security page; actions totp_start, totp_confirm, totp_cancel, m_pk_options, pk_add, pk_rename, remove, codes, devices_revoke
kv_login_security_2fa.manage, .log, .reset, .unlock admin Overview, security log, reset and unlock (privileges View users / Manage users)

?:kv_ls2fa_users (user handle, failure counter, lock) · ?:kv_ls2fa_factors (type T/P, name, encrypted TOTP secret and last step, or credential id, public key PEM, algorithm, counter, relying party id, AAGUID, transports, flags) · ?:kv_ls2fa_recovery · ?:kv_ls2fa_devices · ?:kv_ls2fa_challenges · ?:kv_ls2fa_rate · ?:kv_ls2fa_log (event, user, user type, area, method, keyed IP hash, detail). Deleting a user removes its rows.

PHP: login_user_pre (second step), post_delete_user (rows of the user), user_logout_after (drops a parked sign-in). Templates (storefront): index:login_buttons (.post), profiles:my_account_menu (.post), profiles:account_update (.post), index:scripts, index:styles; (admin): auth:login_form (.post), profiles:tabs_content (.post), profiles:update_buttons (.post), index:scripts, index:styles, addons:tabs_content, addons:manage. Pre/post controllers: frontend init.post; backend auth.pre, profiles.post, addons.post. Test seam: KV_LS2FA_MAIL_CAPTURE (config.local.php only) writes the security e-mails to a file instead of sending them.

Tygh\Addons\KvLoginSecurity2fa\License\Client (generated) verifies the license. Gate points: Config::canEnroll() for new authenticator apps and passkeys, the forced setup and the passwordless buttons. Never gated: the second step of enrolled users, recovery codes, removing methods, admin reset and unlock.

SMS or e-mail codes as a second step (use Phone OTP Login for SMS sign-in), WebAuthn conditional UI (autofill), attestation trust policies (allowed authenticator models), the Ed25519 (-8) algorithm.

Path Purpose
app/addons/kv_login_security_2fa/addon.xml Settings, tables, e-mail templates
init.php, func.php Hooks
Tygh/Addons/KvLoginSecurity2fa/Gate.php Interception, parked sign-in, completion, trusted devices, recovery codes
Http.php, Account.php, Admin.php Request handling, self-service actions, admin data
Passkeys.php, WebAuthn/ WebAuthn options and ceremonies; Verifier, Cbor, CoseKey
Totp.php, QrCode.php, Util.php TOTP, QR encoder, encryption, cookies, URLs
Store.php, Log.php, Notifier.php, Config.php Data, security log, e-mails, settings
License/, controllers/backend/kv_login_security_2fa_license.php License check (generated)
Upgrade/Synchronizer.php Self-healing upgrades (generated)
controllers/frontend/, controllers/backend/ Pages and endpoints
schemas/ Menu, permissions (admin and vendor), CSRF validation
var/themes_repository/responsive/templates/addons/kv_login_security_2fa/ Second-step page, security page, sign-in button, account links
design/backend/templates/addons/kv_login_security_2fa/ Panel second step, security page, overview, log, profile tab
js/addons/kv_login_security_2fa/func.js Passkey ceremonies (navigator.credentials), copy and print of codes