mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-29 01:21:22 +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
46 lines
1.8 KiB
TypeScript
46 lines
1.8 KiB
TypeScript
/** JCS + padding + AES-256-GCM envelope encryption for capsule payloads (§11.7, §11.8, §11.9, §13, §14). */
|
|
import { canonicalJsonBytes } from "../crypto/jcs.js";
|
|
import { padToBucket, unpadFromBucket } from "../crypto/padding.js";
|
|
import { aesGcmSeal, aesGcmOpen } from "../crypto/aesGcm.js";
|
|
import { bytesToBase64url, base64urlToBytes, bytesToUtf8, utf8ToBytes } from "../crypto/encoding.js";
|
|
import type { EncryptedEnvelope } from "./types.js";
|
|
|
|
export function credentialCapsuleAad(locatorPubkeyHex: string): Uint8Array {
|
|
return utf8ToBytes(`bitlogin|password-capsule|v1|${locatorPubkeyHex}|30078|bitlogin:password:v1`);
|
|
}
|
|
|
|
export function recoveryCapsuleAad(recoveryPubkeyHex: string): Uint8Array {
|
|
return utf8ToBytes(`bitlogin|recovery-capsule|v1|${recoveryPubkeyHex}|30078|bitlogin:recovery:v1`);
|
|
}
|
|
|
|
export async function encryptEnvelope(
|
|
payload: unknown,
|
|
key: Uint8Array,
|
|
associatedData: Uint8Array
|
|
): Promise<EncryptedEnvelope> {
|
|
const canonical = canonicalJsonBytes(payload);
|
|
const padded = padToBucket(canonical);
|
|
const sealed = await aesGcmSeal(key, padded, associatedData);
|
|
return {
|
|
version: 1,
|
|
algorithm: "aes-256-gcm",
|
|
nonce: bytesToBase64url(sealed.nonce),
|
|
ciphertext: bytesToBase64url(sealed.ciphertext)
|
|
};
|
|
}
|
|
|
|
export async function decryptEnvelope<T>(
|
|
envelope: EncryptedEnvelope,
|
|
key: Uint8Array,
|
|
associatedData: Uint8Array
|
|
): Promise<T> {
|
|
if (envelope.version !== 1 || envelope.algorithm !== "aes-256-gcm") {
|
|
throw new Error(`Unsupported capsule envelope version/algorithm: ${envelope.version}/${envelope.algorithm}`);
|
|
}
|
|
const nonce = base64urlToBytes(envelope.nonce);
|
|
const ciphertext = base64urlToBytes(envelope.ciphertext);
|
|
const padded = await aesGcmOpen(key, nonce, ciphertext, associatedData);
|
|
const payloadBytes = unpadFromBucket(padded);
|
|
return JSON.parse(bytesToUtf8(payloadBytes)) as T;
|
|
}
|