feat(api): bind encrypted workspaces to accounts and devices

This commit is contained in:
justme committed 2026-10-13 01:38:19 +08:00
1 parent 019b15041a
commit c7d8ef15bc
49 files changed
+1904 -278

No files matched your search

+15 -4
View File
@@ -32,7 +32,7 @@ const hkdf = (ikm, salt, info) => Buffer.from(hkdfSync('sha256', ikm, salt, Buff
// ───────────── 协议常量(protocol.md) ─────────────
const FLAG = { FINAL: 1, SNAPSHOT: 2, GATED: 4, ENC: 8 };
const FLAG = { FINAL: 1, SNAPSHOT: 2, GATED: 4, ENC: 8, CONTROL: 16 };
const TIERS = [1024, 4096, 16384, 65536, 262144, 1048576];
const MAX_FRAME_DATA = 1048576 - 4;
const LABEL = {
@@ -111,11 +111,13 @@ function metaJson(m) {
if (m.size !== undefined) o.size = m.size;
o.ts = m.ts;
if (m.dev !== undefined) o.dev = m.dev;
if (m.threadId !== undefined) o.thread_id = m.threadId;
if (m.folderId !== undefined) o.folder_id = m.folderId;
return Buffer.from(JSON.stringify(o), 'utf8');
}
/** 条目编码的参照实现:小内容单帧(非 GATED),放不下则多帧 GATED。 */
function encodeItem({ itemId, meta, content, maxFrameData = MAX_FRAME_DATA, snapshot = false, forceGated = false }) {
function encodeItem({ itemId, meta, content, maxFrameData = MAX_FRAME_DATA, snapshot = false, forceGated = false, control = false }) {
const mj = metaJson(meta);
const head = Buffer.concat([u16(mj.length), mj]);
const cap0 = maxFrameData - head.length;
@@ -123,7 +125,7 @@ function encodeItem({ itemId, meta, content, maxFrameData = MAX_FRAME_DATA, snap
const frames = [];
if (content.length <= cap0 && !forceGated) {
frames.push({ index: 0, flags: FLAG.FINAL | FLAG.ENC | (snapshot ? FLAG.SNAPSHOT : 0), data: Buffer.concat([head, content]) });
frames.push({ index: 0, flags: FLAG.FINAL | FLAG.ENC | (snapshot ? FLAG.SNAPSHOT : 0) | (control ? FLAG.CONTROL : 0), data: Buffer.concat([head, content]) });
return { gated: false, frames };
}
if (snapshot) throw new Error('快照不能是 GATED');
@@ -216,6 +218,7 @@ vectors.frames = frameCases.map((c) => {
// 条目向量:meta + 内容 + 分帧
const TS = 1700000000000;
const itemCases = [
{ name: 'workspace_control', itemId: itemC, meta: { kind: 'other', ts: TS }, control: true, content: Buffer.from('{"app":"webdrop.workspace","v":1,"op":"thread.create"}', 'utf8') },
{
name: 'text_small',
itemId: itemA,
@@ -241,6 +244,13 @@ const itemCases = [
meta: { kind: 'file', name: 'note.txt', mime: 'text/plain', size: 11, ts: TS },
content: Buffer.from('hello world', 'utf8'),
},
{
name: 'file_in_thread_folder',
itemId: itemC,
meta: { kind: 'file', name: 'readme.txt', mime: 'text/plain', size: 11, ts: TS, dev: 'Windows 11',
threadId: '01234567-89ab-cdef-0123-456789abcdef', folderId: 'fedcba98-7654-3210-fedc-ba9876543210' },
content: Buffer.from('thread test', 'utf8'),
},
{
name: 'empty_text',
itemId: itemB,
@@ -296,6 +306,7 @@ vectors.items = itemCases.map((c) => {
max_frame_data: c.maxFrameData ?? MAX_FRAME_DATA,
snapshot: !!c.snapshot,
force_gated: !!c.forceGated,
control: !!c.control,
gated: plan.gated,
frames: envelopes.map(hex),
};
@@ -381,7 +392,7 @@ vectors.negative = [
{ name: 'header_only', envelope: hex(base.subarray(0, 26)), expect: 'bad_envelope' },
{ name: 'too_short', envelope: hex(base.subarray(0, 10)), expect: 'bad_envelope' },
{ name: 'bad_version', envelope: mutate((b) => { b[0] = 2; }), expect: 'bad_envelope' },
{ name: 'reserved_flag_bit', envelope: mutate((b) => { b[1] |= 0x10; }), expect: 'bad_envelope' },
{ name: 'reserved_flag_bit', envelope: mutate((b) => { b[1] |= 0x20; }), expect: 'bad_envelope' },
{ name: 'payload_len_mismatch', envelope: mutate((b) => { b[25] ^= 1; }), expect: 'bad_envelope' },
// 没有 ENC 标志的帧,在 enc 会话里必须被拒绝(防降级:服务端不能注入明文帧)
{