PassKey

Architecture

This document describes PassKey’s solution structure, dependency graph, and key design patterns.


Solution Structure

PassKey.sln
├── src/
│   ├── PassKey.Core/              # Domain models, crypto, services
│   ├── PassKey.Desktop/           # WinUI 3 app (MVVM, DI)
│   ├── PassKey.BrowserHost/       # Native Messaging bridge
│   └── PassKey.Tests/             # xUnit test suite
└── extensions/
    ├── chrome/                    # Chrome Manifest V3 extension
    └── firefox/                   # Firefox Manifest V3 extension

Projects

PassKey.Core

PassKey.Desktop

PassKey.BrowserHost

PassKey.Tests


Dependency Graph

PassKey.Desktop ────► PassKey.Core
                            ▲
PassKey.BrowserHost ────────┘
                            ▲
PassKey.Tests ──────────────┘

PassKey.Core has no dependency on any UI framework. PassKey.Desktop and PassKey.BrowserHost both depend on Core. PassKey.Tests references Core and BrowserHost.


Key Design Patterns

MVVM ViewModel-First

PassKey uses a ViewModel-first navigation pattern:

  1. ShellViewModel manages the current page via a CurrentViewModel property.
  2. Navigation sets CurrentViewModel to a new ViewModel instance.
  3. MainWindow uses a ContentControl bound to CurrentViewModel, with DataTemplate selectors mapping each ViewModel type to its corresponding View.
  4. Views receive their ViewModel via SetViewModel() in code-behind and set DataContext.

Benefits:

Dependency Injection (Constructor Only)

All services are registered in the DI container at startup (App.xaml.cs). Dependencies are injected via constructor parameters.

Rules:

Dialog Queue (Serial Pump)

WinUI 3 does not allow multiple ContentDialog instances to be open simultaneously. PassKey uses a DialogQueueService with a Queue<Func<Task>> and a serial pump:

  1. Code enqueues a dialog request (a Func<Task> that shows and awaits a ContentDialog).
  2. The pump dequeues and executes one at a time.
  3. The next dialog is shown only after the previous one is dismissed.

Why not SemaphoreSlim? Using SemaphoreSlim.WaitAsync() on the UI thread causes deadlocks because the dialog’s dismissal callback cannot run while the UI thread is blocked.

INavigationStack

ViewModels can push/pop sub-pages via INavigationStack:


Cryptographic Flow

User enters master password
        │
        ▼
 KDF (Argon2id or PBKDF2) + salt from VaultMetadata
        │
        ▼
 KEK (32 bytes) ── AES-GCM Decrypt ──► DEK (32 bytes)
                                            │
                              Stored in PinnedSecureBuffer
                                            │
                              AES-GCM Decrypt vault blob
                                            │
                                            ▼
                                    Vault (JSON → objects)

On save:

Vault objects → JSON → AES-GCM Encrypt with DEK → blob → VaultData table

On master password change:

New password → KDF → new KEK → AES-GCM Encrypt DEK → update VaultMetadata
(vault blob is NOT re-encrypted)

Browser Extension Architecture

┌─────────────────┐    Native Messaging     ┌──────────────────┐    Named Pipe    ┌─────────────────┐
│  Browser        │  (stdio: 4-byte len +   │  PassKey         │   (local IPC)    │  PassKey        │
│  Extension      │   JSON payload)         │  BrowserHost     │                  │  Desktop        │
│                 │◄───────────────────────►│                  │◄────────────────►│                 │
│  - popup.js     │                         │  - stdin/stdout  │                  │  - BrowserIpc   │
│  - content.js   │                         │  - pipe client   │                  │    Service      │
│  - background.js│                         │                  │                  │                 │
└─────────────────┘                         └──────────────────┘                  └─────────────────┘

Session Encryption

  1. Extension generates ephemeral ECDH P-256 key pair.
  2. BrowserHost generates ephemeral ECDH P-256 key pair.
  3. Public keys exchanged → shared secret → HKDF-SHA256 → 32-byte session key.
  4. All subsequent messages encrypted with AES-256-GCM (unique nonce per message).

Message Types

Message Direction Description
get-credentials Extension → Desktop Get credentials matching a URL
get-all-credentials Extension → Desktop Get all vault credentials
unlock-vault Extension → Desktop Unlock vault with master password
show-window Extension → Desktop Bring Desktop app to foreground