npm package: @le-space/orbitdb-identity-provider-webauthn-did
Description: WebAuthn-based DID identity provider for OrbitDB for hardware-secured wallets and biometric Passkey authentication
Author: OrbitDB Community
Latest version: 0.2.13

# OrbitDB WebAuthn Identity Providers

[![CI](https://github.com/le-space/orbitdb-identity-provider-webauthn-did/actions/workflows/ci.yml/badge.svg)](https://github.com/le-space/orbitdb-identity-provider-webauthn-did/actions/workflows/ci.yml)

⚠️ **Security**: Experimental release. No formal audit. Use only after your own review.

This package provides:

- Two WebAuthn-based OrbitDB identity providers.
- A standalone WebAuthn toolkit export (`@le-space/orbitdb-identity-provider-webauthn-did/standalone`) for reuse outside OrbitDB identity wiring.
- **WebAuthn-Varsig**: No insecure OrbitDB keystore at all. Each entry is signed by WebAuthn (varsig envelope), so keys never leave the authenticator, one Passkey (WebAuthn) prompt per write.
- **Keystore-based DID**: Generates an Ed25519/secp256k1 keystore keypair for OrbitDB signing in browser memory. When `encryptKeystore` is enabled, the private key is encrypted with AES-GCM and only rehydrated in memory after a WebAuthn unlock (PRF, largeBlob, or hmac-secret).

## Current WebAuthn Model

- Discoverable credentials are enabled by default across the shared WebAuthn config.
- Authentication/assertion requests omit `allowCredentials` by default, so the browser/authenticator can resolve the credential discoverably.
- You can switch this centrally with `configureWebAuthn({ discoverableCredentials: true|false })`.
- Registration is still the point where this package extracts the credential public key from attestation.
- Later `navigator.credentials.get()` assertions do not reliably return the public key again, so identity reconstruction still needs metadata from somewhere else.

In this repo today, metadata recovery works in two layers:

- **Preferred recovery path**: store identity metadata in WebAuthn `largeBlob` and recover it later through discoverable authentication.
- **Fallback recovery path**: store the same metadata in browser `localStorage`.

This means discoverable credentials remove the need to pre-select the credential for authentication, but they do not by themselves eliminate the need for identity metadata persistence.

**Recommendation (security-first):**

- **Best security:** Varsig provider (hardware-backed key for every write).
- **Best balance:** Keystore provider with WebAuthn-encrypted keystore (fewer prompts, faster writes, key material in memory during session).

Note: WebAuthn varsig support in this repo relies on our forked `@le-space/iso-*` packages of [Hugo Dias iso-repo](https://github.com/hugomrdias/iso-repo/) (notably `@le-space/iso-did` and `@le-space/iso-webauthn-varsig`) to align with the updated varsig flow.

## Install

```bash
npm install @le-space/orbitdb-identity-provider-webauthn-did
```

Note: `@orbitdb/core` is patched (via `patch-package`) to support Ed25519 keystore keys.

## WebAuthn Configuration

The package now exposes a central WebAuthn policy API:

```javascript
import {
  configureWebAuthn,
  getWebAuthnConfig,
  resetWebAuthnConfig,
} from '@le-space/orbitdb-identity-provider-webauthn-did';

configureWebAuthn({ discoverableCredentials: true }); // default
console.log(getWebAuthnConfig());
resetWebAuthnConfig();
```

Behavior:

- `discoverableCredentials: true`
  - registration requests resident/discoverable credentials
  - assertion requests omit `allowCredentials`
- `discoverableCredentials: false`
  - assertion requests target a specific credential ID via `allowCredentials`

## Memory Keystore Quick Start

```javascript
import {
  WebAuthnDIDProvider,
  OrbitDBWebAuthnIdentityProviderFunction,
} from '@le-space/orbitdb-identity-provider-webauthn-did';

const credential = await WebAuthnDIDProvider.createCredential({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

const identity = await identities.createIdentity({
  provider: OrbitDBWebAuthnIdentityProviderFunction({
    webauthnCredential: credential,
  }),
});
```

### Discoverable Recovery Notes

For the DID-based flow:

- `createCredential()` can extract the public key from attestation and derive a DID from it.
- later discoverable `navigator.credentials.get()` proves possession of the credential, but usually returns only `rawId`, `authenticatorData`, `clientDataJSON`, `signature`, and maybe `userHandle`
- that assertion is not enough on its own to reconstruct the DID

To address this, the demos now attempt to:

1. write identity metadata to `largeBlob` after passkey creation
2. recover that metadata later through discoverable authentication
3. fall back to local browser storage if `largeBlob` is unavailable or empty

### Hardware-Secured Varsig Quick Start

```javascript
import {
  WebAuthnVarsigProvider,
  createWebAuthnVarsigIdentity,
} from '@le-space/orbitdb-identity-provider-webauthn-did';

const credential = await WebAuthnVarsigProvider.createCredential({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

const identity = await createWebAuthnVarsigIdentity({ credential });
```

For varsig, the same recovery limitation applies: a discoverable assertion identifies the credential but does not re-export the public key. The demos therefore use the same `largeBlob`-first, local fallback recovery approach for varsig credential metadata.

## Standalone Toolkit (without OrbitDB identity provider wiring)

Use the standalone export when you want WebAuthn signer and worker-keystore features independently from OrbitDB identity provider setup.

```javascript
import {
  createWebAuthnSigner,
  WebAuthnHardwareSignerService,
  createWorkerKeystoreClient,
} from '@le-space/orbitdb-identity-provider-webauthn-did/standalone';

// Create a hardware-backed WebAuthn varsig signer
const signer = await createWebAuthnSigner({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

// Optional: bridge to UCAN signer surface
const ucantoSigner = signer.toUcantoSigner();

// Optional: persisted hardware signer lifecycle
const hardwareService = new WebAuthnHardwareSignerService();
await hardwareService.initialize({
  userId: 'alice@example.com',
  displayName: 'Alice',
});

// Optional: worker-based Ed25519 keystore client
const workerClient = createWorkerKeystoreClient();
```

### Domain Label Guidance (OrbitDB vs UCAN)

`toUcantoSigner()` supports an optional `domainLabel` override:

- OrbitDB entry signing: use the default domain label (`orbitdb-entry:`).
- UCAN flows that require a protocol-specific challenge prefix: pass it explicitly (for example `ucan-webauthn-v1:`).

```javascript
// OrbitDB-style default (no override)
const orbitdbUcantoSigner = signer.toUcantoSigner();

// UCAN-specific override
const ucanUcantoSigner = signer.toUcantoSigner({
  domainLabel: 'ucan-webauthn-v1:',
});
```

The verifier side and app protocol should define which domain label is required. IPFS deployment does not change this requirement.

### Keystore-based DID (WebAuthn + OrbitDB keystore)

```mermaid
sequenceDiagram
  autonumber
  participant User
  participant App
  participant WebAuthn
  participant Auth as Authenticator
  participant KS as OrbitDB Keystore
  participant Enc as KeystoreEncryption
  participant DB as OrbitDB

  User->>App: Create credential
  App->>WebAuthn: create()
  WebAuthn->>Auth: Create passkey
  Auth-->>WebAuthn: Attestation
  WebAuthn-->>App: Credential

  App->>KS: getKey()/createKey(Ed25519)
  KS-->>App: Keystore keypair

  opt encryptKeystore=true
    App->>Enc: generateSecretKey()
    Enc-->>App: sk
    App->>Enc: encrypt keystore private key (AES-GCM)
    alt prf
      App->>WebAuthn: get() with PRF
      WebAuthn->>Auth: User verification
      Auth-->>WebAuthn: PRF output
      WebAuthn-->>App: PRF bytes
      App->>Enc: wrap sk with PRF
    else largeBlob
      App->>WebAuthn: get() with largeBlob write
      WebAuthn->>Auth: User verification
      Auth-->>WebAuthn: Store sk in largeBlob
      WebAuthn-->>App: largeBlob stored
    else hmac-secret
      App->>WebAuthn: get() with hmac-secret
      WebAuthn->>Auth: User verification
      Auth-->>WebAuthn: HMAC output
      WebAuthn-->>App: HMAC bytes
      App->>Enc: wrap sk with HMAC
    end
  end

  App->>DB: db.put()
  DB->>KS: sign entry with keystore key
  KS-->>DB: Entry signature

  Note over App,KS: Keystore private key is encrypted at rest when `encryptKeystore=true`.
```

### Varsig (no keystore)

```mermaid
sequenceDiagram
  autonumber
  participant User
  participant App
  participant WebAuthn
  participant Auth as Authenticator
  participant Var as Varsig Provider
  participant DB as OrbitDB

  User->>App: Create credential
  App->>WebAuthn: create()
  WebAuthn->>Auth: Create passkey
  Auth-->>WebAuthn: Attestation
  WebAuthn-->>App: Credential

  User->>App: Create varsig identity
  App->>Var: createIdentity()
  Var->>WebAuthn: get()
  WebAuthn->>Auth: User verification
  Auth-->>WebAuthn: Assertion
  WebAuthn-->>Var: Assertion
  Var->>Var: encode varsig envelope
  Var-->>App: Identity

  User->>App: Add entry
  App->>DB: db.put()
  DB->>Var: signIdentity(payload)
  Var->>WebAuthn: get()
  WebAuthn->>Auth: User verification
  Auth-->>WebAuthn: Assertion
  WebAuthn-->>Var: Assertion
  Var->>Var: encode varsig envelope
  Var-->>DB: Varsig signature
```

## Examples

Svelte demos:

- `examples/webauthn-todo-demo/` - WebAuthn DID (no keystore signing; identity-only). Includes discoverable credential diagnostics, `Use Existing Passkey`, `largeBlob` identity metadata recovery, and local fallback recovery.
- `examples/ed25519-encrypted-keystore-demo/` - Ed25519 keystore DID; keystore encrypted at rest with WebAuthn (PRF when available, otherwise largeBlob/hmac-secret).
- `examples/webauthn-varsig-demo/` - Varsig provider with passkey signing for each entry. Includes discoverable credential diagnostics, `Use Existing Passkey`, `largeBlob` varsig metadata recovery, and local fallback recovery. Live demo: https://dweb.link/ipfs/bafybeib6tpwiby7pik67ufb3lxpr3j4by2l7r3ov3zzk6hjbzjzgsvckhy

Scripted examples:

- `examples/ed25519-keystore-did-example.js` - Keystore DID flow.
- `examples/encrypted-keystore-example.js` - Keystore encryption flow.
- `examples/simple-encryption-integration.js` - Keystore + database content encryption.

Mermaid sequences for scripts:

- `docs/EXAMPLE-SEQUENCES.md`

## Documentation

- `docs/ED25519-KEYSTORE-DID.md`
- `docs/WEBAUTHN-ENCRYPTED-KEYSTORE-INTEGRATION.md`
- `docs/WEBAUTHN-DID-AND-ORBITDB-IDENTITY.md`
- `docs/STANDALONE-API-PLAN.md`
- `docs/EXAMPLE-SEQUENCES.md`
- `docs/E2E-TEST-SUMMARY.md`

## Identity Recovery Summary

Current identity recovery behavior in this repo:

- Discoverable passkeys are the default.
- Discoverable authentication can recover the credential ID used for assertion.
- Discoverable authentication does not reliably re-expose the credential public key.
- Because of that, OrbitDB identity recovery requires metadata persistence.
- The demos now try `largeBlob` first for identity metadata recovery.
- If `largeBlob` is not supported or has no metadata, the demos fall back to local browser storage.

Practical implication:

- If you create a passkey on one browser profile and later open the app in a fresh profile, the passkey may still exist in the platform passkey manager, but the app can only reconstruct the OrbitDB identity if it can recover metadata from `largeBlob` or some other persisted mapping.

## License

MIT. See `LICENSE`.