fastlane-plugin-flint 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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