Skip to content

feat: [LC-1541] SSS Key Management, AuthCoordinator & Recovery System#986

Merged
Custard7 merged 92 commits intomainfrom
lc-1541
Mar 13, 2026
Merged

feat: [LC-1541] SSS Key Management, AuthCoordinator & Recovery System#986
Custard7 merged 92 commits intomainfrom
lc-1541

Conversation

@Custard7
Copy link
Copy Markdown
Collaborator

@Custard7 Custard7 commented Feb 4, 2026

Replace Web3Auth with Self-Hosted Shamir Secret Sharing (SSS) Key Management

https://www.loom.com/share/c2807fdcb7c64aa1bbe4e261e2fc23a7

Summary

This PR replaces the third-party Web3Auth key management system with a fully self-hosted Shamir Secret Sharing (SSS) architecture, introduces a unified AuthCoordinator state machine, adds modular auth provider support, and builds comprehensive recovery and cross-device login flows — all backed by new server routes in lca-api.

The core goal: users' private keys are split into shares using a 2-of-3 threshold scheme. Any two shares can reconstruct the key, and no single party (device, server, or recovery method) ever holds enough information to derive it alone.


Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    learn-card-app (UI)                       │
│  AuthCoordinatorProvider → useAuthCoordinator hook           │
│  Recovery UI (RecoveryFlowModal, RecoverySetupModal, etc.)  │
│  QR Login UI, ReAuth Overlay, Debug Widget                  │
└────────────────────┬────────────────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────────────────┐
│              learn-card-base (shared library)                │
│  AuthCoordinator  — provider-agnostic state machine         │
│  AuthProviders    — Firebase adapter (+ Supertokens, OIDC)  │
│  Config           — runtime provider registry               │
└────────────────────┬────────────────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────────────────┐
│           sss-key-manager (new package)                     │
│  SSSStrategy      — KeyDerivationStrategy implementation    │
│  SSS primitives   — split, reconstruct, verify              │
│  Storage          — IndexedDB device share persistence      │
│  Passkey (PRF)    — WebAuthn PRF-based share encryption     │
│  Recovery phrase  — BIP39-style mnemonic ↔ share encoding   │
│  QR crypto        — ECDH ephemeral key exchange             │
│  Atomic ops       — split-and-verify, share integrity       │
│  API client       — typed fetch wrapper for lca-api routes  │
└────────────────────┬────────────────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────────────────┐
│               lca-api (server routes)                       │
│  /keys/*          — auth share CRUD, recovery methods,      │
│                     migration, recovery email, email backup, │
│                     phone→email upgrade                     │
│  /qr-login/*      — ephemeral relay for cross-device login  │
│  UserKey model    — MongoDB document per contact method     │
│  Delivery service — pluggable email (Postmark / log)        │
│  Auth helpers     — multi-provider token verification       │
└─────────────────────────────────────────────────────────────┘

New Packages

packages/sss-key-manager (new)

Self-contained SSS key management library. Zero UI dependencies.

Module Purpose
sss.ts Shamir split (2-of-3) and reconstruct primitives
sss-strategy.ts KeyDerivationStrategy implementation — orchestrates setup, recovery, share rotation, server communication
storage.ts IndexedDB persistence for device shares with versioning
crypto.ts AES-GCM encryption/decryption, PBKDF2 password-based key derivation
passkey.ts WebAuthn PRF-based share encryption (hardware-bound recovery)
recovery-phrase.ts BIP39-style mnemonic ↔ share encoding for offline backup
qr-crypto.ts ECDH ephemeral key exchange for QR login relay
qr-login.ts Cross-device login flow (create session, approve, poll)
atomic-operations.ts Split-and-verify with rollback, share integrity checks
api-client.ts Typed fetch wrapper for all lca-api /keys/* and /qr-login/* routes
types.ts All SSS-specific types, re-exports from @learncard/auth-types

13 test files, ~249 unit tests covering every module.

packages/auth-types (new)

Zero-dependency package containing provider-agnostic interfaces (AuthProvider, AuthUser, KeyDerivationStrategy, RecoveryMethodInfo, etc.). Shared by both sss-key-manager and learn-card-base to avoid circular dependencies.


Key Changes by Layer

packages/learn-card-base — AuthCoordinator & Providers

AuthCoordinator (src/auth-coordinator/)

  • Provider-agnostic state machine with states: idle → authenticating → authenticated → checking_key_status → needs_setup | needs_migration | needs_recovery → deriving_key → ready
  • Delegates all server communication and recovery logic to the KeyDerivationStrategy
  • Detects legacy accounts needing migration via configurable legacyAccountThresholdMs
  • Supports "private-key-first" initialization from cached keys
  • React bindings: AuthCoordinatorProvider, useAuthCoordinator hook, useAuthCoordinatorAutoSetup
  • Shared overlay components: EmailLinkOverlay, ErrorOverlay, StalledMigrationOverlay
  • 58 unit tests

Auth Providers (src/auth-providers/)

  • createFirebaseAuthProvider — wraps Firebase Auth SDK into the AuthProvider interface
  • Designed for additional providers (Supertokens, Keycloak, OIDC) via the same interface
  • 12 unit tests

Config (src/config/)

  • authConfig.ts — runtime configuration for auth provider + key derivation strategy
  • providerRegistry.ts — registry pattern for managing multiple auth provider implementations
  • 29 unit tests

services/learn-card-network/lca-api — Server Routes

Key Management Routes (/keys/*)

Route Method Auth Purpose
/keys/auth-share POST open Get auth share (with optional shareVersion for version negotiation)
/keys/auth-share PUT DID Store/rotate auth share (server-side AES-256-GCM encryption at rest)
/keys/recovery POST DID Add recovery method (passkey, backup, phrase, email)
/keys/recovery GET open Get recovery share by type + credential ID
/keys/migrate POST open Mark user as migrated from Web3Auth → SSS
/keys/recovery-email/add POST DID Send 6-digit verification code to recovery email
/keys/recovery-email/verify POST DID Verify code and persist recovery email
/keys/recovery-email GET open Get masked recovery email
/keys/email-backup POST open Send encrypted backup share via email (fire-and-forget)
/keys/upgrade-contact-method POST open Verify OTP, link email to Firebase account, upgrade phone→email contact method
/keys/delete POST DID Delete all key data for a user

QR Login Relay (/qr-login/*)

Route Method Purpose
/qr-login/session POST Create ephemeral session with public key + short code
/qr-login/session/{lookup} GET Look up session by ID or short code
/qr-login/session/{sessionId}/approve POST Approve session with encrypted share payload
/qr-login/notify POST Send push notification to existing devices

Supporting infrastructure:

  • UserKey MongoDB model with contact-method-keyed documents, share versioning, previousAuthShares history, orphaned recovery method pruning
  • auth.helpers.ts — provider-agnostic token verification (Firebase, Supertokens, Keycloak, OIDC) with E2E test bypass
  • shareEncryption.helpers.ts — server-side AES-256-GCM encryption using HKDF-derived keys from server SEED
  • delivery/ — pluggable email delivery service (Postmark adapter for production, log adapter for development/E2E)
  • maskEmail.ts — email masking for privacy-safe display
  • pruneOrphanedRecoveryMethods.ts — automatic cleanup of recovery methods whose share version is no longer in history
  • 51 unit tests across lca-api

apps/learn-card-app — UI Integration

  • AuthCoordinatorProvider.tsx — app-level coordinator wiring with Firebase auth provider + SSS strategy
  • RecoveryFlowModal.tsx — guided recovery flow (passkey, phrase, backup file, email backup)
  • RecoverySetupModal.tsx — post-login recovery method setup prompts
  • RecoveryBanner.tsx — persistent banner prompting users to set up recovery
  • ReAuthOverlay.tsx — re-authentication modal when session expires
  • AuthHandoff.tsx — auth handoff page for cross-device flows
  • EmailLinkOverlay.tsx — phone→email upgrade flow with OTP verification
  • AuthKeyDebugWidget.tsx — developer debug overlay for inspecting auth/key state
  • useFirebase.ts / useLogout.tsx / useBoostRecoveryCheck.tsx — updated hooks
  • Public computer session mode support

packages/plugins/lca-api-plugin

Updated to expose SSS key management methods (storeAuthShare, getAuthShare, addRecoveryMethod, getRecoveryShare, markMigrated, deleteUserKey) via the LearnCard plugin interface for E2E testing and programmatic access.


Share Architecture

Private Key (32 bytes)
    │
    ├── Split via Shamir 2-of-3 ──────────────────────────┐
    │                                                      │
    ▼                  ▼                    ▼               │
Device Share      Auth Share          Recovery Share(s)    │
(IndexedDB)    (lca-api server,      (passkey PRF,        │
                AES-256-GCM          recovery phrase,      │
                encrypted at rest)   backup file,          │
                                     email backup)         │
                                                           │
Any 2 shares → reconstruct private key ◄──────────────────┘

Security levels:

  • Basic — device + server share only (no recovery method)
  • Enhanced — + one recovery method (passkey or phrase)
  • Advanced — + multiple recovery methods

Migration Path (Web3Auth → SSS)

Existing Web3Auth users are auto-detected via legacyAccountThresholdMs. When a user with an existing account but no SSS record logs in:

  1. AuthCoordinator enters needs_migration state
  2. The app retrieves the legacy private key from the cached Web3Auth SFA key
  3. SSS strategy splits the key, stores shares, and marks the user as migrated
  4. User is prompted to set up recovery methods

The StalledMigrationOverlay handles edge cases where migration is interrupted.


✨ PR Description

Purpose: Add SSS-

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 4, 2026

🦋 Changeset detected

Latest commit: 426369d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 52 packages
Name Type
@learncard/lca-api-service Minor
@learncard/network-brain-service Minor
@learncard/sss-key-manager Minor
@learncard/types Minor
@learncard/didkit-plugin Minor
@learncard/lca-api-plugin Minor
@learncard/lca-api-client Minor
learn-card-app Minor
scoutpass-app Minor
@learncard/network-brain-client Patch
learn-card-base Patch
@learncard/chapi-example Patch
@learncard/snap-example-dapp Patch
@learncard/create-http-bridge Patch
@learncard/cli Patch
@learncard/core Patch
@learncard/helpers Patch
@learncard/init Patch
@learncard/partner-connect Patch
@learncard/react Patch
@learncard/learn-cloud-client Patch
@learncard/simple-signing-client Patch
@learncard/ceramic-plugin Patch
@learncard/chapi-plugin Patch
@learncard/claimable-boosts-plugin Patch
@learncard/did-web-plugin Patch
@learncard/didkey-plugin Patch
@learncard/didkit-plugin-node Patch
@learncard/encryption-plugin Patch
@learncard/idx-plugin Patch
@learncard/network-plugin Patch
@learncard/learn-card-plugin Patch
@learncard/learn-cloud-plugin Patch
@learncard/linked-claims-plugin Patch
@learncard/open-badge-v2-plugin Patch
@learncard/vc-api-plugin Patch
@learncard/vc-templates-plugin Patch
@learncard/vc-plugin Patch
@learncard/vpqr-plugin Patch
learn-card-discord-bot Patch
@learncard/meta-mask-snap Patch
@learncard/learn-cloud-service Patch
@learncard/simple-signing-service Patch
@learncard/snap-chapi-example Patch
@learncard/app-store-demo-basic-launchpad Patch
@learncard/app-store-demo-lore-card Patch
@learncard/app-store-demo-mozilla-social-badges Patch
@learncard/expiration-plugin Patch
@learncard/crypto-plugin Patch
@learncard/dynamic-loader-plugin Patch
@learncard/ethereum-plugin Patch
@learncard/simple-signing-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link
Copy Markdown

netlify Bot commented Feb 4, 2026

Deploy Preview for staging-learncardapp ready!

Name Link
🔨 Latest commit 426369d
🔍 Latest deploy log https://app.netlify.com/projects/staging-learncardapp/deploys/69b4828d2dcef400084a3ed0
😎 Deploy Preview https://deploy-preview-986--staging-learncardapp.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented Feb 4, 2026

Deploy Preview for learncarddocs canceled.

Name Link
🔨 Latest commit 426369d
🔍 Latest deploy log https://app.netlify.com/projects/learncarddocs/deploys/69b4828d226e7800086249a7

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 4, 2026

👋 Hey there! It looks like you modified code, but didn't update the documentation in /docs.

If this PR introduces new features, changes APIs, or modifies behavior that users or developers need to know about, please consider updating the docs.


🏄 Windsurf Tip

You can ask Windsurf to help:

"Analyze the changes in this PR and update the gitbook docs in /docs accordingly."

Windsurf will review your changes and suggest appropriate documentation updates based on what was modified.


📚 Documentation Guide
Change Type Doc Location
New feature/API docs/tutorials/ or docs/how-to-guides/
SDK/API changes docs/sdks/
New concepts docs/core-concepts/
App UI/UX flows docs/apps/ (LearnCard App, ScoutPass)
Internal patterns AGENTS.md

This is an automated reminder. If no docs are needed, feel free to ignore this message.

@Custard7 Custard7 temporarily deployed to learn-cloud-network-api-staging March 6, 2026 22:45 — with GitHub Actions Inactive
@Custard7 Custard7 temporarily deployed to learn-cloud-storage-api-staging March 6, 2026 22:45 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@gitstream-cm gitstream-cm Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ PR Review

The PR implements a comprehensive migration from Web3Auth to self-hosted SSS key management with a well-structured AuthCoordinator pattern. The architecture appears sound with proper separation of concerns. However, there is a critical security issue with how cryptographic key shares are stored during the QR login flow that needs immediate attention.

1 issues detected:

🔒 Security - Cryptographic key share stored in sessionStorage is never explicitly cleared, creating an unnecessary window for potential XSS-based exfiltration

Details: The QR login flow stores a cryptographic device share in sessionStorage without cleanup. The 'qr_login_device_share' and 'qr_login_share_version' items persist in sessionStorage until the tab closes, even after they're used by the coordinator. This creates a window where XSS attacks on the same origin could exfiltrate the share. While one share alone isn't sufficient to reconstruct the private key, storing cryptographic key material in browser storage without immediate cleanup violates security best practices and expands the attack surface. The code should clear these sessionStorage items immediately after the coordinator consumes them, or use a more secure temporary storage mechanism.
File: apps/learn-card-app/src/pages/login/LoginPage.tsx

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how

@Custard7 Custard7 temporarily deployed to learn-card-app-api-staging March 8, 2026 19:18 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@gitstream-cm gitstream-cm Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ PR Review

The PR introduces a comprehensive SSS key management system with good architectural separation. However, there are several critical bugs in error handling and async operation management that need to be addressed before merge.

3 issues detected:

🐞 Bug - Async logout operation is not awaited, causing subsequent cleanup to run prematurely 🛠️

Details: The coordinatorLogout() call is not awaited, causing queryClient.resetQueries() and clearDB() to execute before logout completes. This creates a race condition where cleanup operations may run against inconsistent state, potentially leaving the app in a partially logged-out state.

File: apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx (200-200)
🛠️ A suggested code correction is included in the review comments.

🐞 Bug - Session refresh failure is not detected, causing success state to display even when refresh fails 🛠️

Details: After re-authentication, refreshAuthSession() is called but its return value is not checked. If the refresh fails (returns false), the UI still displays "Session Restored" and calls onSuccess, misleading the user that authentication succeeded when it actually failed. This occurs in both Google (line 140) and Apple (line 212) re-auth handlers.

File: apps/learn-card-app/src/components/auth/ReAuthOverlay.tsx (140-140)
🛠️ A suggested code correction is included in the review comments.

🐞 Bug - Loading state is only cleared on error, not on success, causing permanent loading UI

Details: The recovery handlers for device, phrase, backup, and email methods set loading to true at the start but only set it to false in the catch block. When recovery succeeds without throwing, the loading state remains true permanently, leaving the UI in a perpetual loading state that prevents further user interaction. This affects onApproved callback at line 351, handlePhraseRecovery at line 108, handleBackupRecovery at line 128, and handleEmailRecovery at line 153.
File: apps/learn-card-app/src/components/recovery/RecoveryFlowModal.tsx

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how

Comment thread apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx Outdated
Comment thread apps/learn-card-app/src/components/auth/ReAuthOverlay.tsx
Copy link
Copy Markdown
Contributor

@gitstream-cm gitstream-cm Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ PR Review

The PR successfully introduces a self-hosted SSS key management system with comprehensive recovery flows and coordinator-based architecture. The code is well-structured with proper error handling in most areas. However, there are a couple of race conditions and resource management issues that need attention.

2 issues detected:

🐞 Bug - Async logout operation runs concurrently with cleanup instead of sequentially 🛠️

Details: The coordinatorLogout() call is not awaited before queryClient.resetQueries() and clearDB() execute. This creates the same race condition as issue_3 (now resolved in useLogout.tsx), where cleanup operations may run against inconsistent state before logout completes, potentially leaving the app in a partially logged-out state.

File: apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx (200-200)
🛠️ A suggested code correction is included in the review comments.

🐞 Bug - BarcodeScanner listener not cleaned up on error or component unmount

Details: The BarcodeScanner listener may not be cleaned up if startScan() throws an error or if the promise is abandoned (e.g., component unmounts before scan completes). The async Promise executor pattern is also an anti-pattern. If BarcodeScanner.startScan() throws after the listener is added but before the scan begins, the listener remains attached indefinitely.
File: apps/learn-card-app/src/components/device-link/DeviceLinkModal.tsx

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how

Comment thread apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx Outdated
Copy link
Copy Markdown
Contributor

@gitstream-cm gitstream-cm Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ PR Review

The SSS key management migration introduces significant architectural improvements. However, there are a few race conditions and resource management issues that need attention, particularly around logout sequencing and barcode scanner cleanup.

3 issues detected:

🐞 Bug - Async coordinatorLogout() is not awaited, causing subsequent cleanup operations to race with the logout process. 🛠️

Details: The coordinatorLogout() call is not awaited before queryClient.resetQueries() and clearDB() execute. This creates a race condition where cleanup operations may run against inconsistent state before logout completes, potentially leaving the app in a partially logged-out state or corrupting user data.

File: apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx (200-200)
🛠️ A suggested code correction is included in the review comments.

🐞 Bug - Async Promise executor is an anti-pattern and the listener cleanup is not guaranteed if the component unmounts or if certain errors occur, potentially leaving orphaned event listeners.

Details: The BarcodeScanner listener uses an async Promise executor (anti-pattern) and may not be cleaned up if the component unmounts before scan completes or if startScan() throws after the listener is added. The error handler attempts cleanup but doesn't guarantee listener removal in all failure paths.
File: apps/learn-card-app/src/components/device-link/DeviceLinkModal.tsx

🧹 Maintainability - Button trigger references a popover that doesn't exist on mobile, creating an inconsistent component state.

Details: The IonPopover is conditionally rendered based on !isMobile, but the trigger button with id="trigger-button" is always rendered. On mobile devices, the button will reference a non-existent popover via the trigger attribute, which may cause runtime warnings or silent failures. This pattern appears in multiple files (LaunchPadPopOverButton, LearnerInsightsPopOverButton, SkillsHubPopOverButton).
File: apps/learn-card-app/src/components/ai-sessions/AiSessionsSearch/AiSessionsPopOverButton.tsx

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how

Comment thread apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx Outdated
Copy link
Copy Markdown
Contributor

@gitstream-cm gitstream-cm Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ PR Review

This is a substantial architectural change replacing Web3Auth with a self-hosted SSS system. The AuthCoordinator state machine and recovery flows are well-structured. However, one critical race condition was identified in the logout flow that should be addressed before deployment.

1 issues detected:

🐞 Bug - coordinatorLogout() is called without await, causing queryClient.resetQueries() and clearDB() to execute before logout completes 🛠️

Details: The coordinatorLogout() call on line 200 is not awaited before queryClient.resetQueries() and clearDB() execute. This creates a race condition where cleanup operations may run before logout completes, potentially leaving the application in an inconsistent state or corrupting user data. The same pattern was correctly fixed in useLogout.tsx (line 85) by adding await, but this instance was missed.

File: apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx (200-200)
🛠️ A suggested code correction is included in the review comments.

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how

Comment thread apps/learn-card-app/src/pages/consentFlow/ExternalConsentFlowDoor.tsx Outdated
Copy link
Copy Markdown
Collaborator

@TaylorBeeston TaylorBeeston left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phenomenal job!!!

Huge, sweeping changes to auth that were sorely needed, and I really like all the new features! =) did a lot of testing and the only times things were broken always ended up being issues with my own environment!

Tested:

  • Migrating an existing account
  • Making a new account
  • Linking a new device
  • Setting up email recovery
  • Bip39 Seed Phrase Recovery
  • Encrypted Backup file
  • Passkey

All of them worked with no issue! Everything also looks really slick too =)

@Custard7 Custard7 merged commit 34ced8d into main Mar 13, 2026
19 checks passed
@Custard7 Custard7 deleted the lc-1541 branch March 13, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants