Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ Create a `TreeRoot` from a bech32 nsec string or raw 32-byte key. An intermediat

Derive a child `Identity` from a `TreeRoot`. Returns `{ nsec, npub, privateKey, publicKey, purpose, index }`. The index defaults to `0`.

### `deriveRendezvous(root, index?)`

Derive the Vennel `rendezvous` root child for a Link or quiet-drop pairing.
It is deliberately derived from the `TreeRoot`, never an account identity or
persona. Rotate it by incrementing `index`; provision the private half only by
an authenticated private device flow, then call `zeroise()` on the temporary
identity. This helper does not create a pairing link or export a private key.

### `deriveFromIdentity(identity, purpose, index?)`

Derive a child `Identity` from any existing `Identity`, enabling arbitrary-depth
Expand Down
1 change: 1 addition & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { fromNsec } from './root-nsec.js'
export { derive, zeroise } from './derive.js'
export { deriveRendezvous, RENDEZVOUS_PURPOSE } from './rendezvous.js'
export { recover } from './recover.js'
export type { TreeRoot, Identity } from './types.js'
export { NsecTreeError, DEFAULT_SCAN_RANGE, MAX_SCAN_RANGE, MAX_INDEX } from './types.js'
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { fromNsec } from './root-nsec.js'
export { fromMnemonic } from './root-mnemonic.js'
export { derive, zeroise } from './derive.js'
export { deriveRendezvous, RENDEZVOUS_PURPOSE } from './rendezvous.js'
export { recover } from './recover.js'
export { createBlindProof, createFullProof, verifyProof } from './proof.js'
export type { TreeRoot, Identity, LinkageProof } from './types.js'
Expand Down
16 changes: 16 additions & 0 deletions src/rendezvous.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { derive } from './derive.js'
import type { Identity, TreeRoot } from './types.js'

/** The sole root-child purpose used for a person's Vennel rendezvous key. */
export const RENDEZVOUS_PURPOSE = 'rendezvous'

/**
* Derive one rotated rendezvous identity directly from the person's tree root.
*
* This deliberately accepts a `TreeRoot`, not a persona or ordinary identity:
* Link tags and quiet drops must not use an account identity key. The returned
* private key is caller-owned and must be zeroised after private provisioning.
*/
export function deriveRendezvous(root: TreeRoot, index = 0): Identity {
return derive(root, RENDEZVOUS_PURPOSE, index)
}
4 changes: 4 additions & 0 deletions test/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ describe('subpath exports', () => {
expect(mod.fromNsec).toBeDefined()
expect(mod.fromMnemonic).toBeDefined()
expect(mod.derive).toBeDefined()
expect(mod.deriveRendezvous).toBeDefined()
expect(mod.RENDEZVOUS_PURPOSE).toBe('rendezvous')
expect(mod.recover).toBeDefined()
expect(mod.zeroise).toBeDefined()
expect(mod.createBlindProof).toBeDefined()
Expand All @@ -18,6 +20,8 @@ describe('subpath exports', () => {
const mod = await import('../src/core.js')
expect(mod.fromNsec).toBeDefined()
expect(mod.derive).toBeDefined()
expect(mod.deriveRendezvous).toBeDefined()
expect(mod.RENDEZVOUS_PURPOSE).toBe('rendezvous')
expect(mod.recover).toBeDefined()
expect(mod.zeroise).toBeDefined()
expect((mod as any).fromMnemonic).toBeUndefined()
Expand Down
41 changes: 41 additions & 0 deletions test/rendezvous.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { derive, zeroise } from '../src/derive.js'
import { fromNsec } from '../src/root-nsec.js'
import { deriveRendezvous, RENDEZVOUS_PURPOSE } from '../src/rendezvous.js'

describe('deriveRendezvous', () => {
it('uses the sole Vennel root-child purpose and preserves the requested rotation index', () => {
const root = fromNsec(new Uint8Array(32).fill(0x61))
const rendezvous = deriveRendezvous(root, 3)
const generic = derive(root, 'rendezvous', 3)

expect(RENDEZVOUS_PURPOSE).toBe('rendezvous')
expect(rendezvous.purpose).toBe(RENDEZVOUS_PURPOSE)
expect(rendezvous.index).toBe(3)
expect(rendezvous.publicKey).toEqual(generic.publicKey)
expect(rendezvous.privateKey).toEqual(generic.privateKey)

zeroise(rendezvous)
zeroise(generic)
root.destroy()
})

it('rotates by index without changing the person root', () => {
const root = fromNsec(new Uint8Array(32).fill(0x62))
const current = deriveRendezvous(root, 0)
const rotated = deriveRendezvous(root, 1)

expect(rotated.publicKey).not.toEqual(current.publicKey)
expect(rotated.npub).not.toBe(current.npub)

zeroise(current)
zeroise(rotated)
root.destroy()
})

it('cannot derive after the root is destroyed', () => {
const root = fromNsec(new Uint8Array(32).fill(0x63))
root.destroy()
expect(() => deriveRendezvous(root)).toThrow('destroyed')
})
})