Files
bitlogin/packages/core/src/capsules/credentialCapsule.ts
T
Claude 5ee63ae6b0 Implement BitLogin as a modular protocol library, embeddable widget, and demo site
Builds out the v0.3 spec as an npm workspace monorepo any static site can
consume for login and Nostr signing:

- packages/core: framework-free protocol implementation — Argon2id/HKDF/
  ScalarExpand/AES-256-GCM/JCS crypto primitives, NIP-01 event handling and a
  quorum-aware relay pool, credential/recovery capsule construction and
  validation, and the full account lifecycle (registration, any-device
  login with rollback/relay-disagreement detection, phrase recovery with
  recovery-capsule refresh, password rotation with mandatory tombstone +
  NIP-09 deletion, replica repair, recovery export, NIP-19 encoding, a
  NIP-44 v2 implementation, and a NIP-07-shaped signer). 49 tests cover the
  spec's Phase 0 byte-level vectors plus full create/login/recover/rotate
  end-to-end scenarios against an in-memory mock relay.

- packages/widget: a <bitlogin-auth> custom element with a dedicated crypto
  Web Worker (private keys never touch the main thread) and a window.nostr
  NIP-07-compatible provider, so existing Nostr web apps work against it
  with zero code changes. Built with Vite into two relocatable static files.

- packages/demo: a plain static site (no build step of its own) consuming
  the widget — a branding/landing page, an integration guide, and an
  account-manager page covering create/login/recover, password rotation,
  and identity export.

All flows were verified twice: protocol-level tests against a mock relay,
and the actual built widget driven end-to-end in real Chromium (registration,
phrase confirmation, event signing, clean-device login, window.nostr, and
phrase recovery all reproduce the identical everyday pubkey).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
2026-07-21 03:44:10 +00:00

58 lines
2.2 KiB
TypeScript

/** The credential capsule event: kind 30078, `d` = bitlogin:password:v1, signed by the locator identity (§13). */
import { signNostrEvent, verifyNostrEvent, type NostrEvent } from "../nostr/event.js";
import { KIND_APP_DATA, D_TAG_PASSWORD_CAPSULE } from "../nostr/kinds.js";
import { getPublicKeyHex } from "../crypto/secp256k1.js";
import { credentialCapsuleAad, decryptEnvelope, encryptEnvelope } from "./serialization.js";
import { validateCredentialPayload } from "./validation.js";
import type { CredentialPayload, EncryptedEnvelope } from "./types.js";
export async function buildCredentialCapsuleEvent(params: {
locatorPrivateKey: Uint8Array;
capsuleKey: Uint8Array;
payload: CredentialPayload;
}): Promise<NostrEvent> {
const locatorPubkey = getPublicKeyHex(params.locatorPrivateKey);
const envelope = await encryptEnvelope(params.payload, params.capsuleKey, credentialCapsuleAad(locatorPubkey));
return signNostrEvent(
{
pubkey: locatorPubkey,
created_at: params.payload.created_at,
kind: KIND_APP_DATA,
tags: [["d", D_TAG_PASSWORD_CAPSULE]],
content: JSON.stringify(envelope)
},
params.locatorPrivateKey
);
}
/** Signed replacement with empty content: shrinks exposure on honest relays (§18.1). Confers no revocation over already-downloaded copies. */
export function buildCredentialTombstoneEvent(params: {
oldLocatorPrivateKey: Uint8Array;
createdAt: number;
}): NostrEvent {
const pubkey = getPublicKeyHex(params.oldLocatorPrivateKey);
return signNostrEvent(
{
pubkey,
created_at: params.createdAt,
kind: KIND_APP_DATA,
tags: [["d", D_TAG_PASSWORD_CAPSULE]],
content: ""
},
params.oldLocatorPrivateKey
);
}
export async function decryptCredentialCapsuleEvent(
event: NostrEvent,
capsuleKey: Uint8Array
): Promise<CredentialPayload> {
if (!verifyNostrEvent(event)) {
throw new Error("Credential capsule event has an invalid id or signature.");
}
const envelope = JSON.parse(event.content) as EncryptedEnvelope;
const payload = await decryptEnvelope<CredentialPayload>(envelope, capsuleKey, credentialCapsuleAad(event.pubkey));
validateCredentialPayload(payload);
return payload;
}