import { KeyCustody, KeyStorageAdapter, SecureIdentityProvider } from '../core/types'; interface StoredIdentity { privateKey: CryptoKey; publicKeyBase64: string; } /** * Identity provider backed by a non-extractable WebCrypto X25519 key. * * The private key is generated with `extractable: false` and stored in IndexedDB * as a CryptoKey rather than as bytes. CryptoKey is structured-cloneable, so it * survives the round trip, and `crypto.subtle.exportKey` refuses on the way out. * Script on the origin can therefore ask this key to derive, but cannot copy it. * * This is the same curve tweetnacl's `box` uses, and `deriveBits` computes the * same X25519 shared secret, so a message encrypted through this provider is * indistinguishable on the wire from one encrypted through the legacy path. That * property is what makes adoption safe, and it is asserted by tests rather than * assumed. * * Availability is not universal. Call {@link isSupported} before choosing this; * the SDK does, and falls back rather than throwing at send time. */ /** * Where the identity record is kept between sessions. * * Extracted so persistence is not welded to IndexedDB. It lets the equivalence * tests run somewhere without IndexedDB, which is the only way to verify wire * compatibility outside a browser, and it lets a consumer persist the record * somewhere of their own choosing. * * Whatever implements it must preserve the CryptoKey as an object. Serialising * the record to JSON destroys the key silently and leaves a record that looks * present and is unusable. */ export interface IdentityRecordStore { read(): Promise; write(record: StoredIdentity): Promise; } /** Keeps the identity for the lifetime of the process only. Used by tests. */ export declare class MemoryIdentityRecordStore implements IdentityRecordStore { private record; read(): Promise; write(record: StoredIdentity): Promise; } /** * Wrap a raw 32 byte X25519 scalar so WebCrypto will import it. * * This is what makes migration possible at all: an existing tweetnacl identity is * 32 raw bytes, and without this there is no way to hand it to WebCrypto, which * would mean every existing user had to be re-keyed and would lose the ability to * read their own history. */ export declare function rawX25519ToPkcs8(raw: Uint8Array): Uint8Array; export declare class WebCryptoIdentityProvider implements SecureIdentityProvider { readonly keyCustody: KeyCustody; private cached; private readonly store; constructor(store?: IdentityRecordStore); /** * Whether this environment can actually do non-extractable X25519. * * Probes by generating a throwaway key rather than sniffing user agents, * because support has arrived at different times in different engines and a * feature test is the only answer that stays true. */ static isSupported(): Promise; /** * Whether an X25519 key actually survives being stored and read back here. * * Separate from {@link isSupported} because the two are genuinely different * questions and one engine answers them differently. **WebKit can generate and * derive with a non-extractable X25519 key, and then loses it through * structured clone**: the IndexedDB write reports success, and after a reload * the record is simply absent. Measured 2026-08-15; AES-GCM, ECDH P-256 and * ECDSA P-256 all survive there, X25519 alone does not. * * That failure is silent, which is what makes it dangerous. An identity that * regenerates every load means every previous conversation becomes * undecryptable, with no error raised anywhere to explain it, so this must be * checked rather than assumed. * * Costs one write and one read of a throwaway key, so call it once at startup * rather than per message. */ static canPersist(store: IdentityRecordStore): Promise; /** * Adopt an existing tweetnacl identity, so a user keeps their conversations. * * The scalar is imported as a non-extractable key and the raw copy is then * removed from the {@link KeyStorageAdapter}, which is the whole point: after * this, the same identity exists but script can no longer read it. * * Ordering matters and is deliberate. The import and the write happen first, * and only a successful write is followed by deleting the raw copy. Interrupted * at any point, the user still has a usable identity somewhere: either the old * raw key, or the new protected one, never neither. * * Returns false when there is nothing to migrate, so a caller can run it * unconditionally on startup. */ migrateFromKeyStorage(storage: KeyStorageAdapter): Promise; getPublicKey(): Promise; deriveMessageKey(params: { peerPublicKey: string; salt: Uint8Array; info: Uint8Array; }): Promise; private getOrCreate; } /** The default: an IndexedDB object store, which preserves CryptoKey objects. */ export declare class IndexedDbIdentityRecordStore implements IdentityRecordStore { private openDb; read(): Promise; write(record: StoredIdentity): Promise; } export {};