Identity & Keys
DID:key identifiers
Section titled “DID:key identifiers”Every xNet user has a Decentralized Identifier (DID) derived from their Ed25519 public key. The format follows the W3C DID:key method:
did:key:z6Mk...The public key is embedded directly in the DID, making it self-certifying — you can extract the public key and verify signatures without any external resolver or blockchain.
How it’s constructed
Section titled “How it’s constructed”- Take the 32-byte Ed25519 public key
- Prepend the multicodec prefix
0xed01(Ed25519 public key) - Encode with base58btc (multibase prefix
z) - Prefix with
did:key:
Generate an identity
Section titled “Generate an identity”import { generateIdentity, isValidDID } from '@xnet/identity'
const { identity, privateKey } = generateIdentity()// identity.did = "did:key:z6Mk..."// identity.publicKey = Uint8Array (32 bytes)// identity.created = Date.now()
isValidDID(identity.did) // trueParse a DID
Section titled “Parse a DID”import { parseDID } from '@xnet/identity'
const publicKey = parseDID('did:key:z6Mk...')// Returns the 32-byte Ed25519 public keyKey bundles
Section titled “Key bundles”Each user has two key pairs bundled together:
| Key | Algorithm | Purpose |
|---|---|---|
| Signing key | Ed25519 | Sign changes, Yjs updates, UCAN tokens |
| Encryption key | X25519 | Key exchange, encrypted storage |
The DID is derived from the signing public key.
import { generateKeyBundle, deriveKeyBundle } from '@xnet/identity'
// Random generationconst bundle = generateKeyBundle()// bundle.signingKey — Ed25519 private key// bundle.encryptionKey — X25519 private key// bundle.identity — { did, publicKey, created }
// Deterministic from a seed (same seed = same identity)const bundle2 = deriveKeyBundle(masterSeed)Storage
Section titled “Storage”Key bundles are serialized, encrypted with XChaCha20-Poly1305, and stored via PasskeyStorage. In production, the encryption key should be derived from WebAuthn credentials.
import { BrowserPasskeyStorage } from '@xnet/identity'
const storage = new BrowserPasskeyStorage()const storedKey = await storage.store(bundle, credentialId)const recovered = await storage.retrieve(storedKey, credentialId)Cryptographic primitives
Section titled “Cryptographic primitives”The @xnet/crypto package provides all the low-level operations:
Hashing (BLAKE3)
Section titled “Hashing (BLAKE3)”import { hash, hashHex } from '@xnet/crypto'
const digest = hash(data) // Uint8Arrayconst hex = hashHex(data) // hex stringconst b64 = hashBase64(data) // base64url stringconst sha = hash(data, 'sha256') // SHA-256 fallbackSigning (Ed25519)
Section titled “Signing (Ed25519)”import { generateSigningKeyPair, sign, verify } from '@xnet/crypto'
const { publicKey, privateKey } = generateSigningKeyPair()const signature = sign(message, privateKey)const valid = verify(message, signature, publicKey) // booleanEncryption (XChaCha20-Poly1305)
Section titled “Encryption (XChaCha20-Poly1305)”import { generateKey, encrypt, decrypt } from '@xnet/crypto'
const key = generateKey() // 32 bytesconst encrypted = encrypt(plaintext, key) // { nonce, ciphertext }const decrypted = decrypt(encrypted, key) // Uint8ArrayKey exchange (X25519 + HKDF)
Section titled “Key exchange (X25519 + HKDF)”import { generateKeyPair, deriveSharedSecret } from '@xnet/crypto'
const alice = generateKeyPair()const bob = generateKeyPair()const shared = deriveSharedSecret(alice.privateKey, bob.publicKey)// Both sides derive the same 32-byte shared secretUCAN tokens
Section titled “UCAN tokens”UCANs (User Controlled Authorization Networks) are self-signed capability tokens for decentralized authorization. They replace traditional API keys or OAuth tokens.
Structure
Section titled “Structure”interface UCANToken { iss: string // Issuer DID aud: string // Audience DID exp: number // Expiration (Unix seconds) att: UCANCapability[] // Capabilities granted prf: string[] // Proof chain (parent tokens) sig: Uint8Array // Ed25519 signature}
interface UCANCapability { with: string // Resource URI: 'xnet://doc/123' or '*' can: string // Action: 'read', 'write', or '*'}Create a token
Section titled “Create a token”import { createUCAN } from '@xnet/identity'
const token = createUCAN({ issuer: alice.did, issuerKey: alice.privateKey, audience: bob.did, capabilities: [ { with: 'xnet://doc/123', can: 'write' }, { with: 'xnet://doc/*', can: 'read' } ], expiration: Date.now() / 1000 + 3600, // 1 hour (default) proofs: [] // Parent tokens for delegation})Verify a token
Section titled “Verify a token”import { verifyUCAN, hasCapability } from '@xnet/identity'
const result = verifyUCAN(token)if (result.valid) { const canWrite = hasCapability(result.payload, 'xnet://doc/123', 'write')}Verification extracts the issuer’s public key from their DID (no external resolver), checks expiration, and verifies the Ed25519 signature.
Delegation chains
Section titled “Delegation chains”flowchart LR A["Alice\n(document owner)"] -->|"grants write\n(signs UCAN)"| B["Bob"] B -->|"delegates read\n(cites Alice's UCAN as proof)"| C["Carol"] C -->|"verifies chain"| D["Alice → Bob → Carol"]
UCANs support transitive delegation via the prf (proofs) field:
// Alice grants Bob write accessconst aliceToBob = createUCAN({ issuer: alice.did, issuerKey: alice.privateKey, audience: bob.did, capabilities: [{ with: 'xnet://doc/123', can: 'write' }]})
// Bob delegates read access to Carol, citing Alice's grant as proofconst bobToCarol = createUCAN({ issuer: bob.did, issuerKey: bob.privateKey, audience: carol.did, capabilities: [{ with: 'xnet://doc/123', can: 'read' }], proofs: [aliceToBob]})Integration with React
Section titled “Integration with React”Identity is provided to the app through XNetProvider:
import { XNetProvider } from '@xnet/react';<XNetProvider config={{ authorDID: identity.did, signingKey: privateKey, identity: identity // ... }}> <App /></XNetProvider>The useIdentity hook exposes identity state to components:
import { useIdentity } from '@xnet/react'
function Profile() { const { identity, isAuthenticated, did } = useIdentity()
if (!isAuthenticated) return <LoginScreen /> return <p>Signed in as {did}</p>}How identity flows through the stack
Section titled “How identity flows through the stack”flowchart TD A["XNetProvider\n(authorDID + signingKey)"] --> B["NodeStore"] A --> C["SyncManager"] B --> D["mutate.create() / update()"] D --> E["Ed25519 sign Change‹T›"] C --> F["BSM\n(Background Sync Manager)"] F --> G["signYjsUpdate()\nSignedYjsEnvelope"] F --> H["ClientID attestation\n(clientId → DID binding)"] G --> I["Broadcast to peers"] I --> J["Remote peer\nverify all signatures"]
XNetProvidercreates aNodeStorewithauthorDIDandsigningKey- Every
mutate.create()/mutate.update()call signs the change with Ed25519 SyncManagerreceives the signing credentials and passes them to the BSM- Yjs updates are signed as
SignedYjsEnvelopebefore broadcast - ClientID attestations bind the Yjs clientID to the DID
- Remote peers verify all signatures before applying changes