Crypto
WebCrypto helpers — Ed25519 signing, X25519 exchange, AES-GCM, and a client link.
WebCrypto-based helpers that work in browsers, Workers / Durable Objects, Bun, and Node. Keys serialize to self-describing prefixed strings (<algo>::<format>::<data>), so a stored or transported key always knows how to re-import itself.
Crypto helpers require the
@scure/basepeer dependency.
Ed25519 — sign & verify
Section titled “Ed25519 — sign & verify”import { generateEd25519KeyPair, importEd25519Key, serializeEd25519Key_Raw, signTextDataWithKeyEd25519, verifyWithKeyEd25519,} from "@nice-code/util";import { base64 } from "@scure/base";
const keyPair = await generateEd25519KeyPair();
// Signconst signature = await signTextDataWithKeyEd25519("challenge-text", keyPair.privateKey);const signatureBase64 = base64.encode(signature);
// Serialize the public key for transport — "ed25519::raw_base64::<data>"const { prefixed } = await serializeEd25519Key_Raw(keyPair.publicKey);
// Other side: import + verifyconst publicKey = await importEd25519Key.public.fromFormattedString.extractable(prefixed);const isValid = await verifyWithKeyEd25519({ challenge: "challenge-text", signatureBase64, publicKey,});X25519 + AES-GCM — shared-key encryption
Section titled “X25519 + AES-GCM — shared-key encryption”Derive a shared AES-GCM key from two X25519 key pairs (ECDH + HKDF), then encrypt/decrypt:
import { generateX25519KeyPair, createAesGcmKeyFromX25519Keys, encryptTextDataWithAesGcmKey, decryptTextDataWithAesGcmKey,} from "@nice-code/util";
const alice = await generateX25519KeyPair();const bob = await generateX25519KeyPair();
// Both sides derive the same key from their private + the other's public keyconst aliceKey = await createAesGcmKeyFromX25519Keys({ internalX25519PrivateKey: alice.privateKey, externalX25519PublicKey: bob.publicKey, saltString: "optional-session-salt", infoString: "optional-context",});
const payload = await encryptTextDataWithAesGcmKey({ aesGcmKey: aliceKey, dataToEncrypt: "secret message",}); // { nonce, ciphertext } — both base64
const plaintext = await decryptTextDataWithAesGcmKey({ aesGcmKey: bobKey, dataToDecrypt: payload,});ClientCryptoKeyLink — full client-to-client crypto
Section titled “ClientCryptoKeyLink — full client-to-client crypto”A high-level class managing a local identity (Ed25519 verify pair + X25519 exchange pair) and links to other clients, with optional persistence through any StorageAdapter.
import { ClientCryptoKeyLink, createMemoryStorageAdapter_json } from "@nice-code/util";
const link = new ClientCryptoKeyLink({ storageAdapter: createMemoryStorageAdapter_json(), // optional — omit for in-memory only});await link.initialize();
// Share these with the other side (serialized prefixed strings)const { verifyPublicKey, exchangePublicKey } = await link.getLocalPublicKeys();
// Register the other side's keysawait link.linkClient({ linkedClientId: "client::partner-1", verifyPublicKey: theirVerifyKey, exchangePublicKey: theirExchangeKey, bindVerifyKeysIntoDerivation: true, // a tampered relayed key makes the first decryption fail});
// Sign + encrypt for the linked client (shared key derived & cached automatically)const { encryptedData, signatureBase64 } = await link.signAndEncryptDataForLinkedClient({ linkedClientId: "client::partner-1", dataToEncrypt: "hello",});
// Other side: decrypt + verify in one callconst { data, isValid } = await otherLink.decryptAndVerifyDataFromLinkedClient({ linkedClientId: "client::me", dataToDecrypt: encryptedData, signatureBase64,});This is the same ClientCryptoKeyLink used by @nice-code/action to back the crypto identity of secure channels — including the identityMode: "required" provisioning described under Security levels.
TypeScript utilities
Section titled “TypeScript utilities”import type { StringKeys } from "@nice-code/util";
type Keys = StringKeys<{ a: string; b: number; 0: boolean }>;// → "a" | "b"