package com.peachpasswords.codex import com.peachpasswords.vault.VaultRepository /** Generation milestones surfaced to the UI. */ enum class CodexStep { COMPRESS, ERASURE, AZTEC, VERIFY, PDF } /** * Full Codex generation: vault → payload → deflate → Reed-Solomon stripes → * Aztec codes → A4 PDF. Each stage is a verification gate (this is the * disaster-recovery surface), mirroring the browser `generate.ts`. */ object CodexGenerator { suspend fun generate( vault: VaultRepository, now: Long, onProgress: (CodexStep, Int, Int) -> Unit = { _, _, _ -> }, ): Pair { if (!vault.isUnlocked.value) throw CodexException(CodexErrorCode.VAULT_LOCKED, "Vault must be unlocked") val entryCount = vault.getEntries().size if (entryCount == 0) throw CodexException(CodexErrorCode.VAULT_EMPTY, "Vault has no entries to back up") val vaultBlob = vault.codexVaultBlobBase64() ?: throw CodexException(CodexErrorCode.VAULT_EMPTY, "No vault to back up") val fingerprint = CodexCodec.sha256Hex(vaultBlob.toByteArray(Charsets.UTF_8)).take(32) val verifyCode = deriveVerifyCode(fingerprint) val payload = CodexPayload( version = 2, verifyCode = verifyCode, vaultBlob = vaultBlob, timestamp = vault.getVaultVersion(), entryCount = entryCount, vaultFingerprint = fingerprint, generatedAt = now, ) onProgress(CodexStep.COMPRESS, 0, 1) val payloadJson = CodexCodec.json.encodeToString(CodexPayload.serializer(), payload).toByteArray(Charsets.UTF_8) val compressed = CodexCodec.deflate(payloadJson) if (!CodexCodec.inflate(compressed).contentEquals(payloadJson)) { throw CodexException(CodexErrorCode.COMPRESSION_ROUNDTRIP_FAILED, "deflate round-trip failed") } onProgress(CodexStep.ERASURE, 0, 1) val (envelopes, layout) = CodexCodec.encodeToEnvelopes(compressed, verifyCode) onProgress(CodexStep.AZTEC, 0, envelopes.size) val aztecs = ArrayList(envelopes.size) envelopes.forEachIndexed { i, env -> val jsonStr = CodexCodec.json.encodeToString(ChunkEnvelope.serializer(), env) aztecs.add(CodexAztec.encodeMatrix(jsonStr)) onProgress(CodexStep.AZTEC, i + 1, envelopes.size) } onProgress(CodexStep.VERIFY, 0, 1) // Simulate the worst tolerated loss and confirm the payload rebuilds. verifyReconstruction(envelopes, payload) onProgress(CodexStep.PDF, 0, 1) val manifest = CodexManifest( verifyCode = verifyCode, totalChunks = layout.totalChunks, dataChunks = layout.dataChunks, parityChunks = layout.parityChunks, stripeCount = layout.stripeCount, pageCount = Math.ceil(layout.totalChunks.toDouble() / CODES_PER_PAGE).toInt(), entryCount = entryCount, vaultFingerprint = fingerprint, generatedAt = now, ) val pdf = CodexPdf.render(aztecs, manifest) if (pdf.size < 1000) throw CodexException(CodexErrorCode.PDF_GENERATION_FAILED, "PDF too small: ${pdf.size}") return pdf to manifest } private fun verifyReconstruction(envelopes: List, expected: CodexPayload) { // Drop the per-stripe maximum tolerated codes, then rebuild end-to-end. val byStripe = envelopes.groupBy { it.s } val survivors = ArrayList() for ((_, chunks) in byStripe) { val (_, parity) = deriveShardCounts(chunks.first().t) survivors.addAll(chunks.drop(parity)) // lose `parity` codes from this stripe } val compressed = CodexCodec.reconstructCompressed(survivors) val rebuilt = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(compressed))) if (rebuilt.verifyCode != expected.verifyCode) { throw CodexException(CodexErrorCode.RECONSTRUCTION_VERIFY_CODE_MISMATCH, "verify code mismatch on simulation") } if (rebuilt.vaultFingerprint != expected.vaultFingerprint) { throw CodexException(CodexErrorCode.RECONSTRUCTION_FINGERPRINT_MISMATCH, "fingerprint mismatch on simulation") } } /** "PX-XXXX-XXXX" from the vault fingerprint hex — stable per vault state. */ private fun deriveVerifyCode(fingerprintHex: String): String { val alnum = fingerprintHex.filter { it.isLetterOrDigit() }.uppercase().padEnd(8, '0').take(8) return "PX-${alnum.substring(0, 4)}-${alnum.substring(4, 8)}" } }