match/lib/match/encrypt.rb in fastlane-2.81.0.beta.20180206010002 vs match/lib/match/encrypt.rb in fastlane-2.81.0.beta.20180207010002

- old
+ new

@@ -1,13 +1,15 @@ require_relative 'module' require_relative 'change_password' module Match class Encrypt + require 'base64' + require 'openssl' + require 'securerandom' require 'security' require 'shellwords' - require 'open3' def server_name(git_url) ["match", git_url].join("_") end @@ -44,24 +46,22 @@ Security::InternetPassword.delete(server: server_name(git_url)) end def encrypt_repo(path: nil, git_url: nil) iterate(path) do |current| - crypt(path: current, - password: password(git_url), - encrypt: true) + encrypt(path: current, + password: password(git_url)) UI.success("🔒 Encrypted '#{File.basename(current)}'") if FastlaneCore::Globals.verbose? end UI.success("🔒 Successfully encrypted certificates repo") end def decrypt_repo(path: nil, git_url: nil, manual_password: nil) iterate(path) do |current| begin - crypt(path: current, - password: manual_password || password(git_url), - encrypt: false) + decrypt(path: current, + password: manual_password || password(git_url)) rescue UI.error("Couldn't decrypt the repo, please make sure you enter the right password!") UI.user_error!("Invalid password passed via 'MATCH_PASSWORD'") if ENV["MATCH_PASSWORD"] clear_password(git_url) decrypt_repo(path: path, git_url: git_url) @@ -79,51 +79,55 @@ next if File.directory?(path) yield(path) end end - def crypt(path: nil, password: nil, encrypt: true) - if password.to_s.strip.length == 0 && encrypt - UI.user_error!("No password supplied") - end + # We encrypt with MD5 because that was the most common default value in older fastlane versions which used the local OpenSSL installation + # A more secure key and IV generation is needed in the future + # IV should be randomly generated and provided unencrypted + # salt should be randomly generated and provided unencrypted (like in the current implementation) + # key should be generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters + # Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550 + def encrypt(path: nil, password: nil) + UI.user_error!("No password supplied") if password.to_s.strip.length == 0 - tmpfile = File.join(Dir.mktmpdir, "temporary") - command = ["openssl aes-256-cbc"] - command << "-k #{password.shellescape}" - command << "-in #{path.shellescape}" - command << "-out #{tmpfile.shellescape}" - command << "-a" - command << "-d" unless encrypt + data_to_encrypt = File.read(path) + salt = SecureRandom.random_bytes(8) - _out, err, st = Open3.capture3(command.join(' ')) - success = st.success? + cipher = OpenSSL::Cipher.new('AES-256-CBC') + cipher.encrypt + cipher.pkcs5_keyivgen(password, salt, 1, "MD5") + encrypted_data = "Salted__" + salt + cipher.update(data_to_encrypt) + cipher.final - unless err.to_s.empty? - # to show an error message if something goes wrong - if FastlaneCore::Globals.verbose? - UI.error("`openssl` failed with an error:") - UI.error(err) - end + File.write(path, Base64.encode64(encrypted_data)) + rescue FastlaneCore::Interface::FastlaneError + raise + rescue => error + UI.error(error.to_s) + UI.crash!("Error encrypting '#{path}'") + end - # Ubuntu `openssl` does not fail on failure - # but at least outputs an error message - - # so we use that as indication of failure - success = false - end + # The encryption parameters in this implementations reflect the old behaviour which depended on the users' local OpenSSL version + # 1.0.x OpenSSL and earlier versions use MD5, 1.1.0c and newer uses SHA256, we try both before giving an error + def decrypt(path: nil, password: nil, hash_algorithm: "MD5") + stored_data = Base64.decode64(File.read(path)) + salt = stored_data[8..15] + data_to_decrypt = stored_data[16..-1] - UI.crash!("Error decrypting '#{path}'") unless success + decipher = OpenSSL::Cipher.new('AES-256-CBC') + decipher.decrypt + decipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm) - # On non-Mac systems (more specific Ubuntu Linux) it might take some time for the file to actually be there (see #11182). - # To try to circumvent this flakyness (in tests), we wait a bit until the file appears (max 2s) (usually only 0.1 is actually waited) - unless FastlaneCore::Helper.mac? - count = 0 - # sleep until file exists or 20*0.1s (=2s) passed - until File.exist?(tmpfile) || count == 20 - sleep(0.1) - count += 1 - end - end + decrypted_data = decipher.update(data_to_decrypt) + decipher.final - FileUtils.mv(tmpfile, path) + File.write(path, decrypted_data) + rescue => error + fallback_hash_algorithm = "SHA256" + if hash_algorithm != fallback_hash_algorithm + decrypt(path, password, fallback_hash_algorithm) + else + UI.error(error.to_s) + UI.crash!("Error decrypting '#{path}'") + end end end end