fix(web): verify deletions against the signer and read crypto state afresh

This commit is contained in:
justme committed 2026-10-16 18:30:58 +08:00
1 parent fc1dc08ebe
commit 6310f93469
2 files changed
+30 -11

No files matched your search

+27 -9
View File
@@ -37,13 +37,29 @@ export function attachDataKeys(session: ChannelSession, resource: SavedResource)
};
}
type DataKeys = { state: CryptoState; keys: Record<string, string> };
const pendingKeys = new Map<string, Promise<DataKeys>>();
/** 同一空间同时发起的读取合并成一次请求 */
export function readDataKeys(resource: SavedResource, session: ChannelSession): Promise<DataKeys> {
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<string, Promise<DataKeys>>();
const queuedKeys = new Map<string, Promise<DataKeys>>();
function startKeys(resource: SavedResource, session: ChannelSession): Promise<DataKeys> {
const task: Promise<DataKeys> = 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<DataKeys> {
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<DataKeys> {
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);