fix(web): verify deletions against the signer and read crypto state afresh
This commit is contained in:
1 parent
fc1dc08ebe
commit
6310f93469
2 files changed
+30
-11
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user