Skip to content

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/base peer dependency.

import {
generateEd25519KeyPair,
importEd25519Key,
serializeEd25519Key_Raw,
signTextDataWithKeyEd25519,
verifyWithKeyEd25519,
} from "@nice-code/util";
import { base64 } from "@scure/base";
const keyPair = await generateEd25519KeyPair();
// Sign
const 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 + verify
const 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 key
const 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,
});
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 keys
await 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 call
const { 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.

import type { StringKeys } from "@nice-code/util";
type Keys = StringKeys<{ a: string; b: number; 0: boolean }>;
// → "a" | "b"