/** * scrypt cost. N=2^15/r=8/p=1 is ~32 MiB and ~100 ms on a modern CPU, high * enough that guessing a human-chosen passphrase from a captured verifier is * expensive, low enough that a Raspberry Pi joining a group does not appear to * hang. `maxmem` has to be raised explicitly: 128*N*r is exactly Node's 32 MiB * default ceiling, so the default would reject these parameters. */ export declare const JOIN_SCRYPT_PARAMS: { readonly N: 32768; readonly r: 8; readonly p: 1; readonly maxmem: number; }; /** Base64url of raw bytes, the encoding every stored key uses. */ export declare function encodeKeyBytes(bytes: Buffer): string; /** Inverse of {@link encodeKeyBytes}, rejecting anything that is not the right length. */ export declare function decodeKeyBytes(value: string, expectedLength?: number): Buffer | null; /** Constant-time equality for two strings that may not be the same length. */ export declare function secretsMatch(received: string, expected: string): boolean; /** * A fresh join key: 20 random bytes (160 bits) rendered in Crockford base32 and * grouped for reading aloud. Prefixed `gvj1` so a key pasted into the wrong box * is recognisable, and so the format can change later without ambiguity. * * Groups of four, because a human retyping this over SSH from a phone screen is * the actual use case. */ export declare function generateJoinKey(): string; /** * Normalize a join key for comparison. * * A key the operator retyped with different case, with spaces instead of * dashes, or with a trailing newline from a shell heredoc is the same key. A * PASSPHRASE, however, is whatever the operator typed, normalizing it would * quietly shrink its keyspace, so normalization is applied only to keys in the * generated `gvj1-` shape. */ export declare function normalizeJoinKey(value: string): string; /** * The scrypt salt for one group, derived from its id. * * A salt's job is to be DIFFERENT per group, so that work spent guessing one * group's passphrase buys nothing against another. It does not need to be * secret and it does not need to be random, deriving it from the group id gets * uniqueness for free, and means a machine that has been given a join key and a * group id has everything it needs to attempt a join without the group first * having to advertise a salt on the network. * * It is deliberately independent of the join key, so changing the join key * leaves the salt alone and a joiner's derivation keeps working. */ export declare function deriveJoinSalt(groupId: string): string; /** * Derive the verifier a member checks a join proof against. * * Async on purpose: scrypt at these parameters takes long enough that doing it * synchronously would stall the daemon's event loop, and a daemon that stops * answering its control plane for a tenth of a second every time a neighbour * tries to join is a daemon that looks broken. */ export declare function deriveJoinVerifier(joinKey: string, joinSalt: string): Promise; /** A fresh group root secret. Minted once per group and never rotated. */ export declare function generateGroupRoot(): string; /** * The group's public name on the wire. * * Derived from the ROOT secret and the fixed label 'group', never from a group * key, which is the whole reason it survives rotation: rotating produces a new * signing key and touches nothing this depends on. Truncated to 10 bytes (80 * bits) and rendered base32, which is short enough to read out over the phone * and far too long to collide with a neighbour's group by accident. * * It is a one-way function of a secret nobody transmits, so publishing it in a * discovery beacon reveals only that a goodvibes group exists here. */ export declare function deriveGroupId(groupRoot: string): string; /** True when `value` has the shape {@link deriveGroupId} produces. */ export declare function isValidGroupId(value: unknown): value is string; /** A fresh group key for one generation. */ export declare function generateGroupKey(): string; /** * Hash a surface identity down to something safe to put on a LAN. * * An ntfy topic, a bot token's chat id, an account address, all of those name * a way to reach the operator, and none of them belong in a datagram any * neighbour can capture. A value that is ALREADY a digest of this shape passes * through unchanged so a caller that hashed upstream is not double-hashing. * * TWO digest shapes pass through, and the second one matters more than it * looks. The per-surface election layer derives its own surface ids in * surface-id.ts, 128 bits of a domain-separated SHA-256, bare hex with no * prefix, and those ids are what the election routes on, what the holdings * ledger is keyed by, and what the envelope carries. Re-hashing one here would * produce a value nothing can route and would break the election's own inner * signature, whose canonical form covers `surfaceId`. So a surface id that has * already been through surface-id.ts is returned as it stands. * * The cost of the pass-through is that a raw discriminator which happened to be * exactly 32 hex characters would go unhashed. No real topic, bot id or account * address has that shape, and the `s`-prefixed guard has always had the same * property; the routing correctness bought here is worth it. */ export declare function digestSurfaceId(value: string, groupId: string): string; /** A node's long-lived key pair, stored as base64url raw components. */ export interface NodeKeyPairMaterial { readonly publicKey: string; readonly privateKey: string; } /** Both key pairs a node needs to be a member. */ export interface NodeKeyMaterial { /** ed25519, proves "I am this node id" after every group key has expired. */ readonly identity: NodeKeyPairMaterial; /** x25519, the address a new group key is wrapped to. */ readonly agreement: NodeKeyPairMaterial; } /** Mint the identity and agreement key pairs for this node. */ export declare function generateNodeKeyMaterial(): NodeKeyMaterial; /** * Mint the GROUP's own signing key pair. * * Distinct from any node's identity key, and that distinction is the point. * When a machine that has been away asks to come back, the reply has to be * authenticated against something the returning machine ALREADY HELD when it * left. Its stored roster is stale by definition, so a reply signed by whichever * member happened to answer may be signed by a machine it has never heard of, * which previously left the seal as the only thing standing behind that reply. * * The group signing key fixes that: every member can sign as the GROUP, and the * returning machine verifies against the group public key it has held since the * day it joined. It is carried in the roster (public half) and handed out in * every admission grant (private half), and it rotates on removal alongside the * group key, so an ejected machine's copy stops being able to speak for the * group. */ export declare function generateGroupSigningKeyPair(): NodeKeyPairMaterial; /** True when `value` is a plausible raw 32-byte OKP public key. */ export declare function isValidPublicKey(value: unknown): value is string; /** * Authenticate bytes with this node's long-lived identity key. * * This is the path a machine that has been switched off for months takes back * into the group. It depends on nothing that rotates and nothing the operator * can change: not the group-key generation it last held, not the join key. See * the admission rule in group-membership.ts for why that is safe, and for why * it is gated on roster presence rather than on the proof alone. */ export declare function signWithIdentity(identity: NodeKeyPairMaterial, bytes: string): string; /** Check an identity signature against the public key the roster holds for that node. */ export declare function verifyWithIdentity(identityPublicKey: string, bytes: string, signature: string): boolean; /** A group key sealed to exactly one member's agreement public key. */ export interface WrappedKeyEnvelope { /** Ephemeral x25519 public key of the sender, raw base64url. */ readonly epk: string; readonly iv: string; readonly tag: string; readonly data: string; } /** * Seal `plaintext` so only the holder of `recipientAgreementKey`'s private half * can read it. * * Ephemeral-static x25519 then AES-256-GCM. `context` is mixed into the key * derivation so a payload sealed for one purpose cannot be replayed as another. */ export declare function sealForMember(recipientAgreementKey: string, plaintext: string, context: string): WrappedKeyEnvelope; /** Open an envelope sealed by {@link sealForMember}. Returns null on any failure. */ export declare function openSealedEnvelope(agreement: NodeKeyPairMaterial, envelope: WrappedKeyEnvelope, context: string): string | null; /** True when `value` has every field {@link openSealedEnvelope} needs. */ export declare function isWrappedKeyEnvelope(value: unknown): value is WrappedKeyEnvelope; //# sourceMappingURL=group-crypto.d.ts.map