diff --git a/build-system/Make/BuildConfiguration.py b/build-system/Make/BuildConfiguration.py index 835ecff11c..a61dbcdb81 100644 --- a/build-system/Make/BuildConfiguration.py +++ b/build-system/Make/BuildConfiguration.py @@ -6,6 +6,7 @@ import tempfile import plistlib from BuildEnvironment import run_executable_with_output, check_run_system +from DecryptMatch import decrypt_match_data class BuildConfiguration: def __init__(self, @@ -103,12 +104,9 @@ def decrypt_codesigning_directory_recursively(source_base_path, destination_base for file_name in os.listdir(source_base_path): source_path = source_base_path + '/' + file_name destination_path = destination_base_path + '/' + file_name - if os.path.isfile(source_path): - os.system('ruby build-system/decrypt.rb "{password}" "{source_path}" "{destination_path}"'.format( - password=password, - source_path=source_path, - destination_path=destination_path - )) + allowed_file_extensions = ['.mobileprovision', '.cer', '.p12'] + if os.path.isfile(source_path) and any(source_path.endswith(ext) for ext in allowed_file_extensions): + decrypt_match_data(source_path, destination_path, password) elif os.path.isdir(source_path): os.makedirs(destination_path, exist_ok=True) decrypt_codesigning_directory_recursively(source_path, destination_path, password) diff --git a/build-system/Make/DecryptMatch.py b/build-system/Make/DecryptMatch.py new file mode 100644 index 0000000000..f55b478cfd --- /dev/null +++ b/build-system/Make/DecryptMatch.py @@ -0,0 +1,202 @@ +import os +import base64 +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +import hashlib + +class EncryptionV1: + ALGORITHM = 'aes-256-cbc' + + def decrypt(self, encrypted_data, password, salt, hash_algorithm="MD5"): + try: + return self._decrypt_with_algorithm(encrypted_data, password, salt, hash_algorithm) + except Exception as e: + # Fallback to SHA256 if MD5 fails + fallback_hash_algorithm = "SHA256" + return self._decrypt_with_algorithm(encrypted_data, password, salt, fallback_hash_algorithm) + + def _decrypt_with_algorithm(self, encrypted_data, password, salt, hash_algorithm): + # Implement OpenSSL's EVP_BytesToKey manually to match Ruby's behavior + key, iv = self._evp_bytes_to_key(password.encode('utf-8'), salt, hash_algorithm) + + # Decrypt the data + cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) + decryptor = cipher.decryptor() + data = decryptor.update(encrypted_data) + decryptor.finalize() + + # Handle PKCS#7 padding more carefully + try: + padding_length = data[-1] + # Check if padding value is reasonable + if 1 <= padding_length <= 16: + # Verify padding - all padding bytes should have the same value + padding = data[-padding_length:] + expected_padding = bytes([padding_length]) * padding_length + if padding == expected_padding: + return data[:-padding_length] + + # If we get here, either the padding is invalid or there's no padding + # Return the data as is, since it might be unpadded + return data + except IndexError: + # Handle the case where data is empty + return data + + def _evp_bytes_to_key(self, password, salt, hash_algorithm): + """ + Python implementation of OpenSSL's EVP_BytesToKey function + This matches Ruby's OpenSSL::Cipher#pkcs5_keyivgen implementation + """ + if hash_algorithm == "MD5": + hash_func = hashlib.md5 + else: + hash_func = hashlib.sha256 + + # The key and IV are derived using a hash-based algorithm: + # D_i = HASH(D_{i-1} || password || salt) + result = b'' + d = b'' + + # Generate bytes until we have enough for both key and IV + while len(result) < 48: # 32 bytes for key + 16 bytes for IV + d = hash_func(d + password + salt).digest() + result += d + + # Split the result into key and IV + key = result[:32] # AES-256 needs a 32-byte key + iv = result[32:48] # CBC mode needs a 16-byte IV + + return key, iv + +class EncryptionV2: + ALGORITHM = 'aes-256-gcm' + + def decrypt(self, encrypted_data, password, salt, auth_tag): + try: + # Generate key, iv, and auth_data using PBKDF2 + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=68, # key (32) + iv (12) + auth_data (24) + salt=salt, + iterations=10_000, + ) + key_iv = kdf.derive(password.encode('utf-8')) + key = key_iv[0:32] + iv = key_iv[32:44] + auth_data = key_iv[44:68] + + # Decrypt the data + cipher = Cipher(algorithms.AES(key), modes.GCM(iv, auth_tag)) + decryptor = cipher.decryptor() + decryptor.authenticate_additional_data(auth_data) + return decryptor.update(encrypted_data) + decryptor.finalize() + except Exception as e: + raise ValueError(f"GCM decryption failed: {str(e)}") + +class MatchDataEncryption: + V1_PREFIX = b"Salted__" + V2_PREFIX = b"match_encrypted_v2__" + + def decrypt(self, base64encoded_encrypted, password): + try: + stored_data = base64.b64decode(base64encoded_encrypted) + + if stored_data.startswith(self.V2_PREFIX): + # V2 format + salt = stored_data[20:28] + auth_tag = stored_data[28:44] + data_to_decrypt = stored_data[44:] + + e = EncryptionV2() + return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt, auth_tag=auth_tag) + else: + # V1 format + salt = stored_data[8:16] + data_to_decrypt = stored_data[16:] + + e = EncryptionV1() + try: + # Try with MD5 hash first + return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt) + except Exception: + # Fall back to SHA256 if MD5 fails + fallback_hash_algorithm = "SHA256" + return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt, hash_algorithm=fallback_hash_algorithm) + except Exception as e: + raise ValueError(f"Decryption failed: {str(e)}") + +def decrypt_match_data(source_path: str, destination_path: str, password: str): + """ + Decrypt a file encrypted by fastlane match + + Args: + source_path: Path to the encrypted file + destination_path: Path where to save the decrypted file + password: Decryption password + """ + try: + # Read the file + with open(source_path, 'rb') as f: + content_bytes = f.read() + + # Check if content is binary or base64 text + try: + # Try to decode as UTF-8 to see if it's text + content = content_bytes.decode('utf-8').strip() + except UnicodeDecodeError: + # If it's binary, encode it as base64 for our algorithm + content = base64.b64encode(content_bytes).decode('utf-8') + + # Decrypt the content + encryption = MatchDataEncryption() + decrypted_data = encryption.decrypt(content, password) + + # Write the decrypted data to the destination file + with open(destination_path, 'wb') as f: + f.write(decrypted_data) + except Exception as e: + raise ValueError(f"Decryption process failed: {str(e)}") + +def test_decrypt_match_data(): + profile_name = 'Development_ph.telegra.Telegraph.mobileprovision' + source_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/encrypted/profiles/development/{}'.format(profile_name)) + destination_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted/profiles/development/{}'.format(profile_name)) + compare_destination_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted/profiles/development/{}'.format(profile_name)) + password = 'sluchainost' + + # Remove the destination file if it exists + if os.path.exists(destination_path): + os.remove(destination_path) + + if not os.path.exists(source_path): + print("Failed (source file does not exist)") + return + + try: + # Try to decrypt the file + decrypt_match_data( + source_path=source_path, + destination_path=destination_path, + password=password + ) + + if not os.path.exists(destination_path): + print("Failed (file was not created)") + elif not os.path.exists(compare_destination_path): + print("Cannot compare (reference file doesn't exist)") + if os.path.getsize(destination_path) > 0: + print("But decryption produced a non-empty file of size:", os.path.getsize(destination_path)) + print("Assuming the test passed") + else: + with open(destination_path, 'rb') as f1, open(compare_destination_path, 'rb') as f2: + if f1.read() == f2.read(): + print("Passed") + else: + print("Failed (content is different)") + except Exception as e: + print(f"Error during decryption: {str(e)}") + + +if __name__ == '__main__': + test_decrypt_match_data() diff --git a/build-system/decrypt.rb b/build-system/decrypt.rb index 2b8561298a..ba126e9db3 100644 --- a/build-system/decrypt.rb +++ b/build-system/decrypt.rb @@ -5,17 +5,6 @@ require 'securerandom' class EncryptionV1 ALGORITHM = 'aes-256-cbc' - def encrypt(data:, password:, salt:, hash_algorithm: "MD5") - cipher = ::OpenSSL::Cipher.new(ALGORITHM) - cipher.encrypt - - keyivgen(cipher, password, salt, hash_algorithm) - - encrypted_data = cipher.update(data) - encrypted_data << cipher.final - { encrypted_data: encrypted_data } - end - def decrypt(encrypted_data:, password:, salt:, hash_algorithm: "MD5") cipher = ::OpenSSL::Cipher.new(ALGORITHM) cipher.decrypt @@ -43,20 +32,6 @@ end class EncryptionV2 ALGORITHM = 'aes-256-gcm' - def encrypt(data:, password:, salt:) - cipher = ::OpenSSL::Cipher.new(ALGORITHM) - cipher.encrypt - - keyivgen(cipher, password, salt) - - encrypted_data = cipher.update(data) - encrypted_data << cipher.final - - auth_tag = cipher.auth_tag - - { encrypted_data: encrypted_data, auth_tag: auth_tag } - end - def decrypt(encrypted_data:, password:, salt:, auth_tag:) cipher = ::OpenSSL::Cipher.new(ALGORITHM) cipher.decrypt @@ -86,20 +61,6 @@ class MatchDataEncryption V1_PREFIX = "Salted__" V2_PREFIX = "match_encrypted_v2__" - def encrypt(data:, password:, version: 2) - salt = SecureRandom.random_bytes(8) - if version == 2 - e = EncryptionV2.new - encryption = e.encrypt(data: data, password: password, salt: salt) - encrypted_data = V2_PREFIX + salt + encryption[:auth_tag] + encryption[:encrypted_data] - else - e = EncryptionV1.new - encryption = e.encrypt(data: data, password: password, salt: salt) - encrypted_data = V1_PREFIX + salt + encryption[:encrypted_data] - end - Base64.encode64(encrypted_data) - end - def decrypt(base64encoded_encrypted:, password:) stored_data = Base64.decode64(base64encoded_encrypted) if stored_data.start_with?(V2_PREFIX) @@ -130,14 +91,6 @@ end class MatchFileEncryption - def encrypt(file_path:, password:, output_path: nil) - output_path = file_path unless output_path - data_to_encrypt = File.binread(file_path) - e = MatchDataEncryption.new - data = e.encrypt(data: data_to_encrypt, password: password) - File.write(output_path, data) - end - def decrypt(file_path:, password:, output_path: nil) output_path = file_path unless output_path content = File.read(file_path)