Skip to content

Commit

Permalink
rework the core decryption algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
anojht committed Feb 9, 2020
1 parent 95c6c76 commit 965c222
Showing 1 changed file with 106 additions and 87 deletions.
193 changes: 106 additions & 87 deletions syndecrypt/core.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import syndecrypt.util as util
#import util
#from util import switch
from syndecrypt.util import switch
import codecs

from Crypto.Cipher import AES
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from Cryptodome.Cipher import AES
from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.PublicKey import RSA
import hashlib

import logging
import struct
from collections import OrderedDict
import base64
import binascii

LOGGER=logging.getLogger(__name__)

Expand All @@ -29,30 +29,30 @@
# algorithm to generate key+iv from the password.

# pwd and salt must be bytes objects
def _openssl_kdf(algo, pwd, salt, key_size, iv_size, iteration):
temp = b''
def _openssl_kdf(algo, pwd, salt, key_size, iv_size):
count = 1 if salt == b'' else 1000

temp = b''
fd = temp
while len(fd) < key_size + iv_size:
try:
pwd = pwd.encode()
pwd = pwd.encode()
except AttributeError:
pass
temp = _hasher(algo, temp + pwd + salt, iteration)
pass
hashed_count_times = temp + pwd + salt
for i in range(count):
hashed_count_times = _hasher(algo, hashed_count_times)
temp = hashed_count_times
fd += temp

key = fd[0:key_size]
iv = fd[key_size:key_size+iv_size]

return key, iv

def _hasher(algo, data, iteration):
digest = data
for i in range(iteration):
h = hashlib.new(algo)
h.update(digest)
digest = h.digest()
return digest
def _hasher(algo, data):
h = hashlib.new(algo)
h.update(data)
return h.digest()

# From pyaes, since pycrypto does not implement padding

Expand All @@ -69,37 +69,34 @@ def strip_PKCS7_padding(data):


def decrypted_with_password(ciphertext, password, salt):
decryptor = _decryptor_with_keyiv(_csenc_pbkdf(password, salt, iteration=1000))
plaintext = decryptor_update(decryptor, ciphertext)
decryptor = decryptor_with_password(password, salt)
plaintext = strip_PKCS7_padding(decryptor.decrypt(ciphertext))
return plaintext

def decrypted_with_private_key(ciphertext, private_key):
return PKCS1_OAEP.new(RSA.importKey(private_key)).decrypt(ciphertext)

def decryptor_with_password(password, salt=b''):
return _decryptor_with_keyiv(_csenc_pbkdf(codecs.decode(password, 'hex_codec'), salt))
def decryptor_with_password(password, salt):
return _decryptor_with_keyiv(_csenc_pbkdf(password, salt))

def _csenc_pbkdf(password, salt, iteration=1):
def _csenc_pbkdf(password, salt):
AES_KEY_SIZE_BITS = 256
AES_IV_LENGTH_BYTES = AES.block_size
assert AES_IV_LENGTH_BYTES == 16
(key, iv) = _openssl_kdf('md5', password, salt, AES_KEY_SIZE_BITS//8, AES_IV_LENGTH_BYTES, iteration)
(key, iv) = _openssl_kdf('md5', password, salt, AES_KEY_SIZE_BITS//8, AES_IV_LENGTH_BYTES)
return (key, iv)

def _decryptor_with_keyiv(key_iv_pair):
(key, iv) = key_iv_pair
return AES.new(key, AES.MODE_CBC, iv)

def decryptor_update(decryptor, ciphertext):
return strip_PKCS7_padding(decryptor.decrypt(ciphertext))
def decrypted_with_private_key(ciphertext, private_key):
return PKCS1_OAEP.new(RSA.importKey(private_key)).decrypt(ciphertext)

def salted_hash_of(salt, data):
m = hashlib.md5()
m.update(salt.encode('ascii'))
try:
data = data.encode()
data = data.encode()
except AttributeError:
pass
pass
m.update(data)
return salt + m.hexdigest()

Expand Down Expand Up @@ -162,86 +159,108 @@ def bytes_to_bigendian_int(b):
import binascii
return int(binascii.hexlify(b), 16) if b != b'' else 0

def read_header(f):
def decode_csenc_stream(f):
MAGIC = b'__CLOUDSYNC_ENC__'

s = f.read(len(MAGIC))
if s != MAGIC:
LOGGER.error('magic should not be ' + str(s) + ' but ' + str(MAGIC))
return None
s = f.read(32)
magic_hash = hashlib.md5(MAGIC).hexdigest().encode('ascii')
if s != magic_hash:
LOGGER.error('magic hash should not be ' + str(s) + ' but ' + str(magic_hash))
return None

header = _read_object_from(f)
if header['type'] != 'metadata':
LOGGER.error('first object must have "metadata" type but found ' + header['type'])
return None
return header
for obj in _read_objects_from(f):
assert isinstance(obj, dict)
if obj['type'] == 'metadata':
for (k,v) in obj.items():
if k != 'type': yield (k,v)
elif obj['type'] == 'data':
yield (None, obj['data'])

def read_chunks(f):
while True:
chunk = _read_object_from(f)
if not chunk: return
yield chunk

def decrypt_stream(instream, outstream, password=None, private_key=None, public_key=None):

session_key = None
decryptor = None
decrypt_stream.md5_digestor = None # special kind of local variable...
expected_md5_digest = None
enc_key1_bytes = None
enc_key2_bytes = None
salt = b''
session_key_hash = None

def outstream_writer_and_md5_digestor(decompressed_chunk):
outstream.write(decompressed_chunk)
if decrypt_stream.md5_digestor != None:
decrypt_stream.md5_digestor.update(decompressed_chunk)

# create session key and decryptor
header = read_header(instream)
if not header:
LOGGER.info('failed to parse header; skipping file...')
return
# TODO: assert version and hash algo
decrypt_stream.md5_digestor = hashlib.md5()
if password != None:
# password
actual_password_hash = salted_hash_of(header['key1_hash'][:10], password)
if header['key1_hash'] != actual_password_hash:
LOGGER.warning('found key1_hash %s but expected %s', actual_password_hash, header['key1_hash'])
session_key = decrypted_with_password(base64.b64decode(header['enc_key1'].encode('ascii')), password, header['salt'].encode('ascii'))
decryptor = decryptor_with_password(session_key)
elif private_key != None and public_key != None:
# RSA
actual_public_key_hash = salted_hash_of(header['key2_hash'][:10], public_key)
if header['key2_hash'] != actual_public_key_hash:
LOGGER.warning('found key2_hash %s but expected %s', actual_public_key_hash, header['key2_hash'])
session_key = decrypted_with_private_key(base64.b64decode(header['enc_key2'].encode('ascii')), private_key)
decryptor = decryptor_with_password(session_key)
else:
raise Exception("Key material is not found")
actual_session_key_hash = salted_hash_of(header['session_key_hash'][:10], session_key)
if header['session_key_hash'] != actual_session_key_hash:
LOGGER.warning('found session_key_hash %s but expected %s', actual_session_key_hash, header['session_key_hash'])

# decrypt chunks
data = bytearray()

with util.Lz4Decompressor(decompressed_chunk_handler=outstream_writer_and_md5_digestor) as decompressor:
for chunk in read_chunks(instream):
if chunk['type'] == 'metadata':
# decrypt file
decrypted_data = decryptor_update(decryptor, bytes(data))
decompressor.write(decrypted_data)
#
expected_md5_digest = chunk['file_md5']
break
elif chunk['type'] == 'data':
data += chunk['data']
else:
raise Exception("Bad chunk found: " + str(chunk))
# verify md5
decrypted_chunk = None
for (key,value) in decode_csenc_stream(instream):
for case in switch(key):
if case('digest'):
if value != 'md5':
LOGGER.warning('found unexpected digest "%s": cannot verify checksum', value)
decrypt_stream.md5_digestor = hashlib.md5()
break
if case('enc_key1'):
enc_key1_bytes = base64.b64decode(value.encode('ascii'))
break
if case('enc_key2'):
enc_key2_bytes = base64.b64decode(value.encode('ascii'))
break
if case('key1_hash'):
if password != None:
actual_password_hash = salted_hash_of(value[:10], password)
if value != actual_password_hash:
LOGGER.warning('found key1_hash %s but expected %s', actual_password_hash, value)
break
if case('key2_hash'):
# TODO: verify some public/private key pair hash here
break
if case('salt'):
salt = value.encode('ascii')
assert isinstance(salt, bytes)
break
if case('session_key_hash'):
session_key_hash = value
break
if case('version'):
version = value
expected_version_numbers = [OrderedDict([('major',1),('minor',0)]), OrderedDict([('major',3),('minor',0)]), OrderedDict([('major',3),('minor',1)])]
if version not in expected_version_numbers:
raise Exception('found version number ' + str(value) + \
' instead of one of the expected ' + str(expected_version_numbers))
if (version['major'] > 1) != (salt != b''):
version_string = '%d.%d' % (version['major'], version['minor'])
LOGGER.warning('salt is expected in version 3+ (version: %s, salt present: %s)', version_string, (salt != b''))
break
if case(None):
if decryptor == None:
if password != None and enc_key1_bytes != None:
session_key = decrypted_with_password(enc_key1_bytes, password, salt)
elif private_key != None and enc_key2_bytes != None:
session_key = decrypted_with_private_key(enc_key2_bytes, private_key)
if session_key == None:
raise Exception('not enough information to decrypt data: need either password and enc_key1 or private key and enc_key2')
if session_key_hash == None:
LOGGER.warning('did not find session_key_hash to verify the session key')
else:
actual_session_key_hash = salted_hash_of(session_key_hash[:10], session_key)
if session_key_hash != actual_session_key_hash:
LOGGER.warning('found session_key_hash %s but expected %s', actual_session_key_hash, session_key_hash)
decryptor = decryptor_with_password(binascii.unhexlify(session_key) if salt else session_key, salt=b'')
if decrypted_chunk:
decompressor.write(decrypted_chunk)
decrypted_chunk = decryptor.decrypt(value)
break
if case('file_md5'):
expected_md5_digest = value
break
if decrypted_chunk:
decompressor.write(strip_PKCS7_padding(decrypted_chunk))

if decrypt_stream.md5_digestor != None and expected_md5_digest != None:
actual_md5_digest = decrypt_stream.md5_digestor.hexdigest()
if actual_md5_digest != expected_md5_digest:
Expand Down

0 comments on commit 965c222

Please sign in to comment.