refactor(core): drop per-terminal device labels
Remove sealLabel/openLabel and their label constants; devices are no longer named per channel. Rename the create setting seats to max_members and point the e2e join helper at POST /join.
This commit is contained in:
1 parent
d946fc5e8a
commit
5fd4ddd411
6 files changed
+12
-171
No files matched your search
@@ -38,11 +38,7 @@ export const Labels = {
|
||||
|
||||
/** AAD 前缀。 */
|
||||
export const AAD_FRAME = 'WD1';
|
||||
export const AAD_LABEL = 'WD1L';
|
||||
|
||||
/** 设备名总长上限(iv + 密文 + tag),与服务端 MaxSeatLabelBytes 一致。 */
|
||||
export const MAX_LABEL_BYTES = 256;
|
||||
export const LABEL_IV_SIZE = 12;
|
||||
|
||||
export const POW_NONCE_SIZE = 8;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AAD_LABEL, ID_SIZE, KEY_SIZE, LABEL_IV_SIZE, MAX_LABEL_BYTES, TAG_SIZE, type ChannelMode } from './constants.js';
|
||||
import { concat, fromBase64Url, randomBytes, toBase64Url, tryDecodeUtf8, utf8, wipe, type Bytes } from './bytes.js';
|
||||
import { CapabilityError, IntegrityError, LimitError, LinkError, ModeMismatchError } from './errors.js';
|
||||
import { ID_SIZE, KEY_SIZE, type ChannelMode } from './constants.js';
|
||||
import { randomBytes, toBase64Url, utf8, wipe, type Bytes } from './bytes.js';
|
||||
import { CapabilityError, LinkError, ModeMismatchError } from './errors.js';
|
||||
import { encodeEnvelope, sealFrame } from './frame.js';
|
||||
import { deriveChannelKeys, deriveItemKey, deriveOwnerToken, sha256, type ChannelKeys } from './kdf.js';
|
||||
import { buildLink, parseLink, type ParsedLink } from './link.js';
|
||||
@@ -15,7 +15,7 @@ export interface ChannelSettings {
|
||||
join_policy?: 'open' | 'invite';
|
||||
allow_guests?: boolean;
|
||||
name?: string;
|
||||
seats?: number;
|
||||
max_members?: number;
|
||||
members_can_upload?: boolean;
|
||||
delete_policy?: 'owner_only' | 'own' | 'anyone';
|
||||
ttl_seconds?: number;
|
||||
@@ -331,68 +331,6 @@ export class ChannelSession {
|
||||
return decryptItemStream(this.#readerContext, source, opts);
|
||||
}
|
||||
|
||||
// ───────────────────────── 设备名(protocol §10) ─────────────────────────
|
||||
|
||||
/**
|
||||
* 加密设备名,作为席位名称 PUT 请求体。格式:iv(12) ‖ AES-GCM(K_label, 名称) ‖ tag(16),
|
||||
* AAD = "WD1L" ‖ channel_id ‖ endpoint_hash。raw 模式下设备名是明文 UTF-8。
|
||||
* @param opts.endpointHash 指定目标席位;所有者可给其他终端重命名。
|
||||
* @param opts.iv 仅用于测试向量,真实使用必须让库随机生成。
|
||||
*/
|
||||
async sealLabel(name: string, opts: { iv?: Uint8Array; endpointHash?: Uint8Array | string } = {}): Promise<Bytes> {
|
||||
const endpointHash = typeof opts.endpointHash === 'string'
|
||||
? fromBase64Url(opts.endpointHash, 32)
|
||||
: opts.endpointHash ?? this.endpointHash;
|
||||
if (!endpointHash || endpointHash.length !== 32) throw new RangeError('终端标识必须是 32 字节');
|
||||
const plain = utf8(name);
|
||||
if (this.mode === 'raw') {
|
||||
if (plain.length > MAX_LABEL_BYTES) throw new LimitError('设备名过长');
|
||||
return plain;
|
||||
}
|
||||
if (plain.length === 0) throw new RangeError('设备名不能为空');
|
||||
if (plain.length + LABEL_IV_SIZE + TAG_SIZE > MAX_LABEL_BYTES) {
|
||||
throw new LimitError(`设备名过长(最多 ${MAX_LABEL_BYTES - LABEL_IV_SIZE - TAG_SIZE} 字节 UTF-8)`);
|
||||
}
|
||||
const iv = opts.iv ?? randomBytes(LABEL_IV_SIZE);
|
||||
const sealed = await globalThis.crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: iv as Bytes, additionalData: this.#labelAad(endpointHash), tagLength: 128 },
|
||||
(this.#keys as ChannelKeys).labelKey,
|
||||
plain,
|
||||
);
|
||||
return concat(iv, new Uint8Array(sealed));
|
||||
}
|
||||
|
||||
/** 解密席位列表里某个终端的设备名。endpointHash 可以是 base64url 文本或字节。 */
|
||||
async openLabel(blob: Uint8Array | string, endpointHash: Uint8Array | string): Promise<string> {
|
||||
const raw = typeof blob === 'string' ? fromBase64Url(blob) : blob;
|
||||
const hash = typeof endpointHash === 'string' ? fromBase64Url(endpointHash, 32) : endpointHash;
|
||||
if (!raw || !hash) throw new IntegrityError('bad_envelope');
|
||||
|
||||
if (this.mode === 'raw') {
|
||||
const text = tryDecodeUtf8(raw);
|
||||
if (text === null) throw new IntegrityError('bad_meta');
|
||||
return text;
|
||||
}
|
||||
if (raw.length < LABEL_IV_SIZE + TAG_SIZE + 1) throw new IntegrityError('bad_envelope');
|
||||
try {
|
||||
const plain = await globalThis.crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: raw.slice(0, LABEL_IV_SIZE), additionalData: this.#labelAad(hash), tagLength: 128 },
|
||||
(this.#keys as ChannelKeys).labelKey,
|
||||
raw.slice(LABEL_IV_SIZE),
|
||||
);
|
||||
const text = tryDecodeUtf8(new Uint8Array(plain));
|
||||
if (text === null) throw new IntegrityError('bad_meta');
|
||||
return text;
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError) throw e;
|
||||
throw new IntegrityError('auth_failed');
|
||||
}
|
||||
}
|
||||
|
||||
#labelAad(endpointHash: Uint8Array): Bytes {
|
||||
return concat(utf8(AAD_LABEL), this.idBytes, endpointHash);
|
||||
}
|
||||
|
||||
// ───────────────────────── 清理 ─────────────────────────
|
||||
|
||||
/** 尽力清零可以清零的字节并清空缓存。JS 无法保证擦除,这只是缩小暴露窗口。 */
|
||||
|
||||
Reference in new issue
Block a user