@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.
- package/README.md +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- 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
|
+
}
|