forgesworn/nsec-tree
GitHub: forgesworn/nsec-tree
基于单一主密钥确定性派生无限不可关联的 Nostr 层级子身份,支持用途标签、盲关联证明和助记词一键恢复。
Stars: 0 | Forks: 0
# nsec-tree
**Nostr:** [`npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`](https://njump.me/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2)
确定性 Nostr 身份层级结构。一个主密钥,无限身份。
```
npm install nsec-tree
```
仅支持 ESM。零自定义加密——所有原语均来自 @noble/@scure。
## 为什么使用 nsec-tree?
NIP-06 标准化了基于助记词的密钥派生,但大多数客户端只展示一个主密钥。nsec-tree 为你提供了一个带有用途标签的身份树。
- **默认不可关联** — 没有观察者能证明两个子 npub 共属于同一个主密钥
- **可恢复** — 12 个单词即可重建你的整个身份树
- **带用途标签** — 人类可读的派生(`"social"`、`"commerce"`、`"trott:rider"`)
- **可组合层级** — 建立类似 `work -> company:a -> signing` 的树结构
子密钥是普通的 Nostr 密钥对。不理解关联证明的客户端会将它们视为独立的身份。
这不仅仅是“从一个种子生成多个账户”。你可以为人设、组织、应用、环境和轮换替换密钥派生出结构化的子树:
- `personal -> social -> main`
- `personal -> social -> alt`
- `work -> company:a -> payroll`
- `work -> company:b -> ops`
- `work -> company:b -> ops -> emergency`
## 快速开始
### 从助记词开始(全新用户)
```
import { fromMnemonic, derive } from 'nsec-tree'
const root = fromMnemonic('abandon abandon ... about')
const social = derive(root, 'social')
const commerce = derive(root, 'commerce')
console.log(social.npub) // npub1...
console.log(commerce.npub) // npub1... (different, unlinkable)
root.destroy()
```
### 从现有的 nsec 开始(现有用户)
```
import { fromNsec, derive } from 'nsec-tree/core' // no BIP deps
const root = fromNsec('nsec1...')
const throwaway = derive(root, 'throwaway', 42)
```
### 构建层级
```
import { fromMnemonic, deriveFromIdentity } from 'nsec-tree'
import { derivePersona } from 'nsec-tree/persona'
const root = fromMnemonic('abandon abandon ... about')
const work = derivePersona(root, 'work')
const companyA = deriveFromIdentity(work.identity, 'company:a')
const payroll = deriveFromIdentity(companyA, 'payroll')
const companyB = deriveFromIdentity(work.identity, 'company:b')
const ops = deriveFromIdentity(companyB, 'ops')
console.log(work.identity.npub) // master -> work
console.log(payroll.npub) // master -> work -> company:a -> payroll
console.log(ops.npub) // master -> work -> company:b -> ops
```
每一级都是确定且隔离的。泄露 `company:a -> payroll` 不会暴露兄弟分支 `company:b -> ops`。
### 证明所有权(关联证明)
```
import { createBlindProof, verifyProof } from 'nsec-tree/proof'
const proof = createBlindProof(root, child)
// Send proof to verifier...
const valid = verifyProof(proof) // true
```
### 将证明发布到 Nostr (NIP-78)
```
import { toUnsignedEvent } from 'nsec-tree/event'
const unsigned = toUnsignedEvent(proof)
// Sign with your Nostr library, then publish to relays
```
## API
### `fromMnemonic(mnemonic, passphrase?)`
从 BIP-39 助记词创建一个 `TreeRoot`。在 `m/44'/1237'/727'/0'/0'` 路径派生树根。
### `fromNsec(nsec)`
从 bech32 编码的 nsec 字符串或原始的 32 字节密钥创建一个 `TreeRoot`。中间的 HMAC 会将签名密钥与派生密钥分离。
### `derive(root, purpose, index?)`
从 `TreeRoot` 派生一个子 `Identity`。返回 `{ nsec, npub, privateKey, publicKey, purpose, index }`。`index` 默认为 `0`。
### `deriveFromIdentity(identity, purpose, index?)`
从任何现有的 `Identity` 派生子 `Identity`,支持任意深度的层级,例如 `work -> company:a -> payroll -> hot-wallet`。
### `recover(root, purposes, scanRange?)`
扫描多个用途和索引,返回 `Map`。默认扫描范围:20(BIP-44 间隙限制)。
### `zeroise(identity)`
将派生身份的私钥字节归零。
### `createBlindProof(root, child)`
BIP-340 Schnorr 证明,证明主密钥拥有该子密钥,但不泄露派生槽位。
### `createFullProof(root, child)`
类似盲证明,但同时会揭示用途和索引。
### `verifyProof(proof)`
验证 `LinkageProof`。返回 `boolean`。
### `toUnsignedEvent(proof)`
将 `LinkageProof` 转换为未签名的 NIP-78 Kind 30078 Nostr 事件。应用程序需对其进行签名并发布。
### `fromEvent(event)`
从 NIP-78 事件的标签中提取 `LinkageProof`。将结果传递给 `verifyProof()` 以检查其加密有效性。
## 子路径导出
| 导入项 | 内容 | 依赖 BIP? |
|--------|------|-----------|
| `nsec-tree` | 完整 API | 是 |
| `nsec-tree/core` | fromNsec, derive, recover, zeroise | 否 |
| `nsec-tree/mnemonic` | fromMnemonic | 是 |
| `nsec-tree/proof` | 关联证明 | 否 |
| `nsec-tree/persona` | 人设派生,二级层级结构,恢复 | 否 |
| `nsec-tree/event` | NIP-78 事件转换 (toUnsignedEvent, fromEvent) | 否 |
| `nsec-tree/encoding` | NIP-19 bech32 助手 (encodeNsec, decodeNsec, encodeNpub, decodeNpub) | 否 |
如果你只需要基于 nsec 的派生,请使用 `nsec-tree/core`——它可以避免引入 BIP-32/39 依赖。
## 工作原理
- **树根** 来自助记词(BIP-32 路径 `m/44'/1237'/727'/0'/0'`)或 nsec(中间 HMAC)
- **子密钥:** `HMAC-SHA256(tree_root, "nsec-tree\0" || purpose || "\0" || index_be32)`
- **关联证明:** 基于证明字符串的 BIP-340 Schnorr 签名
- **层级结构:** 任何派生的身份都可以通过 `deriveFromIdentity(...)` 成为子树的根
- 请参阅 `PROTOCOL.md` 获取包含测试向量的完整派生规范
## 人设 (Personas)
人设是一个使用 `nostr:persona:{name}` 约定从你的主密钥派生出的具名 Nostr 身份。每个人设都有自己的密钥对——适合作为独立的 kind-0 配置文件——并且默认情况下与其他人设不可关联。
### 派生人设
```
import { fromMnemonic } from 'nsec-tree'
import { derivePersona } from 'nsec-tree/persona'
const root = fromMnemonic('abandon abandon ... about')
const personal = derivePersona(root, 'personal')
const bitcoiner = derivePersona(root, 'bitcoiner')
const work = derivePersona(root, 'work')
console.log(personal.identity.npub) // npub1...
console.log(bitcoiner.identity.npub) // npub1... (different, unlinkable)
```
### 二级层级结构
`deriveFromPersona` 在人设之下创建子身份。这非常适合用于群组签名密钥——每个群组都获得一个从人设(而非主密钥)派生的隔离密钥对。
```
import { deriveFromPersona } from 'nsec-tree/persona'
const meetup = deriveFromPersona(bitcoiner, 'canary:group:local-meetup')
const conference = deriveFromPersona(bitcoiner, 'canary:group:btcpp-2026')
```
层级为:**master → persona → group identity**。泄露群组密钥不会暴露人设密钥,而泄露人设也不会暴露主密钥。
### 任意深度层级
`deriveFromPersona(...)` 只是一个便捷的辅助函数。更通用的 `deriveFromIdentity(...)` API 允许你根据需要进行无限深度的分支。
```
import { deriveFromIdentity } from 'nsec-tree'
const companyA = deriveFromIdentity(work.identity, 'company:a')
const payroll = deriveFromIdentity(companyA, 'payroll')
const hotWallet = deriveFromIdentity(payroll, 'hot-wallet')
const companyB = deriveFromIdentity(work.identity, 'company:b')
const ops = deriveFromIdentity(companyB, 'ops')
```
这会生成如下路径:
- `master -> work -> company:a -> payroll -> hot-wallet`
- `master -> work -> company:b -> ops`
这就是 nsec-tree 不仅仅是一个“多账户”库的地方。它让你能够直接在确定性密钥中建模真实的运营结构:人员、团队、客户、设备、环境或服务。一次备份即可重建整个树。
### 恢复
`recoverPersonas` 通过扫描已知名称列表,从助记词重新派生所有人设。未提供名称时,它将使用 `DEFAULT_PERSONA_NAMES`:
`personal`、`bitcoiner`、`work`、`social`、`anonymous`。
```
import { recoverPersonas, DEFAULT_PERSONA_NAMES } from 'nsec-tree/persona'
const root = fromMnemonic('abandon abandon ... about')
const recovered = recoverPersonas(root, DEFAULT_PERSONA_NAMES, 2)
for (const [name, personas] of recovered) {
console.log(`${name}: ${personas.length} indices scanned`)
}
```
恢复是确定性的——相同的助记词总会生成相同的人设。
你只需知道(或约定)要扫描的人设名称即可。
同样的原则也适用于更深的树:当分支名称和轮换约定保持稳定时,恢复非常容易。你使用的层级结构越多,命名规范就越重要。
### 轮换
如果某个人设被泄露,可以在更高的索引处派生它:
```
const bitcoinerV0 = derivePersona(root, 'bitcoiner', 0) // compromised
const bitcoinerV1 = derivePersona(root, 'bitcoiner', 1) // replacement
```
使用盲关联证明来证明连续性——新的人设由同一个主密钥控制——而不暴露使用了哪个派生槽位:
```
import { createBlindProof, verifyProof } from 'nsec-tree/proof'
const proof = createBlindProof(root, bitcoinerV1.identity)
verifyProof(proof) // true — same master, new identity
```
### 生态系统集成
nsec-tree 的人设旨在与其他库组合使用:
- **canary-kit** — `deriveFromPersona(persona, 'canary:group:...')` 生成 canary-kit 用于加密位置信标、紧急警报和存活检查的群组签名密钥。
- **spoken-token** — 将口头验证令牌绑定到人设的公钥,以便在语音通话中进行身份确认。
### 安全模型
不同级别的泄露会产生不同的影响范围:
| 被泄露内容 | 影响 | 缓解措施 |
|-------------|--------|------------|
| 群组密钥 | 暴露一个群组身份 | 轮换群组密钥(新 purpose 或 index) |
| 人设密钥 | 可派生该人设下的所有群组密钥 | 轮换人设(递增 index),发布关联证明 |
| 主密钥 | 可派生所有的人设和群组密钥 | 轮换助记词,迁移所有身份 |
二级层级结构确保群组密钥的泄露不会升级到人设,而人设的泄露也不会升级到主密钥。
## 示例
[`examples/`](examples/) 目录中包含可运行的示例:
| 示例 | 展示内容 |
|---------|---------------|
| [basic-derivation.ts](examples/basic-derivation.ts) | 派生 social + commerce 身份 |
| [existing-nsec.ts](examples/existing-nsec.ts) | 使用现有的 nsec,无需助记词 |
| [recovery.ts](examples/recovery.ts) | 从助记词恢复所有身份 |
| [linkage-proofs.ts](examples/linkage-proofs.ts) | 盲证明和完全所有权证明 |
| [bot-fleet.ts](examples/bot-fleet.ts) | 一个种子生成 10 个机器人 |
| [nostr-event-signing.ts](examples/nostr-event-signing.ts) | 使用 nostr-tools 签名 kind-1 事件 |
| [persona.ts](examples/persona.ts) | 人设派生、深层层级、轮换、恢复 |
运行任何示例:`npx tsx examples/.ts`
## 延伸阅读
- [常见问题](docs/faq.md) — 常见问题和异议
- [对比](docs/comparison.md) — nsec-tree 对比 NIP-06、NIP-26、已链接的子密钥
- [NIP 草案](docs/NIP-XXXX.md) — NIP 格式的正式规范
- [PROTOCOL.md](PROTOCOL.md) — 包含测试向量的完整派生规范
## 安全
- **零自定义加密** — HMAC-SHA256 (RFC 2104)、BIP-32、BIP-340 Schnorr。全部来自 @noble/@scure。
- **默认不可关联** — 仅通过关联证明进行选择性披露
- **数据清零** — 完成后调用 `root.destroy()` 和 `zeroise(identity)`。如果你忘记清理,`FinalizationRegistry` 会提供尽力而为的清理。
- **主密钥泄露** — 如果主密钥泄漏,所有子密钥都将是可派生的。请像保护任何 nsec 一样严密保护它。
- 完整威胁模型请参阅 `PROTOCOL.md`
## 许可证
MIT
如果你觉得 nsec-tree 有用,可以考虑打赏:
- **Lightning:** `thedonkey@strike.me`
- **Nostr zaps:** `npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`
标签:BIP39, CMS安全, ESM, GNU通用公共许可证, HD Wallet, JavaScript, MITM代理, NIP-06, Node.js, Nostr, npub, nsec, nsec-tree, TypeScript, Web3, 不可链接性, 分层确定性钱包, 助记词, 区块链, 匿名性, 去中心化身份, 子身份管理, 安全插件, 密码学, 密钥派生, 手动系统调用, 数字身份, 网络安全, 自动化攻击, 账户恢复, 身份隔离, 隐私保护, 零信任定制加密