diff --git a/web/src/core-bridge/worker/channelDataKeys.ts b/web/src/core-bridge/worker/channelDataKeys.ts index 6bebab9..b694cce 100644 --- a/web/src/core-bridge/worker/channelDataKeys.ts +++ b/web/src/core-bridge/worker/channelDataKeys.ts @@ -37,13 +37,29 @@ export function attachDataKeys(session: ChannelSession, resource: SavedResource) }; } type DataKeys = { state: CryptoState; keys: Record }; -const pendingKeys = new Map>(); -/** 同一空间同时发起的读取合并成一次请求 */ -export function readDataKeys(resource: SavedResource, session: ChannelSession): Promise { - let task = pendingKeys.get(resource.id); - if (!task) { task = loadDataKeys(resource, session).finally(() => pendingKeys.delete(resource.id)); pendingKeys.set(resource.id, task); } +const runningKeys = new Map>(); +const queuedKeys = new Map>(); +function startKeys(resource: SavedResource, session: ChannelSession): Promise { + const task: Promise = loadDataKeys(resource, session).finally(() => { if (runningKeys.get(resource.id) === task) runningKeys.delete(resource.id); }); + runningKeys.set(resource.id, task); return task; } +/** + * 读取空间的加密状态。fresh(默认)表示调用方刚发现本地缺对象或事件签名,需要一份比现在更新的结果: + * 已有请求在途时不能直接借用它(它可能在新对象登记之前就发出了),而是排在它后面再请求一次,同时到达的调用合并成这一次。 + * 不要求最新(首次打开)的调用可以直接借用在途请求。 + */ +export function readDataKeys(resource: SavedResource, session: ChannelSession, fresh = true): Promise { + const running = runningKeys.get(resource.id); + if (!running) return startKeys(resource, session); + if (!fresh) return running; + let next = queuedKeys.get(resource.id); + if (!next) { + next = running.catch(() => undefined).then(() => { queuedKeys.delete(resource.id); return startKeys(resource, session); }); + queuedKeys.set(resource.id, next); + } + return next; +} /** 内存里的结果满足 enough 就复用,不满足再请求 */ async function cachedKeys(resource: SavedResource, session: ChannelSession, enough: (data: DataKeys) => boolean): Promise { const have = loaded.get(resource.id); @@ -222,18 +238,20 @@ export async function verifyObjectDeletion(resource: SavedResource, session: Cha const eventId = fromBase64Url(deleted.eventId, 16); const manifestHash = fromBase64Url(object.event.manifestHash, 32); const signature = fromBase64Url(deleted.signature, 64); const certificate = fromBase64Url(deleted.deviceCertificateSignature, 64); if (!spaceId || !itemBytes || !eventId || !manifestHash || !signature || !certificate) return false; + // 撤回别人的消息时(空间主),删除事件由撤回者签名,不是作者;旧记录没有 signerAccount,按作者处理 + const signer = deleted.signerAccount || object.authorAccount; const rootJwk = JSON.parse(deleted.signerRootPublicKey) as JsonWebKey; const deviceJwk = JSON.parse(deleted.signerDevicePublicKey) as JsonWebKey; const root = await importSigningPublicKey(rootJwk); const { unlockedAccount } = await import('./account'); const local = unlockedAccount(); - if (object.authorAccount === local.username && local.profile.keys.signing + if (signer === local.username && local.profile.keys.signing && (rootJwk.x !== local.profile.keys.signing.publicKey.x || rootJwk.y !== local.profile.keys.signing.publicKey.y)) return false; - await pinSigningRoot(object.authorAccount, rootJwk); - if (!await verifyDeviceCertificate(object.authorAccount, deleted.signerDeviceId, deviceJwk, certificate, root)) return false; + await pinSigningRoot(signer, rootJwk); + if (!await verifyDeviceCertificate(signer, deleted.signerDeviceId, deviceJwk, certificate, root)) return false; const domain = object.domain; const expectedKind = domain === 'chat' ? 'chat.delete' : 'file.delete'; if (deleted.kind !== expectedKind) return false; - const payload = contentEventPayload({ spaceId, channelId: object.channelId, username: object.authorAccount, + const payload = contentEventPayload({ spaceId, channelId: object.channelId, username: signer, deviceId: deleted.signerDeviceId, eventId, eventKind: expectedKind, domain, epoch: object.epoch, itemId: itemBytes, frameCount: object.event.frameCount, manifestHash }); const deviceKey = await importSigningPublicKey(deviceJwk); diff --git a/web/src/core-bridge/worker/channelEvents.ts b/web/src/core-bridge/worker/channelEvents.ts index 4afbe76..9b632c3 100644 --- a/web/src/core-bridge/worker/channelEvents.ts +++ b/web/src/core-bridge/worker/channelEvents.ts @@ -110,7 +110,8 @@ export async function applyChannelRecord(resource: SavedResource, session: Chann break; } case 'removed': - if (event.reason === 'deleted' && !await verifyObjectDeletion(resource, session, event.itemId)) { + // 验签过程中的异常(例如签名公钥与已固定的不一致)按「拒绝这条记录」处理,不能让整条连接断开重连 + if (event.reason === 'deleted' && !await verifyObjectDeletion(resource, session, event.itemId).catch(() => false)) { showRejected(resource.id, { type: 'rejected', itemId: event.itemId, seq: event.seq, reason: 'auth_failed' }); break; } @@ -145,7 +146,7 @@ export async function syncChannel(resource: SavedResource, session: ChannelSessi clearFileCatalog(resource.id); readers.set(resource.id, session.createItemReader()); // 密钥与第一页日志互不依赖,同时发出,首次打开少等一个往返 - const keys = resource.mode === 'enc' ? readDataKeys(resource, session) : undefined; + const keys = resource.mode === 'enc' ? readDataKeys(resource, session, false) : undefined; keys?.catch(() => undefined); let from = 0; for (let first = true; ; first = false) {