This commit is contained in:
Shiz 2021-08-11 00:12:49 +02:00
commit 920228e75b
9 changed files with 321 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
__pycache__
*.py
*.egg-info
build
dist

7
.gitmodules vendored Normal file
View File

@ -0,0 +1,7 @@
[submodule "ext/sx"]
path = ext/sx
url = https://weeaboo.software/Shiz/sx
branch = feat/sxplore
[submodule "ext/atwork"]
path = ext/atwork
url = https://github.com/Shizmob/atwork

1
atwork.py Symbolic link
View File

@ -0,0 +1 @@
ext/atwork/atwork.py

1
ext/atwork Submodule

@ -0,0 +1 @@
Subproject commit 63d8e3e619fc4514b7a6ecf48ebd1bfab94304e6

1
ext/sx Submodule

@ -0,0 +1 @@
Subproject commit 9f3f0a024bdcc12462ce1bfb65797190ca06b43c

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
pycryptodome
-e git+https://github.com/Shizmob/atwork#egg=atwork
-e git+https://weeaboo.software/Shiz/sx@feat/sxplore#egg=sx

20
setup.py Normal file
View File

@ -0,0 +1,20 @@
from setuptools import setup
setup(
name='truedecrypt',
version='0.1.0',
author='Shiz <hi@shiz.me>',
url='https://weeaboo.software/Shiz/truedecrypt',
description='Truecrypt version 4.1 - 4.3 volume decrypter',
py_modules=['truedecrypt'],
install_requires=["atwork", "sx", "pycryptodome"],
dependency_links=[
"https://github.com/Shizmob/atwork/archive/refs/heads/master.tar.gz#egg=atwork",
"https://weeaboo.software/Shiz/sx/archive/feat/sxplore.tar.gz#egg=sx",
],
entry_points={
'console_scripts': ['truedecrypt=truedecrypt:main'],
},
zip_safe=True,
)

1
sx Symbolic link
View File

@ -0,0 +1 @@
ext/sx/sx

282
truedecrypt.py Executable file
View File

@ -0,0 +1,282 @@
#!/usr/bin/env python3
from __future__ import annotations
import enum
import zlib
import hashlib
import itertools
from sx import Struct, parse
from Crypto.Cipher import AES
IV_LEN = 32
def prf_ripemd160(key: bytes, salt: bytes, nout: int) -> bytes:
return hashlib.pbkdf2_hmac('ripemd160', key, salt, 2000, nout)
def prf_sha512(key: bytes, salt: bytes, nout: int) -> bytes:
return hashlib.pbkdf2_hmac('sha512', key, salt, 1000, nout)
def prf_sha1(key: bytes, salt: bytes, nout: int) -> bytes:
return hashlib.pbkdf2_hmac('sha1', key, salt, 1000, nout)
def prf_whirlpool(key: bytes, salt: bytes, nout: int) -> bytes:
return hashlib.pbkdf2_hmac('whirlpool', key, salt, 1000, nout)
def prf_single(key: bytes, salt: bytes, nout: int, method) -> tuple[bytes, bytes]:
r = method(key, salt, nout + IV_LEN)
return r[IV_LEN:], r[:IV_LEN]
PRF_METHODS = [prf_ripemd160, prf_sha512, prf_sha1, prf_whirlpool]
def prf_all(key: bytes, salt: bytes, nout: int) -> list[tuple[bytes, bytes]]:
d = []
for method in PRF_METHODS:
key, iv = prf_single(key, salt, nout, method)
yield (method, key, iv)
def gf2_add(a: int, b: int):
""" Add two numbers in GF(2 ^ n) """
return a ^ b
def gf2_mul(a, b, mod):
""" Multiply two numbers in GF(2^n) with a given polynomial - adapted from http://www.bjrn.se/code/pytruecrypt/lrwpy.txt """
def xor_mod(n, mod):
while True:
x = n.bit_length() - mod.bit_length()
if x == 0:
n = n ^ mod
if x <= 0:
break
lower = n & ((1 << x) - 1)
n = (((n >> x) ^ mod) << x) | lower
return n
# Naively mutiply two polynomials together. Lets say a is x^8+x^3+1
# and b is x^4+x^2, then we can write this as the following pseudo code:
res = 0
a_cnt = 0
# for each term in [x^8, x^3, 1]:
while a:
b2 = b
b_cnt = 0
if a & 1:
# for each term in [x^4, x^2]:
while b2:
if b2 & 1:
# 1 << (a_cnt + b_cnt) constructs the new term
# and the xor adds it to the result modulo 2.
res ^= 1 << (a_cnt + b_cnt)
b2 >>= 1
b_cnt += 1
a >>= 1
a_cnt += 1
return xor_mod(res, mod)
def gf2b_add(a: bytes, b: bytes) -> bytes:
n = len(a)
return gf2_add(
int.from_bytes(a, byteorder='big'),
int.from_bytes(b, byteorder='big'),
).to_bytes(n, byteorder='big')
def gf2b_mul(a: bytes, b: bytes, poly: int) -> bytes:
n = len(a)
return gf2_mul(
int.from_bytes(a, byteorder='big'),
int.from_bytes(b, byteorder='big'),
poly,
).to_bytes(n, byteorder='big')
def gf2b_128_mul(a: bytes, b: bytes):
return gf2b_mul(a, b, 0x100000000000000000000000000000087) # x^128+x^7+x^2+x+1
class LRW:
""" LRW cipher mode. """
def __init__(self, cipher, tweak: bytes):
self.cipher = cipher
self.block_size = 16
self.tweak = tweak[:self.block_size]
def get_tweak(self, offset: int) -> bytes:
return gf2b_128_mul(
self.tweak,
(offset // self.block_size).to_bytes(self.block_size, byteorder='big'),
)
def encrypt(self, input: bytes, offset: int) -> bytes:
output = b''
for i in range(0, len(input), self.block_size):
tweak = self.get_tweak(offset + i)
inblk = gf2b_add(tweak, input[i:i + self.block_size])
output += gf2b_add(tweak, self.cipher.encrypt(inblk))
return output
def decrypt(self, input: bytes, offset: int) -> bytes:
output = b''
for i in range(0, len(input), self.block_size):
tweak = self.get_tweak(offset + i)
inblk = gf2b_add(tweak, input[i:i + self.block_size])
output += gf2b_add(tweak, self.cipher.decrypt(inblk))
return output
def cipher_mode_lrw(cipher, iv: bytes):
return LRW(cipher, iv)
def cipher_algo_aes(key: bytes):
return AES.new(key[:32], AES.MODE_ECB)
CIPHER_ALGOS = [cipher_algo_aes]
CIPHER_MODES = [cipher_mode_lrw]
def encrypt_single(key: bytes, iv: bytes, block: bytes, offset: int, cipher_algo, cipher_mode):
cipher = cipher_mode(cipher_algo(key), iv)
return cipher.encrypt(block, offset)
def decrypt_single(key: bytes, iv: bytes, block: bytes, offset: int, cipher_algo, cipher_mode):
cipher = cipher_mode(cipher_algo(key), iv)
return cipher.decrypt(block, offset)
def decrypt_all(key: bytes, iv: bytes, block: bytes, offset: int):
d = []
for (algo, mode) in itertools.product(CIPHER_ALGOS, CIPHER_MODES):
dec = decrypt_single(key, iv, block, offset, algo, mode)
yield (algo, mode, dec)
def crc32p(b: bytes, v: int) -> int:
return (~zlib.crc32(b, (~v) & 0xFFFFFFFF)) & 0xFFFFFFFF
class TCKeyfilePool:
STATE_SIZE = 64
KEYFILE_SIZE = 1 * 1024 * 1024
def __init__(self):
self.state = bytearray(self.STATE_SIZE)
def add(self, file) -> None:
crc = 0xFFFFFFFF
pos = 0
for _ in range(self.KEYFILE_SIZE):
b = file.read(1)
if not b:
break
crc = crc32p(b, crc)
for b in crc.to_bytes(4, byteorder='big'):
self.state[pos] = (self.state[pos] + b) & 0xff
pos += 1
pos %= len(self.state)
def apply(self, password: bytes) -> bytes:
if len(password) < len(self.state):
password = password.ljust(len(self.state), b'\x00')
return bytes((p + s) & 0xFF for (p, s) in zip(password, self.state)) + password[len(self.state):]
class TCVolumeFlags(enum.Flag):
System = (1 << 0)
InPlace = (1 << 1)
class TCVolumeHeader(Struct):
magic: Fixed(b'TRUE')
# 1.0: TrueCrypt 1.0
# 2.0: TrueCrypt 4.1
# 3.0: TrueCrypt 5.0
# 4.0: TrueCrypt 6.0
# 5.0: TrueCrypt 7.0
fmt_version: uint16be
prog_version: uint16be
key_checksum: uint32be
vol_timestamp: uint64be
hdr_timestamp: uint64be
hidvol_size: If(self.fmt_version >= 2, uint64be, Ignored(Data(8)))
vol_size: If(self.fmt_version >= 3, uint64be, Ignored(Data(8)))
master_key_off: If(self.fmt_version >= 3, uint64be, Ignored(Data(8)))
master_key_size: If(self.fmt_version >= 3, uint64be, Ignored(Data(8)))
flags: If(self.fmt_version >= 4, Enum(TCVolumeFlags, uint32be), Ignored(Data(4)))
sector_size: If(self.fmt_version >= 5, uint32be, Ignored(Data(4)))
_reserved84: Ignored(Data(120))
hdr_checksum: If(self.fmt_version >= 4, uint32be, Ignored(Data(4)))
master_iv: Data(32)
master_key: Data(224)
SECTOR_SIZE = 512
def main():
import os
import math
import argparse
import atwork
parser = argparse.ArgumentParser()
parser.add_argument('-k', '--keyfile', type=argparse.FileType('rb'), action='append')
parser.add_argument('-p', '--password')
parser.add_argument('-x', '--hidden', action='store_true')
parser.add_argument('infile', type=argparse.FileType('rb'))
parser.add_argument('outfile', type=argparse.FileType('wb'), nargs='?')
args = parser.parse_args()
with atwork.Task('generating key') as t:
key = b''
if args.password:
key += args.password.encode('utf-8')
if args.keyfile:
pool = TCKeyfilePool()
for f in args.keyfile:
pool.add(f)
key = pool.apply(key)
with atwork.Task('decrypting header') as t:
found = False
for offset in (0, -1536):
if offset < 0:
args.infile.seek(offset, os.SEEK_END)
else:
args.infile.seek(offset, os.SEEK_SET)
salt = args.infile.read(64)
hdr = args.infile.read(448)
for (prf_method, crypt_key, crypt_iv) in prf_all(key, salt, 32):
for (cipher_algo, cipher_mode, dec_hdr) in decrypt_all(crypt_key, crypt_iv, hdr, 16):
if dec_hdr.startswith(b'TRUE'):
found = True
break
if found:
break
if found:
break
else:
raise ValueError('nope')
tchdr = parse(TCVolumeHeader, dec_hdr)
t.success('hidden' if offset < 0 else 'normal')
t.message(tchdr)
if args.outfile:
CHUNK_SIZE = 0x8000
with atwork.Task('decrypting volume', total=math.ceil(tchdr.hidvol_size / CHUNK_SIZE), unit='chunk') as t:
if tchdr.hidvol_size:
t.message('hidden')
args.infile.seek(-1536 - tchdr.hidvol_size, os.SEEK_END)
else:
t.message('normal')
args.infile.seek(SECTOR_SIZE, os.SEEK_SET)
c = cipher_mode(cipher_algo(tchdr.master_key), tchdr.master_iv)
for i in range(0, tchdr.hidvol_size, CHUNK_SIZE):
dec = c.decrypt(args.infile.read(CHUNK_SIZE), 16 + i)
args.outfile.write(dec)
t.step()
if __name__ == '__main__':
main()