import { type KeyObject } from "crypto"; export declare const NOISE_PROTOCOL_NAME = "Noise_IKpsk1_25519_ChaChaPoly_SHA256"; /** * The psk-less pattern, used by `POST /api/e2ee/open` (NONCE-DESIGN §11). * * The transport handshake runs against static keys pairing already stored and * has no pair token, so there is no pre-shared secret to mix. A constant public * "PSK" was considered and rejected: it reduces to plain `IK` anyway and * encodes "there is a PSK here that means nothing", which a reader takes as a * freshness guarantee that does not exist. * * The name is not decoration — it is the string that seeds `h`, so it is itself * domain separation: a message from one pattern cannot be read by the other * even before the prologue is considered. */ export declare const NOISE_IK_PROTOCOL_NAME = "Noise_IK_25519_ChaChaPoly_SHA256"; /** * Which of the two patterns a handshake call means. * * **Stated, never inferred.** This used to be decided by whether `psk` was * present, which made a forgotten argument select the WEAKER protocol: a * pairing call site that omitted its psk would have silently run plain `IK` * and lost the pair-token binding that is the entire reason pairing uses * `IKpsk1` (spec §9.3, design.md §2.4). A missing argument must never be a * downgrade, so the pattern is named and the two are checked against each * other in both directions — a psk handed to `IK` is the same caller confusion * wearing the other face. * * The default is the STRONGER pattern, so an un-updated call site that forgets * its psk throws instead of quietly weakening. */ export type NoisePattern = "IKpsk1" | "IK"; /** * Prologue for the PAIRING handshake, mixed into the transcript before any * token (spec §5.3, `MixHash(prologue)`). * * Not in design.md, and added deliberately. The design has two `IK` handshakes: * this one at `/api/pair/exchange` with the pair token as PSK, and a later one * at `/api/e2ee/open` against stored static keys with no PSK. Without a * prologue the two transcripts differ only by the presence of the PSK, so * "could a message from one be replayed into the other" becomes a question you * answer by reasoning about the PSK. A prologue answers it by construction, and * it costs one hash. * * **`"threadbase-e2ee/ "` is a namespace, not one string.** * Its sibling is `"threadbase-e2ee/1 open"` for `/api/e2ee/open`, named here * rather than left for that phase to invent: half a domain separation is a * property nobody has, and a convention chosen once with both instances visible * is the one that survives a third handshake. The constant itself is not * declared until something uses it. * * Changing this string is a silent, total incompatibility with tb-mobile — it * is hashed into `h` before any token, so the only symptom is "decryption * failed". It is pinned by a committed vector for that reason. */ export declare const PAIR_PROLOGUE: Buffer; /** * The sibling named above, now that `/api/e2ee/open` exists to use it. * * Same namespace, different purpose, so a message from the pairing handshake * can never be replayed into the transport handshake or the other way round — * by construction, rather than by an argument about the PSK. */ export declare const OPEN_PROLOGUE: Buffer; /** `e` (32) + `s` sealed (32 + 16) — everything before message 1's payload. */ export declare const NOISE_MESSAGE_1_OVERHEAD: number; /** * The initiator's ephemeral public key, or `null` when the message is too short * to contain one. * * `e` is the FIRST field of message 1 and travels in the clear (spec §7.4), so * it can be read before any Diffie-Hellman — which is what lets * `/api/e2ee/open` reject a replayed msg1 without paying for it. Reading it * lives here, with the rest of the wire layout, rather than in a route indexing * into a buffer by a number it inferred. * * Returns a VIEW, not a copy: callers must not mutate it. */ export declare function messageEphemeral(message1: Buffer): Buffer | null; /** `e` (32) — everything before message 2's payload. */ export declare const NOISE_MESSAGE_2_OVERHEAD: number; /** * Cap on a handshake message, checked BEFORE anything is allocated or parsed. * * The Noise spec's own transport limit is 65535 bytes. This is far below it * because our payloads are a handful of JSON fields, and because * `/api/pair/exchange` is a public, unauthenticated endpoint — the same * reasoning D-9 applies to the unseal middleware, reaching this far forward. */ export declare const NOISE_MAX_MESSAGE_BYTES = 4096; export declare class NoiseError extends Error { constructor(message: string); } export interface KeyPair { publicKey: KeyObject; privateKey: KeyObject; /** Raw 32 bytes, which is what goes on the wire. */ publicKeyRaw: Buffer; } export declare function generateKeyPair(): KeyPair; /** * A `KeyPair` around a private key this process already holds. * * The bridge from `loadOrCreateServerIdentity()`, which owns the key file and * returns a bare `KeyObject`, to the handshake, which needs the public half * alongside it. Kept here rather than in server-identity.ts so that module * stays about storing a key rather than about the protocol that uses it. */ export declare function keyPairFrom(privateKey: KeyObject): KeyPair; /** * Raw 32 bytes out of a KeyObject. JWK OKP `x` is base64url of exactly that. * * Accepts either half: `createPublicKey` derives the public key from a private * one but rejects a KeyObject that is already public, so the type is checked * rather than the call being made unconditionally. */ export declare function rawPublicKey(key: KeyObject): Buffer; /** * A KeyObject from raw bytes, or a `NoiseError`. * * This is a trust boundary: the bytes are the QR's `spk` on the client side and * an attacker-supplied handshake message on the server side. A wrong length is * rejected here rather than being padded, truncated, or handed to a DH that * would fail somewhere less legible. */ export declare function publicKeyFromRaw(raw: Buffer): KeyObject; /** * A keypair from a raw 32-byte private scalar. * * Exists for the committed test vectors: a vector is only a contract if the * keys are fixed, and there is no other way to hand Node a chosen X25519 * private key. Not used by any production path — every real keypair comes from * `generateKeyPair()` or the identity key file. * * Goes through PKCS#8 DER rather than JWK because a JWK OKP private key must * carry a matching `x`, and the point here is to supply only `d`. The prefix is * the fixed X25519 PrivateKeyInfo header (RFC 8410): SEQUENCE, version 0, * AlgorithmIdentifier 1.3.101.110, then an OCTET STRING wrapping the 32-byte * scalar. */ export declare function keyPairFromRawPrivate(raw: Buffer): KeyPair; /** * The pair token as a 32-byte PSK. * * The token is `pt_<32 hex chars>` — 35 bytes of ASCII, and the spec requires * the PSK to be exactly 32. Hashing with a domain-separating label is the * standard way to fit it, and the label means this value can never collide with * some other use of the same token elsewhere in the system. * * Both implementations must compute this identically or the handshake fails * with no diagnostic beyond "decryption failed", which is why it is pinned by a * test vector rather than left as an obvious detail. */ export declare function pskFromPairToken(token: string): Buffer; /** * Noise's `CipherState` (spec §5.1). * * Exported for two reasons, neither of them scaffolding. Phase 3's record layer * has to build a transport cipher from the buffers `split()` returns, so this is * the type it will construct. And the §5.1 rule that a failed decryption must * not advance `n` is unobservable through the handshake API — every AEAD * operation there runs at `n = 0`, because each is preceded by a `MixKey` that * calls `initializeKey`. Without a way to reach a cipher state directly, that * rule is protected by nothing: a "simplification" that advances before * verifying passes every other test in the suite. */ export declare class CipherState { #private; private n; constructor(); initializeKey(key: Buffer | null): void; hasKey(): boolean; encryptWithAd(ad: Buffer, plaintext: Buffer): Buffer; decryptWithAd(ad: Buffer, ciphertext: Buffer): Buffer; } declare class SymmetricState { #private; constructor(protocolName: string); /** * Noise's HKDF (spec §4.3) is RFC 5869 with `salt = chaining_key`, * `ikm = input_key_material` and an EMPTY info, so Node's `hkdfSync` is * exactly it rather than approximately it. Using the platform's HKDF removes * the HMAC chain that would otherwise be the easiest thing here to get * subtly wrong. */ private hkdf; mixKey(ikm: Buffer): void; mixHash(data: Buffer): void; /** Spec §5.2. Used only by the `psk` token. */ mixKeyAndHash(ikm: Buffer): void; encryptAndHash(plaintext: Buffer): Buffer; decryptAndHash(ciphertext: Buffer): Buffer; /** The transcript hash. Both sides must arrive at the same value. */ handshakeHash(): Buffer; /** Spec §5.2 `Split()`: the two directional transport keys. */ split(): { k1: Buffer; k2: Buffer; }; } /** * The transport keys a completed handshake yields. * * **A class with `#private` fields, not an object literal**, because this is * the value that TRAVELS — out of `respond()`, through the route, into * `createRecordState` — and so it is the one most likely to end up inside an * error, a log line or a test diff. As a literal its two traffic keys were * ordinary properties: `defineProperty(enumerable: false)` hid them from * default `inspect` and `{ showHidden: true }` printed them anyway; the custom * inspect handler hid them from that and `{ customInspect: false }` printed * them anyway. A `#` field is not a property, so there is no mode to find. * * The getters are on the prototype, which `inspect` does not walk and * `getOwnPropertyDescriptors` does not report, so `keys.clientToServer` reads * exactly as it did while rendering nothing. */ export interface TrafficKeys { /** Initiator → responder. An OpenSSL key handle, never bytes. */ readonly clientToServer: KeyObject; /** Responder → initiator. */ readonly serverToClient: KeyObject; /** The transcript hash. Public, and an unpooled copy. */ readonly handshakeHash: Buffer; } /** * The transport keys a completed handshake yields. * * **The keys are `KeyObject`s and there is no byte getter at all.** Hiding was * tried four ways and defeated four times — non-enumerable by `showHidden`, the * inspect handler by `customInspect: false`, `#private` fields by * `{ getters: true }` reading through the accessors, and finally `#private` * with no getter by the ALLOCATION POOL: Node pool-allocates small Buffers, a * Buffer's `.buffer` exposes the shared 8 KiB block, and a public Buffer on the * same object hands out a window onto the pool the private key was allocated * in. Hiding a Buffer cannot work, so no traffic-key bytes exist as a JS Buffer * after the handshake: they are imported into OpenSSL and the copies wiped. * * `consume()` is the only way out and it works once, so ownership of the keys * is a fact about the object rather than a convention. */ export declare class HandshakeKeys { #private; constructor(handshakeHash: Buffer, clientToServer: Buffer, serverToClient: Buffer); /** * Hand the keys to the record layer. Once. * * NOT the transcript hash's guard — that is public — but the keys': a second * caller getting the same handles would mean two record layers believing they * own one counter space, which is the shape every nonce rule here exists to * prevent. */ consume(): TrafficKeys; } /** * Message 1, from the phone. * * Exported from the *server* module although the server never sends one. It is * the only way to drive the responder in a test, and it generates the committed * vectors the tb-mobile implementation is checked against — a second, drifting * initiator written inside a test file is the precise failure two independent * implementations invite. */ export declare function writeMessage1(args: { staticKeyPair: KeyPair; responderStaticPub: Buffer; /** Required by `IKpsk1`, forbidden by `IK`. */ psk?: Buffer; /** Defaults to the stronger `IKpsk1`. `/api/e2ee/open` passes `"IK"` (§11). */ pattern?: NoisePattern; payload: Buffer; /** REQUIRED: `PAIR_PROLOGUE` or `OPEN_PROLOGUE`. Never defaulted (§11). */ prologue: Buffer; /** Test seam only. Production always generates a fresh ephemeral. */ ephemeral?: KeyPair; }): { message: Buffer; state: HandshakeInitiatorState; }; export interface HandshakeInitiatorState { symmetric: SymmetricState; ephemeral: KeyPair; staticKeyPair: KeyPair; } /** Message 2, read by the phone. Completes the handshake. */ export declare function readMessage2(state: HandshakeInitiatorState, message: Buffer): { payload: Buffer; keys: HandshakeKeys; }; export interface ResponderResult { /** The initiator's static public key, authenticated by the handshake. */ initiatorStaticPub: Buffer; payload: Buffer; message2: Buffer; keys: HandshakeKeys; } /** * Half-completed responder state, between reading message 1 and writing 2. * * Deliberately only ever a local: it is handed straight back into * `writeMessage2` within the same synchronous stretch of one request, never * stored, never keyed by anything a caller supplies. A responder state that * outlived a request would be a thing to allocate and expire on a public * endpoint, which is what the single-call shape originally avoided. */ export interface HandshakeResponderState { symmetric: SymmetricState; /** The initiator's static public key, authenticated by message 1. */ initiatorStaticPub: Buffer; /** Message 1's decrypted payload. */ payload: Buffer; initiatorEphemeral: KeyObject; initiatorStatic: KeyObject; staticKeyPair: KeyPair; } /** * Read message 1. Authenticates the initiator and recovers its static key. * * Split from `writeMessage2` because the server DOES now have something to do * between them: message 2's payload carries the `deviceId`, and the device row * cannot be written until the pair token has been spent, which cannot happen * until this half has succeeded. The original single-call shape assumed nothing * sat in the middle; the pairing handler is the caller that proved otherwise. * * SYNCHRONOUS on purpose, and it must stay that way: the caller runs this * between validating the pair token and consuming it, and `PairTokenStore` has * no lock. An `await` in that gap would let two concurrent requests with the * same token both pass validation. */ export declare function readMessage1(args: { staticKeyPair: KeyPair; /** Required by `IKpsk1`, forbidden by `IK`. */ psk?: Buffer; /** Defaults to the stronger `IKpsk1`. `/api/e2ee/open` passes `"IK"` (§11). */ pattern?: NoisePattern; message1: Buffer; /** REQUIRED: `PAIR_PROLOGUE` or `OPEN_PROLOGUE`. Never defaulted (§11). */ prologue: Buffer; }): HandshakeResponderState; /** Write message 2 and derive the transport keys. Completes the handshake. */ export declare function writeMessage2(state: HandshakeResponderState, responsePayload: Buffer, /** Test seam only. Production always generates a fresh ephemeral. */ ephemeral?: KeyPair): { message2: Buffer; keys: HandshakeKeys; }; /** * Both halves in one call. * * Kept because most callers — every test, and any future responder with nothing * to do in the middle — want the whole handshake, and because the two halves * being separable should not force every one of them to sequence it by hand. */ export declare function respond(args: { staticKeyPair: KeyPair; /** Required by `IKpsk1`, forbidden by `IK`. */ psk?: Buffer; /** Defaults to the stronger `IKpsk1`. `/api/e2ee/open` passes `"IK"` (§11). */ pattern?: NoisePattern; message1: Buffer; /** Built from the authenticated message-1 payload; sealed into message 2. */ buildPayload: (initiatorStaticPub: Buffer, payload: Buffer) => Buffer; /** REQUIRED: `PAIR_PROLOGUE` or `OPEN_PROLOGUE`. Never defaulted (§11). */ prologue: Buffer; /** Test seam only. */ ephemeral?: KeyPair; }): ResponderResult; export {}; //# sourceMappingURL=noise.d.ts.map