class Match::Encryption::OpenSSL

Attributes

keychain_name[RW]
working_directory[RW]

Public Class Methods

configure(params) click to toggle source
# File match/lib/match/encryption/openssl.rb, line 17
def self.configure(params)
  return self.new(
    keychain_name: params[:keychain_name],
    working_directory: params[:working_directory]
  )
end
new(keychain_name: nil, working_directory: nil) click to toggle source

@param keychain_name: The identifier used to store the passphrase in the Keychain @param working_directory: The path to where the certificates are stored

# File match/lib/match/encryption/openssl.rb, line 26
def initialize(keychain_name: nil, working_directory: nil)
  self.keychain_name = keychain_name
  self.working_directory = working_directory
end

Public Instance Methods

clear_password() click to toggle source

removes the password from the keychain again

# File match/lib/match/encryption/openssl.rb, line 69
def clear_password
  Security::InternetPassword.delete(server: server_name(self.keychain_name))
end
decrypt_files() click to toggle source
# File match/lib/match/encryption/openssl.rb, line 43
def decrypt_files
  files = []
  password = fetch_password!
  iterate(self.working_directory) do |current|
    files << current
    begin
      decrypt_specific_file(path: current, password: password)
    rescue => ex
      UI.verbose(ex.to_s)
      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
      self.decrypt_files # Call itself
      return
    end
    UI.success("🔓  Decrypted '#{File.basename(current)}'") if FastlaneCore::Globals.verbose?
  end
  UI.success("🔓  Successfully decrypted certificates repo")
  return files
end
encrypt_files(password: nil) click to toggle source
# File match/lib/match/encryption/openssl.rb, line 31
def encrypt_files(password: nil)
  files = []
  password ||= fetch_password!
  iterate(self.working_directory) do |current|
    files << current
    encrypt_specific_file(path: current, password: password)
    UI.success("🔒  Encrypted '#{File.basename(current)}'") if FastlaneCore::Globals.verbose?
  end
  UI.success("🔒  Successfully encrypted certificates repo")
  return files
end
store_password(password) click to toggle source
# File match/lib/match/encryption/openssl.rb, line 64
def store_password(password)
  Security::InternetPassword.add(server_name(self.keychain_name), "", password)
end

Private Instance Methods

decrypt_specific_file(path: nil, password: nil, hash_algorithm: "MD5") click to toggle source

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

# File match/lib/match/encryption/openssl.rb, line 140
def decrypt_specific_file(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]

  decipher = ::OpenSSL::Cipher.new('AES-256-CBC')
  decipher.decrypt
  decipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm)

  decrypted_data = decipher.update(data_to_decrypt) + decipher.final

  File.binwrite(path, decrypted_data)
rescue => error
  fallback_hash_algorithm = "SHA256"
  if hash_algorithm != fallback_hash_algorithm
    decrypt_specific_file(path: path, password: password, hash_algorithm: fallback_hash_algorithm)
  else
    UI.error(error.to_s)
    UI.crash!("Error decrypting '#{path}'")
  end
end
encrypt_specific_file(path: nil, password: nil) click to toggle source

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: stackoverflow.com/a/1950674/6324550

# File match/lib/match/encryption/openssl.rb, line 118
def encrypt_specific_file(path: nil, password: nil)
  UI.user_error!("No password supplied") if password.to_s.strip.length == 0

  data_to_encrypt = File.binread(path)
  salt = SecureRandom.random_bytes(8)

  # The :: is important, as there is a name clash
  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

  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
fetch_password!() click to toggle source

Access the MATCH_PASSWORD, either from ENV variable, Keychain or user input

# File match/lib/match/encryption/openssl.rb, line 88
def fetch_password!
  password = ENV["MATCH_PASSWORD"]
  unless password
    item = Security::InternetPassword.find(server: server_name(self.keychain_name))
    password = item.password if item
  end

  unless password
    if !UI.interactive?
      UI.error("Neither the MATCH_PASSWORD environment variable nor the local keychain contained a password.")
      UI.error("Bailing out instead of asking for a password, since this is non-interactive mode.")
      UI.user_error!("Try setting the MATCH_PASSWORD environment variable, or temporarily enable interactive mode to store a password.")
    else
      UI.important("Enter the passphrase that should be used to encrypt/decrypt your certificates")
      UI.important("This passphrase is specific per repository and will be stored in your local keychain")
      UI.important("Make sure to remember the password, as you'll need it when you run match on a different machine")
      password = FastlaneCore::Helper.ask_password(message: "Passphrase for Match storage: ", confirm: true)
      store_password(password)
    end
  end

  return password
end
iterate(source_path) { |path| ... } click to toggle source
# File match/lib/match/encryption/openssl.rb, line 75
def iterate(source_path)
  Dir[File.join(source_path, "**", "*.{cer,p12,mobileprovision,provisionprofile}")].each do |path|
    next if File.directory?(path)
    yield(path)
  end
end
server_name(keychain_name) click to toggle source

server name used for accessing the macOS keychain

# File match/lib/match/encryption/openssl.rb, line 83
def server_name(keychain_name)
  ["match", keychain_name].join("_")
end