mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-30 10:01:21 +00:00
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
58 lines
2.2 KiB
TypeScript
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;
|
|
}
|