Some important clues from your output:
Decrypted data size is 383774 bytes, but encrypted input length is 383776 bytes.
That means decrypted data is 2 bytes shorter than ciphertext.
This usually happens if decryption includes padding removal (PKCS7 padding), which trims the padding bytes.
Possible issues & next steps:
1. File includes a 2-byte header before actual ciphertext (or footer)
Your skipping logic currently skipped 0 bytes since ciphertext length was divisible by 16.
However, earlier you noted protected files are exactly 2 bytes longer than unprotected ones.
This suggests you might need to skip 2 bytes (a salt or header) before decryption.
That would make ciphertext length 383774 (original - 2), matching decrypted size and divisible by 16.
2. Try skipping 2 bytes explicitly
Change the skip bytes logic to prioritize 2 bytes if possible.

Quick fix:
Replace in your code:


skip_bytes = None
for skip in range(0, 17, 2):
    candidate = file_data[skip:]
    if len(candidate) % 16 == 0:
        print(f"[*] Found ciphertext aligned to AES block size by skipping {skip} bytes")
        skip_bytes = skip
        break
with:


skip_bytes = None
# First try skipping 2 bytes first (based on your original finding)
if (len(file_data) - 2) % 16 == 0:
    skip_bytes = 2
    print("[*] Found ciphertext aligned by skipping 2 bytes (based on file size difference)")
else:
    for skip in range(0, 17, 2):
        candidate = file_data[skip:]
        if len(candidate) % 16 == 0:
            print(f"[*] Found ciphertext aligned to AES block size by skipping {skip} bytes")
            skip_bytes = skip
            break

if skip_bytes is None:
    print("[!] WARNING: No ciphertext alignment found by skipping 0 to 16 bytes. Trying full file.")
    skip_bytes = 0
Run the script now and check:
Does it skip 2 bytes?
Is the output better? (no more Brotli errors)
Explain why this works:
The packer adds 2 bytes at file start as salt or header
The encryption itself starts after these 2 bytes
The encrypted portion must be block aligned (multiple of 16)
Skipping these extra 2 bytes is essential for decrypting correct ciphertext
If it still fails:
Please share:

Full first ~64 bytes hex dump of your .halva2 file
Unprotected vs protected first 8-16 bytes hex diff
This helps to ensure correct header handling.

Final note:
If skip=2 fixes length and data streams, your problem is solved.

You can then also try both IV variants (with/without ivkey) on that data.

If you want, I can update the full script accordingly with forced 2-byte skip priority. Just ask!

update the full script accordingly with forced 2-byte skip priority

Certainly! Below is the full updated script that prioritizes skipping 2 bytes assuming that your protected file has a 2-byte extra header before AES-encrypted ciphertext. If skipping 2 bytes results in a ciphertext length divisible by 16, it uses that; otherwise it falls back to the earlier logic.

This script will try all IV key variants (including empty, user-supplied, upper and lower case, and password as IV key) on the properly aligned ciphertext.

Save this as unpack_halva2_auto_skip_and_iv.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import argparse
import brotli
import tarfile
from io import BytesIO
from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Util.Padding import unpad
from Cryptodome.Hash import SHA512

def derive_key_and_iv(password: str, ivkey: str, use_ivkey: bool):
    password_bytes = password.encode('utf-8')
    ivkey_bytes = ivkey.encode('utf-8') if ivkey else b""

    hashCode = SHA512.new(password_bytes).digest()
    if use_ivkey and ivkey:
        hashIV = SHA512.new(ivkey_bytes).digest()
    else:
        hashIV = SHA512.new(hashCode).digest()

    key = PBKDF2(password_bytes, hashCode, dkLen=32, count=50000, hmac_hash_module=SHA512)
    iv = PBKDF2(ivkey_bytes if use_ivkey else b"", hashIV, dkLen=16, count=50000, hmac_hash_module=SHA512)
    return key, iv

def aes_decrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted = cipher.decrypt(data)
    try:
        return unpad(decrypted, AES.block_size)
    except ValueError:
        print("[!] Warning: Padding unpad failed. Using raw decrypted data.")
        return decrypted

def try_decrypt_and_decompress(encrypted_data: bytes, password: str, ivkey: str, use_ivkey: bool):
    key, iv = derive_key_and_iv(password, ivkey, use_ivkey)
    iv_mode = "with IV key" if use_ivkey else "without IV key (default IV from password)"
    print(f"[*] Trying decryption {iv_mode} (IV key='{ivkey}')...")
    print(f"    Key: {key.hex()}")
    print(f"    IV:  {iv.hex()}")
    decrypted = aes_decrypt(encrypted_data, key, iv)
    print(f"    Decrypted data size: {len(decrypted)} bytes")
    print(f"    Decrypted data start (32 bytes): {decrypted[:32].hex()}")

    try:
        decompressed = brotli.decompress(decrypted)
        print(f"    Brotli decompression succeeded, size: {len(decompressed)} bytes")
        return decompressed
    except brotli.error as e:
        print(f"    Brotli decompression failed: {e}")
        return None

def extract_tar(decompressed_data: bytes, output_dir: str):
    os.makedirs(output_dir, exist_ok=True)
    try:
        with tarfile.open(fileobj=BytesIO(decompressed_data)) as tar:
            tar.extractall(path=output_dir)
        print(f"[+] Extraction successful! Files extracted to: {output_dir}")
        return True
    except tarfile.TarError as e:
        print(f"[!] TAR extraction failed: {e}")
        return False

def main():
    parser = argparse.ArgumentParser(
        description="Auto-detect ciphertext start by skipping bytes and try multiple IV key variants to decrypt and unpack .halva2",
        formatter_class=argparse.RawTextHelpFormatter
    )
    parser.add_argument("archive_file", help="Path to .halva2 archive")
    parser.add_argument("--password", "-p", required=True, help="Password for decryption")
    parser.add_argument("--ivkey", "-i", default="", help="IV key used for decryption (optional)")
    parser.add_argument("--output", "-o", default=None, help="Output dir (default: <archive>_unpacked)")

    args = parser.parse_args()

    if not os.path.isfile(args.archive_file):
        print(f"[!] ERROR: File '{args.archive_file}' not found.")
        return

    with open(args.archive_file, "rb") as f:
        file_data = f.read()

    print(f"Full file length: {len(file_data)} bytes")

    skip_bytes = None
    # Priority 1: skip 2 bytes if that yields block-size aligned ciphertext
    if (len(file_data) - 2) % 16 == 0:
        skip_bytes = 2
        print("[*] Found ciphertext aligned to AES block size by skipping 2 bytes (based on file size difference)")
    else:
        # Otherwise try skipping 0, 2, 4... bytes until length %16 == 0
        for skip in range(0, 17, 2):
            candidate = file_data[skip:]
            if len(candidate) % 16 == 0:
                print(f"[*] Found ciphertext aligned to AES block size by skipping {skip} bytes")
                skip_bytes = skip
                break

    if skip_bytes is None:
        print("[!] WARNING: No ciphertext alignment found by skipping 0 to 16 bytes. Using full file length.")
        skip_bytes = 0

    encrypted_data = file_data[skip_bytes:]
    print(f"Using encrypted data length {len(encrypted_data)} bytes after skipping {skip_bytes} bytes")

    output_base = args.output
    if output_base is None:
        base = os.path.splitext(os.path.basename(args.archive_file))[0]
        output_base = base + "_unpacked"

    ivkey_variants = [
        args.ivkey,
        "",
        args.ivkey.lower(),
        args.ivkey.upper(),
        args.password,
    ]

    tried = set()
    for vkey in ivkey_variants:
        vkey = vkey or ""
        if vkey in tried:
            continue
        tried.add(vkey)
        use_iv = bool(vkey)
        decompressed = try_decrypt_and_decompress(encrypted_data, args.password, vkey, use_iv)
        if decompressed:
            # Safe folder name
            safe_vkey = vkey if vkey else "ivkey_empty"
            safe_vkey = "".join(c if c.isalnum() else "_" for c in safe_vkey)
            output_dir = f"{output_base}_{safe_vkey}"
            if extract_tar(decompressed, output_dir):
                print("[*] Success with this IV key variant! Stopping attempts.")
                break
    else:
        print("[!] No IV key variant succeeded to decrypt, decompress, and extract.")

if __name__ == "__main__":
    main()
Usage

python unpack_halva2_auto_skip_and_iv.py myfile.halva2 --password 123456
Or with IV key:


python unpack_halva2_auto_skip_and_iv.py myfile.halva2 --password 123456 --ivkey myIVKey
What this version does:
Skips 2 bytes first if that aligns ciphertext length correctly, based on your file size observation.
Otherwise falls back to trying skips of 0, 2, 4, ..., 16 bytes to find valid length.
Tries all IV key variants to find a working decryption + decompression combo.
Prints helpful debug info.
Extracts to folders named by the IV key variant used.
Give this a run; this should cover your file's quirks and hopefully decrypt & extract successfully. Let me know how it goes!

python unpack_halva2.py myfile.halva2 --password 123456
Full file length: 383776 bytes
[] Found ciphertext aligned to AES block size by skipping 0 bytes
Using encrypted data length 383776 bytes after skipping 0 bytes
[] Trying decryption without IV key (default IV from password) (IV key='')...
Key: 41b56fcfee841d818bc24a7d01c7d7f933e88dfe553cb80ad8f4854c2e9125c2
IV: ab06ca12c900966ae83143e69c1fbd6a
Decrypted data size: 383774 bytes
Decrypted data start (32 bytes): b1dba3a887a99d1e97f5b55d73fb6f8a4395411bdb9b7b781d51e0edaf9a1275
Brotli decompression failed: Decompression error: b'PADDING_2'
[*] Trying decryption with IV key (IV key='123456')...
Key: 41b56fcfee841d818bc24a7d01c7d7f933e88dfe553cb80ad8f4854c2e9125c2
IV: 41b56fcfee841d818bc24a7d01c7d7f9
Decrypted data size: 383774 bytes
Decrypted data start (32 bytes): 5b680675a02d16f5f406bcc6ee2305194395411bdb9b7b781d51e0edaf9a1275
Brotli decompression failed: Decompression error: b'HUFFMAN_SPACE'
[!] No IV key variant succeeded to decrypt, decompress, and extract.

The errors and the symptoms strongly suggest the actual ciphertext does not start right at byte 0 of the file. Since skipping 0 bytes worked aligned-wise but not logically, and skipping 2 bytes didn’t happen in your last run, you should explicitly force skipping 2 bytes based on your earlier observation that protected files are exactly 2 bytes longer than unprotected.

Please try forcing skipping 2 bytes explicitly instead of the current logic:
Modify the start of your main function to this snippet to test skipping exactly 2 bytes — no fallback — and see if that fixes your problem:


with open(args.archive_file, "rb") as f:
    file_data = f.read()

print(f"Full file length: {len(file_data)} bytes")

# FORCE skip 2 bytes
skip_bytes = 2
if (len(file_data) - skip_bytes) % 16 != 0:
    print("[!] WARNING: Cipher length after skipping 2 bytes is not multiple of 16!")
encrypted_data = file_data[skip_bytes:]
print(f"Using encrypted data length {len(encrypted_data)} bytes after skipping {skip_bytes} bytes")
Then run the rest of the code unchanged.

Why force skipping 2 bytes?
Your protected file is 2 bytes longer than unprotected.
The first 2 bytes are almost certainly a salt or header prefix not included in encryption.
The encryption starts at byte offset 2.
This aligns the ciphertext length properly with AES block size (multiple of 16).
If you don’t skip these bytes, you decrypt garbage + get Brotli errors.
What to expect?
Decrypted data that can be Brotli decompressed without PADDING_2 or HUFFMAN_SPACE errors.
Successful TAR extraction.
If this works, you should then proceed to try the multiple IV key variants on this 2-bytes-skip ciphertext exactly as your current script does.
I can provide the entire updated script with forced 2-byte skip plus IV key variants incorporated if you want. Just ask!

provide the entire updated script with forced 2-byte skip plus IV key variants incorporated

Certainly! Below is the complete updated script that:

Forces skipping the first 2 bytes of the file before AES decryption, based on your protected file’s extra 2-byte header;
Tries multiple IV key variants (original ivKey, empty string, lowercase/uppercase versions, and password as IV key);
Stops and extracts as soon as it successfully decrypts, decompresses, and extracts the TAR archive.