View raw

import cryptography.hazmat.primitives.ciphers as ciphers
import cryptography.hazmat.backends as backends
import pathlib
import struct
import uuid
import zlib
import os

def pad(b, i):
    n = (i - (len(b) % i))
    return (b + bytes(([n] * n)))

def unpad(s):
    return s[:-s[-1]]

def read(path):
    with pathlib.Path(path).open('rb') as f:
        return f.read()

class Buffer():
    def __init__(self, s):
        self.s = s
        self.i = 0

    def take(self, n):
        s = self.peek(n)
        self.i += n
        return s

    def peek(self, n):
        return self.s[self.i:self.i + n]

    def unpack(self, fmt, n):
        return struct.unpack(fmt, self.take(n))

    def unpack_I(self):
        return self.unpack('I', 4)

class ExtractedKey():
    def __init__(self, cipher, mapping):
        self.cipher = cipher
        self.mapping = mapping

class ExtractedFile():
    def __init__(self, meta, encrypted):
        self.meta = meta
        self.encrypted = encrypted
        self.block_count = len(self.encrypted) // 128

    def decrypt(self, key):
        decryptor = key.cipher.decryptor()

        blocks = [self.encrypted[(n * 128):((n + 1) * 128)]
                  for n in range(self.block_count)]

        remapped_blocks = {}
        for i in range(self.block_count):
            remapped_blocks[key.mapping[i]] = blocks[i]

        s = b''
        for i in range(self.block_count):
            s += remapped_blocks[i]

        s = unpad(s)
        s = decryptor.update(s) + decryptor.finalize()
        s = unpad(s)
        s = zlib.decompress(s)

        return s

class Archiver():
    def __init__(self):
        print('Reading archive and keystore from disk')
        self.a = Buffer(read('a'))
        self.k = Buffer(read('k'))
        self.files = []
        self.keys = []

    def check_file_headers(self):
        if self.a.take(8) != b'L0LARCH\x00':
            raise 'Archive invalid'
        if self.k.take(8) != b'L0LKSTR\x00':
            raise 'Keystore invalid'

    def extract(self):
        self.check_file_headers()

        (file_count,) = self.a.unpack_I()
        (key_count,) = self.k.unpack_I()

        print('Found %d files' % file_count)

        if file_count != key_count:
            raise 'File and key count mismatch'

        for i in range(file_count):
            file = self.extract_next_file()
            key = self.extract_next_key()
            print("Extracting %s: bytes=%d uuid=%s" % (
                file.meta['filename'],
                file.meta['size'],
                file.meta['uuid'])
            )

            decrypted = file.decrypt(key)
            filename = file.meta['filename']
            path = os.path.dirname(filename)

            if not os.path.exists(path):
                os.makedirs(path)

            with open(filename, 'wb+') as f:
                f.write(decrypted)

            os.chmod(filename, file.meta['mode'])

    def extract_next_key(self):
        file_uuid = uuid.UUID(bytes=self.k.take(16))
        cipher_info = list(self.k.take(32))
        cipher_info.reverse()
        iv = bytes(list(cipher_info[0:16]))
        key = bytes(list(cipher_info[16:32]))
        default_backend = backends.default_backend()

        cipher = ciphers.Cipher(
            ciphers.algorithms.AES(key),
            ciphers.modes.CBC(iv),
            backend=default_backend
        )

        (block_count,) = self.k.unpack_I()

        mapping = {}
        for i in range(block_count):
            (left, right) = self.k.unpack('2I', 8)
            mapping[left] = right

        return ExtractedKey(cipher, mapping)

    def extract_next_file(self):
        meta = self.extract_meta()
        (encrypted_len,) = self.a.unpack_I()
        encrypted = self.a.take(encrypted_len)
        return ExtractedFile(meta, encrypted)

    def extract_meta(self):
        (filename_length,) = self.a.unpack_I()
        filename = self.a.take(filename_length)
        filename = filename[:-1].decode('utf-8')
        file_uuid = uuid.UUID(bytes=self.a.take(16))
        (size, mode) = self.a.unpack('2I', 2 * 4)
        (st_atime, st_mtime, st_ctime) = self.a.unpack('3d', 3 * 8)

        return {
            'filename': filename,
            'uuid': file_uuid,
            'size': size,
            'mode': mode,
            'st_atime': st_atime,
            'st_mtime': st_mtime,
            'st_ctime': st_ctime,
        }

Archiver().extract()