feat(06-02): add XOR header bootstrapping and encrypted TOC support to Kotlin decoder

- Add XOR_KEY constant matching FORMAT.md Section 9.1
- Add xorHeader() function with signed byte masking (and 0xFF)
- Update decode() with XOR bootstrapping: check magic, XOR if mismatch
- Update decode() with TOC decryption: decrypt when flags bit 1 is set
- Backward compatible: plain headers and unencrypted TOC still work
This commit is contained in:
NikitolProject
2026-02-25 02:24:25 +03:00
parent 4eaedc2872
commit cef681fd13

View File

@@ -32,6 +32,15 @@ val KEY = byteArrayOf(
0xFA.toByte(), 0x84.toByte(), 0x06, 0xCD.toByte(), 0x3E, 0x79, 0xB5.toByte(), 0x50, 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 // Data classes
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -243,6 +252,23 @@ fun verifySha256(data: ByteArray, expectedSha256: ByteArray): Boolean {
return computed.contentEquals(expectedSha256) 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) // Main decode orchestration (FORMAT.md Section 10)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -261,15 +287,32 @@ fun decode(archivePath: String, outputDir: String) {
// Read 40-byte header // Read 40-byte header
val headerBytes = ByteArray(HEADER_SIZE) val headerBytes = ByteArray(HEADER_SIZE)
raf.readFully(headerBytes) 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) val header = parseHeader(headerBytes)
// Seek to TOC and read all TOC bytes // Read TOC bytes -- decrypt if TOC encryption flag is set (bit 1)
raf.seek(header.tocOffset) val entries: List<TocEntry>
val tocBytes = ByteArray(header.tocSize.toInt()) if (header.flags and 0x02 != 0) {
raf.readFully(tocBytes) // TOC is encrypted: read encrypted bytes, decrypt, then parse
raf.seek(header.tocOffset)
// Parse all TOC entries val encryptedToc = ByteArray(header.tocSize.toInt())
val entries = parseToc(tocBytes, header.fileCount) 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 var successCount = 0