diff --git a/kotlin/ArchiveDecoder.kt b/kotlin/ArchiveDecoder.kt index 11815a3..d9fe0a0 100644 --- a/kotlin/ArchiveDecoder.kt +++ b/kotlin/ArchiveDecoder.kt @@ -32,6 +32,15 @@ val KEY = byteArrayOf( 0xFA.toByte(), 0x84.toByte(), 0x06, 0xCD.toByte(), 0x3E, 0x79, 0xB5.toByte(), 0x50, ) +/** + * Fixed 8-byte XOR obfuscation key (FORMAT.md Section 9.1). + * Applied cyclically across the 40-byte header for obfuscation/de-obfuscation. + */ +val XOR_KEY = byteArrayOf( + 0xA5.toByte(), 0x3C, 0x96.toByte(), 0x0F, + 0xE1.toByte(), 0x7B, 0x4D, 0xC8.toByte() +) + // --------------------------------------------------------------------------- // Data classes // --------------------------------------------------------------------------- @@ -243,6 +252,23 @@ fun verifySha256(data: ByteArray, expectedSha256: ByteArray): Boolean { return computed.contentEquals(expectedSha256) } +// --------------------------------------------------------------------------- +// XOR header de-obfuscation (FORMAT.md Section 9.1) +// --------------------------------------------------------------------------- + +/** + * XOR-obfuscate or de-obfuscate a header buffer in-place. + * + * XOR is its own inverse, so the same function encodes and decodes. + * Applies the 8-byte XOR_KEY cyclically across the first 40 bytes. + * Uses `and 0xFF` on BOTH operands to avoid Kotlin signed byte issues. + */ +fun xorHeader(buf: ByteArray) { + for (i in 0 until minOf(buf.size, 40)) { + buf[i] = ((buf[i].toInt() and 0xFF) xor (XOR_KEY[i % 8].toInt() and 0xFF)).toByte() + } +} + // --------------------------------------------------------------------------- // Main decode orchestration (FORMAT.md Section 10) // --------------------------------------------------------------------------- @@ -261,15 +287,32 @@ fun decode(archivePath: String, outputDir: String) { // Read 40-byte header val headerBytes = ByteArray(HEADER_SIZE) raf.readFully(headerBytes) + + // XOR bootstrapping (FORMAT.md Section 10, step 2): + // Check if first 4 bytes match MAGIC; if not, attempt XOR de-obfuscation + if (!(headerBytes[0] == MAGIC[0] && headerBytes[1] == MAGIC[1] && + headerBytes[2] == MAGIC[2] && headerBytes[3] == MAGIC[3])) { + xorHeader(headerBytes) + } + val header = parseHeader(headerBytes) - // Seek to TOC and read all TOC bytes - raf.seek(header.tocOffset) - val tocBytes = ByteArray(header.tocSize.toInt()) - raf.readFully(tocBytes) - - // Parse all TOC entries - val entries = parseToc(tocBytes, header.fileCount) + // Read TOC bytes -- decrypt if TOC encryption flag is set (bit 1) + val entries: List + if (header.flags and 0x02 != 0) { + // TOC is encrypted: read encrypted bytes, decrypt, then parse + raf.seek(header.tocOffset) + val encryptedToc = ByteArray(header.tocSize.toInt()) + raf.readFully(encryptedToc) + val decryptedToc = decryptAesCbc(encryptedToc, header.tocIv, KEY) + entries = parseToc(decryptedToc, header.fileCount) + } else { + // TOC is plaintext (backward compatibility) + raf.seek(header.tocOffset) + val tocBytes = ByteArray(header.tocSize.toInt()) + raf.readFully(tocBytes) + entries = parseToc(tocBytes, header.fileCount) + } var successCount = 0