perf(web): reuse cached crypto state and structure when decrypting and sending
Decrypting each history item no longer refetches the whole crypto state, and sending a message only needs the object and frame requests.
This commit is contained in:
1 parent
593eeba18e
commit
d77ca1f113
2 files changed
+23
-6
No files matched your search
@@ -5,7 +5,7 @@ import { apiFetch } from './http';
|
||||
import { accountPublicKey, accountToken, requireAccount } from './accountSession';
|
||||
import { decryptInvitation, encryptInvitation } from './accountCrypto';
|
||||
import { randomDomainKey, newObjectKey, unwrapObjectKey } from './domainCrypto';
|
||||
import { loadWorkspace } from './workspaceStructure';
|
||||
import { cachedWorkspace, loadWorkspace } from './workspaceStructure';
|
||||
import { pinSigningRoot } from './accountPersistence';
|
||||
import { signExistingContentEvent, verifyDeviceCertificate } from './accountSigning';
|
||||
|
||||
@@ -13,12 +13,17 @@ const objectKeys = new Map<string, CryptoKey>();
|
||||
const states = new Map<string, CryptoState>();
|
||||
const domainId = (channelId: string, domain: string, epoch: number) => `${channelId}:${domain}:${epoch}`;
|
||||
const contextId = (spaceId: string, channelId: string, domain: string, epoch: number) => `${spaceId}:${channelId}:${domain}:${epoch}`;
|
||||
export function clearDataKeys(): void { objectKeys.clear(); states.clear(); }
|
||||
const loaded = new Map<string, DataKeys>();
|
||||
export function clearDataKeys(): void { objectKeys.clear(); states.clear(); loaded.clear(); }
|
||||
export function attachDataKeys(session: ChannelSession, resource: SavedResource): void {
|
||||
if (resource.kind !== 'channel') return;
|
||||
session.objectKeyResolver = async itemId => {
|
||||
const cached = objectKeys.get(`${resource.id}:${itemId}`); if (cached) return cached;
|
||||
const { keys } = await readDataKeys(resource, session);
|
||||
// 内存里已有这个对象和它的数据域密钥就直接用;否则才重新拉(每次都拉整份 crypto 状态,历史越长越慢)
|
||||
const { keys } = await cachedKeys(resource, session, data => {
|
||||
const known = data.state.objects.find(o => o.itemId === itemId);
|
||||
return Boolean(known && data.keys[domainId(known.channelId, known.domain, known.epoch)]);
|
||||
});
|
||||
const object = states.get(resource.id)?.objects.find(o => o.itemId === itemId);
|
||||
if (!object || object.domain !== 'chat' && object.domain !== 'files') throw new Error('该对象没有已授权的解密数据域');
|
||||
const key = keys[domainId(object.channelId, object.domain, object.epoch)];
|
||||
@@ -35,6 +40,11 @@ export function readDataKeys(resource: SavedResource, session: ChannelSession):
|
||||
if (!task) { task = loadDataKeys(resource, session).finally(() => pendingKeys.delete(resource.id)); pendingKeys.set(resource.id, task); }
|
||||
return task;
|
||||
}
|
||||
/** 内存里的结果满足 enough 就复用,不满足再请求 */
|
||||
async function cachedKeys(resource: SavedResource, session: ChannelSession, enough: (data: DataKeys) => boolean): Promise<DataKeys> {
|
||||
const have = loaded.get(resource.id);
|
||||
return have && enough(have) ? have : readDataKeys(resource, session);
|
||||
}
|
||||
async function loadDataKeys(resource: SavedResource, session: ChannelSession): Promise<DataKeys> {
|
||||
const state = await apiFetch(`/spaces/${resource.id}/crypto`, { headers: session.authHeaders(session.can('owner') ? 'owner' : 'read') }).then(r => r.json() as Promise<CryptoState>);
|
||||
states.set(resource.id, state);
|
||||
@@ -49,7 +59,9 @@ async function loadDataKeys(resource: SavedResource, session: ChannelSession): P
|
||||
keys[id] = value.key;
|
||||
}
|
||||
if (latest && JSON.stringify(keys) !== JSON.stringify(latest.domainKeys ?? {})) await saveResource({ ...latest, domainKeys: keys });
|
||||
return { state, keys };
|
||||
const result = { state, keys };
|
||||
loaded.set(resource.id, result);
|
||||
return result;
|
||||
}
|
||||
async function envelope(spaceId: string, channelId: string, domain: 'chat' | 'files', epoch: number, key: string, username: string): Promise<DomainEnvelope> {
|
||||
const publicKey = username === requireAccount() ? accountPublicKey() : JSON.parse((await apiFetch(`/accounts/users/${username}/key`).then(r => r.json() as Promise<PublicKeyBody>)).public_key) as JsonWebKey;
|
||||
@@ -102,7 +114,8 @@ export async function initializeChannelKeys(resource: SavedResource, session: Ch
|
||||
export interface PreparedObject { itemIdBytes: Uint8Array; itemId: string; channelId: string; domain: 'chat' | 'files'; epoch: number }
|
||||
export async function prepareObject(resource: SavedResource, session: ChannelSession, channelId: string, domain: 'chat' | 'files'): Promise<PreparedObject> {
|
||||
const idBytes = crypto.getRandomValues(new Uint8Array(16)); const itemId = toBase64Url(idBytes);
|
||||
const { structure } = await loadWorkspace(resource.id); let channel = structure.channels.find(c => c.id === channelId);
|
||||
let channel = (await cachedWorkspace(resource.id)).structure.channels.find(c => c.id === channelId);
|
||||
if (!channel) channel = (await loadWorkspace(resource.id)).structure.channels.find(c => c.id === channelId);
|
||||
if (!channel) throw new Error('请先创建频道');
|
||||
if (resource.mode === 'enc' && !(domain === 'chat' ? channel.chatEpoch : channel.filesEpoch)) {
|
||||
await initializeChannelKeys(resource, session, channelId);
|
||||
@@ -111,7 +124,7 @@ export async function prepareObject(resource: SavedResource, session: ChannelSes
|
||||
const epoch = domain === 'chat' ? channel.chatEpoch : channel.filesEpoch;
|
||||
let keyEnvelope = '';
|
||||
if (resource.mode === 'enc') {
|
||||
const { keys } = await readDataKeys(resource, session); const key = keys[domainId(channelId, domain, epoch)];
|
||||
const { keys } = await cachedKeys(resource, session, data => Boolean(data.keys[domainId(channelId, domain, epoch)])); const key = keys[domainId(channelId, domain, epoch)];
|
||||
if (!key) throw new Error('未获得该频道数据域的访问授权');
|
||||
const created = await newObjectKey(key, { spaceId: resource.id, channelId, domain, epoch, itemId }); keyEnvelope = created.envelope;
|
||||
objectKeys.set(`${resource.id}:${itemId}`, created.key);
|
||||
|
||||
@@ -24,6 +24,10 @@ async function publish(id: string, body: WorkspaceBody): Promise<WorkspaceBody>
|
||||
publishWorkspace(id, { t: 'structure', state: body.structure, people });
|
||||
return body;
|
||||
}
|
||||
/** 结构由流里的 settings 通知保持最新;发消息等热路径直接用缓存,不再每次多等一个往返 */
|
||||
export async function cachedWorkspace(id: string): Promise<WorkspaceBody> {
|
||||
return current.get(id) ?? loadWorkspace(id);
|
||||
}
|
||||
export async function loadWorkspace(id: string): Promise<WorkspaceBody> {
|
||||
const { session } = await getSession(id);
|
||||
const body = await apiFetch(`/spaces/${id}/structure`, { headers: session.authHeaders('read') }).then(r => r.json() as Promise<WorkspaceBody>);
|
||||
|
||||
Reference in new issue
Block a user