14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-kotlin-decoder | 01 | execute | 1 |
|
true |
|
|
Purpose: Provide the Android 13 extraction path (primary decoder on target device) using only javax.crypto and java.util.zip -- no native libraries or third-party dependencies. Also create a cross-validation script that proves byte-identical output against the Rust archiver.
Output: kotlin/ArchiveDecoder.kt (standalone decoder with CLI main), kotlin/test_decoder.sh (cross-validation script)
<execution_context> @/home/nick/.claude/get-shit-done/workflows/execute-plan.md @/home/nick/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-kotlin-decoder/04-RESEARCH.mdCritical references for binary format parsing
@docs/FORMAT.md @src/key.rs @src/format.rs
Task 1: Implement ArchiveDecoder.kt with full decode pipeline kotlin/ArchiveDecoder.kt Create `kotlin/ArchiveDecoder.kt` as a single self-contained Kotlin file with a `main()` function for CLI usage.IMPORTANT: Before writing ANY code, the executor MUST use Context7 to verify current Kotlin/JVM API usage:
mcp__context7__resolve-library-idfor "kotlin" to confirm byte array handling, toByte() semantics- Do NOT rely on training data -- verify
ByteBuffer,Cipher,Mac,MessageDigestAPI signatures
Structure (single file, top to bottom):
-
Imports -- Only Android SDK / JVM standard library:
java.io.ByteArrayInputStream,java.io.File,java.io.RandomAccessFilejava.nio.ByteBuffer,java.nio.ByteOrderjava.security.MessageDigestjava.util.zip.GZIPInputStreamjavax.crypto.Cipher,javax.crypto.Macjavax.crypto.spec.IvParameterSpec,javax.crypto.spec.SecretKeySpec
-
Constants:
MAGIC=byteArrayOf(0x00, 0xEA.toByte(), 0x72, 0x63)-- match FORMAT.md Section 4HEADER_SIZE= 40KEY= exact 32-byte array fromsrc/key.rs:0x7A, 0x35, 0xC1.toByte(), 0xD9.toByte(), 0x4F, 0xE8.toByte(), 0x2B, 0x6A, 0x91.toByte(), 0x0D, 0xF3.toByte(), 0x58, 0xBC.toByte(), 0x74, 0xA6.toByte(), 0x1E, 0x42, 0x8F.toByte(), 0xD0.toByte(), 0x63, 0xE5.toByte(), 0x17, 0x9B.toByte(), 0x2C, 0xFA.toByte(), 0x84.toByte(), 0x06, 0xCD.toByte(), 0x3E, 0x79, 0xB5.toByte(), 0x50,- Note: ALL byte values > 0x7F need
.toByte()cast (Kotlin signed bytes)
-
Data classes:
ArchiveHeader(version: Int, flags: Int, fileCount: Int, tocOffset: Long, tocSize: Long, tocIv: ByteArray)TocEntry(name: String, originalSize: Long, compressedSize: Long, encryptedSize: Int, dataOffset: Long, iv: ByteArray, hmac: ByteArray, sha256: ByteArray, compressionFlag: Int, paddingAfter: Int)
-
Helper functions for LE integer reading (using ByteBuffer):
readLeU16(data: ByteArray, offset: Int): Int--ByteBuffer.wrap(data, offset, 2).order(LITTLE_ENDIAN).short.toInt() and 0xFFFFreadLeU32(data: ByteArray, offset: Int): Long--ByteBuffer.wrap(data, offset, 4).order(LITTLE_ENDIAN).int.toLong() and 0xFFFFFFFFL
-
parseHeader(data: ByteArray): ArchiveHeader -- FORMAT.md Section 4:
- Verify magic bytes (data[0..3] vs MAGIC)
- Read version at offset 4, require == 1
- Read flags at offset 5, reject if bits 4-7 are non-zero
- Read file_count (u16 LE at offset 6)
- Read toc_offset (u32 LE at offset 8)
- Read toc_size (u32 LE at offset 12)
- Read toc_iv (16 bytes at offset 16)
- Use
require()for all validation failures
-
parseTocEntry(data: ByteArray, offset: Int): Pair<TocEntry, Int> -- FORMAT.md Section 5:
- Read name_length (u16 LE)
- Read name (UTF-8 bytes,
String(data, pos, nameLength, Charsets.UTF_8)) - Read fixed fields sequentially: original_size(u32), compressed_size(u32), encrypted_size(u32), data_offset(u32)
- Read iv (16 bytes via copyOfRange)
- Read hmac (32 bytes via copyOfRange)
- Read sha256 (32 bytes via copyOfRange)
- Read compression_flag (u8)
- Read padding_after (u16 LE)
- Return Pair(entry, newOffset)
- Entry size formula: 101 + name_length bytes
-
parseToc(data: ByteArray, fileCount: Int): List -- parse all entries sequentially, assert cursor == data.size at end
-
Crypto utility functions:
verifyHmac(iv: ByteArray, ciphertext: ByteArray, key: ByteArray, expectedHmac: ByteArray): BooleanMac.getInstance("HmacSHA256"), init withSecretKeySpec(key, "HmacSHA256")mac.update(iv),mac.update(ciphertext),mac.doFinal()- Compare with
contentEquals()(NOT==)
decryptAesCbc(ciphertext: ByteArray, iv: ByteArray, key: ByteArray): ByteArrayCipher.getInstance("AES/CBC/PKCS5Padding")-- NOT PKCS7Padding, NOT specify providercipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))cipher.doFinal(ciphertext)-- auto-removes PKCS7 padding
decompressGzip(compressed: ByteArray): ByteArrayGZIPInputStream(ByteArrayInputStream(compressed)).readBytes()
verifySha256(data: ByteArray, expectedSha256: ByteArray): BooleanMessageDigest.getInstance("SHA-256").digest(data)- Compare with
contentEquals()
-
decode(archivePath: String, outputDir: String) -- main decode orchestration:
- Open archive with
RandomAccessFile(archivePath, "r") - Read 40-byte header, call parseHeader()
- Seek to tocOffset, read tocSize bytes, call parseToc()
- For each TocEntry:
a. Seek to dataOffset, read encryptedSize bytes (ciphertext)
b. HMAC FIRST: call verifyHmac(). If fails: print "HMAC failed for {name}, skipping" to stderr, continue
c. Decrypt: call decryptAesCbc()
d. Decompress: if compressionFlag == 1, call decompressGzip(); else use decrypted directly
e. Verify SHA-256: call verifySha256(). If fails: print "WARNING: SHA-256 mismatch for {name}" to stderr (but still write)
f. Write output:
File(outputDir, entry.name).writeBytes(original)g. Print "Extracted: {name} ({originalSize} bytes)" to stdout - Close RandomAccessFile
- Error handling: HMAC failure skips file (continue), SHA-256 mismatch warns but writes (matching Rust behavior from Phase 2 decisions)
- Open archive with
-
main(args: Array) -- CLI entry:
- Usage:
java -jar ArchiveDecoder.jar <archive> <output_dir> - Validate args.size == 2
- Create output directory if not exists:
File(args[1]).mkdirs() - Call decode(args[0], args[1])
- Print summary: "Done: {N} files extracted"
- Usage:
Critical anti-patterns to avoid (from RESEARCH.md):
- Do NOT use BouncyCastle provider ("BC") -- deprecated on Android
- Do NOT use
==for ByteArray comparison -- usecontentEquals() - Do NOT forget
.toByte()for hex literals > 0x7F - Do NOT manually remove PKCS7 padding --
cipher.doFinal()handles it - Do NOT decrypt before HMAC verification -- verify HMAC FIRST
- Do NOT try to decompress when
compressionFlag == 0grep -c "contentEquals" kotlin/ArchiveDecoder.kt | xargs test 4 -le && grep -c "PKCS5Padding" kotlin/ArchiveDecoder.kt | xargs test 1 -le && grep -c "HmacSHA256" kotlin/ArchiveDecoder.kt | xargs test 1 -le && grep "0x7A" kotlin/ArchiveDecoder.kt | grep -q "0x50" && echo "PASS: Key structural checks" || echo "FAIL" Verify ArchiveDecoder.kt contains: (1) magic byte check, (2) HMAC verification before decryption, (3) correct key bytes matching src/key.rs, (4) PKCS5Padding not PKCS7Padding, (5) contentEquals for all byte comparisons kotlin/ArchiveDecoder.kt exists with 200+ lines, contains complete decode pipeline: header parsing, TOC parsing, HMAC-then-decrypt flow, gzip decompression, SHA-256 verification, CLI main(). Uses only javax.crypto + java.util.zip + java.nio -- zero third-party dependencies.
-
Checks prerequisites:
- Verify
kotlincis available (or print install instructions and exit) - Verify
javais available - Verify
cargois available - Build Rust archiver if needed:
cargo build --release -q
- Verify
-
Compile Kotlin decoder:
kotlinc kotlin/ArchiveDecoder.kt -include-runtime -d kotlin/ArchiveDecoder.jar 2>&1- Check exit code
-
Test case 1: Single text file
- Create temp dir
- Create
hello.txtwith content "Hello, World!" - Pack:
./target/release/encrypted_archive pack hello.txt -o test1.archive - Decode:
java -jar kotlin/ArchiveDecoder.jar test1.archive output1/ - Verify:
sha256sumof extracted file matches original - Print PASS/FAIL
-
Test case 2: Multiple files with mixed content
- Create
text.txtwith multiline UTF-8 text (including Cyrillic characters) - Create
binary.binwithdd if=/dev/urandom bs=1024 count=10 - Pack both files:
./target/release/encrypted_archive pack text.txt binary.bin -o test2.archive - Decode:
java -jar kotlin/ArchiveDecoder.jar test2.archive output2/ - Verify SHA-256 of each extracted file matches original
- Print PASS/FAIL for each file
- Create
-
Test case 3: Already-compressed file (APK-like, --no-compress)
- Create
fake.apkwith random bytes - Pack with no-compress:
./target/release/encrypted_archive pack --no-compress fake.apk -o test3.archive - Decode:
java -jar kotlin/ArchiveDecoder.jar test3.archive output3/ - Verify byte-identical
- Print PASS/FAIL
- Create
-
Test case 4: Empty file
- Create empty
empty.txt(touch) - Pack:
./target/release/encrypted_archive pack empty.txt -o test4.archive - Decode:
java -jar kotlin/ArchiveDecoder.jar test4.archive output4/ - Verify empty output file
- Print PASS/FAIL
- Create empty
-
Summary:
- Print total PASS/FAIL count
- Clean up temp files
- Exit with 0 if all pass, 1 if any fail
Script should:
- Use
set -euo pipefailat the top - Use absolute paths relative to
$SCRIPT_DIR(dirname of script) - Create all temp files in a temp directory cleaned up on exit (trap)
- Use colored output (green PASS, red FAIL) if terminal supports it
- Be executable (chmod +x header with #!/usr/bin/env bash)
Note: The script requires kotlinc and java to be installed. It should detect their absence and print helpful installation instructions (e.g., sdk install kotlin or apt install default-jdk) rather than failing cryptically.
bash -n kotlin/test_decoder.sh && test -x kotlin/test_decoder.sh && grep -q "sha256sum" kotlin/test_decoder.sh && grep -q "kotlinc" kotlin/test_decoder.sh && echo "PASS: Script is valid bash and contains key commands" || echo "FAIL"
Review test_decoder.sh covers: text file, binary file, Cyrillic names, no-compress mode, empty file. Check it verifies SHA-256 of each extracted file against original.
kotlin/test_decoder.sh exists, is executable, contains 5+ test cases covering text/binary/Cyrillic/no-compress/empty files, creates archives with Rust CLI, decodes with Kotlin JAR, and verifies byte-identical output via SHA-256 comparison.
<success_criteria>
- ArchiveDecoder.kt is a complete, standalone Kotlin decoder that can extract files from archives produced by the Rust archiver
- Decoder uses only Android SDK built-in libraries (KOT-01, KOT-02)
- HMAC verification happens before decryption for every file (KOT-03)
- SHA-256 checksum verification happens after decompression (KOT-04)
- Cross-validation test script is ready to run when kotlinc/java are installed </success_criteria>