# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # compat7z.py # # Implement a bare-bones 7z encrypted file read/writer. Does not do compression, but # always does AES-256. Not really expecting to be able to read any 7z file, except # those we created ourselves. # import os, sys, ckcc, ngu from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex from ubinascii import crc32 from ustruct import unpack, pack, calcsize from ucollections import namedtuple from uhashlib import sha256 from uio import BytesIO def masked_crc(bits): return crc32(bits) & 0xffffffff def urandom(l): rv = bytearray(l) ckcc.rng_bytes(rv) return rv def encode_utf_16_le(s): # emulate: str.encode('utf-16-le') # by assuming ascii values if isinstance(s, str): s = s.encode() return bytes((s[i//2] if i%2==0 else 0) for i in range(len(s)*2)) def decode_utf_16_le(s): # emulate: bytes.dencode('utf-16-le') # by assuming simple ascii values if isinstance(s, str): s = s.encode() return bytes(s[i] for i in range(0, len(s), 2)).decode() ''' Size of encoding sequence depends from first byte: First_Byte Extra_Bytes Value (binary) 0xxxxxxx : ( xxxxxxx ) 10xxxxxx BYTE y[1] : ( xxxxxx << (8 * 1)) + y 110xxxxx BYTE y[2] : ( xxxxx << (8 * 2)) + y ... 1111110x BYTE y[6] : ( x << (8 * 6)) + y 11111110 BYTE y[7] : y 11111111 BYTE y[8] : y ''' def read_var64(f): # Decode their silly 64-bit encoding. first = ord(f.read(1)) if first < 128: return first elif first == 0xfe or first == 0xff: return unpack("> pos) return (x << pos) + y def write_var64(n): # write their funky 64-bit variable-width unsigned number. # up to 64 bits of uint, but typically just single bytes # cheating a little here, these aren't optimal if n < 127: return chr(n) if n < 65536: return b'\xc0' + pack(' 10000: raise ValueError("Second header too big") # FileHeader.read() always reads exactly calcsize('<6sBBL') = 12 bytes # SectionHeader.read() always reads exactly calcsize(' %r" % (fname, unpacked_size, shdr)) files.append((fname, unpacked_size)) # should be at end of file now. assert not fd.read(10) return files def add_data(self, raw): if not self.aes: # do this late, so easier to test w/ known values. self.aes = ngu.aes.CBC(True, self.key, self.iv) here = len(raw) self.pt_crc = crc32(raw, self.pt_crc) padded_len = (here + 15) & ~15 if padded_len != here: if self.padding is not None: raise ValueError() # "can't do less than a block except at end" self.padding = (padded_len - here) raw += bytes(self.padding) self.unpacked_size += here assert len(raw) % 16 == 0 self.body += self.aes.cipher(raw) def calculate_key(self, password, progress_fcn=None): # do the expected key-derivation # emulate CKeyInfo::CalculateDigest in p7zip_9.38.1/CPP/7zip/Crypto/7zAes.cpp rounds = 1 << self.rounds_pow password = encode_utf_16_le(password) result = sha256() for i in range(rounds): result.update(self.salt) result.update(password) temp = pack('> 4) & 0xf) + 1 iv_len = (second & 0xf) + 1 assert salt_len >= 16 assert iv_len >= 16 self.salt = rv.read(salt_len) self.iv = rv.read(iv_len) end_pos = rv.seek(0, 1) # .tell() is missing assert end_pos - start_pos == crypto_props_len, (end_pos, start_pos, crypto_props_len) rv = patmatch('01 00 0c', rv.getvalue()) unpacked_size = read_var64(rv) assert rv.read(1) == b'\0' rv = patmatch('08 0a 01', rv.getvalue()) expect_crc = unpack('