@xkore/dices
v0.1.3
Published
Transport-agnostic quantum-resistant encryption layer for P2P applications.
Readme
@xkore/dices
Transport-agnostic quantum-resistant encryption layer for P2P applications.
Table of Contents
What is Dices?
Building P2P applications requires secure encrypted data transfer between peers. You need:
- End-to-end encryption without central servers
- Protection against future quantum computer attacks
- Forward secrecy (compromised keys don't reveal past messages)
- Backward secrecy (compromised keys don't reveal future messages)
- Simple API that doesn't expose cryptographic complexity
DICES provides encrypted channels between peers using just their node IDs. Two key components:
1. Encrypt and decrypt your data
The entire encryption system is exposed through two functions:
// Encrypt for a recipient
const cipherData = await encrypt(nodeId, data, overlay);
// Decrypt from sender
const data = await decrypt(cipherData, overlay);Behind the scenes, DICES uses a bounded triple ratchet - three independent mechanisms that continuously update encryption keys to provide forward secrecy, backward secrecy, and quantum resistance.
2. Automatic channel establishment via DHT
When you encrypt data for a recipient, DICES automatically:
- Fetches their public keys from the distributed hash table
- Establishes an encrypted session
- Handles all key rotations and updates
No manual key exchange. No central servers. Just node IDs.
The encryption provides quantum resistance (via ML-KEM-1024), forward secrecy, and backward secrecy. See detailed cryptography explanation below.
Installation
npm install @xkore/dicesQuick Start
1. Opening the Overlay
import { Overlay } from "@xkore/dices";
import { Client as DiceClient } from "@xkore/dice";
import { Level } from "level";
// Create and open overlay instance
const overlay = new Overlay({
database: new Level("./db"),
diceClient: new DiceClient({
/* ... */
}),
secretKey: mySecretKey, // optional, generates random if omitted
});
await overlay.open();
// Overlay is now connected to the DHT and has published its initiation keys2. Encrypting and Decrypting Messages
import { encrypt, decrypt } from "@xkore/dices";
// Encrypt message for Bob (automatically fetches his initiation keys from DHT)
const encrypted = await encrypt(bobNodeId, messageData, overlay);
// encrypted is a Uint8Array - send it via YOUR transport layer
// (UDP, TCP, WebRTC, WebSocket, carrier pigeon, etc.)
// Example: await yourTransport.send(bobAddress, encrypted);
// When you receive an encrypted buffer from your transport:
// yourTransport.on("message", async (buffer, sender) => {
const data = await decrypt(buffer, overlay);
console.log("Received message:", data);
// });3. Closing the Overlay
// Cleanup resources when done
await overlay.close();Core API
Overlay Constructor
Creates a new DICES overlay instance for quantum-resistant encrypted P2P communication.
const overlay = new Overlay(options);Options:
database(required): LevelDB instance for persistent ratchet state storagediceClient(required): DICE client instance for DHT operations and peer discoverysecretKey(optional): 32-byte secp256k1 secret key (generates random if not provided)bootstrapTargets(optional): Initial DHT nodes to connect to (defaults to built-in bootstrap nodes)concurrency(optional): Number of concurrent DHT operations (default: 3)healthcheckIntervalMs(optional): Interval for DHT healthchecks in ms (default: 60000)pruneIntervalMs(optional): Interval for cleaning expired ratchet state in ms (default: 3600000)ratchetKeyTtl(optional): Time-to-live for ratchet keys in ms (default: 3600000)timeoutMs(optional): Default timeout for operations in ms (default: 30000)
Example:
import { Level } from "level";
import { Client as DiceClient } from "@xkore/dice";
const overlay = new Overlay({
database: new Level("./my-app-db"),
diceClient: new DiceClient({
secretKey: myDiceSecretKey,
// ... other DICE options
}),
secretKey: myOverlaySecretKey,
healthcheckIntervalMs: 30000, // 30 seconds
timeoutMs: 10000, // 10 second timeout
});open()
Opens the overlay and starts network operations.
await overlay.open(isBootstrapping?: boolean);Opens the database, connects the DICE client to the network, loads or generates ratchet keys, publishes initiation keys to the DHT, and starts healthcheck and prune intervals.
Parameters:
isBootstrapping(optional): Whether to connect to bootstrap nodes (default:true)
Returns: Promise<void>
Example:
// Open with bootstrapping (default)
await overlay.open();
// Open without bootstrapping (already connected to DHT)
await overlay.open(false);
// Overlay is now ready for encrypted communicationencrypt()
Encrypts data for a remote peer.
import { encrypt } from "@xkore/dices";
const encrypted = await encrypt(remoteNodeId, data, overlay);Creates an authenticated encrypted buffer using the bounded triple ratchet protocol. Automatically fetches initiation keys from the DHT for first messages. Initializes new ratchet sessions automatically. Handles ML-KEM rotation when message or time bounds are reached.
Parameters:
remoteNodeId: 20-byte nodeId of the recipient (Uint8Array)data: Plaintext data to encrypt (Uint8Array)overlay: The DICES overlay instance
Returns: Promise<Uint8Array> - Encrypted buffer
Throws: DicesOverlayError if unable to fetch initiation keys or state save fails
Example:
import { encrypt } from "@xkore/dices";
// Both first and subsequent messages use the same simple API
const encrypted = await encrypt(bobNodeId, messageData, overlay);
// Send encrypted buffer via your transport
await transport.send(bobAddress, encrypted);decrypt()
Decrypts data from a remote peer.
import { decrypt } from "@xkore/dices";
const data = await decrypt(buffer, overlay);Performs signature verification, initializes ratchet state if needed (for first message), handles DH ratchet updates, decrypts the message, and persists updated ratchet state.
Parameters:
buffer: The encrypted buffer to decrypt (Uint8Array)overlay: The DICES overlay instance
Returns: Promise<Uint8Array> - Decrypted plaintext data
Throws: DicesOverlayError if signature verification fails, ratchet state invalid, or decryption fails
Example:
import { decrypt } from "@xkore/dices";
// Listen for incoming messages
transport.on("message", async (buffer, sender) => {
try {
const data = await decrypt(buffer, overlay);
console.log("Received message:", new TextDecoder().decode(data));
} catch (error) {
console.error("Failed to decrypt:", error);
}
});close()
Closes the overlay and cleans up resources.
await overlay.close();Stops healthcheck and prune intervals, disconnects the DICE client, and closes the database connection. Should be called when the overlay is no longer needed to prevent resource leaks.
Returns: Promise<void>
Example:
// Open overlay
await overlay.open();
// ... use overlay for encrypted communication ...
// Cleanup when done
await overlay.close();
console.log("Overlay closed and resources cleaned up");Cryptography
DICES is inspired by and builds upon Signal's SPQR (Signal Protocol Post-Quantum Ratchet) system, adapting it for P2P networks with important improvements.
How It Works
The system uses a triple ratchet - three independent mechanisms that continuously update encryption keys:
Symmetric Ratchet: Derives new message keys from a chain key using a one-way function (HMAC). Even if someone captures a message key, they can't reverse-engineer previous keys. This provides forward secrecy within a session.
DH Ratchet: Uses X25519 (classical elliptic curve) ephemeral key exchanges. Each side can generate new ephemeral keys, and when both sides have updated, a completely new shared secret is created. This provides backward secrecy - if your keys are compromised, new messages stay protected once you rotate.
KEM Ratchet: Uses ML-KEM-1024 (quantum-resistant key encapsulation). Like the DH ratchet but protected against quantum computer attacks. ML-KEM is part of NIST's post-quantum cryptography standards (FIPS 203).
All three ratchets work together - the output of the KEM and DH ratchets feeds into the symmetric ratchet, which generates the actual encryption keys for messages.
Additional Cryptography Components:
- XChaCha20-Poly1305: The actual message encryption - authenticated encryption with extended nonce space
- HKDF-SHA256: Derives multiple keys from ratchet outputs with domain separation (ensures keys used for different purposes can't interfere)
- secp256k1: Node identity and message signatures (same curve as Bitcoin)
- X25519: Classical elliptic curve key exchange for defense-in-depth alongside ML-KEM
Bounded Rotation
Signal's SPQR system has a potential weakness: unbounded one-sided messaging. If Alice keeps sending messages to Bob without getting replies, the ML-KEM keys never rotate because rotation requires participation from both parties. This means a very powerful attacker could potentially store all messages and wait for quantum computers.
DICES solves this with bounded rotation - automatic ML-KEM key rotation enforced by limits:
- Message bound: New ML-KEM keys generated after N messages (default: 100)
- Time bound: New ML-KEM keys generated after T time passes (default: 1 hour)
When either limit is reached, the sending node generates fresh ML-KEM keys, publishes them to the DHT, and forces rotation. The DH ratchet continues normally. This maintains all the forward/backward secrecy properties while ensuring quantum resistance doesn't degrade over time.
You can configure these bounds when creating an overlay:
const overlay = new Overlay({
// ... other options
maxMessagesBeforeRotation: 50, // Rotate after 50 messages
maxTimeBeforeRotation: 1800000, // Rotate after 30 minutes
});Events
The overlay emits events for monitoring and message handling:
// Connection events
overlay.events.on("open", () => {
console.log("Overlay opened");
});
overlay.events.on("close", () => {
console.log("Overlay closed");
});
// Error events
overlay.events.on("error", (error) => {
console.error("Overlay error:", error);
});
// Message events
overlay.events.on("message", (message, context) => {
// Handle incoming message
});
// Node updates (when local node's diceAddress changes)
overlay.events.on("node", (previousNode, nextNode) => {
console.log("Node updated:", nextNode);
});
// Key rotation
overlay.events.on("rotate", (initiationKeys) => {
console.log("Rotated initiation keys:", initiationKeys.keyId);
});Development
Setup
# Install dependencies
npm install
# Build the package
npm run buildTesting
# Run all tests
npm test
# Run unit tests only
npm run test:unit
# Run integration tests only
npm run test:integration
# Run specific integration test
npm run integration -- testNameProject Structure
packages/dices/
├── src/
│ ├── models/
│ │ ├── Overlay/ # Main class, wrap/unwrap functions
│ │ ├── Keys/ # secp256k1 identity
│ │ ├── RatchetKeysItem/ # Initiation keys (ML-KEM + X25519)
│ │ ├── RatchetStateItem/# Per-peer ratchet sessions
│ │ ├── Value/ # DHT record with signatures
│ │ ├── Envelope/ # Wire format
│ │ ├── CipherData/ # XChaCha20-Poly1305 encryption
│ │ └── Nodes/ # Kademlia routing table
│ └── utilities/
│ └── (KDF functions, key derivation)
└── package.jsonLicense
MIT
Related
- @xkore/dice - DHT-based UDP connectivity protocol
- kademlia-table - Kademlia routing table implementation
- bufferfy - Binary serialization library
