fastlane-plugin-flint 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/flint_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ class FlintChangePasswordAction < FlintAction
7
+ def self.run(params)
8
+ params.load_configuration_file('Flintfile')
9
+
10
+ FastlaneCore::PrintTable.print_values(config: params,
11
+ hide_keys: [:workspace],
12
+ title: "Summary for flint #{Fastlane::VERSION}")
13
+
14
+ Flint::ChangePassword.update(params: params)
15
+ UI.success("Successfully changed the password. Make sure to update the password on all your clients and servers")
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/flint_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ class FlintNukeAction < FlintAction
7
+ def self.run(params)
8
+ params.load_configuration_file('Flintfile')
9
+
10
+ Flint::Nuke.new.run(params, type: 'development')
11
+ Flint::Nuke.new.run(params, type: 'release')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,59 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/git_helper'
3
+ require_relative '../helper/encrypt'
4
+
5
+ module Fastlane
6
+ module Actions
7
+ class FlintSetupAction < FlintAction
8
+ def self.run(params)
9
+ containing = FastlaneCore::Helper.fastlane_enabled_folder_path
10
+ path = File.join(containing, "Flintfile")
11
+
12
+ if File.exist?(path)
13
+ FastlaneCore::UI.user_error!("You already have a Flintfile in this directory (#{path})")
14
+ return 0
15
+ end
16
+
17
+ template = File.read("#{Flint::ROOT}/assets/FlintfileTemplate")
18
+
19
+ UI.important("Please create a new, private git repository")
20
+ UI.important("to store the keystores there")
21
+
22
+ url = UI.input("URL of the Git Repo: ")
23
+
24
+ template.gsub!("[[GIT_URL]]", url)
25
+ File.write(path, template)
26
+ UI.success("Successfully created '#{path}'. You can open the file using a code editor.")
27
+
28
+ UI.important("Please mopdify build.gradle file (usually under app/build.gradle):")
29
+ UI.message("Add before `android {` line:")
30
+ UI.message(" // Load keystore")
31
+ UI.message(" def keystorePropertiesFile = rootProject.file(\"keystore.properties\");")
32
+ UI.message(" def keystoreProperties = new Properties()")
33
+ UI.message(" keystoreProperties.load(new FileInputStream(keystorePropertiesFile))")
34
+ UI.message("Add after the closing bracket for `defaultConfig {`:")
35
+ UI.message(" signingConfigs {")
36
+ UI.message(" development {")
37
+ UI.message(" storeFile file(keystoreProperties['storeFile'])")
38
+ UI.message(" storePassword keystoreProperties['storePassword']")
39
+ UI.message(" keyAlias keystoreProperties['keyAlias']")
40
+ UI.message(" keyPassword keystoreProperties['keyPassword']")
41
+ UI.message(" }")
42
+ UI.message(" release {")
43
+ UI.message(" storeFile file(keystoreProperties['storeFile'])")
44
+ UI.message(" storePassword keystoreProperties['storePassword']")
45
+ UI.message(" keyAlias keystoreProperties['keyAlias']")
46
+ UI.message(" keyPassword keystoreProperties['keyPassword']")
47
+ UI.message(" }")
48
+ UI.message(" }")
49
+ UI.important("This will load the appropriate keystore during release builds")
50
+
51
+ UI.important("You can now run `fastlane flint type:development` and `fastlane flint type:release`")
52
+ UI.message("On the first run for each environment it will create the keystore for you.")
53
+ UI.message("From then on, it will automatically import the existing keystores.")
54
+ UI.message("For more information visit https://docs.fastlane.tools/actions/flint/")
55
+
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,67 @@
1
+ require_relative 'encrypt'
2
+ require_relative 'git_helper'
3
+ require_relative 'generator'
4
+
5
+ module Fastlane
6
+ module Flint
7
+ # These functions should only be used while in (UI.) interactive mode
8
+ class ChangePassword
9
+ def self.update(params: nil, from: nil, to: nil)
10
+ ensure_ui_interactive
11
+ from ||= ChangePassword.ask_password(message: "Old passphrase for Git Repo: ", confirm: false)
12
+ to ||= ChangePassword.ask_password(message: "New passphrase for Git Repo: ", confirm: true)
13
+ GitHelper.clear_changes
14
+ encrypt = Encrypt.new
15
+ workspace = GitHelper.clone(params[:git_url],
16
+ params[:shallow_clone],
17
+ manual_password: from,
18
+ skip_docs: params[:skip_docs],
19
+ branch: params[:git_branch],
20
+ git_full_name: params[:git_full_name],
21
+ git_user_email: params[:git_user_email],
22
+ clone_branch_directly: params[:clone_branch_directly],
23
+ encrypt: encrypt)
24
+ encrypt.clear_password(params[:git_url])
25
+ encrypt.store_password(params[:git_url], to)
26
+
27
+ if params[:app_identifier].kind_of?(Array)
28
+ app_identifiers = params[:app_identifier]
29
+ else
30
+ app_identifiers = params[:app_identifier].to_s.split(/\s*,\s*/).uniq
31
+ end
32
+ app_identifier = app_identifiers[0].gsub! '.', '_'
33
+
34
+ for cert_type in Flint.environments do
35
+ alias_name = "%s-%s" % [app_identifier, cert_type]
36
+ keystore_name = "%s.keystore" % alias_name
37
+ Flint::Generator.update_keystore_password(workspace, keystore_name, alias_name, from, to)
38
+ end
39
+
40
+ message = "[fastlane] Changed passphrase"
41
+ GitHelper.commit_changes(workspace, message, params[:git_url], params[:git_branch], nil, encrypt)
42
+ end
43
+
44
+ def self.ask_password(message: "Passphrase for Git Repo: ", confirm: true)
45
+ ensure_ui_interactive
46
+ loop do
47
+ password = UI.password(message)
48
+ if confirm
49
+ password2 = UI.password("Type passphrase again: ")
50
+ if password == password2
51
+ return password
52
+ end
53
+ else
54
+ return password
55
+ end
56
+ UI.error("Passphrases differ. Try again")
57
+ end
58
+ end
59
+
60
+ def self.ensure_ui_interactive
61
+ raise "This code should only run in interactive mode" unless UI.interactive?
62
+ end
63
+
64
+ private_class_method :ensure_ui_interactive
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,100 @@
1
+ require 'commander'
2
+
3
+ require 'fastlane_core/configuration/configuration'
4
+ require_relative 'nuke'
5
+ require_relative 'git_helper'
6
+ require_relative 'change_password'
7
+ require_relative 'encrypt'
8
+
9
+ HighLine.track_eof = false
10
+
11
+ module Flint
12
+ class CommandsGenerator
13
+ include Commander::Methods
14
+
15
+ def run
16
+
17
+ global_option('--verbose') { FastlaneCore::Globals.verbose = true }
18
+
19
+ command :run do |c|
20
+ c.syntax = 'fastlane flint'
21
+ c.description = Flint::DESCRIPTION
22
+
23
+ FastlaneCore::CommanderGenerator.new.generate(Flint::Options.available_options, command: c)
24
+
25
+ c.action do |args, options|
26
+ if args.count > 0
27
+ FastlaneCore::UI.user_error!("Please run `fastlane flint [type]`, allowed values: development or release")
28
+ end
29
+
30
+ params = FastlaneCore::Configuration.create(Flint::Options.available_options, options.__hash__)
31
+ params.load_configuration_file("Flintfile")
32
+ Flint::Runner.new.run(params)
33
+ end
34
+ end
35
+
36
+ Flint.environments.each do |type|
37
+ command type do |c|
38
+ c.syntax = "fastlane flint #{type}"
39
+ c.description = "Run flint for a #{type} profile"
40
+
41
+ FastlaneCore::CommanderGenerator.new.generate(Flint::Options.available_options, command: c)
42
+
43
+ c.action do |args, options|
44
+ params = FastlaneCore::Configuration.create(Flint::Options.available_options, options.__hash__)
45
+ params.load_configuration_file("Flintfile") # this has to be done *before* overwriting the value
46
+ params[:type] = type.to_s
47
+ Flint::Runner.new.run(params)
48
+ end
49
+ end
50
+ end
51
+
52
+ command :decrypt do |c|
53
+ c.syntax = "fastlane flint decrypt"
54
+ c.description = "Decrypts the repository and keeps it on the filesystem"
55
+
56
+ FastlaneCore::CommanderGenerator.new.generate(Flint::Options.available_options, command: c)
57
+
58
+ c.action do |args, options|
59
+ params = FastlaneCore::Configuration.create(Flint::Options.available_options, options.__hash__)
60
+ params.load_configuration_file("Flintfile")
61
+ encrypt = Encrypt.new
62
+ decrypted_repo = Flint::GitHelper.clone(params[:git_url],
63
+ params[:shallow_clone],
64
+ branch: params[:git_branch],
65
+ clone_branch_directly: params[:clone_branch_directly],
66
+ encrypt: encrypt)
67
+ UI.success("Repo is at: '#{decrypted_repo}'")
68
+ end
69
+ end
70
+
71
+ command "nuke" do |c|
72
+ # We have this empty command here, since otherwise the normal `flint` command will be executed
73
+ c.syntax = "fastlane flint nuke"
74
+ c.description = "Delete all keystores"
75
+ c.action do |args, options|
76
+ FastlaneCore::UI.user_error!("Please run `fastlane flint nuke [type], allowed values: development and release.")
77
+ end
78
+ end
79
+
80
+ ["development", "release"].each do |type|
81
+ command "nuke #{type}" do |c|
82
+ c.syntax = "fastlane flint nuke #{type}"
83
+ c.description = "Delete all keystores of the type #{type}"
84
+
85
+ FastlaneCore::CommanderGenerator.new.generate(Flint::Options.available_options, command: c)
86
+
87
+ c.action do |args, options|
88
+ params = FastlaneCore::Configuration.create(Flint::Options.available_options, options.__hash__)
89
+ params.load_configuration_file("Flintfile")
90
+ Flint::Nuke.new.run(params, type: type.to_s)
91
+ end
92
+ end
93
+ end
94
+
95
+ default_command(:run)
96
+
97
+ run!
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,138 @@
1
+ require_relative 'change_password'
2
+
3
+ module Fastlane
4
+ module Flint
5
+ class Encrypt
6
+ require 'base64'
7
+ require 'openssl'
8
+ require 'securerandom'
9
+ require 'shellwords'
10
+
11
+ def initialize()
12
+ # Keep the password in the memory so we can reuse it later on
13
+ @tmp_password = nil
14
+ end
15
+
16
+ def server_name(git_url)
17
+ ["flint", git_url].join("_")
18
+ end
19
+
20
+ def password(git_url)
21
+ password = ENV["FLINT_PASSWORD"]
22
+ unless password
23
+ if @tmp_password
24
+ password = @tmp_password
25
+ end
26
+ end
27
+
28
+ unless password && password != ''
29
+ if !UI.interactive?
30
+ UI.error("The FLINT_PASSWORD environment variable did not contain a password.")
31
+ UI.error("Bailing out instead of asking for a password, since this is non-interactive mode.")
32
+ UI.user_error!("Try setting the FLINT_PASSWORD environment variable, or temporarily enable interactive mode to store a password.")
33
+ else
34
+ UI.important("Enter the passphrase that should be used to encrypt/decrypt your keystores")
35
+ UI.important("Make sure to remember the password, as you'll need it when you run flint again")
36
+ password = ChangePassword.ask_password(confirm: true)
37
+ store_password(git_url, password)
38
+ end
39
+ end
40
+
41
+ return password
42
+ end
43
+
44
+ def store_password(git_url, password)
45
+ @tmp_password = password
46
+ end
47
+
48
+ def clear_password(git_url)
49
+ @tmp_password = ""
50
+ end
51
+
52
+ def encrypt_repo(path: nil, git_url: nil)
53
+ iterate(path) do |current|
54
+ encrypt(path: current,
55
+ password: password(git_url))
56
+ UI.success("🔒 Encrypted '#{File.basename(current)}'") if FastlaneCore::Globals.verbose?
57
+ end
58
+ UI.success("🔒 Successfully encrypted keystores repo")
59
+ end
60
+
61
+ def decrypt_repo(path: nil, git_url: nil, manual_password: nil)
62
+ iterate(path) do |current|
63
+ begin
64
+ decrypt(path: current,
65
+ password: manual_password || password(git_url))
66
+ rescue
67
+ UI.error("Couldn't decrypt the repo, please make sure you enter the right password! %s" % manual_password || password(git_url))
68
+ UI.user_error!("Invalid password passed via 'FLINT_PASSWORD'") if ENV["FLINT_PASSWORD"]
69
+ clear_password(git_url)
70
+ password(git_url)
71
+ decrypt_repo(path: path, git_url: git_url)
72
+ return
73
+ end
74
+ UI.success("🔓 Decrypted '#{File.basename(current)}'") if FastlaneCore::Globals.verbose?
75
+ end
76
+ UI.success("🔓 Successfully decrypted keystores repo")
77
+ end
78
+
79
+ private
80
+
81
+ def iterate(source_path)
82
+ Dir[File.join(source_path, "**", "*.{keystore}")].each do |path|
83
+ next if File.directory?(path)
84
+ yield(path)
85
+ end
86
+ end
87
+
88
+ # We encrypt with MD5 because that was the most common default value in older fastlane versions which used the local OpenSSL installation
89
+ # A more secure key and IV generation is needed in the future
90
+ # IV should be randomly generated and provided unencrypted
91
+ # salt should be randomly generated and provided unencrypted (like in the current implementation)
92
+ # key should be generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters
93
+ # Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550
94
+ def encrypt(path: nil, password: nil)
95
+ UI.user_error!("No password supplied") if password.to_s.strip.length == 0
96
+
97
+ data_to_encrypt = File.read(path)
98
+ salt = SecureRandom.random_bytes(8)
99
+
100
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
101
+ cipher.encrypt
102
+ cipher.pkcs5_keyivgen(password, salt, 1, "MD5")
103
+ encrypted_data = "Salted__" + salt + cipher.update(data_to_encrypt) + cipher.final
104
+
105
+ File.write(path, Base64.encode64(encrypted_data))
106
+ rescue FastlaneCore::Interface::FastlaneError
107
+ raise
108
+ rescue => error
109
+ UI.error(error.to_s)
110
+ UI.crash!("Error encrypting '#{path}'")
111
+ end
112
+
113
+ # The encryption parameters in this implementations reflect the old behaviour which depended on the users' local OpenSSL version
114
+ # 1.0.x OpenSSL and earlier versions use MD5, 1.1.0c and newer uses SHA256, we try both before giving an error
115
+ def decrypt(path: nil, password: nil, hash_algorithm: "MD5")
116
+ stored_data = Base64.decode64(File.read(path))
117
+ salt = stored_data[8..15]
118
+ data_to_decrypt = stored_data[16..-1]
119
+
120
+ decipher = OpenSSL::Cipher.new('AES-256-CBC')
121
+ decipher.decrypt
122
+ decipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm)
123
+
124
+ decrypted_data = decipher.update(data_to_decrypt) + decipher.final
125
+
126
+ File.binwrite(path, decrypted_data)
127
+ rescue => error
128
+ fallback_hash_algorithm = "SHA256"
129
+ if hash_algorithm != fallback_hash_algorithm
130
+ decrypt(path, password, fallback_hash_algorithm)
131
+ else
132
+ UI.error(error.to_s)
133
+ UI.crash!("Error decrypting '#{path}'")
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,16 @@
1
+ require 'fastlane_core/ui/ui'
2
+
3
+ module Fastlane
4
+ UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
5
+
6
+ module Helper
7
+ class FlintHelper
8
+ # class methods that you define here become available in your action
9
+ # as `Helper::FlintHelper.your_method`
10
+ #
11
+ def self.show_message
12
+ UI.message("Hello from the flint plugin helper!")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ require 'fileutils'
2
+
3
+
4
+ module Fastlane
5
+ module Flint
6
+ # Generate missing resources
7
+ class Generator
8
+ def self.generate_keystore(params, keystore_name, alias_name, password)
9
+ # Ensure output dir exists
10
+ output_dir = File.join(params[:workspace], "certs")
11
+ FileUtils.mkdir_p output_dir
12
+ output_path = File.join(output_dir, keystore_name)
13
+
14
+ full_name = params[:full_name]
15
+ org = params[:orgization]
16
+ org_unit = params[:orgization_unit]
17
+ city_locality = params[:city]
18
+ state_province = params[:state]
19
+ country = params[:country]
20
+ valid_days = 10000 # > 27 years
21
+
22
+ cmd = "keytool -genkey -v -keystore %s -alias %s " % [output_path, alias_name]
23
+ cmd << "-keyalg RSA -keysize 2048 -validity %s -keypass %s -storepass %s " % [valid_days, password, password]
24
+ cmd << "-dname \"CN=#{full_name}, OU=#{org_unit}, O=#{org}, L=#{city_locality}, S=#{state_province}, C=#{country}\""
25
+
26
+ begin
27
+ output = IO.popen(cmd)
28
+ error = output.read
29
+ output.close
30
+ raise error unless $?.exitstatus == 0
31
+ rescue => ex
32
+ raise ex
33
+ end
34
+
35
+ return output_path
36
+ end
37
+
38
+ def self.update_keystore_password(workspace, keystore_name, alias_name, password, new_password)
39
+ output_dir = File.join(workspace, "certs")
40
+ output_path = File.join(output_dir, keystore_name)
41
+
42
+ if File.file?(output_path)
43
+ cmd = "keytool -storepasswd -v -keystore %s -storepass %s -new %s" % [output_path, password, new_password]
44
+ begin
45
+ output = IO.popen(cmd)
46
+ error = output.read
47
+ output.close
48
+ raise error unless $?.exitstatus == 0
49
+ rescue => ex
50
+ raise ex
51
+ end
52
+
53
+ cmd = "keytool -keypasswd -v -keystore %s -alias %s -keypass %s -storepass %s -new %s" % [
54
+ output_path, alias_name, password, new_password, new_password]
55
+
56
+ begin
57
+ output = IO.popen(cmd)
58
+ error = output.read
59
+ output.close
60
+ raise error unless $?.exitstatus == 0
61
+ rescue => ex
62
+ raise ex
63
+ end
64
+ else
65
+ UI.message("output_path does not exist %s" % output_path)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end