1package com.peachpasswords.codex23import kotlinx.serialization.json.Json4import java.security.MessageDigest5import java.util.Base646import java.util.zip.Deflater7import java.util.zip.Inflater89101112131415object CodexCodec {16 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }1718 private val urlEncoder = Base64.getUrlEncoder().withoutPadding()19 private val urlDecoder = Base64.getUrlDecoder()2021 fun base64Url(bytes: ByteArray): String = urlEncoder.encodeToString(bytes)22 fun fromBase64Url(s: String): ByteArray = urlDecoder.decode(s)2324 25 fun sha256Hex(bytes: ByteArray): String {26 val digest = MessageDigest.getInstance("SHA-256").digest(bytes)27 val sb = StringBuilder(digest.size * 2)28 for (b in digest) sb.append("%02X".format(b.toInt() and 0xff))29 return sb.toString()30 }3132 fun deflate(data: ByteArray): ByteArray {33 val deflater = Deflater(Deflater.BEST_COMPRESSION).apply { setInput(data); finish() }34 val out = java.io.ByteArrayOutputStream(data.size / 2 + 32)35 val buf = ByteArray(8192)36 while (!deflater.finished()) out.write(buf, 0, deflater.deflate(buf))37 deflater.end()38 return out.toByteArray()39 }4041 fun inflate(data: ByteArray): ByteArray {42 val inflater = Inflater().apply { setInput(data) }43 val out = java.io.ByteArrayOutputStream(data.size * 3 + 32)44 val buf = ByteArray(8192)45 while (!inflater.finished()) {46 val n = inflater.inflate(buf)47 if (n == 0 && inflater.needsInput()) break 48 out.write(buf, 0, n)49 }50 inflater.end()51 return out.toByteArray()52 }5354 5556 fun planStripeShardCounts(totalDataShards: Int): IntArray {57 if (totalDataShards <= 0) return IntArray(0)58 val numStripes = maxOf(1, Math.ceil(totalDataShards.toDouble() / MAX_DATA_SHARDS_PER_STRIPE).toInt())59 val base = totalDataShards / numStripes60 val remainder = totalDataShards % numStripes61 return IntArray(numStripes) { base + if (it < remainder) 1 else 0 }62 }6364 data class Stripe(val dataShards: Int, val parityShards: Int, val shards: Array<ByteArray>) {65 val totalChunks: Int get() = dataShards + parityShards66 }6768 69 fun encodeToEnvelopes(compressed: ByteArray, verifyCode: String): Pair<List<ChunkEnvelope>, CodexLayout> {70 val totalDataShards = maxOf(1, Math.ceil(compressed.size.toDouble() / CHUNK_SIZE).toInt())71 val counts = planStripeShardCounts(totalDataShards)72 val envelopes = ArrayList<ChunkEnvelope>()73 var offset = 074 var totalData = 075 var totalParity = 076 for ((sIdx, dataShards) in counts.withIndex()) {77 val region = dataShards * CHUNK_SIZE78 val parityShards = computeParityShards(dataShards)79 val total = dataShards + parityShards80 val shards = Array(total) { ByteArray(CHUNK_SIZE) }81 for (d in 0 until dataShards) {82 val start = offset + d * CHUNK_SIZE83 val len = (compressed.size - start).coerceIn(0, CHUNK_SIZE)84 if (len > 0) System.arraycopy(compressed, start, shards[d], 0, len)85 }86 ReedSolomon.encode(shards, dataShards, parityShards)87 for (i in 0 until total) {88 envelopes.add(buildEnvelope(shards[i], i, total, i >= dataShards, verifyCode, sIdx, counts.size))89 }90 offset += region91 totalData += dataShards92 totalParity += parityShards93 }94 return envelopes to CodexLayout(counts.size, totalData, totalParity, totalData + totalParity)95 }9697 private fun buildEnvelope(98 chunk: ByteArray, index: Int, total: Int, isParity: Boolean,99 verifyCode: String, stripe: Int, stripeCount: Int,100 ): ChunkEnvelope = ChunkEnvelope(101 v = 3, c = verifyCode, s = stripe, capS = stripeCount,102 i = index, t = total, p = isParity,103 d = base64Url(chunk), h = sha256Hex(chunk).take(32),104 )105106 fun verifyChunkIntegrity(env: ChunkEnvelope): Boolean {107 if (env.h.length != 16 && env.h.length != 32) return false108 return sha256Hex(fromBase64Url(env.d)).take(env.h.length).equals(env.h, ignoreCase = true)109 }110111 112113114115116 fun reconstructCompressed(envelopes: List<ChunkEnvelope>): ByteArray {117 val good = envelopes.filter { verifyChunkIntegrity(it) }118 if (good.isEmpty()) throw CodexException(CodexErrorCode.INSUFFICIENT_CHUNKS, "no valid chunks")119 val byStripe = good.groupBy { it.s }120 val stripeCount = good.maxOf { it.capS }121 val out = java.io.ByteArrayOutputStream()122 for (sIdx in 0 until stripeCount) {123 val chunks = byStripe[sIdx]124 ?: throw CodexException(CodexErrorCode.INSUFFICIENT_CHUNKS, "stripe $sIdx missing entirely")125 val total = chunks.first().t126 val (dataShards, parityShards) = deriveShardCounts(total)127 val shards = Array(total) { ByteArray(0) }128 val present = BooleanArray(total)129 for (env in chunks) {130 if (env.i in 0 until total) {131 shards[env.i] = fromBase64Url(env.d)132 present[env.i] = true133 }134 }135 ReedSolomon.reconstruct(shards, dataShards, parityShards, present)136 for (d in 0 until dataShards) out.write(shards[d])137 }138 return out.toByteArray()139 }140}141142data class CodexLayout(val stripeCount: Int, val dataChunks: Int, val parityChunks: Int, val totalChunks: Int)