CodexGenerator.kt

4.6 KB · raw

1package com.peachpasswords.codex23import com.peachpasswords.vault.VaultRepository45/** Generation milestones surfaced to the UI. */6enum class CodexStep { COMPRESS, ERASURE, AZTEC, VERIFY, PDF }78/**9 * Full Codex generation: vault → payload → deflate → Reed-Solomon stripes →10 * Aztec codes → A4 PDF. Each stage is a verification gate (this is the11 * disaster-recovery surface), mirroring the browser `generate.ts`.12 */13object CodexGenerator {1415    suspend fun generate(16        vault: VaultRepository,17        now: Long,18        onProgress: (CodexStep, Int, Int) -> Unit = { _, _, _ -> },19    ): Pair<ByteArray, CodexManifest> {20        if (!vault.isUnlocked.value) throw CodexException(CodexErrorCode.VAULT_LOCKED, "Vault must be unlocked")21        val entryCount = vault.getEntries().size22        if (entryCount == 0) throw CodexException(CodexErrorCode.VAULT_EMPTY, "Vault has no entries to back up")23        val vaultBlob = vault.codexVaultBlobBase64()24            ?: throw CodexException(CodexErrorCode.VAULT_EMPTY, "No vault to back up")2526        val fingerprint = CodexCodec.sha256Hex(vaultBlob.toByteArray(Charsets.UTF_8)).take(32)27        val verifyCode = deriveVerifyCode(fingerprint)28        val payload = CodexPayload(29            version = 2, verifyCode = verifyCode, vaultBlob = vaultBlob,30            timestamp = vault.getVaultVersion(), entryCount = entryCount,31            vaultFingerprint = fingerprint, generatedAt = now,32        )3334        onProgress(CodexStep.COMPRESS, 0, 1)35        val payloadJson = CodexCodec.json.encodeToString(CodexPayload.serializer(), payload).toByteArray(Charsets.UTF_8)36        val compressed = CodexCodec.deflate(payloadJson)37        if (!CodexCodec.inflate(compressed).contentEquals(payloadJson)) {38            throw CodexException(CodexErrorCode.COMPRESSION_ROUNDTRIP_FAILED, "deflate round-trip failed")39        }4041        onProgress(CodexStep.ERASURE, 0, 1)42        val (envelopes, layout) = CodexCodec.encodeToEnvelopes(compressed, verifyCode)4344        onProgress(CodexStep.AZTEC, 0, envelopes.size)45        val aztecs = ArrayList<com.google.zxing.common.BitMatrix>(envelopes.size)46        envelopes.forEachIndexed { i, env ->47            val jsonStr = CodexCodec.json.encodeToString(ChunkEnvelope.serializer(), env)48            aztecs.add(CodexAztec.encodeMatrix(jsonStr))49            onProgress(CodexStep.AZTEC, i + 1, envelopes.size)50        }5152        onProgress(CodexStep.VERIFY, 0, 1)53        // Simulate the worst tolerated loss and confirm the payload rebuilds.54        verifyReconstruction(envelopes, payload)5556        onProgress(CodexStep.PDF, 0, 1)57        val manifest = CodexManifest(58            verifyCode = verifyCode, totalChunks = layout.totalChunks,59            dataChunks = layout.dataChunks, parityChunks = layout.parityChunks,60            stripeCount = layout.stripeCount,61            pageCount = Math.ceil(layout.totalChunks.toDouble() / CODES_PER_PAGE).toInt(),62            entryCount = entryCount, vaultFingerprint = fingerprint, generatedAt = now,63        )64        val pdf = CodexPdf.render(aztecs, manifest)65        if (pdf.size < 1000) throw CodexException(CodexErrorCode.PDF_GENERATION_FAILED, "PDF too small: ${pdf.size}")66        return pdf to manifest67    }6869    private fun verifyReconstruction(envelopes: List<ChunkEnvelope>, expected: CodexPayload) {70        // Drop the per-stripe maximum tolerated codes, then rebuild end-to-end.71        val byStripe = envelopes.groupBy { it.s }72        val survivors = ArrayList<ChunkEnvelope>()73        for ((_, chunks) in byStripe) {74            val (_, parity) = deriveShardCounts(chunks.first().t)75            survivors.addAll(chunks.drop(parity)) // lose `parity` codes from this stripe76        }77        val compressed = CodexCodec.reconstructCompressed(survivors)78        val rebuilt = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(compressed)))79        if (rebuilt.verifyCode != expected.verifyCode) {80            throw CodexException(CodexErrorCode.RECONSTRUCTION_VERIFY_CODE_MISMATCH, "verify code mismatch on simulation")81        }82        if (rebuilt.vaultFingerprint != expected.vaultFingerprint) {83            throw CodexException(CodexErrorCode.RECONSTRUCTION_FINGERPRINT_MISMATCH, "fingerprint mismatch on simulation")84        }85    }8687    /** "PX-XXXX-XXXX" from the vault fingerprint hex — stable per vault state. */88    private fun deriveVerifyCode(fingerprintHex: String): String {89        val alnum = fingerprintHex.filter { it.isLetterOrDigit() }.uppercase().padEnd(8, '0').take(8)90        return "PX-${alnum.substring(0, 4)}-${alnum.substring(4, 8)}"91    }92}