/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/sync/localMirrorTransaction.ts
188 строк
7 KB
Robert Kuzhin
feat: harden encrypted sync transactions
09 авг 2026, 13:46
09 авг 2026, 13:46
cf0c0dc
Код
Авторство
О чём код?
import type { CryptoService } from "../crypto/cryptoService.js"; import { assertUuid } from "../crypto/encoding.js"; import type { KeyHandle } from "../crypto/keyHandle.js"; import type { EncryptedMirror } from "../mirror/encryptedMirror.js"; import type { ManifestStore } from "../mirror/manifestStore.js"; import type { Manifest } from "../mirror/manifestTypes.js"; import { MAX_ENCRYPTED_PAYLOAD_BYTES } from "../mirror/limits.js"; import { LocalSyncError } from "./errors.js"; const MAGIC = new Uint8Array([0x4f, 0x45, 0x47, 0x54]); // OEGT const FORMAT_VERSION = 1; const WRITE_OPERATION = 1; const DELETE_OPERATION = 2; const UUID_BYTES = 36; const HEADER_BYTES = MAGIC.length + 1 + 1 + UUID_BYTES + 4 + 4; const decoder = new TextDecoder("utf-8", { fatal: true }); const encoder = new TextEncoder(); interface PendingTransaction { readonly operation: "write" | "delete"; readonly fileId: string; readonly objectEnvelope: Uint8Array | undefined; readonly manifestEnvelope: Uint8Array; } export class LocalMirrorTransaction { public constructor( private readonly mirror: EncryptedMirror, private readonly manifestStore: ManifestStore, private readonly crypto: CryptoService, ) {} public prepareWrite( fileId: string, objectEnvelope: Uint8Array, manifestEnvelope: Uint8Array, ): Promise<void> { return this.prepare({ operation: "write", fileId, objectEnvelope, manifestEnvelope, }); } public prepareDelete(fileId: string, manifestEnvelope: Uint8Array): Promise<void> { return this.prepare({ operation: "delete", fileId, objectEnvelope: undefined, manifestEnvelope, }); } public clear(): Promise<void> { return this.mirror.deletePendingLocalTransaction(); } public async recover(key: KeyHandle): Promise<Manifest | undefined> { const encoded = await this.mirror.readPendingLocalTransaction(); if (encoded === undefined) return undefined; try { const pending = decodeTransaction(encoded); const manifestEnvelope = new Uint8Array(pending.manifestEnvelope); const objectEnvelope = pending.objectEnvelope === undefined ? undefined : new Uint8Array(pending.objectEnvelope); try { const manifest = await this.manifestStore.decrypt(manifestEnvelope, key); const entry = manifest.files[pending.fileId]; if (pending.operation === "write") { if (entry === undefined || entry.deleted === true || objectEnvelope === undefined) { throw new LocalSyncError("Pending object write does not match its manifest"); } const plaintext = await this.crypto.decryptObject({ key, fileId: pending.fileId, envelope: objectEnvelope, }); plaintext.fill(0); await this.mirror.writeObjectAtomic(pending.fileId, objectEnvelope); await this.mirror.writeManifestAtomic(manifestEnvelope); } else { if (entry === undefined || entry.deleted !== true) { throw new LocalSyncError("Pending object deletion does not match its manifest"); } await this.mirror.writeManifestAtomic(manifestEnvelope); await this.mirror.deleteObject(pending.fileId); } await this.clear(); return manifest; } finally { manifestEnvelope.fill(0); objectEnvelope?.fill(0); } } finally { encoded.fill(0); } } private async prepare(pending: PendingTransaction): Promise<void> { const existing = await this.mirror.readPendingLocalTransaction(); if (existing !== undefined) { existing.fill(0); throw new LocalSyncError("A pending local mirror transaction must be recovered first"); } const encoded = encodeTransaction(pending); try { await this.mirror.writePendingLocalTransactionAtomic(encoded); } finally { encoded.fill(0); } } } function encodeTransaction(pending: PendingTransaction): Uint8Array { assertUuid(pending.fileId, "pending fileId"); const fileId = encoder.encode(pending.fileId); if (fileId.length !== UUID_BYTES) throw new LocalSyncError("Pending file ID has invalid length"); const objectEnvelope = pending.objectEnvelope ?? new Uint8Array(); if (pending.operation === "write" && objectEnvelope.length === 0) { throw new LocalSyncError("Pending object write has no ciphertext"); } if (pending.operation === "delete" && objectEnvelope.length !== 0) { throw new LocalSyncError("Pending object deletion contains unexpected ciphertext"); } if ( objectEnvelope.length > MAX_ENCRYPTED_PAYLOAD_BYTES || pending.manifestEnvelope.length === 0 || pending.manifestEnvelope.length > MAX_ENCRYPTED_PAYLOAD_BYTES ) { throw new LocalSyncError("Pending local transaction payload is outside its size limit"); } const result = new Uint8Array( HEADER_BYTES + objectEnvelope.length + pending.manifestEnvelope.length, ); result.set(MAGIC, 0); result[MAGIC.length] = FORMAT_VERSION; result[MAGIC.length + 1] = pending.operation === "write" ? WRITE_OPERATION : DELETE_OPERATION; result.set(fileId, MAGIC.length + 2); const view = new DataView(result.buffer); view.setUint32(MAGIC.length + 2 + UUID_BYTES, objectEnvelope.length, false); view.setUint32(MAGIC.length + 2 + UUID_BYTES + 4, pending.manifestEnvelope.length, false); result.set(objectEnvelope, HEADER_BYTES); result.set(pending.manifestEnvelope, HEADER_BYTES + objectEnvelope.length); return result; } function decodeTransaction(encoded: Uint8Array): PendingTransaction { if (encoded.length < HEADER_BYTES || !MAGIC.every((byte, index) => encoded[index] === byte)) { throw new LocalSyncError("Pending local transaction has invalid framing"); } if (encoded[MAGIC.length] !== FORMAT_VERSION) { throw new LocalSyncError("Pending local transaction version is unsupported"); } const operationByte = encoded[MAGIC.length + 1]; if (operationByte !== WRITE_OPERATION && operationByte !== DELETE_OPERATION) { throw new LocalSyncError("Pending local transaction operation is invalid"); } const fileId = decoder.decode(encoded.subarray(MAGIC.length + 2, MAGIC.length + 2 + UUID_BYTES)); try { assertUuid(fileId, "pending fileId"); } catch (error) { throw new LocalSyncError("Pending local transaction file ID is invalid", { cause: error }); } const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength); const objectLength = view.getUint32(MAGIC.length + 2 + UUID_BYTES, false); const manifestLength = view.getUint32(MAGIC.length + 2 + UUID_BYTES + 4, false); if ( manifestLength === 0 || HEADER_BYTES + objectLength + manifestLength !== encoded.length || (operationByte === WRITE_OPERATION && objectLength === 0) || (operationByte === DELETE_OPERATION && objectLength !== 0) ) { throw new LocalSyncError("Pending local transaction lengths are invalid"); } return { operation: operationByte === WRITE_OPERATION ? "write" : "delete", fileId, objectEnvelope: objectLength === 0 ? undefined : encoded.subarray(HEADER_BYTES, HEADER_BYTES + objectLength), manifestEnvelope: encoded.subarray(HEADER_BYTES + objectLength), }; }