class Fastlane::Actions::MatchAndroidKeystoreAction

Public Class Methods

authors() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 309
def self.authors
  ["Christopher NEY"]
end
available_options() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 331
def self.available_options
  [
    FastlaneCore::ConfigItem.new(key: :git_url,
                             env_name: "MATCH_KEYSTORE_GIT_URL",
                          description: "The URL of the Git repository (Github, BitBucket...)",
                             optional: false,
                                 type: String),
    FastlaneCore::ConfigItem.new(key: :package_name,
                             env_name: "MATCH_KEYSTORE_PACKAGE_NAME",
                          description: "The package name of the App",
                             optional: false,
                                 type: String),
    FastlaneCore::ConfigItem.new(key: :apk_path,
                             env_name: "MATCH_KEYSTORE_APK_PATH",
                          description: "Path of the APK file to sign",
                             optional: true,
                                 type: String),
    FastlaneCore::ConfigItem.new(key: :match_secret,
                             env_name: "MATCH_KEYSTORE_SECRET",
                          description: "Secret to decrypt keystore.properties file (CI)",
                             optional: true,
                                 type: String),
    FastlaneCore::ConfigItem.new(key: :existing_keystore,
                             env_name: "MATCH_KEYSTORE_EXISTING",
                          description: "Path of an existing Keystore",
                             optional: true,
                                 type: String),
    FastlaneCore::ConfigItem.new(key: :override_keystore,
                             env_name: "MATCH_KEYSTORE_OVERRIDE",
                          description: "Override an existing Keystore (false by default)",
                             optional: true,
                                 type: Boolean),
    FastlaneCore::ConfigItem.new(key: :keystore_data,
                             env_name: "MATCH_KEYSTORE_JSON_PATH",
                          description: "Required data to import an existing keystore, or create a new one",
                             optional: true,
                                 type: String)
  ]
end
check_openssl_version() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 70
def self.check_openssl_version
  output = `openssl version`
  if !output.start_with?("OpenSSL")
    raise "Please install OpenSSL 1.1.1 at least https://www.openssl.org/"
  end
  UI.message("OpenSSL version: " + output.strip)
end
decrypt_file(encrypt_file, clear_file, key_path) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 88
def self.decrypt_file(encrypt_file, clear_file, key_path)
  `rm -f '#{clear_file}'`
  `openssl enc -d -aes-256-cbc -pbkdf2 -in '#{encrypt_file}' -out '#{clear_file}' -pass file:'#{key_path}'`
end
description() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 305
def self.description
  "Easily sync your Android keystores across your team"
end
details() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 326
def self.details
  # Optional:
  "This way, your entire team can use the same account and have one code signing identity without any manual work or confusion."
end
encrypt_file(clear_file, encrypt_file, key_path) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 83
def self.encrypt_file(clear_file, encrypt_file, key_path)
  `rm -f '#{encrypt_file}'`
  `openssl enc -aes-256-cbc -salt -pbkdf2 -in '#{clear_file}' -out '#{encrypt_file}' -pass file:'#{key_path}'`
end
gen_key(key_path, password) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 78
def self.gen_key(key_path, password)
  `rm -f '#{key_path}'`
  `echo "#{password}" | openssl dgst -sha512 | awk '{print $2}' | cut -c1-128 > '#{key_path}'`
end
get_android_home() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 48
def self.get_android_home
  `rm -f android_home.txt`
  `echo $ANDROID_HOME > android_home.txt`
  data = File.read("android_home.txt")
  android_home = data.strip
  `rm -f android_home.txt`
  android_home
end
get_build_tools() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 57
def self.get_build_tools
  android_home = self.get_android_home()
  build_tools_root = File.join(android_home, '/build-tools')

  sub_dirs = Dir.glob(File.join(build_tools_root, '*', ''))
  build_tools_last_version = ''
  for sub_dir in sub_dirs
    build_tools_last_version = sub_dir
  end

  build_tools_last_version
end
get_file_content(file_path) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 93
def self.get_file_content(file_path)
  data = File.read(file_path)
  data
end
is_supported?(platform) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 371
def self.is_supported?(platform)
  # Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example)
  # See: https://docs.fastlane.tools/advanced/#control-configuration-by-lane-and-by-platform
  [:android].include?(platform)
end
load_json(json_path) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 24
def self.load_json(json_path)
  file = File.read(json_path)
  data_hash = JSON.parse(file)
  data_hash
end
load_properties(properties_filename) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 30
def self.load_properties(properties_filename)
  properties = {}
  File.open(properties_filename, 'r') do |properties_file|
    properties_file.read.each_line do |line|
      line.strip!
      if (line[0] != ?# and line[0] != ?=)
        i = line.index('=')
        if (i)
          properties[line[0..i - 1].strip] = line[i + 1..-1].strip
        else
          properties[line] = ''
        end
      end
    end      
  end
  properties
end
output() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 317
def self.output
  [
    ['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.'],
    ['MATCH_KEYSTORE_APK_SIGNED', 'Path of the signed APK.'],
    ['MATCH_KEY_PASSWORD', 'keystore password'],
    ['MATCH_ALIAS_PASSWORD', 'alias password']
  ]
end
prompt2(params) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 98
def self.prompt2(params)
  # UI.message("prompt2: #{params[:value]}")
  if params[:value].to_s.empty?
    return_value = other_action.prompt(text: params[:text], secure_text: params[:secure_text], ci_input: params[:ci_input])
  else
    return_value = params[:value]
  end
  return_value
end
return_value() click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 313
def self.return_value
  "Prepare Keystore local path, alias name, and passwords for the specified App."
end
run(params) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 108
def self.run(params)

  # Get input parameters:
  git_url = params[:git_url]
  package_name = params[:package_name]
  existing_keystore = params[:existing_keystore]
  match_secret = params[:match_secret]
  override_keystore = params[:override_keystore]
  keystore_data = params[:keystore_data]

  # Init constants:
  keystore_name = 'keystore.jks'
  properties_name = 'keystore.properties'
  keystore_info_name = 'keystore.txt'
  properties_encrypt_name = 'keystore.properties.enc'

  # Check Android Home env:
  android_home = self.get_android_home()
  UI.message("Android SDK: #{android_home}")
  if android_home.to_s.strip.empty?
    raise "The environment variable ANDROID_HOME is not defined, or Android SDK is not installed!"
  end

  # Check OpenSSL:
  self.check_openssl_version

  # Init workign local directory:
  dir_name = ENV['HOME'] + '/.match_keystore'
  unless File.directory?(dir_name)
    UI.message("Creating '.match_keystore' working directory...")
    FileUtils.mkdir_p(dir_name)
  end

  # Init 'security password' for AES encryption:
  key_name = "#{self.to_md5(git_url)}.hex"
  key_path = File.join(dir_name, key_name)
  # UI.message(key_path)
  if !File.file?(key_path)
    security_password = self.prompt2(text: "Security password: ", secure_text: true, value: match_secret)
    if security_password.to_s.strip.empty?
      raise "Security password is not defined! Please use 'match_secret' parameter for CI."
    end
    UI.message "Generating security key '#{key_name}'..."
    self.gen_key(key_path, security_password)
  end

  # Check is 'security password' is well initialized:
  tmpkey = self.get_file_content(key_path).strip
  if tmpkey.length == 128
    UI.message "Security key '#{key_name}' initialized"
  else
    raise "The security key '#{key_name}' is malformed, or not initialized!"
  end

  # Create repo directory to sync remote Keystores repository:
  repo_dir = File.join(dir_name, self.to_md5(git_url))
  # UI.message(repo_dir)
  unless File.directory?(repo_dir)
    UI.message("Creating 'repo' directory...")
    FileUtils.mkdir_p(repo_dir)
  end

  # Cloning GIT remote repository:
  gitDir = File.join(repo_dir, '/.git')
  unless File.directory?(gitDir)
    UI.message("Cloning remote Keystores repository...")
    puts ''
    `git clone #{git_url} #{repo_dir}`
    puts ''
  end

  # Create sub-directory for Android app:
  if package_name.to_s.strip.empty?
    raise "Package name is not defined!"
  end
  keystoreAppDir = File.join(repo_dir, package_name)
  unless File.directory?(keystoreAppDir)
    UI.message("Creating '#{package_name}' keystore directory...")
    FileUtils.mkdir_p(keystoreAppDir)
  end

  keystore_path = File.join(keystoreAppDir, keystore_name)
  properties_path = File.join(keystoreAppDir, properties_name)
  properties_encrypt_path = File.join(keystoreAppDir, properties_encrypt_name)

  # Load parameters from JSON for CI or Unit Tests:
  if keystore_data != nil && File.file?(keystore_data)
    data_json = self.load_json(keystore_data)
    data_key_password = data_json['key_password']
    data_alias_name = data_json['alias_name']
    data_alias_password = data_json['alias_password']
    data_full_name = data_json['full_name']
    data_org_unit = data_json['org_unit']
    data_org = data_json['org']
    data_city_locality = data_json['city_locality']
    data_state_province = data_json['state_province']
    data_country = data_json['country']
  end

  # Create keystore with command
  override_keystore = !existing_keystore.to_s.strip.empty? && File.file?(existing_keystore)
  if !File.file?(keystore_path) || override_keystore 

    if File.file?(keystore_path)
      FileUtils.remove_dir(keystore_path)
    end

    key_password = self.prompt2(text: "Keystore Password: ", value: data_key_password)
    if key_password.to_s.strip.empty?
      raise "Keystore Password is not definined!"
    end
    alias_name = self.prompt2(text: "Keystore Alias name: ", value: data_alias_name)
    if alias_name.to_s.strip.empty?
      raise "Keystore Alias name is not definined!"
    end
    alias_password = self.prompt2(text: "Keystore Alias password: ", value: data_alias_password)
    if alias_password.to_s.strip.empty?
      raise "Keystore Alias password is not definined!"
    end

    # https://developer.android.com/studio/publish/app-signing
    if !File.file?(existing_keystore)
      UI.message("Generating Android Keystore...")
      
      full_name = self.prompt2(text: "Certificate First and Last Name: ", value: data_full_name)
      org_unit = self.prompt2(text: "Certificate Organisation Unit: ", value: data_org_unit)
      org = self.prompt2(text: "Certificate Organisation: ", value: data_org)
      city_locality = self.prompt2(text: "Certificate City or Locality: ", value: data_city_locality) 
      state_province = self.prompt2(text: "Certificate State or Province: ", value: data_state_province)
      country = self.prompt2(text: "Certificate Country Code (XX): ", value: data_country)
      
      keytool_parts = [
        "keytool -genkey -v",
        "-keystore '#{keystore_path}'",
        "-alias '#{alias_name}'",
        "-keyalg RSA -keysize 2048 -validity 10000",
        "-storepass '#{alias_password}'",
        "-keypass '#{key_password}'",
        "-dname \"CN=#{full_name}, OU=#{org_unit}, O=#{org}, L=#{city_locality}, S=#{state_province}, C=#{country}\"",
      ]
      sh keytool_parts.join(" ")
    else
      UI.message("Copy existing keystore to match_keystore repository...") 
      `cp #{existing_keystore} #{keystore_path}`
    end

    UI.message("Generating Keystore properties...")
   
    if File.file?(properties_path)
      FileUtils.remove_dir(properties_path)
    end
  
    # Build URL:
    store_file = git_url + '/' + package_name + '/' + keystore_name

    out_file = File.new(properties_path, "w")
    out_file.puts("keyFile=#{store_file}")
    out_file.puts("keyPassword=#{key_password}")
    out_file.puts("aliasName=#{alias_name}")
    out_file.puts("aliasPassword=#{alias_password}")
    out_file.close

    self.encrypt_file(properties_path, properties_encrypt_path, key_path)
    File.delete(properties_path)

    # Print Keystore data in repo:
    keystore_info_path = File.join(keystoreAppDir, keystore_info_name)
    `yes "" | keytool -list -v -keystore '#{keystore_path}' -storepass '#{key_password}' > '#{keystore_info_path}'`
    
    UI.message("Upload new Keystore to remote repository...")
    puts ''
    `cd '#{repo_dir}' && git add .`
    `cd '#{repo_dir}' && git commit -m "[ADD] Keystore for app '#{package_name}'."`
    `cd '#{repo_dir}' && git push`
    puts ''

  else  
    UI.message "Keystore file already exists, continue..."

    self.decrypt_file(properties_encrypt_path, properties_path, key_path)

    properties = self.load_properties(properties_path)
    key_password = properties['keyPassword']
    alias_name = properties['aliasName']
    alias_password = properties['aliasPassword']

    File.delete(properties_path)
  end

  # Prepare contect shared values for next lanes:
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
  Actions.lane_context[SharedValues::MATCH_KEY_PASSWORD] = key_password
  Actions.lane_context[SharedValues::MATCH_ALIAS_PASSWORD] = alias_password

end
to_md5(value) click to toggle source
# File lib/fastlane/plugin/match_android_keystore/actions/match_android_keystore_action.rb, line 19
def self.to_md5(value)
  hash_value = Digest::MD5.hexdigest value
  hash_value
end