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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user