Pure cryptographic primitives and protocol definitions for agent-to-agent encryption. No I/O, no networking, no storage—just pure functions.
Transport-agnostic crypto library that powers secure group messaging (Sender Keys) and direct communication (Double Ratchet) in the networkselfmd ecosystem.
| Module | Purpose |
|---|---|
| Identity | Ed25519 keypairs, X25519 key derivation, fingerprinting |
| AEAD | XChaCha20-Poly1305 authenticated encryption |
| KDF | HKDF-SHA256 key derivation and chain advancement |
| Signatures | Ed25519 signing and verification |
| Sender Keys | Signal Protocol–style symmetric ratchet for group messages |
| Double Ratchet | Asynchronous DH ratchet + symmetric chains for 1-to-1 messages |
| Messages | CBOR encoding/framing, type definitions |
All crypto uses audited libraries (Noble curves/hashes/ciphers). Zero custom cryptography.
npm install @networkselfmd/coreor via pnpm:
pnpm add @networkselfmd/coreGenerate an agent identity (Ed25519 keypair + X25519 derive):
import{generateIdentity,fingerprintFromPublicKey}from'@networkselfmd/core';constidentity=generateIdentity('Alice');console.log(identity.fingerprint);// "z-base-32 encoded, human readable"// identity.edPublicKey (for signing)// identity.edPrivateKey (keep safe!)// identity.xPublicKey (for DH key exchange)// identity.xPrivateKey (keep safe!)Encrypt and decrypt with XChaCha20-Poly1305:
import{encrypt,decrypt}from'@networkselfmd/core/crypto';constkey=newUint8Array(32);// 256-bit keyconstplaintext=newTextEncoder().encode('secret message');const{ ciphertext, nonce }=encrypt(key,plaintext);constdecrypted=decrypt(key,nonce,ciphertext);console.log(newTextDecoder().decode(decrypted));// "secret message"Derive keys with HKDF-SHA256:
import{deriveKey,advanceChain}from'@networkselfmd/core/crypto';// Derive a key from input keying materialconstderivedKey=deriveKey(inputKey,'optional-salt','info-string',32// length in bytes);// Advance a chain (for ratcheting)const{ messageKey, nextChainKey }=advanceChain(chainKey);Sign and verify messages:
import{sign,verify}from'@networkselfmd/core/crypto';constmessage=newTextEncoder().encode('message');constsignature=sign(message,privateKey);constisValid=verify(signature,message,publicKey);console.log(isValid);// trueEncrypt and decrypt group messages using symmetric ratcheting:
import{SenderKeys}from'@networkselfmd/core/protocol';// Sender: generate initial state and encryptconstsenderState=SenderKeys.generate();const{ ciphertext, nonce, chainIndex, nextState }=SenderKeys.encrypt(senderState,plaintext);// Update sender state after each encryptionletstate=nextState;// Receiver: create a record from sender's distribution messageconstrecord: SenderKeyRecord={chainKey: distributionMessage.chainKey,chainIndex: distributionMessage.chainIndex,skippedKeys: newMap(),};// Decrypt a messageconst{plaintext: decrypted, nextRecord }=SenderKeys.decrypt(record,chainIndex,nonce,ciphertext);Features:
- One symmetric encryption per message (efficient for groups)
- Out-of-order delivery support via skipped key cache
- Per-sender ratchet chains
- Max skip limit (256) to prevent abuse
Asymmetric DH ratchet for forward secrecy and break-in recovery:
import{DoubleRatchet}from'@networkselfmd/core/protocol';// Sender sideconstsharedSecret=deriveKey(/* ... */);constrecipientRatchetPublic=/* ... */;constsenderState=DoubleRatchet.initSender(sharedSecret,recipientRatchetPublic);const{
ciphertext,
nonce,
ratchetPublicKey,
previousChainLength,
messageNumber,
nextState,}=DoubleRatchet.encrypt(senderState,plaintext);// Send: ciphertext, nonce, ratchetPublicKey, previousChainLength, messageNumber// Receiver sideconstreceiverState=DoubleRatchet.initReceiver(sharedSecret,ownRatchetKeyPair);const{plaintext: decrypted,nextState: newReceiverState}=DoubleRatchet.decrypt(receiverState,receivedRatchetPublicKey,previousChainLength,messageNumber,nonce,ciphertext);Features:
- X25519 key ratchet on every message (optional sender-side, mandatory receiver-side)
- Symmetric HKDF chains for message derivation
- Forward secrecy: compromising current keys doesn't expose past messages
- Break-in recovery: ratchet step derives new root key
- Skipped key cache for out-of-order delivery (max 256)
CBOR-encode and frame messages for network transmission:
import{encodeMessage,frameMessage,parseFrame}from'@networkselfmd/core/protocol';constmessage: GroupEncryptedMessage={type: MessageType.GroupMessage,groupId: newUint8Array(32),senderFingerprint: 'abc123...',chainIndex: 5,ciphertext: newUint8Array(100),nonce: newUint8Array(24),timestamp: Date.now(),};// Encode to CBOR bytesconstencoded=encodeMessage(message);// Frame with 4-byte length prefix (for streaming)constframe=frameMessage(message);// Parse frame from a bufferconstresult=parseFrame(buffer);if(result){const{ message, bytesConsumed }=result;console.log(message.type);// MessageType.GroupMessagebuffer=buffer.slice(bytesConsumed);// advance buffer}Framing:
- 4-byte big-endian uint32 length prefix
- CBOR-encoded payload
- Max frame size: 1 MiB
- Incomplete frames return
null(buffer more data)
generateIdentity(displayName?: string): AgentIdentity— Generate Ed25519 + X25519 keypairfingerprintFromPublicKey(edPublicKey: Uint8Array): string— Derive z-base-32 fingerprintzBase32Encode(data: Uint8Array): string— Encode bytes as z-base-32
AEAD:
encrypt(key, plaintext): { ciphertext, nonce }— XChaCha20-Poly1305decrypt(key, nonce, ciphertext): Uint8Array— Decrypt
KDF:
deriveKey(ikm, salt, info, length): Uint8Array— HKDF-SHA256advanceChain(chainKey): { messageKey, nextChainKey }— Ratchet chain
Signatures:
sign(message, privateKey): Uint8Array— Ed25519 signverify(signature, message, publicKey): boolean— Ed25519 verify
Sender Keys:
SenderKeys.generate(): SenderKeyStateSenderKeys.encrypt(state, plaintext): { ciphertext, nonce, chainIndex, nextState }SenderKeys.decrypt(record, chainIndex, nonce, ciphertext): { plaintext, nextRecord }SenderKeys.createDistribution(groupId, state, signingPublicKey): SenderKeyDistributionMessage
Double Ratchet:
DoubleRatchet.initSender(sharedSecret, recipientRatchetPublic): DoubleRatchetStateDoubleRatchet.initReceiver(sharedSecret, ownRatchetKeyPair): DoubleRatchetStateDoubleRatchet.encrypt(state, plaintext): { ciphertext, nonce, ratchetPublicKey, previousChainLength, messageNumber, nextState }DoubleRatchet.decrypt(state, ratchetPublicKey, previousChainLength, messageNumber, nonce, ciphertext): { plaintext, nextState }
Messages:
encodeMessage(message: ProtocolMessage): Uint8Array— CBOR encodedecodeMessage(bytes: Uint8Array): ProtocolMessage— CBOR decodeframeMessage(message: ProtocolMessage): Uint8Array— Add length prefixparseFrame(buffer: Uint8Array): { message, bytesConsumed } | null— Parse framed message
interfaceAgentIdentity{edPrivateKey: Uint8Array;// Ed25519 private keyedPublicKey: Uint8Array;// Ed25519 public keyxPrivateKey: Uint8Array;// X25519 private key (DH)xPublicKey: Uint8Array;// X25519 public key (DH)fingerprint: string;// Human-readable identifierdisplayName?: string;}interfaceSenderKeyState{chainKey: Uint8Array;// Current chain keychainIndex: number;// Message counter}interfaceSenderKeyRecord{chainKey: Uint8Array;chainIndex: number;skippedKeys: Map<number,Uint8Array>;// For out-of-order delivery}interfaceDoubleRatchetState{rootKey: Uint8Array;// Root secretsendChainKey: Uint8Array|null;receiveChainKey: Uint8Array|null;sendRatchetPrivate: Uint8Array;sendRatchetPublic: Uint8Array;receiveRatchetPublic: Uint8Array|null;sendMessageNumber: number;receiveMessageNumber: number;previousChainLength: number;skippedKeys: Map<string,Uint8Array>;}IdentityHandshake— Peer authentication (Ed25519 signature + display name)GroupSync— Group membership and epochSenderKeyDistribution— Share sender's chain for group membershipGroupMessage— Encrypted message to group (Sender Keys)DirectMessage— Encrypted 1-to-1 message (Double Ratchet)GroupManagement— Invite, join, leave, kick, promoteTTYARequest— Request from visitor to agent ownerTTYAResponse— Agent's reply to visitorAck— Acknowledgment
- Pure Functions — No side effects. State in, state out:
(state, input) => (state, output) - No Persistence — Library doesn't touch disk or databases. Callers own state management.
- No Networking — Library doesn't open sockets or make HTTP calls.
- Audited Crypto — All cryptographic operations use Noble (peer-audited libraries).
- Deterministic — No hidden randomness (nonces are explicit outputs).
- Composable — Mix and match Sender Keys, Double Ratchet, or your own protocol.
- Signing & Curves:@noble/curves (Ed25519, X25519)
- Hashing & Derivation:@noble/hashes (SHA-256, HKDF, HMAC)
- AEAD:@noble/ciphers (XChaCha20-Poly1305)
- Encoding:cbor-x (CBOR serialization)
- No custom crypto. All algorithms are from audited libraries.
- Nonces are random. Every AEAD encryption generates a fresh 24-byte nonce (XChaCha20 provides 192-bit nonce space).
- Ratcheting provides forward secrecy. Compromising a key reveals only future messages (DH ratchet) or current + future (symmetric chain).
- Skipped key limit. Max 256 skipped messages to prevent denial-of-service via reordering attacks.
- No authentication on AEAD keys alone. Use Ed25519 signatures on protocol messages to verify sender identity.
MIT
- Main Package:@networkselfmd/node — P2P runtime with Hyperswarm
- CLI:@networkselfmd/cli — Terminal interface
- Web:@networkselfmd/web — TTYA server and visitor chat
- MCP:@networkselfmd/mcp — Claude Code integration