diff --git a/build-system/Make/BuildConfiguration.py b/build-system/Make/BuildConfiguration.py index 8407dad9c5..fb100e851a 100644 --- a/build-system/Make/BuildConfiguration.py +++ b/build-system/Make/BuildConfiguration.py @@ -104,11 +104,16 @@ def decrypt_codesigning_directory_recursively(source_base_path, destination_base source_path = source_base_path + '/' + file_name destination_path = destination_base_path + '/' + file_name if os.path.isfile(source_path): - os.system('openssl aes-256-cbc -md md5 -k "{password}" -in "{source_path}" -out "{destination_path}" -a -d 2>/dev/null'.format( + os.system('ruby build-system/decrypt.rb "{password}" "{source_path}" "{destination_path}"'.format( password=password, source_path=source_path, destination_path=destination_path )) + '''os.system('openssl aes-256-cbc -md md5 -k "{password}" -in "{source_path}" -out "{destination_path}" -a -d 2>/dev/null'.format( + password=password, + source_path=source_path, + destination_path=destination_path + ))''' 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/decrypt.rb b/build-system/decrypt.rb new file mode 100644 index 0000000000..2b8561298a --- /dev/null +++ b/build-system/decrypt.rb @@ -0,0 +1,156 @@ +require 'base64' +require 'openssl' +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 + + keyivgen(cipher, password, salt, hash_algorithm) + + data = cipher.update(encrypted_data) + data << cipher.final + end + + private + + def keyivgen(cipher, password, salt, hash_algorithm) + cipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm) + end +end + +# The newer encryption mechanism, which features a more secure key and IV generation. +# +# The IV is randomly generated and provided unencrypted. +# The salt should be randomly generated and provided unencrypted (like in the current implementation). +# The key is generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters. +# +# Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550 +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 + + keyivgen(cipher, password, salt) + + cipher.auth_tag = auth_tag + + data = cipher.update(encrypted_data) + data << cipher.final + end + + private + + def keyivgen(cipher, password, salt) + keyIv = ::OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10_000, length: 32 + 12 + 24, hash: "sha256") + key = keyIv[0..31] + iv = keyIv[32..43] + auth_data = keyIv[44..-1] + cipher.key = key + cipher.iv = iv + cipher.auth_data = auth_data + end +end + +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) + salt = stored_data[20..27] + auth_tag = stored_data[28..43] + data_to_decrypt = stored_data[44..-1] + + e = EncryptionV2.new + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, auth_tag: auth_tag) + else + salt = stored_data[8..15] + data_to_decrypt = stored_data[16..-1] + e = EncryptionV1.new + begin + # Note that we are not guaranteed to catch the decryption errors here if the password or the hash is wrong + # as there's no integrity checks. + # see https://github.com/fastlane/fastlane/issues/21663 + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt) + # With the wrong hash_algorithm, there's here 0.4% chance that the decryption failure will go undetected + rescue => _ex + # With a wrong password, there's a 0.4% chance it will decrypt garbage and not fail + fallback_hash_algorithm = "SHA256" + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, hash_algorithm: fallback_hash_algorithm) + end + end + end +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) + e = MatchDataEncryption.new + decrypted_data = e.decrypt(base64encoded_encrypted: content, password: password) + File.binwrite(output_path, decrypted_data) + end +end + + +if ARGV.length != 3 + print 'Invalid command line' +else + dec = MatchFileEncryption.new + dec.decrypt(file_path: ARGV[1], password: ARGV[0], output_path: ARGV[2]) +end