1package com.peachpasswords.codex23import com.peachpasswords.vault.VaultRepository456enum class CodexStep { COMPRESS, ERASURE, AZTEC, VERIFY, PDF }78910111213object 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 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 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)) 76 }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 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}