@steve02081504/fount-p2p 0.0.4 → 0.0.5
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/core/bytes_codec.mjs +0 -27
- package/core/constants.mjs +0 -31
- package/core/hexIds.mjs +0 -13
- package/crypto/key.mjs +14 -15
- package/dag/index.mjs +0 -73
- package/dag/storage.mjs +0 -22
- package/federation/entity_key_chain.mjs +0 -9
- package/federation/topo_order_memo.mjs +0 -15
- package/files/assemble.mjs +55 -39
- package/files/assemble_stream.mjs +96 -44
- package/files/chunk_provider_registry.mjs +0 -6
- package/files/chunk_store.mjs +1 -11
- package/files/evfs.mjs +64 -37
- package/files/manifest.mjs +11 -2
- package/files/manifest_acl_registry.mjs +0 -6
- package/files/transfer_key.mjs +12 -6
- package/files/transfer_key_registry.mjs +0 -7
- package/governance/branch.mjs +0 -11
- package/link/channel_mux.mjs +2 -0
- package/mailbox/importance.mjs +0 -33
- package/mailbox/settings.mjs +0 -9
- package/node/denylist.mjs +0 -24
- package/node/network.mjs +0 -51
- package/node/personal_block.mjs +0 -35
- package/node/reputation_store.mjs +0 -9
- package/node/retention_policy.mjs +0 -15
- package/package.json +1 -1
- package/registries/event_type.mjs +0 -12
- package/registries/inbound.mjs +0 -22
- package/reputation/engine.mjs +0 -11
- package/schemas/part_query.mjs +156 -0
- package/transport/ice_servers.mjs +0 -8
- package/transport/peer_identity_maps.mjs +0 -58
- package/transport/remote_user_room.mjs +0 -12
- package/transport/rtc_connection_budget.mjs +2 -0
- package/transport/user_room.mjs +2 -11
- package/trust_graph/cache.mjs +0 -13
- package/trust_graph/engine.mjs +0 -24
- package/utils/async_mutex.mjs +0 -9
- package/wire/part_fanout.mjs +0 -19
- package/wire/part_query.mjs +487 -0
- package/wire/part_query.tunables.json +14 -0
- package/wire/part_query_cache.mjs +94 -0
package/core/bytes_codec.mjs
CHANGED
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
* Base64 / hex / bytes 互转(无 Node 依赖,浏览器与 Node 共用)。
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @param {ArrayBuffer} buffer 原始二进制缓冲
|
|
7
|
-
* @returns {string} 标准 Base64 文本
|
|
8
|
-
*/
|
|
9
|
-
export function arrayBufferToBase64(buffer) {
|
|
10
|
-
let binary = ''
|
|
11
|
-
const bytes = new Uint8Array(buffer)
|
|
12
|
-
for (let index = 0; index < bytes.byteLength; index++)
|
|
13
|
-
binary += String.fromCharCode(bytes[index])
|
|
14
|
-
return btoa(binary)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
5
|
/**
|
|
18
6
|
* @param {Uint8Array} u8 原始字节
|
|
19
7
|
* @returns {string} 标准 Base64 文本
|
|
@@ -34,18 +22,3 @@ export function b64ToU8(b64) {
|
|
|
34
22
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
35
23
|
return out
|
|
36
24
|
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {string} hex 十六进制文本(可带 0x 前缀,长度须为 2×nBytes)
|
|
40
|
-
* @param {number} nBytes 目标字节数
|
|
41
|
-
* @returns {Uint8Array} 解析得到的字节数组
|
|
42
|
-
*/
|
|
43
|
-
export function hexToNBytes(hex, nBytes) {
|
|
44
|
-
const normalizedHex = String(hex).replace(/^0x/iu, '').trim()
|
|
45
|
-
if (normalizedHex.length !== nBytes * 2 || !/^[\da-f]+$/iu.test(normalizedHex))
|
|
46
|
-
throw new RangeError('invalid hex length or characters')
|
|
47
|
-
const out = new Uint8Array(nBytes)
|
|
48
|
-
for (let index = 0; index < nBytes; index++)
|
|
49
|
-
out[index] = parseInt(normalizedHex.slice(index * 2, index * 2 + 2), 16)
|
|
50
|
-
return out
|
|
51
|
-
}
|
package/core/constants.mjs
CHANGED
|
@@ -1,33 +1,5 @@
|
|
|
1
|
-
import { resolveFederationFanoutTopK } from '../trust_graph/resolve.mjs'
|
|
2
1
|
import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
|
|
3
2
|
|
|
4
|
-
/** @type {readonly string[]} 权限键注册表顺序(运算期 BigInt 编码用) */
|
|
5
|
-
export const PERMISSION_REGISTRY_ORDER = Object.freeze([
|
|
6
|
-
'VIEW_CHANNEL',
|
|
7
|
-
'SEND_MESSAGES',
|
|
8
|
-
'SEND_STICKERS',
|
|
9
|
-
'ADD_REACTIONS',
|
|
10
|
-
'MANAGE_MESSAGES',
|
|
11
|
-
'MANAGE_CHANNELS',
|
|
12
|
-
'KICK_MEMBERS',
|
|
13
|
-
'BAN_MEMBERS',
|
|
14
|
-
'MANAGE_ROLES',
|
|
15
|
-
'MANAGE_ADMINS',
|
|
16
|
-
'INVITE_MEMBERS',
|
|
17
|
-
'STREAM',
|
|
18
|
-
'CREATE_THREADS',
|
|
19
|
-
'UPLOAD_FILES',
|
|
20
|
-
'MANAGE_FILES',
|
|
21
|
-
'PIN_MESSAGES',
|
|
22
|
-
'SET_WORLD',
|
|
23
|
-
'ADMIN',
|
|
24
|
-
'BYPASS_RATE_LIMIT',
|
|
25
|
-
])
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 默认晚消息冻结时间(毫秒)
|
|
29
|
-
*/
|
|
30
|
-
export const DEFAULT_LATE_MESSAGE_FREEZE_MS = 30_000
|
|
31
3
|
/**
|
|
32
4
|
* 占位 `message` 无 `message_edit` 终稿时的空闲截断(毫秒);§6.4 `streamGeneratingIdleMs` 默认。
|
|
33
5
|
*/
|
|
@@ -46,8 +18,5 @@ export const EPOCH_CHAIN_MAX = 256
|
|
|
46
18
|
/** 群文件经联邦复制的单块上限(字节,§10.2) */
|
|
47
19
|
export const FEDERATION_CHUNK_MAX_BYTES = trustGraphTunables.federationChunkMaxBytes
|
|
48
20
|
|
|
49
|
-
/** TrustGraph fanout 参考 Top-K(N=8 roster 时的缩放值;运行时请用 resolveFederationFanoutTopK) */
|
|
50
|
-
export const FEDERATION_FANOUT_TOP_K = resolveFederationFanoutTopK(8, trustGraphTunables)
|
|
51
|
-
|
|
52
21
|
/** 全局 fed_chunk_get miss 时 fanout 邻居数 */
|
|
53
22
|
export const FEDERATION_CHUNK_FETCH_FANOUT_K = trustGraphTunables.federationChunkFetchFanoutK
|
package/core/hexIds.mjs
CHANGED
|
@@ -58,16 +58,3 @@ export function assertHex64(value, label = 'hex64') {
|
|
|
58
58
|
export function isSignatureHex128(value) {
|
|
59
59
|
return SIGNATURE_HEX_128.test(String(value ?? '').trim())
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 外部入站专用:断言 128 位签名 hex。
|
|
64
|
-
* @param {unknown} value 原始值
|
|
65
|
-
* @param {string} [label='signature'] 字段名
|
|
66
|
-
* @returns {string} 签名 hex
|
|
67
|
-
*/
|
|
68
|
-
export function assertSignatureHex128(value, label = 'signature') {
|
|
69
|
-
const normalized = String(value ?? '').trim().toLowerCase()
|
|
70
|
-
if (!SIGNATURE_HEX_128.test(normalized))
|
|
71
|
-
throw new Error(`${label} must be 128 hex characters`)
|
|
72
|
-
return normalized
|
|
73
|
-
}
|
package/crypto/key.mjs
CHANGED
|
@@ -66,18 +66,6 @@ function toHBuf(H) {
|
|
|
66
66
|
|
|
67
67
|
// ─── 密钥推导 ─────────────────────────────────────────────────────────────────
|
|
68
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
69
|
/**
|
|
82
70
|
* 推导群文件加密密钥:`KDF(H, "file", fileId)`(§10.3、§6.3)
|
|
83
71
|
* @param {string | Buffer} H 群秘密
|
|
@@ -327,16 +315,27 @@ export function encryptConvergentPlaintext(plaintext) {
|
|
|
327
315
|
* @returns {{ contentHash: string, ciphertextHash: string, contentKey: Buffer, raw: Buffer }} 哈希、随机密钥与密文
|
|
328
316
|
*/
|
|
329
317
|
export function encryptRandomPlaintext(plaintext) {
|
|
318
|
+
const contentKey = randomBytes(32)
|
|
319
|
+
const enc = encryptRandomPlaintextWithKey(plaintext, contentKey)
|
|
320
|
+
return { ...enc, contentKey }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 用已有 contentKey 加密一块明文(多块 random 文件共用一把 key)。
|
|
325
|
+
* @param {Buffer | Uint8Array} plaintext 明文字节
|
|
326
|
+
* @param {Buffer | Uint8Array} contentKey 32 字节
|
|
327
|
+
* @returns {{ contentHash: string, ciphertextHash: string, raw: Buffer }} 哈希与密文
|
|
328
|
+
*/
|
|
329
|
+
export function encryptRandomPlaintextWithKey(plaintext, contentKey) {
|
|
330
330
|
const plain = Buffer.from(plaintext)
|
|
331
331
|
const contentHash = createHash('sha256').update(plain).digest('hex')
|
|
332
|
-
const contentKey = randomBytes(32)
|
|
333
332
|
const iv = randomBytes(12)
|
|
334
|
-
const cipher = createCipheriv('aes-256-gcm', contentKey, iv)
|
|
333
|
+
const cipher = createCipheriv('aes-256-gcm', Buffer.from(contentKey), iv)
|
|
335
334
|
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
|
|
336
335
|
const authTag = cipher.getAuthTag()
|
|
337
336
|
const raw = Buffer.concat([iv, authTag, ciphertext])
|
|
338
337
|
const ciphertextHash = createHash('sha256').update(raw).digest('hex')
|
|
339
|
-
return { contentHash, ciphertextHash,
|
|
338
|
+
return { contentHash, ciphertextHash, raw }
|
|
340
339
|
}
|
|
341
340
|
|
|
342
341
|
/**
|
package/dag/index.mjs
CHANGED
|
@@ -4,15 +4,12 @@ import { createHash } from 'node:crypto'
|
|
|
4
4
|
|
|
5
5
|
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
6
6
|
import { HEX_ID_64 } from '../core/hexIds.mjs'
|
|
7
|
-
import { HLC } from '../core/hlc.mjs'
|
|
8
|
-
import { sign, verify } from '../crypto/crypto.mjs'
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* 重导出 64 位十六进制事件 ID 正则(`HEX_ID_64` 别名)。
|
|
12
10
|
*/
|
|
13
11
|
export { HEX_ID_64 as EVENT_ID_HEX }
|
|
14
12
|
const EVENT_ID_HEX = HEX_ID_64
|
|
15
|
-
const HEX_PAIR = /^[\da-f]{2}$/iu
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
15
|
* 对事件 id 列表做二叉 Merkle 根(字典序叶子,§7 checkpoint)。
|
|
@@ -241,76 +238,6 @@ export function topologicalCanonicalOrder(metas) {
|
|
|
241
238
|
return ordered
|
|
242
239
|
}
|
|
243
240
|
|
|
244
|
-
/**
|
|
245
|
-
* @param {string} hex 十六进制串
|
|
246
|
-
* @param {number} expectedByteLength 期望字节长度
|
|
247
|
-
* @returns {Uint8Array | null} 解析后的字节
|
|
248
|
-
*/
|
|
249
|
-
function parseHexBytes(hex, expectedByteLength) {
|
|
250
|
-
if (!hex || hex.length !== expectedByteLength * 2 || hex.length % 2 !== 0) return null
|
|
251
|
-
const bytes = hex.match(/.{2}/g)
|
|
252
|
-
if (!bytes?.every(pair => HEX_PAIR.test(pair))) return null
|
|
253
|
-
return new Uint8Array(bytes.map(byte => Number.parseInt(byte, 16)))
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* 签名事件(签名为不含 id 的 canonical body)
|
|
258
|
-
* @param {object} event 含完整字段;可含占位 id
|
|
259
|
-
* @param {Uint8Array} privateKey 私钥
|
|
260
|
-
* @returns {Promise<string>} 十六进制签名
|
|
261
|
-
*/
|
|
262
|
-
export async function signEvent(event, privateKey) {
|
|
263
|
-
const body = eventBodyForSign(event)
|
|
264
|
-
const signature = await sign(signPayloadBytes(body), privateKey)
|
|
265
|
-
return Array.from(signature).map(byte => byte.toString(16).padStart(2, '0')).join('')
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* 验证事件签名
|
|
270
|
-
* @param {object} event 含 signature、sender 与正文字段的完整事件
|
|
271
|
-
* @returns {Promise<boolean>} 验签是否通过
|
|
272
|
-
*/
|
|
273
|
-
export async function verifyEventSignature(event) {
|
|
274
|
-
try {
|
|
275
|
-
const signature = parseHexBytes(event?.signature, 64)
|
|
276
|
-
const publicKey = parseHexBytes(event?.sender, 32)
|
|
277
|
-
if (!signature || !publicKey) return false
|
|
278
|
-
return await verify(signature, signPayloadBytes(eventBodyForSign(event)), publicKey)
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
console.error('Signature verification failed:', error)
|
|
282
|
-
return false
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* 创建新事件
|
|
288
|
-
* @param {object} params 事件字段与签名私钥等构造参数
|
|
289
|
-
* @param {string[]} [params.prev_event_ids] 父事件 id;根事件 `[]`
|
|
290
|
-
* @returns {Promise<object>} 含 id、signature 的完整事件对象
|
|
291
|
-
*/
|
|
292
|
-
export async function createEvent(params) {
|
|
293
|
-
const {
|
|
294
|
-
type, groupId, channelId, sender, charId, content, prev_event_ids, privateKey, hlc,
|
|
295
|
-
} = params
|
|
296
|
-
|
|
297
|
-
const event = {
|
|
298
|
-
type,
|
|
299
|
-
groupId,
|
|
300
|
-
channelId: channelId || null,
|
|
301
|
-
sender,
|
|
302
|
-
charId: charId || null,
|
|
303
|
-
timestamp: Date.now(),
|
|
304
|
-
hlc: hlc || HLC.now(),
|
|
305
|
-
prev_event_ids: sortedPrevEventIds(prev_event_ids),
|
|
306
|
-
content,
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
event.id = computeEventId(eventBodyForSign(event))
|
|
310
|
-
event.signature = await signEvent(event, privateKey)
|
|
311
|
-
return event
|
|
312
|
-
}
|
|
313
|
-
|
|
314
241
|
/**
|
|
315
242
|
* @param {object[]} events DAG 事件行
|
|
316
243
|
* @returns {Array<{ id: string, prev_event_ids?: unknown, hlc?: object, node_id?: string, sender?: string }>} 拓扑 meta 列表
|
package/dag/storage.mjs
CHANGED
|
@@ -196,17 +196,6 @@ export async function readJsonlTipId(filePath) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/**
|
|
200
|
-
* 将单个 JSON 对象作为一行追加写入 JSONL(必要时创建父目录)。
|
|
201
|
-
* @param {string} filePath 目标文件路径
|
|
202
|
-
* @param {object} record 要序列化写入的对象
|
|
203
|
-
* @returns {Promise<void>}
|
|
204
|
-
*/
|
|
205
|
-
export async function appendJsonl(filePath, record) {
|
|
206
|
-
await mkdir(dirname(filePath), { recursive: true })
|
|
207
|
-
await appendFile(filePath, `${JSON.stringify(record)}\n`, 'utf8')
|
|
208
|
-
}
|
|
209
|
-
|
|
210
199
|
/**
|
|
211
200
|
* 流式重写 JSONL(临时文件 + rename),避免大数组 join 的内存峰值。
|
|
212
201
|
* @param {string} filePath 目标路径
|
|
@@ -244,17 +233,6 @@ export async function writeJsonlSynced(filePath, records) {
|
|
|
244
233
|
return withAsyncMutex(jsonlMutexKey(filePath), () => writeJsonl(filePath, records))
|
|
245
234
|
}
|
|
246
235
|
|
|
247
|
-
/**
|
|
248
|
-
* 在 per-file 互斥锁内过滤重写 JSONL。
|
|
249
|
-
* @param {string} filePath 目标路径
|
|
250
|
-
* @param {(row: object) => boolean} keep 保留谓词
|
|
251
|
-
* @param {{ sanitize?: (row: object) => object }} [options] 读行净化
|
|
252
|
-
* @returns {Promise<{ kept: number, dropped: number }>} 统计
|
|
253
|
-
*/
|
|
254
|
-
export async function rewriteJsonlKeepingSynced(filePath, keep, options = {}) {
|
|
255
|
-
return withAsyncMutex(jsonlMutexKey(filePath), () => rewriteJsonlKeeping(filePath, keep, options))
|
|
256
|
-
}
|
|
257
|
-
|
|
258
236
|
/**
|
|
259
237
|
* 追加一行 JSONL 并 `fsync`。
|
|
260
238
|
* @param {string} filePath 目标路径
|
|
@@ -63,15 +63,6 @@ export function resolveActiveKeyAtGeneration(keyHistory, generation) {
|
|
|
63
63
|
return entry?.activePubKeyHex ?? null
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
|
|
68
|
-
* @returns {number} 最新代际,无则 -1
|
|
69
|
-
*/
|
|
70
|
-
export function resolveLatestActiveGeneration(keyHistory) {
|
|
71
|
-
if (!keyHistory?.length) return -1
|
|
72
|
-
return Math.max(...keyHistory.map(row => row.generation))
|
|
73
|
-
}
|
|
74
|
-
|
|
75
66
|
/**
|
|
76
67
|
* @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
|
|
77
68
|
* @param {number} generation 代际
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import { eventsToMetas, topologicalCanonicalOrder } from '../dag/index.mjs'
|
|
2
|
-
|
|
3
1
|
/** @type {Map<string, { fp: string, order: string[] }>} */
|
|
4
2
|
const memoByKey = new Map()
|
|
5
3
|
|
|
6
|
-
/**
|
|
7
|
-
* 进程内拓扑序 memo(key 由调用方提供,如 username:groupId)。
|
|
8
|
-
* @param {string} memoKey 缓存键
|
|
9
|
-
* @param {string} fingerprint 文件 stat + 事件数指纹
|
|
10
|
-
* @param {object[]} events 全量事件
|
|
11
|
-
* @param {{ force?: boolean }} [opts] 强制重算
|
|
12
|
-
* @returns {string[]} 拓扑序 event id
|
|
13
|
-
*/
|
|
14
|
-
export function resolveTopologicalOrderMemo(memoKey, fingerprint, events, opts = {}) {
|
|
15
|
-
return resolveTopologicalOrderMemoCached(memoKey, fingerprint, () =>
|
|
16
|
-
topologicalCanonicalOrder(eventsToMetas(events)), opts)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
4
|
/**
|
|
20
5
|
* 进程内拓扑序 memo;`resolveOrder` 可接入磁盘缓存等实现。
|
|
21
6
|
* @param {string} memoKey 缓存键
|
package/files/assemble.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
|
-
import { createHash } from 'node:crypto'
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
3
|
import { setImmediate } from 'node:timers'
|
|
4
4
|
|
|
5
5
|
import { FEDERATION_CHUNK_MAX_BYTES } from '../core/constants.mjs'
|
|
6
6
|
import {
|
|
7
7
|
encryptConvergentPlaintext,
|
|
8
8
|
encryptRandomPlaintext,
|
|
9
|
+
encryptRandomPlaintextWithKey,
|
|
9
10
|
wrapContentKey,
|
|
10
11
|
} from '../crypto/key.mjs'
|
|
11
12
|
|
|
@@ -48,17 +49,57 @@ function encryptionStrategyFor(ceMode) {
|
|
|
48
49
|
return strategy
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* @param {{ contentHash: string, ciphertextHash: string, raw: Buffer }} enc 分块加密结果
|
|
54
|
+
* @param {CeMode} ceMode 模式
|
|
55
|
+
* @returns {{ hash: string, size: number, raw: Buffer, contentHash?: string }} manifest part
|
|
56
|
+
*/
|
|
57
|
+
function partFromEnc(enc, ceMode) {
|
|
58
|
+
/** @type {{ hash: string, size: number, raw: Buffer, contentHash?: string }} */
|
|
59
|
+
const part = { hash: enc.ciphertextHash, size: enc.raw.length, raw: enc.raw }
|
|
60
|
+
if (ceMode === 'convergent' || ceMode === 'plain')
|
|
61
|
+
part.contentHash = enc.contentHash
|
|
62
|
+
return part
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 加密单个分块(random 模式复用调用方提供的 contentKey)。
|
|
67
|
+
* @param {Buffer | Uint8Array} slice 明文分块
|
|
68
|
+
* @param {CeMode} ceMode 模式
|
|
69
|
+
* @param {Buffer | null} [contentKey] random 模式密钥
|
|
70
|
+
* @returns {{ hash: string, size: number, raw: Buffer, contentHash?: string }} manifest part
|
|
71
|
+
*/
|
|
72
|
+
export function encryptSliceToPart(slice, ceMode, contentKey = null) {
|
|
73
|
+
const enc = ceMode === 'random'
|
|
74
|
+
? encryptRandomPlaintextWithKey(slice, contentKey)
|
|
75
|
+
: encryptionStrategyFor(ceMode)(slice)
|
|
76
|
+
return partFromEnc(enc, ceMode)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {Array<{ hash: string, size: number, contentHash?: string }>} parts 分块
|
|
81
|
+
* @returns {Array<{ hash: string, size: number, contentHash?: string }>} 写入 manifest 的 parts
|
|
82
|
+
*/
|
|
83
|
+
export function manifestPartsForPersist(parts) {
|
|
84
|
+
return parts.map(part => {
|
|
85
|
+
/** @type {{ hash: string, size: number, contentHash?: string }} */
|
|
86
|
+
const out = { hash: part.hash, size: part.size }
|
|
87
|
+
if (part.contentHash) out.contentHash = part.contentHash
|
|
88
|
+
return out
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
51
92
|
/**
|
|
52
93
|
* @param {Buffer | Uint8Array} plaintext 明文
|
|
53
94
|
* @param {CeMode} ceMode 模式
|
|
54
|
-
* @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer }>, contentKey?: Buffer }} 加密结果
|
|
95
|
+
* @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>, contentKey?: Buffer }} 加密结果
|
|
55
96
|
*/
|
|
56
97
|
export function encryptPlaintextToParts(plaintext, ceMode = 'convergent') {
|
|
57
98
|
const plain = Buffer.from(plaintext)
|
|
58
99
|
const enc = encryptionStrategyFor(ceMode)(plain)
|
|
59
100
|
return {
|
|
60
101
|
contentHash: enc.contentHash,
|
|
61
|
-
parts: [
|
|
102
|
+
parts: [partFromEnc(enc, ceMode)],
|
|
62
103
|
contentKey: enc.contentKey,
|
|
63
104
|
}
|
|
64
105
|
}
|
|
@@ -67,7 +108,7 @@ export function encryptPlaintextToParts(plaintext, ceMode = 'convergent') {
|
|
|
67
108
|
* 将明文拆分为多块加密(大文件)。
|
|
68
109
|
* @param {Buffer | Uint8Array} plaintext 明文
|
|
69
110
|
* @param {CeMode} ceMode 模式
|
|
70
|
-
* @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer }>, contentKey?: Buffer }} 分块加密结果
|
|
111
|
+
* @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>, contentKey?: Buffer }} 分块加密结果
|
|
71
112
|
*/
|
|
72
113
|
export function encryptPlaintextToMultiParts(plaintext, ceMode = 'convergent') {
|
|
73
114
|
const plain = Buffer.from(plaintext)
|
|
@@ -75,15 +116,11 @@ export function encryptPlaintextToMultiParts(plaintext, ceMode = 'convergent') {
|
|
|
75
116
|
if (plain.length <= FEDERATION_CHUNK_MAX_BYTES)
|
|
76
117
|
return encryptPlaintextToParts(plain, ceMode)
|
|
77
118
|
|
|
78
|
-
/** @type {Array<{ hash: string, size: number, raw: Buffer }>} */
|
|
119
|
+
/** @type {Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>} */
|
|
79
120
|
const parts = []
|
|
80
|
-
|
|
81
|
-
for (let offset = 0; offset < plain.length; offset += FEDERATION_CHUNK_MAX_BYTES)
|
|
82
|
-
|
|
83
|
-
const enc = encryptionStrategyFor(ceMode)(slice)
|
|
84
|
-
if (ceMode === 'random') contentKey = enc.contentKey
|
|
85
|
-
parts.push({ hash: enc.ciphertextHash, size: enc.raw.length, raw: enc.raw })
|
|
86
|
-
}
|
|
121
|
+
const contentKey = ceMode === 'random' ? randomBytes(32) : null
|
|
122
|
+
for (let offset = 0; offset < plain.length; offset += FEDERATION_CHUNK_MAX_BYTES)
|
|
123
|
+
parts.push(encryptSliceToPart(plain.subarray(offset, offset + FEDERATION_CHUNK_MAX_BYTES), ceMode, contentKey))
|
|
87
124
|
return { contentHash, parts, contentKey: contentKey || undefined }
|
|
88
125
|
}
|
|
89
126
|
|
|
@@ -99,15 +136,12 @@ export async function encryptPlaintextToMultiPartsAsync(plaintext, ceMode = 'con
|
|
|
99
136
|
if (plain.length <= FEDERATION_CHUNK_MAX_BYTES)
|
|
100
137
|
return encryptPlaintextToParts(plain, ceMode)
|
|
101
138
|
|
|
102
|
-
/** @type {Array<{ hash: string, size: number, raw: Buffer }>} */
|
|
139
|
+
/** @type {Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>} */
|
|
103
140
|
const parts = []
|
|
104
|
-
|
|
141
|
+
const contentKey = ceMode === 'random' ? randomBytes(32) : null
|
|
105
142
|
for (let offset = 0; offset < plain.length; offset += FEDERATION_CHUNK_MAX_BYTES) {
|
|
106
143
|
if (offset > 0) await new Promise(resolve => setImmediate(resolve))
|
|
107
|
-
|
|
108
|
-
const enc = encryptionStrategyFor(ceMode)(slice)
|
|
109
|
-
if (ceMode === 'random') contentKey = enc.contentKey
|
|
110
|
-
parts.push({ hash: enc.ciphertextHash, size: enc.raw.length, raw: enc.raw })
|
|
144
|
+
parts.push(encryptSliceToPart(plain.subarray(offset, offset + FEDERATION_CHUNK_MAX_BYTES), ceMode, contentKey))
|
|
111
145
|
}
|
|
112
146
|
return { contentHash, parts, contentKey: contentKey || undefined }
|
|
113
147
|
}
|
|
@@ -144,7 +178,7 @@ export function buildFileManifest(params) {
|
|
|
144
178
|
size: Buffer.from(plaintext).length,
|
|
145
179
|
contentHash: enc.contentHash,
|
|
146
180
|
ceMode,
|
|
147
|
-
parts: enc.parts
|
|
181
|
+
parts: manifestPartsForPersist(enc.parts),
|
|
148
182
|
transferKeyDescriptor: transferKeyDescriptor || publicTransferKeyDescriptor(),
|
|
149
183
|
meta,
|
|
150
184
|
})
|
|
@@ -155,7 +189,7 @@ export function buildFileManifest(params) {
|
|
|
155
189
|
/**
|
|
156
190
|
* 由已加密分块构建 manifest(vault / file-master-key-wrap 等需自定义 transferKeyDescriptor)。
|
|
157
191
|
* @param {object} params 与 buildFileManifest 相同字段(不含 plaintext 重加密)
|
|
158
|
-
* @param {{ contentHash: string, parts: Array<{ hash: string, size: number, raw?: Buffer }> }} enc 加密结果
|
|
192
|
+
* @param {{ contentHash: string, parts: Array<{ hash: string, size: number, raw?: Buffer, contentHash?: string }> }} enc 加密结果
|
|
159
193
|
* @returns {FileManifest} manifest
|
|
160
194
|
*/
|
|
161
195
|
export function buildFileManifestFromEnc(params, enc) {
|
|
@@ -177,7 +211,7 @@ export function buildFileManifestFromEnc(params, enc) {
|
|
|
177
211
|
size: Buffer.from(plaintext).length,
|
|
178
212
|
contentHash: enc.contentHash,
|
|
179
213
|
ceMode,
|
|
180
|
-
parts: enc.parts
|
|
214
|
+
parts: manifestPartsForPersist(enc.parts),
|
|
181
215
|
transferKeyDescriptor: transferKeyDescriptor || publicTransferKeyDescriptor(),
|
|
182
216
|
meta,
|
|
183
217
|
})
|
|
@@ -185,24 +219,6 @@ export function buildFileManifestFromEnc(params, enc) {
|
|
|
185
219
|
return manifest
|
|
186
220
|
}
|
|
187
221
|
|
|
188
|
-
/**
|
|
189
|
-
* @param {string} groupId 群 ID
|
|
190
|
-
* @param {Buffer} contentKey 随机 contentKey
|
|
191
|
-
* @param {Buffer | string} H 群/vault 秘密
|
|
192
|
-
* @param {string} fileId 文件 ID
|
|
193
|
-
* @param {number} keyGeneration 密钥代次
|
|
194
|
-
* @returns {import('./manifest.mjs').TransferKeyDescriptor} 传输密钥描述
|
|
195
|
-
*/
|
|
196
|
-
export function fileMasterKeyWrapDescriptor(groupId, fileId, contentKey, H, keyGeneration = 0) {
|
|
197
|
-
return {
|
|
198
|
-
type: 'file-master-key-wrap',
|
|
199
|
-
groupId,
|
|
200
|
-
fileId,
|
|
201
|
-
keyGeneration,
|
|
202
|
-
wrappedKey: wrapContentKey(contentKey, H, fileId),
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
222
|
/**
|
|
207
223
|
* @param {string} entityHash 所有者
|
|
208
224
|
* @param {string} fileId 文件 ID
|