96e60825c044
fix: cap CodexCodec.inflate at 16 MiB against decompression bombs (v2.10.18)
- time
- 2026-08-03 23:45 UTC · 2 months ago
- machine
- koh2
- parent
- 6eb341d43801
↓ peach-android-96e60825.tar.gzatlas at this save
files changed4 files
VERSION+1 −1app/src/main/java/com/peachpasswords/codex/CodexCodec.kt+14 −0app/src/main/java/com/peachpasswords/codex/CodexTypes.kt+1 −0app/src/test/java/com/peachpasswords/codex/CodexCodecTest.kt+17 −0app/src/main/java/com/peachpasswords/codex/CodexCodec.kt+14 −0
@@ -18,6 +18,13 @@1818 private val urlEncoder = Base64.getUrlEncoder().withoutPadding()1919 private val urlDecoder = Base64.getUrlDecoder()2020 21+ /**22+ * F4: decompression bomb guard — mirrors PeachPayloadCodec's23+ * MAX_SECTION_RAW_BYTES (16 MiB). A codex payload is the same vault JSON24+ * the PeachScript sections carry, so the cap is identical by definition.25+ */26+ const val MAX_INFLATED_BYTES = 16L * 1024 * 102427+2128 fun base64Url(bytes: ByteArray): String = urlEncoder.encodeToString(bytes)2229 fun fromBase64Url(s: String): ByteArray = urlDecoder.decode(s)2330 @@ -46,6 +53,13 @@4653 val n = inflater.inflate(buf)4754 if (n == 0 && inflater.needsInput()) break // trailing zero-pad reached4855 out.write(buf, 0, n)56+ if (out.size() > MAX_INFLATED_BYTES) {57+ inflater.end()58+ throw CodexException(59+ CodexErrorCode.PAYLOAD_TOO_LARGE,60+ "Inflated payload exceeds the ${MAX_INFLATED_BYTES / (1024 * 1024)} MiB cap",61+ )62+ }4963 }5064 inflater.end()5165 return out.toByteArray()app/src/main/java/com/peachpasswords/codex/CodexTypes.kt+1 −0
@@ -86,6 +86,7 @@8686 VAULT_LOCKED, VAULT_EMPTY, COMPRESSION_ROUNDTRIP_FAILED, ERASURE_VERIFICATION_FAILED,8787 AZTEC_GENERATION_FAILED, INSUFFICIENT_CHUNKS, RECONSTRUCTION_VERIFY_CODE_MISMATCH,8888 RECONSTRUCTION_FINGERPRINT_MISMATCH, PDF_GENERATION_FAILED, INVALID_VAULT_BLOB, RESTORE_FAILED,89+ PAYLOAD_TOO_LARGE,8990 }9091 9192 class CodexException(val code: CodexErrorCode, message: String) : Exception(message)app/src/test/java/com/peachpasswords/codex/CodexCodecTest.kt+17 −0
@@ -60,4 +60,21 @@6060 val tampered = envelopes.first().copy(d = CodexCodec.base64Url("tampered".toByteArray()))6161 assertTrue(!CodexCodec.verifyChunkIntegrity(tampered))6262 }63+64+ @Test fun inflate_exactly_at_the_cap_round_trips() {65+ val data = ByteArray(CodexCodec.MAX_INFLATED_BYTES.toInt()) // zeros compress to ~17 KB66+ assertArrayEquals(data, CodexCodec.inflate(CodexCodec.deflate(data)))67+ }68+69+ @Test fun inflate_refuses_decompression_bombs() {70+ val bomb = CodexCodec.deflate(ByteArray(CodexCodec.MAX_INFLATED_BYTES.toInt() + 1))71+ assertTrue("bomb compresses tiny", bomb.size < 64 * 1024)72+ val error = try {73+ CodexCodec.inflate(bomb)74+ null75+ } catch (e: CodexException) {76+ e77+ }78+ assertEquals(CodexErrorCode.PAYLOAD_TOO_LARGE, error?.code)79+ }6380 }