import { MerkleArchiveCommitment } from "../../osBindings" import { DirectoryContents } from "./directoryContents" import { FileContents } from "./fileContents" import { ed25519ph } from "@noble/curves/ed25519" import { sha512 } from "@noble/hashes/sha2" import { VarIntProcessor } from "./varint" import { compare } from ".." const maxVarstringLen = 1024 * 1024 export type Signer = { pubkey: Uint8Array signature: Uint8Array maxSize: bigint context: string } export class ArrayBufferReader { constructor(private buffer: ArrayBuffer) {} next(length: number): ArrayBuffer { const res = this.buffer.slice(0, length) this.buffer = this.buffer.slice(length) return res } nextU64(): bigint { return new DataView(this.next(8)).getBigUint64(0) } nextVarint(): number { const p = new VarIntProcessor() while (!p.finished()) { p.push(new Uint8Array(this.buffer.slice(0, 1))[0]) this.buffer = this.buffer.slice(1) } const res = p.decode() if (res === null) { throw new Error("Reached EOF") } return res } nextVarstring(): string { const len = Math.min(this.nextVarint(), maxVarstringLen) return new TextDecoder().decode(this.next(len)) } } export class MerkleArchive { static readonly headerSize = 32 + // pubkey 64 + // signature 32 + // sighash 8 + // size DirectoryContents.headerSize private constructor( readonly signer: Signer, readonly contents: DirectoryContents, ) {} static async deserialize( source: Blob, context: string, header: ArrayBufferReader, commitment: MerkleArchiveCommitment | null, ): Promise { const pubkey = new Uint8Array(header.next(32)) const signature = new Uint8Array(header.next(64)) const sighash = new Uint8Array(header.next(32)) const rootMaxSizeBytes = header.next(8) const maxSize = new DataView(rootMaxSizeBytes).getBigUint64(0) if ( !ed25519ph.verify( signature, new Uint8Array( await new Blob([sighash, rootMaxSizeBytes]).arrayBuffer(), ), pubkey, { context: new TextEncoder().encode(context), zip215: true, }, ) ) { throw new Error("signature verification failed") } if (commitment) { if ( !compare( sighash, new Uint8Array(Buffer.from(commitment.rootSighash, "base64").buffer), ) ) { throw new Error("merkle root mismatch") } if (maxSize > commitment.rootMaxsize) { throw new Error("root directory max size too large") } } else if (maxSize > 1024 * 1024) { throw new Error( "root directory max size over 1MiB, cancelling download in case of DOS attack", ) } const contents = await DirectoryContents.deserialize( source, header, sighash, maxSize, ) return new MerkleArchive( { pubkey, signature, maxSize, context, }, contents, ) } } export class Entry { private constructor( readonly hash: Uint8Array, readonly size: bigint, readonly contents: EntryContents, ) {} static async deserialize( source: Blob, header: ArrayBufferReader, ): Promise { const hash = new Uint8Array(header.next(32)) const size = header.nextU64() const contents = await deserializeEntryContents(source, header, hash, size) return new Entry(new Uint8Array(hash), size, contents) } async verifiedFileContents(): Promise { if (!this.contents) { throw new Error("file is missing from archive") } if (!(this.contents instanceof FileContents)) { throw new Error("is not a regular file") } return this.contents.verified(this.hash) } } export type EntryContents = null | FileContents | DirectoryContents async function deserializeEntryContents( source: Blob, header: ArrayBufferReader, hash: Uint8Array, size: bigint, ): Promise { const typeId = new Uint8Array(header.next(1))[0] switch (typeId) { case 0: return null case 1: return FileContents.deserialize(source, header, size) case 2: return DirectoryContents.deserialize(source, header, hash, size) default: throw new Error(`Unknown type id ${typeId} found in MerkleArchive`) } }