@steve02081504/fount-p2p 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. package/wire/wait.mjs +127 -0
@@ -0,0 +1,67 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ import { canonicalStringify } from '../core/canonical_json.mjs'
4
+ import { merkleRoot } from '../dag/index.mjs'
5
+
6
+ import { sign, verify } from './crypto.mjs'
7
+
8
+ /**
9
+ * 为 checkpoint 载荷附加签名(签名字段不包含 `checkpoint_signature` 自身)。
10
+ * @param {object} payload 待签名载荷
11
+ * @param {Uint8Array} secretKey 32 字节种子私钥
12
+ * @returns {Promise<object>} 带 `checkpoint_signature` 的载荷
13
+ */
14
+ export async function signCheckpoint(payload, secretKey) {
15
+ const body = { ...payload }
16
+ delete body.checkpoint_signature
17
+ const messageBytes = Buffer.from(canonicalStringify(body), 'utf8')
18
+ const signature = await sign(messageBytes, secretKey)
19
+ return { ...payload, checkpoint_signature: Buffer.from(signature).toString('hex') }
20
+ }
21
+
22
+ /**
23
+ * 校验 `checkpoint_signature` 与载荷的一致性。
24
+ * @param {object} checkpoint 完整检查点对象
25
+ * @param {Uint8Array} ownerPublicKey 32 字节公钥
26
+ * @returns {Promise<boolean>} 合法为 true
27
+ */
28
+ export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
29
+ const raw = checkpoint.checkpoint_signature.trim()
30
+ if (!/^[\da-f]{128}$/iu.test(raw)) return false
31
+ const body = { ...checkpoint }
32
+ delete body.checkpoint_signature
33
+ const messageBytes = Buffer.from(canonicalStringify(body), 'utf8')
34
+ return verify(Buffer.from(raw, 'hex'), messageBytes, ownerPublicKey)
35
+ }
36
+
37
+ /**
38
+ * 判断 checkpoint 是否带合法 Ed25519 签名。
39
+ * @param {object | null | undefined} checkpoint checkpoint 对象
40
+ * @returns {boolean} 签名格式合法为 true
41
+ */
42
+ export function isSignedCheckpoint(checkpoint) {
43
+ return /^[\da-f]{128}$/iu.test(String(checkpoint?.checkpoint_signature || '').trim())
44
+ }
45
+
46
+ /**
47
+ * 校验远端 checkpoint 的结构、Merkle 根与 delegated owner 签名。
48
+ * @param {object | null | undefined} checkpoint checkpoint 对象
49
+ * @returns {Promise<{ valid: boolean, reason?: string }>} 校验结果;`valid` 为 false 时 `reason` 说明原因
50
+ */
51
+ export async function verifyRemoteCheckpoint(checkpoint) {
52
+ if (!checkpoint || typeof checkpoint !== 'object')
53
+ return { valid: false, reason: 'checkpoint missing or not an object' }
54
+ if (!Array.isArray(checkpoint.eventIdsInEpoch) || checkpoint.eventIdsInEpoch.length === 0)
55
+ return { valid: false, reason: 'eventIdsInEpoch missing or empty' }
56
+ const root = merkleRoot(checkpoint.eventIdsInEpoch)
57
+ if (checkpoint.epoch_root_hash !== root)
58
+ return { valid: false, reason: 'epoch_root_hash does not match Merkle root of eventIdsInEpoch' }
59
+ const ownerHash = checkpoint.members_record?.delegatedOwnerPubKeyHash
60
+ const owner = checkpoint.members_record?.members?.[ownerHash]
61
+ const pubHex = owner?.pubKeyHex
62
+ if (!pubHex || !/^[\da-f]{64}$/iu.test(pubHex))
63
+ return { valid: false, reason: 'delegated owner pubkey missing' }
64
+ if (!await verifyCheckpointSignature(checkpoint, Buffer.from(pubHex, 'hex')))
65
+ return { valid: false, reason: 'checkpoint signature invalid' }
66
+ return { valid: true }
67
+ }
@@ -0,0 +1,158 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import {
3
+ createHash,
4
+ createPrivateKey,
5
+ createPublicKey,
6
+ randomBytes,
7
+ sign as nodeSign,
8
+ verify as nodeVerify,
9
+ } from 'node:crypto'
10
+
11
+ /** 固定 DER 头(非秘密),让 Node 把 32 字节种子当成私钥 */
12
+ const PKCS8_RAW_SEED_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex')
13
+ /** 固定 DER 头(非秘密),让 Node 把 32 字节当成公钥 */
14
+ const SPKI_RAW_PUB_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
15
+
16
+ /**
17
+ * @param {Uint8Array|Buffer} seed32 32 字节私钥种子
18
+ * @returns {import('node:crypto').KeyObject} Node 私钥对象
19
+ */
20
+ function privateKeyFromSeed(seed32) {
21
+ return createPrivateKey({
22
+ key: Buffer.concat([PKCS8_RAW_SEED_PREFIX, Buffer.from(seed32).subarray(0, 32)]),
23
+ format: 'der',
24
+ type: 'pkcs8',
25
+ })
26
+ }
27
+
28
+ /**
29
+ * @param {Uint8Array|Buffer} rawPublicKey 32 字节公钥
30
+ * @returns {import('node:crypto').KeyObject} Node 公钥对象
31
+ */
32
+ function publicKeyFromRaw(rawPublicKey) {
33
+ return createPublicKey({
34
+ key: Buffer.concat([SPKI_RAW_PUB_PREFIX, Buffer.from(rawPublicKey).subarray(0, 32)]),
35
+ format: 'der',
36
+ type: 'spki',
37
+ })
38
+ }
39
+
40
+ /**
41
+ * @param {Uint8Array|Buffer} seed32 32 字节私钥种子
42
+ * @returns {Uint8Array} 32 字节公钥
43
+ */
44
+ function rawPublicKeyFromSeed(seed32) {
45
+ const der = createPublicKey(privateKeyFromSeed(seed32)).export({ type: 'spki', format: 'der' })
46
+ return new Uint8Array(der.subarray(-32))
47
+ }
48
+
49
+ /**
50
+ * 由种子字节派生密钥对
51
+ *
52
+ * @param {Uint8Array|Buffer} seed 任意长度;非 32 字节时 sha256 派生
53
+ * @returns {{ publicKey: Uint8Array, secretKey: Uint8Array }} 32 字节私钥与对应公钥
54
+ */
55
+ export function keyPairFromSeed(seed) {
56
+ const u = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
57
+ const sk = u.length === 32 ? u : new Uint8Array(createHash('sha256').update(u).digest())
58
+ return { publicKey: rawPublicKeyFromSeed(sk), secretKey: new Uint8Array(sk) }
59
+ }
60
+
61
+ /**
62
+ * 随机生成密钥对
63
+ *
64
+ * @returns {Promise<{ publicKey: Uint8Array, secretKey: Uint8Array }>} 随机密钥对
65
+ */
66
+ export async function randomKeyPair() {
67
+ const sk = randomBytes(32)
68
+ return keyPairFromSeed(sk)
69
+ }
70
+
71
+ /**
72
+ * 由 32 字节私钥种子导出对应公钥
73
+ * @param {Uint8Array} secretKey 私钥种子
74
+ * @returns {Uint8Array} 公钥
75
+ */
76
+ export function publicKeyFromSeed(secretKey) {
77
+ return rawPublicKeyFromSeed(secretKey)
78
+ }
79
+
80
+ /**
81
+ * 签名
82
+ *
83
+ * @param {Uint8Array} message 待签名消息字节
84
+ * @param {Uint8Array} secretKey 私钥(取前 32 字节为种子)
85
+ * @returns {Promise<Uint8Array>} 64 字节签名
86
+ */
87
+ export async function sign(message, secretKey) {
88
+ const sig = nodeSign(null, Buffer.from(message), privateKeyFromSeed(secretKey))
89
+ return new Uint8Array(sig)
90
+ }
91
+
92
+ /**
93
+ * 校验签名
94
+ *
95
+ * @param {Uint8Array} signature 64 字节签名
96
+ * @param {Uint8Array} message 原始消息字节
97
+ * @param {Uint8Array} publicKey 公钥
98
+ * @returns {Promise<boolean>} 合法为 true;异常或失败为 false
99
+ */
100
+ export async function verify(signature, message, publicKey) {
101
+ try {
102
+ return nodeVerify(null, Buffer.from(message), publicKeyFromRaw(publicKey), Buffer.from(signature))
103
+ }
104
+ catch {
105
+ return false
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 公钥的 sha256 十六进制指纹(小写、无 0x 前缀)
111
+ *
112
+ * @param {Uint8Array} publicKey 公钥字节
113
+ * @returns {string} 64 字符 hex
114
+ */
115
+ export function pubKeyHash(publicKey) {
116
+ return bufferToHexSimple(hashPubKeyBytes(publicKey))
117
+ }
118
+
119
+ /**
120
+ * 公钥 sha256 摘要
121
+ *
122
+ * @param {Uint8Array} publicKey 公钥字节
123
+ * @returns {Buffer} 32 字节 digest
124
+ */
125
+ function hashPubKeyBytes(publicKey) {
126
+ return createHash('sha256').update(publicKey).digest()
127
+ }
128
+
129
+ /**
130
+ * 任意输入的 SHA-256 十六进制(小写、无 0x 前缀)。
131
+ *
132
+ * @param {Uint8Array|Buffer|string} data 字节或 UTF-8 文本
133
+ * @returns {string} 64 字符 hex
134
+ */
135
+ export function sha256Hex(data) {
136
+ const input = typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data)
137
+ return createHash('sha256').update(input).digest('hex')
138
+ }
139
+
140
+ /**
141
+ * UTF-8 文本 SHA-256 十六进制(小写、无 0x 前缀)。
142
+ *
143
+ * @param {string} text 文本
144
+ * @returns {string} 64 字符 hex
145
+ */
146
+ export function sha256TextHex(text) {
147
+ return sha256Hex(String(text ?? ''))
148
+ }
149
+
150
+ /**
151
+ * Buffer / Uint8Array → 小写 hex 字符串
152
+ *
153
+ * @param {Uint8Array|Buffer} buf 二进制缓冲
154
+ * @returns {string} 小写十六进制文本
155
+ */
156
+ function bufferToHexSimple(buf) {
157
+ return Buffer.from(buf).toString('hex')
158
+ }
package/crypto/key.mjs ADDED
@@ -0,0 +1,431 @@
1
+ /**
2
+ * 群/实体主密钥 KDF 与 ECIES 封装(频道 K_ch、fileMasterKey、vault master key)。
3
+ */
4
+
5
+ import { Buffer } from 'node:buffer'
6
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'node:crypto'
7
+
8
+ import { x25519 } from '@noble/curves/ed25519.js'
9
+
10
+ import { createLruMap } from '../utils/lru.mjs'
11
+
12
+ // ─── KDF ──────────────────────────────────────────────────────────────────────
13
+
14
+ const KDF_CACHE_MAX = 512
15
+
16
+ const kdfCache = createLruMap(KDF_CACHE_MAX)
17
+
18
+ /** 清空 KDF 派生缓存(H 轮换后调用)。 */
19
+ export function clearMasterKeyKdfCache() {
20
+ kdfCache.clear()
21
+ }
22
+
23
+ /**
24
+ * @param {string | Buffer | Uint8Array} H 群秘密
25
+ * @param {string} label 用途标签
26
+ * @param {string} id 上下文 id
27
+ * @returns {string} 缓存键
28
+ */
29
+ function kdfCacheKey(H, label, id) {
30
+ const hHex = typeof H === 'string' ? H : Buffer.from(H).toString('hex')
31
+ return `${hHex}:${label}:${id}`
32
+ }
33
+
34
+ /**
35
+ * HMAC-SHA256(key=H, data=label + "\x00" + id) → 32 字节 AES-256 密钥
36
+ * @param {Buffer} H 32 字节群秘密
37
+ * @param {string} label 用途标签("broadcast" / "file" / "dm")
38
+ * @param {string} id channelId / fileId 等
39
+ * @returns {Buffer} 32 字节派生密钥
40
+ */
41
+ function kdf(H, label, id) {
42
+ const cacheKey = kdfCacheKey(H, label, id)
43
+ const cached = kdfCache.get(cacheKey)
44
+ if (cached) {
45
+ kdfCache.touch(cacheKey, cached)
46
+ return Buffer.from(cached)
47
+ }
48
+ const derived = createHmac('sha256', H)
49
+ .update(label)
50
+ .update('\x00')
51
+ .update(id)
52
+ .digest()
53
+ kdfCache.touch(cacheKey, Buffer.from(derived))
54
+ return derived
55
+ }
56
+
57
+ /**
58
+ * 从十六进制 H 字符串或 Buffer 中安全取出 Buffer。
59
+ * @param {string | Buffer | Uint8Array} H 群秘密(hex 字符串或字节)
60
+ * @returns {Buffer} 32 字节 Buffer
61
+ */
62
+ function toHBuf(H) {
63
+ if (typeof H === 'string') return Buffer.from(H, 'hex')
64
+ return Buffer.from(H)
65
+ }
66
+
67
+ // ─── 密钥推导 ─────────────────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * 推导群内两两直连额外隔离密钥:`KDF(H, "dm", sorted(a,b).join(":"))`(§11.1)
71
+ * @param {string | Buffer} H 群秘密
72
+ * @param {string} pubKeyHashA 第一方 pubKeyHash
73
+ * @param {string} pubKeyHashB 第二方 pubKeyHash
74
+ * @returns {Buffer} 32 字节 AES-256 密钥
75
+ */
76
+ export function derivePairKey(H, pubKeyHashA, pubKeyHashB) {
77
+ const sorted = [pubKeyHashA, pubKeyHashB].sort().join(':')
78
+ return kdf(toHBuf(H), 'dm', sorted)
79
+ }
80
+
81
+ /**
82
+ * 推导群文件加密密钥:`KDF(H, "file", fileId)`(§10.3、§6.3)
83
+ * @param {string | Buffer} H 群秘密
84
+ * @param {string} fileId 文件 ID
85
+ * @returns {Buffer} 32 字节 AES-256 密钥
86
+ */
87
+ export function deriveFileKey(H, fileId) {
88
+ return kdf(toHBuf(H), 'file', String(fileId))
89
+ }
90
+
91
+ /**
92
+ * 推导 social 帖子加密密钥:`KDF(H, "post", postId)`(social shell §6)
93
+ * @param {string | Buffer} H vault 秘密
94
+ * @param {string} postId 帖子 ID
95
+ * @returns {Buffer} 32 字节 AES-256 密钥
96
+ */
97
+ export function deriveSocialPostKey(H, postId) {
98
+ return kdf(toHBuf(H), 'post', String(postId))
99
+ }
100
+
101
+ /**
102
+ * 推导流媒体观看令牌 HMAC 密钥:`KDF(H, "streaming", groupId)`(与群密钥同步轮换)。
103
+ * @param {string | Buffer} H 群秘密
104
+ * @param {string} groupId 群 ID
105
+ * @returns {Buffer} 32 字节 HMAC 密钥
106
+ */
107
+ export function deriveStreamingAuthKey(H, groupId) {
108
+ return kdf(toHBuf(H), 'streaming', String(groupId))
109
+ }
110
+
111
+ // ─── H 轮换(§11.2、§6.3 key_rotate)──────────────────────────────────────
112
+
113
+ /**
114
+ * 踢人/主动轮换后推导新 fileMasterKey:`K_new = SHA256(K_old || eventId || nonce)`
115
+ *
116
+ * @param {string} oldKeyHex 旧密钥(十六进制)
117
+ * @param {string} eventId 踢人/轮换事件 ID(签名后的 SHA256 hex)
118
+ * @param {string} nonce `new_key_nonce` 字段(字符串)
119
+ * @returns {string} 新密钥(十六进制)
120
+ */
121
+ export function deriveNextFileMasterKey(oldKeyHex, eventId, nonce) {
122
+ return createHash('sha256')
123
+ .update(Buffer.from(oldKeyHex, 'hex'))
124
+ .update(String(eventId))
125
+ .update(String(nonce))
126
+ .digest('hex')
127
+ }
128
+
129
+ /**
130
+ * 生成随机 32 字节 fileMasterKey(十六进制)。
131
+ * @returns {string} 随机密钥 hex
132
+ */
133
+ export function generateFileMasterKey() {
134
+ return randomBytes(32).toString('hex')
135
+ }
136
+
137
+ /**
138
+ * 生成随机 `new_key_nonce`,用于踢人事件或 key_rotate 事件。
139
+ * @returns {string} 32 字节 hex 随机 nonce
140
+ */
141
+ export function generateKeyRotationNonce() {
142
+ return randomBytes(32).toString('hex')
143
+ }
144
+
145
+ // ─── Ed25519 → X25519 互转(内部用)────────────────────────────────────────
146
+
147
+ const P25519 = (1n << 255n) - 19n
148
+
149
+ /**
150
+ * 费马小定理求模逆元(要求模数为素数)。
151
+ * @param {bigint} a 被求逆的整数
152
+ * @param {bigint} m 模数(必须为素数)
153
+ * @returns {bigint} a 在模 m 下的逆元
154
+ */
155
+ function modInv(a, m) {
156
+ let result = 1n
157
+ let base = ((a % m) + m) % m
158
+ let exp = m - 2n
159
+ while (exp > 0n) {
160
+ if (exp & 1n) result = result * base % m
161
+ base = base * base % m
162
+ exp >>= 1n
163
+ }
164
+ return result
165
+ }
166
+
167
+ /**
168
+ * Ed25519 公钥(Edwards y 坐标)→ X25519 公钥(Montgomery u 坐标)。
169
+ * @param {Uint8Array} edPub Ed25519 公钥(32 字节,Edwards 曲线 y 坐标,小端序)
170
+ * @returns {Uint8Array} 32 字节 X25519 公钥(Montgomery u 坐标,小端序)
171
+ */
172
+ function edPubToX25519(edPub) {
173
+ const yCopy = new Uint8Array(edPub)
174
+ yCopy[31] &= 0x7f
175
+ let y = 0n
176
+ for (let i = 31; i >= 0; i--) y = (y << 8n) | BigInt(yCopy[i])
177
+ const u = (1n + y) * modInv(1n - y + P25519, P25519) % P25519
178
+ const result = new Uint8Array(32)
179
+ let tmp = u
180
+ for (let i = 0; i < 32; i++) { result[i] = Number(tmp & 0xffn); tmp >>= 8n }
181
+ return result
182
+ }
183
+
184
+ /**
185
+ * Ed25519 私钥种子(32 字节)→ X25519 私钥(RFC 7748 §4.1)。
186
+ * @param {Uint8Array} seed Ed25519 私钥种子(32 字节)
187
+ * @returns {Uint8Array} X25519 私钥(32 字节,已 clamp)
188
+ */
189
+ function edPrivToX25519(seed) {
190
+ const hash = createHash('sha512').update(seed).digest()
191
+ const key = new Uint8Array(32)
192
+ for (let i = 0; i < 32; i++) key[i] = hash[i]
193
+ key[0] &= 248; key[31] &= 127; key[31] |= 64
194
+ return key
195
+ }
196
+
197
+ // ─── ECIES 密钥包装(§11.1 peer_invite、频道/文件主密钥分发)────────────────
198
+
199
+ /**
200
+ * X25519 ECIES 包装任意 32 字节 hex 密钥给成员 Ed25519 公钥。
201
+ * @param {string} keyHex 32 字节 hex 密钥
202
+ * @param {string} recipientEdPubKeyHex 64 位十六进制 Ed25519 公钥
203
+ * @returns {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} ECIES 包装
204
+ */
205
+ export function wrapKeyEcies(keyHex, recipientEdPubKeyHex) {
206
+ const memberX25519Pub = edPubToX25519(Buffer.from(recipientEdPubKeyHex, 'hex'))
207
+ const ephemPriv = x25519.utils.randomSecretKey()
208
+ const ephemPub = x25519.getPublicKey(ephemPriv)
209
+ const sharedSecret = x25519.getSharedSecret(ephemPriv, memberX25519Pub)
210
+ const wrapKey = createHash('sha256').update(sharedSecret).digest()
211
+ const iv = randomBytes(12)
212
+ const cipher = createCipheriv('aes-256-gcm', wrapKey, iv)
213
+ const H_bytes = Buffer.from(keyHex, 'hex')
214
+ const ciphertext = Buffer.concat([cipher.update(H_bytes), cipher.final()])
215
+ const authTag = cipher.getAuthTag()
216
+ return {
217
+ ephemPub: Buffer.from(ephemPub).toString('base64'),
218
+ iv: iv.toString('base64'),
219
+ ciphertext: ciphertext.toString('base64'),
220
+ authTag: authTag.toString('base64'),
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 用本节点 Ed25519 私钥种子解密 ECIES 包装的 32 字节 hex 密钥。
226
+ * @param {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} encryptedH ECIES 包装
227
+ * @param {Uint8Array} myEdPrivKeySeed 32 字节私钥种子
228
+ * @returns {string | null} 解密后的密钥 hex;失败为 null
229
+ */
230
+ export function unwrapKeyEcies(encryptedH, myEdPrivKeySeed) {
231
+ try {
232
+ const myX25519Priv = edPrivToX25519(myEdPrivKeySeed)
233
+ const ephemPub = Buffer.from(encryptedH.ephemPub, 'base64')
234
+ const sharedSecret = x25519.getSharedSecret(myX25519Priv, ephemPub)
235
+ const wrapKey = createHash('sha256').update(sharedSecret).digest()
236
+ const iv = Buffer.from(encryptedH.iv, 'base64')
237
+ const ciphertext = Buffer.from(encryptedH.ciphertext, 'base64')
238
+ const authTag = Buffer.from(encryptedH.authTag, 'base64')
239
+ const decipher = createDecipheriv('aes-256-gcm', wrapKey, iv)
240
+ decipher.setAuthTag(authTag)
241
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('hex')
242
+ }
243
+ catch { return null }
244
+ }
245
+
246
+ /**
247
+ * ECIES 封装任意 UTF-8 载荷(联邦房间凭证 bootstrap 等)。
248
+ * @param {string} utf8Text 明文
249
+ * @param {string} memberEdPubKeyHex 成员 Ed25519 公钥 hex
250
+ * @returns {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} ECIES 加密结果
251
+ */
252
+ export function encryptUtf8ForMember(utf8Text, memberEdPubKeyHex) {
253
+ const memberX25519Pub = edPubToX25519(Buffer.from(memberEdPubKeyHex, 'hex'))
254
+ const ephemPriv = x25519.utils.randomSecretKey()
255
+ const ephemPub = x25519.getPublicKey(ephemPriv)
256
+ const sharedSecret = x25519.getSharedSecret(ephemPriv, memberX25519Pub)
257
+ const wrapKey = createHash('sha256').update(sharedSecret).digest()
258
+ const iv = randomBytes(12)
259
+ const cipher = createCipheriv('aes-256-gcm', wrapKey, iv)
260
+ const plain = Buffer.from(String(utf8Text), 'utf8')
261
+ const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
262
+ const authTag = cipher.getAuthTag()
263
+ return {
264
+ ephemPub: Buffer.from(ephemPub).toString('base64'),
265
+ iv: iv.toString('base64'),
266
+ ciphertext: ciphertext.toString('base64'),
267
+ authTag: authTag.toString('base64'),
268
+ }
269
+ }
270
+
271
+ /**
272
+ * @param {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} encrypted 密文对象
273
+ * @param {Uint8Array} myEdPrivKeySeed 本机私钥种子
274
+ * @returns {string | null} UTF-8 明文
275
+ */
276
+ export function decryptUtf8ForMember(encrypted, myEdPrivKeySeed) {
277
+ try {
278
+ const myX25519Priv = edPrivToX25519(myEdPrivKeySeed)
279
+ const ephemPub = Buffer.from(encrypted.ephemPub, 'base64')
280
+ const sharedSecret = x25519.getSharedSecret(myX25519Priv, ephemPub)
281
+ const wrapKey = createHash('sha256').update(sharedSecret).digest()
282
+ const iv = Buffer.from(encrypted.iv, 'base64')
283
+ const ciphertext = Buffer.from(encrypted.ciphertext, 'base64')
284
+ const authTag = Buffer.from(encrypted.authTag, 'base64')
285
+ const decipher = createDecipheriv('aes-256-gcm', wrapKey, iv)
286
+ decipher.setAuthTag(authTag)
287
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
288
+ }
289
+ catch { return null }
290
+ }
291
+
292
+ // ─── 收敛内容加密(§10.3 两层方案)──────────────────────────────────────────
293
+
294
+ /**
295
+ * `contentKey = KDF(SHA256(plaintext), "ce")` 的 KDF 输入为 contentHash 字节。
296
+ * @param {string} contentHashHex 明文 SHA-256(hex)
297
+ * @returns {Buffer} 32 字节 contentKey
298
+ */
299
+ export function deriveContentKey(contentHashHex) {
300
+ return createHmac('sha256', Buffer.from(contentHashHex, 'hex'))
301
+ .update('ce')
302
+ .update('\x00')
303
+ .digest()
304
+ }
305
+
306
+ /**
307
+ * 收敛加密:同明文 → 同密文(IV 由 contentHash 前 12 字节派生)。
308
+ * @param {Buffer | Uint8Array} plaintext 明文字节
309
+ * @returns {{ contentHash: string, ciphertextHash: string, raw: Buffer }} 哈希与密文原始字节
310
+ */
311
+ export function encryptConvergentPlaintext(plaintext) {
312
+ const plain = Buffer.from(plaintext)
313
+ const contentHash = createHash('sha256').update(plain).digest('hex')
314
+ const contentKey = deriveContentKey(contentHash)
315
+ const iv = Buffer.from(contentHash, 'hex').subarray(0, 12)
316
+ const cipher = createCipheriv('aes-256-gcm', contentKey, iv)
317
+ const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
318
+ const authTag = cipher.getAuthTag()
319
+ const raw = Buffer.concat([iv, authTag, ciphertext])
320
+ const ciphertextHash = createHash('sha256').update(raw).digest('hex')
321
+ return { contentHash, ciphertextHash, raw }
322
+ }
323
+
324
+ /**
325
+ * 高隐私模式:随机 contentKey + 随机 IV(放弃跨文件 dedup)。
326
+ * @param {Buffer | Uint8Array} plaintext 明文字节
327
+ * @returns {{ contentHash: string, ciphertextHash: string, contentKey: Buffer, raw: Buffer }} 哈希、随机密钥与密文
328
+ */
329
+ export function encryptRandomPlaintext(plaintext) {
330
+ const plain = Buffer.from(plaintext)
331
+ const contentHash = createHash('sha256').update(plain).digest('hex')
332
+ const contentKey = randomBytes(32)
333
+ const iv = randomBytes(12)
334
+ const cipher = createCipheriv('aes-256-gcm', contentKey, iv)
335
+ const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
336
+ const authTag = cipher.getAuthTag()
337
+ const raw = Buffer.concat([iv, authTag, ciphertext])
338
+ const ciphertextHash = createHash('sha256').update(raw).digest('hex')
339
+ return { contentHash, ciphertextHash, contentKey, raw }
340
+ }
341
+
342
+ /**
343
+ * 解密收敛密文块(`raw` = iv(12) || authTag(16) || ciphertext)。
344
+ * @param {Buffer | Uint8Array} raw 磁盘上的密文块
345
+ * @param {string} contentHashHex 期望的明文哈希(校验用)
346
+ * @returns {Buffer | null} 明文;校验失败为 null
347
+ */
348
+ export function decryptConvergentCiphertext(raw, contentHashHex) {
349
+ try {
350
+ const buf = Buffer.from(raw)
351
+ if (buf.length < 28) return null
352
+ const iv = buf.subarray(0, 12)
353
+ const authTag = buf.subarray(12, 28)
354
+ const ciphertext = buf.subarray(28)
355
+ const contentKey = deriveContentKey(contentHashHex)
356
+ const decipher = createDecipheriv('aes-256-gcm', contentKey, iv)
357
+ decipher.setAuthTag(authTag)
358
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()])
359
+ const check = createHash('sha256').update(plain).digest('hex')
360
+ if (check !== contentHashHex.toLowerCase()) return null
361
+ return plain
362
+ }
363
+ catch { return null }
364
+ }
365
+
366
+ /**
367
+ * 用随机 contentKey 解密密文块(`raw` = iv(12) || authTag(16) || ciphertext)。
368
+ * @param {Buffer | Uint8Array} raw 磁盘上的密文块
369
+ * @param {Buffer | Uint8Array} contentKey 32 字节随机 contentKey
370
+ * @param {string} [contentHashHex] 可选明文哈希(用于完整性校验)
371
+ * @returns {Buffer | null} 明文;校验失败返回 null
372
+ */
373
+ export function decryptRandomCiphertext(raw, contentKey, contentHashHex = '') {
374
+ try {
375
+ const buf = Buffer.from(raw)
376
+ if (buf.length < 28) return null
377
+ const iv = buf.subarray(0, 12)
378
+ const authTag = buf.subarray(12, 28)
379
+ const ciphertext = buf.subarray(28)
380
+ const decipher = createDecipheriv('aes-256-gcm', Buffer.from(contentKey), iv)
381
+ decipher.setAuthTag(authTag)
382
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()])
383
+ const expect = String(contentHashHex || '').trim().toLowerCase()
384
+ if (expect) {
385
+ const check = createHash('sha256').update(plain).digest('hex')
386
+ if (check !== expect) return null
387
+ }
388
+ return plain
389
+ }
390
+ catch { return null }
391
+ }
392
+
393
+ /**
394
+ * 用 `KDF(H,"file",fileId)` 包裹 contentKey(§10.3 wrappedKey)。
395
+ * @param {Buffer} contentKey 32 字节
396
+ * @param {string | Buffer} H 群秘密
397
+ * @param {string} fileId 文件 ID
398
+ * @returns {{ iv: string, ciphertext: string, authTag: string }} 可 JSON 序列化的包裹密钥
399
+ */
400
+ export function wrapContentKey(contentKey, H, fileId) {
401
+ const wrapKey = deriveFileKey(H, fileId)
402
+ const iv = randomBytes(12)
403
+ const cipher = createCipheriv('aes-256-gcm', wrapKey, iv)
404
+ const ciphertext = Buffer.concat([cipher.update(contentKey), cipher.final()])
405
+ const authTag = cipher.getAuthTag()
406
+ return {
407
+ iv: iv.toString('base64'),
408
+ ciphertext: ciphertext.toString('base64'),
409
+ authTag: authTag.toString('base64'),
410
+ }
411
+ }
412
+
413
+ /**
414
+ * 解开 wrappedKey 得到 contentKey。
415
+ * @param {{ iv: string, ciphertext: string, authTag: string }} wrapped 包裹结构
416
+ * @param {string | Buffer} H 群秘密
417
+ * @param {string} fileId 文件 ID
418
+ * @returns {Buffer | null} 32 字节 contentKey
419
+ */
420
+ export function unwrapContentKey(wrapped, H, fileId) {
421
+ try {
422
+ const wrapKey = deriveFileKey(H, fileId)
423
+ const iv = Buffer.from(wrapped.iv, 'base64')
424
+ const ciphertext = Buffer.from(wrapped.ciphertext, 'base64')
425
+ const authTag = Buffer.from(wrapped.authTag, 'base64')
426
+ const decipher = createDecipheriv('aes-256-gcm', wrapKey, iv)
427
+ decipher.setAuthTag(authTag)
428
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()])
429
+ }
430
+ catch { return null }
431
+ }