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`) } }