package com.peachpasswords.codex import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import kotlin.random.Random class CodexCodecTest { @Test fun rs_recovers_dropped_shards() { val data = 6 val parity = computeParityShards(data) // 2 val size = 40 val rng = Random(7) val original = Array(data) { ByteArray(size) { rng.nextInt().toByte() } } val shards = Array(data + parity) { i -> if (i < data) original[i].copyOf() else ByteArray(size) } ReedSolomon.encode(shards, data, parity) // Drop `parity` shards (the max tolerable) — here drop 2 data shards. val present = BooleanArray(data + parity) { true } shards[1] = ByteArray(0); present[1] = false shards[4] = ByteArray(0); present[4] = false ReedSolomon.reconstruct(shards, data, parity, present) for (i in 0 until data) assertArrayEquals("shard $i", original[i], shards[i]) } @Test fun gf_mul_div_are_inverse() { for (a in 1..255) for (b in 1..255) { assertEquals(a, Galois.div(Galois.mul(a, b), b)) } } @Test fun full_codec_round_trip() { val payload = CodexPayload( version = 2, verifyCode = "PX-AB12-CD34", vaultBlob = "x".repeat(5000), timestamp = 111, entryCount = 7, vaultFingerprint = "DEADBEEF".repeat(4), generatedAt = 222, ) val bytes = CodexCodec.json.encodeToString(CodexPayload.serializer(), payload).toByteArray() val compressed = CodexCodec.deflate(bytes) assertArrayEquals(bytes, CodexCodec.inflate(compressed)) val (envelopes, layout) = CodexCodec.encodeToEnvelopes(compressed, payload.verifyCode) assertTrue(envelopes.all { CodexCodec.verifyChunkIntegrity(it) }) assertEquals(layout.dataChunks + layout.parityChunks, envelopes.size) // Lose the maximum tolerable codes in stripe 0, then rebuild. val parity = layout.parityChunks / layout.stripeCount val survivors = envelopes.toMutableList() repeat(parity) { survivors.removeAt(0) } // drop first `parity` data shards val recovered = CodexCodec.reconstructCompressed(survivors) val parsed = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(recovered))) assertEquals(payload, parsed) } @Test fun corrupted_chunk_fails_integrity() { val (envelopes, _) = CodexCodec.encodeToEnvelopes(CodexCodec.deflate("hello world".toByteArray()), "PX-0000-0000") val tampered = envelopes.first().copy(d = CodexCodec.base64Url("tampered".toByteArray())) assertTrue(!CodexCodec.verifyChunkIntegrity(tampered)) } @Test fun inflate_exactly_at_the_cap_round_trips() { val data = ByteArray(CodexCodec.MAX_INFLATED_BYTES.toInt()) // zeros compress to ~17 KB assertArrayEquals(data, CodexCodec.inflate(CodexCodec.deflate(data))) } /** * PAI-2026-002: the per-chunk hash covers `d` only, so a hostile code * passes verifyChunkIntegrity with a tiny payload and any `t`/`S`/`i` it * likes. Every case below used to reach Array(t) before being rejected. */ private fun hostileChunk( total: Int = 4, index: Int = 0, stripe: Int = 0, stripeCount: Int = 1, data: ByteArray = ByteArray(CHUNK_SIZE), verifyCode: String = "PX-AB12-CD34", ): ChunkEnvelope = ChunkEnvelope( v = 3, c = verifyCode, s = stripe, capS = stripeCount, i = index, t = total, p = index >= total - 2, d = CodexCodec.base64Url(data), h = CodexCodec.sha256Hex(data).take(32), ) @Test fun hostile_shard_total_is_rejected_before_allocation() { val hostile = hostileChunk(total = Int.MAX_VALUE) assertTrue("geometry is rejected", !CodexCodec.isValidEnvelope(hostile)) val error = try { CodexCodec.reconstructCompressed(listOf(hostile)) null } catch (e: CodexException) { e } assertEquals(CodexErrorCode.INSUFFICIENT_CHUNKS, error?.code) } @Test fun out_of_range_shard_and_stripe_indices_are_rejected() { assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(total = 4, index = 4))) assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(total = 4, index = -1))) assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(stripe = 2, stripeCount = 2))) assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(stripeCount = 0))) assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(stripeCount = 100_000))) assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(total = 256))) } @Test fun oversized_chunk_data_is_rejected() { // One byte past the ceiling, and far past a full-range Aztec code's capacity. assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(data = ByteArray(4097)))) assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(data = ByteArray(64 * 1024)))) assertTrue(CodexCodec.isValidEnvelope(hostileChunk())) } /** * Cross-platform: the browser's decoder derives the shard size from the * first scanned chunk so that Codexes printed before the current * [CHUNK_SIZE] (the previous value was 800 bytes) still restore, and it * enforces no chunk length of its own. Android's Reed-Solomon decoder * derives shard size the same way. Pinning the chunk length to the current * generator's would turn every such printed Codex into an unreadable one. */ @Test fun a_codex_printed_with_an_older_chunk_size_still_restores() { val legacyChunkSize = 800 val payload = ByteArray(legacyChunkSize * 2) { (it % 251).toByte() } // t = 4 inverts to 2 data + 2 parity shards; every shard is present, so // reconstruction is just the concatenation of the two data shards. val shards = listOf( payload.copyOfRange(0, legacyChunkSize), payload.copyOfRange(legacyChunkSize, legacyChunkSize * 2), ByteArray(legacyChunkSize), ByteArray(legacyChunkSize), ) val envelopes = shards.mapIndexed { i, bytes -> ChunkEnvelope( v = 3, c = "PX-AB12-CD34", s = 0, capS = 1, i = i, t = 4, p = i >= 2, d = CodexCodec.base64Url(bytes), h = CodexCodec.sha256Hex(bytes).take(32), ) } assertTrue("legacy envelopes are accepted", envelopes.all { CodexCodec.isValidEnvelope(it) }) assertArrayEquals(payload, CodexCodec.reconstructCompressed(envelopes)) } /** * Bounding each envelope is not enough on its own: a hostile Codex can * simply carry many valid stripes. The whole reconstruction is budgeted * against the inflate cap, which it can never legitimately exceed. */ @Test fun a_reconstruction_past_the_inflate_cap_is_refused() { val chunk = ByteArray(4096) // the largest chunk a single envelope may carry val stripeCount = 20 // 20 × 230 × 4096 is past the 16 MiB inflate cap val envelopes = (0 until stripeCount).flatMap { s -> (0 until 230).map { i -> ChunkEnvelope( v = 3, c = "PX-AB12-CD34", s = s, capS = stripeCount, i = i, t = 230, p = false, d = CodexCodec.base64Url(chunk), h = CodexCodec.sha256Hex(chunk).take(32), ) } } val error = try { CodexCodec.reconstructCompressed(envelopes); null } catch (e: CodexException) { e } assertEquals(CodexErrorCode.PAYLOAD_TOO_LARGE, error?.code) } @Test fun malformed_verify_code_is_rejected() { assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(verifyCode = "PX-AB12-CD3"))) assertTrue(CodexCodec.isValidEnvelope(hostileChunk())) } @Test fun mixed_verify_codes_and_shard_totals_are_rejected() { val (envelopes, _) = CodexCodec.encodeToEnvelopes( CodexCodec.deflate("hello codex".toByteArray()), "PX-AB12-CD34", ) val other = CodexCodec.encodeToEnvelopes( CodexCodec.deflate("hello codex".toByteArray()), "PX-ZZ99-YY88", ).first.first() var error = try { CodexCodec.reconstructCompressed(envelopes + other); null } catch (e: CodexException) { e } assertEquals(CodexErrorCode.INVALID_ENVELOPE, error?.code) val mixedTotal = envelopes.first().copy(t = envelopes.first().t + 1, i = 0) error = try { CodexCodec.reconstructCompressed(listOf(envelopes.first(), mixedTotal)); null } catch (e: CodexException) { e } assertEquals(CodexErrorCode.INVALID_ENVELOPE, error?.code) } @Test fun hostile_envelopes_are_skipped_and_real_codes_still_restore() { val (envelopes, _) = CodexCodec.encodeToEnvelopes( CodexCodec.deflate("survives hostile neighbours".toByteArray()), "PX-AB12-CD34", ) val recovered = CodexCodec.reconstructCompressed( listOf(hostileChunk(total = Int.MAX_VALUE)) + envelopes + listOf(hostileChunk(verifyCode = "nope")), ) assertEquals("survives hostile neighbours", String(CodexCodec.inflate(recovered))) } @Test fun inflate_refuses_decompression_bombs() { val bomb = CodexCodec.deflate(ByteArray(CodexCodec.MAX_INFLATED_BYTES.toInt() + 1)) assertTrue("bomb compresses tiny", bomb.size < 64 * 1024) val error = try { CodexCodec.inflate(bomb) null } catch (e: CodexException) { e } assertEquals(CodexErrorCode.PAYLOAD_TOO_LARGE, error?.code) } /** * PB-26-004: a malformed compressed stream that returns 0 bytes without * finishing or needing input must not loop — it must fail as CORRUPTED. * A truncated stream (valid header, cut off mid-block) triggers exactly * this: the inflater has input but cannot make progress. */ @Test fun inflate_rejects_malformed_no_progress_stream() { val valid = CodexCodec.deflate("hello world".toByteArray()) // Truncate past the zlib header so the inflater has data but can't finish. val truncated = valid.copyOf(valid.size / 2) val error = try { CodexCodec.inflate(truncated) null } catch (e: CodexException) { e } assertEquals(CodexErrorCode.CORRUPTED, error?.code) } }