fastlane-plugin-match_keystore 0.1.8 → 0.1.13

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49e735f7fda8b92409a84f94a1e826254555ee6b33a9f2333a8891e37fee25a5
4
- data.tar.gz: 1e8b9230a2e0f0d3f315d1c587e08589ab03308e3570adb8d703b6f84b65fce1
3
+ metadata.gz: c5c402baf5a7ce59cb81cc842718697d1854539f5a9d9ea37f5005110a0b7a82
4
+ data.tar.gz: 4885c27fb682f3fb03b6e9e8c30406f1888365df72b6a9d5172b0650753f5cea
5
5
  SHA512:
6
- metadata.gz: 5ce5971aa35734b0ce58746631d107db20833edb7d8d6fa2f1c9ae6a700efce48db7ca36f8ed59800a95ed6fae660001e3ec3477dc06090594ab8dca88d9c30c
7
- data.tar.gz: aa7171f32c1818427d0b8b8b6fc2f117e78aa7f688c53135099c80c1465762819c906e35f6adf91645f3b81f956c149fd3426c6a722496af199ff8d3b89dd4aa
6
+ metadata.gz: ccb884061e5c1a9151539fc4eaa4466a334e5ada0174b5fef38030bb386f82714a843e4ec0b06baea1245744c91dfffafd5aa5d808d0cdeb2c54d756743e6899
7
+ data.tar.gz: 763e7decf059eb0343e9dedf79f561b904ed24b7daa8a3be0bdbf6cc7ddbf51b86a02e991325ebe875f4fb29f7232859b78d7015f3c26c70c6149aa088d6b292
data/README.md CHANGED
@@ -24,14 +24,18 @@ The keystore properties are encrypted with AES in order to secure sensitive data
24
24
 
25
25
  ```ruby
26
26
  lane :release_and_sign do |options|
27
-
28
- gradle(task: "clean")
27
+ gradle(task: "clean")
29
28
  gradle(task: 'assemble', build_type: 'Release')
30
29
 
31
30
  signed_apk_path = match_keystore(
32
31
  git_url: "https://github.com/<GITHUB_USERNAME>/keystores.git", # Please use a private Git repository !
33
32
  package_name: "com.your.package.name",
34
33
  apk_path: "/app/build/outputs/apk/app-release.apk" # Or path without APK: /app/build/outputs/apk/
34
+ # Optional:
35
+ match_secret: "A-very-str0ng-password!", # The secret use to encrypt/decrypt Keystore passwords on Git repo (for CI)
36
+ existing_keystore: "assets/existing-keystore.jks", # Optional, if needed to import an existing keystore
37
+ override_keystore: true, # Optional, override an existing Keystore on Git repo
38
+ keystore_data: "assets/keystore.json" # Optional, all data required to create a new Keystore (use to bypass prompt)
35
39
  )
36
40
 
37
41
  # Return the path of signed APK (useful for other lanes such as `publish_to_firebase`, `upload_to_play_store`)
@@ -1,6 +1,8 @@
1
1
  require 'fastlane/action'
2
2
  require 'fileutils'
3
3
  require 'os'
4
+ require 'json'
5
+ require 'digest'
4
6
  require_relative '../helper/match_keystore_helper'
5
7
 
6
8
  module Fastlane
@@ -13,6 +15,17 @@ module Fastlane
13
15
 
14
16
  class MatchKeystoreAction < Action
15
17
 
18
+ def self.to_md5(value)
19
+ hash_value = Digest::MD5.hexdigest value
20
+ hash_value
21
+ end
22
+
23
+ def self.load_json(json_path)
24
+ file = File.read(json_path)
25
+ data_hash = JSON.parse(file)
26
+ data_hash
27
+ end
28
+
16
29
  def self.load_properties(properties_filename)
17
30
  properties = {}
18
31
  File.open(properties_filename, 'r') do |properties_file|
@@ -42,7 +55,7 @@ module Fastlane
42
55
 
43
56
  def self.get_build_tools
44
57
  android_home = self.get_android_home()
45
- build_tools_root = android_home + '/build-tools'
58
+ build_tools_root = File.join(android_home, '/build-tools')
46
59
 
47
60
  sub_dirs = Dir.glob(File.join(build_tools_root, '*', ''))
48
61
  build_tools_last_version = ''
@@ -52,46 +65,55 @@ module Fastlane
52
65
 
53
66
  build_tools_last_version
54
67
  end
68
+
69
+ def self.check_openssl_version
70
+ output = `openssl version`
71
+ if !output.start_with?("OpenSSL")
72
+ raise "Please install OpenSSL 1.1.1 at least https://www.openssl.org/"
73
+ end
74
+ UI.message("OpenSSL version: " + output.strip)
75
+ end
55
76
 
56
77
  def self.gen_key(key_path, password)
57
- `rm -f #{key_path}`
58
- if OS.mac?
59
- `echo "#{password}" | openssl dgst -sha512 | cut -c1-128 > #{key_path}`
60
- else
61
- `echo "#{password}" | openssl dgst -sha512 | awk '{print $2}' | cut -c1-128 > #{key_path}`
62
- end
78
+ `rm -f '#{key_path}'`
79
+ `echo "#{password}" | openssl dgst -sha512 | awk '{print $2}' | cut -c1-128 > '#{key_path}'`
63
80
  end
64
81
 
65
82
  def self.encrypt_file(clear_file, encrypt_file, key_path)
66
- `rm -f #{encrypt_file}`
67
- `openssl enc -aes-256-cbc -salt -in #{clear_file} -out #{encrypt_file} -pass file:#{key_path}`
83
+ `rm -f '#{encrypt_file}'`
84
+ `openssl enc -aes-256-cbc -salt -pbkdf2 -in '#{clear_file}' -out '#{encrypt_file}' -pass file:'#{key_path}'`
68
85
  end
69
86
 
70
87
  def self.decrypt_file(encrypt_file, clear_file, key_path)
71
- `rm -f #{clear_file}`
72
- `openssl enc -d -aes-256-cbc -in #{encrypt_file} -out #{clear_file} -pass file:#{key_path}`
88
+ `rm -f '#{clear_file}'`
89
+ `openssl enc -d -aes-256-cbc -pbkdf2 -in '#{encrypt_file}' -out '#{clear_file}' -pass file:'#{key_path}'`
73
90
  end
74
91
 
75
92
  def self.sign_apk(apk_path, keystore_path, key_password, alias_name, alias_password, zip_align)
76
93
 
77
94
  build_tools_path = self.get_build_tools()
95
+ UI.message("BUild tools path: #{build_tools_path}")
78
96
 
79
97
  # https://developer.android.com/studio/command-line/zipalign
80
98
  if zip_align == true
81
99
  apk_path_aligned = apk_path.gsub(".apk", "-aligned.apk")
82
- `rm -f #{apk_path_aligned}`
83
- `#{build_tools_path}zipalign 4 #{apk_path} #{apk_path_aligned}`
100
+ `rm -f '#{apk_path_aligned}'`
101
+ UI.message("Aligning APK (zipalign): #{apk_path_aligned}")
102
+ `#{build_tools_path}zipalign -f -c -v 4 '#{apk_path}' '#{apk_path_aligned}'`
84
103
  else
104
+ UI.message("No zip align!")
85
105
  apk_path_aligned = apk_path
86
106
  end
107
+ apk_path_signed = apk_path.gsub(".apk", "-signed.apk")
108
+ apk_path_signed = apk_path_signed.gsub("unsigned", "")
109
+ apk_path_signed = apk_path_signed.gsub("--", "-")
87
110
 
88
111
  # https://developer.android.com/studio/command-line/apksigner
89
- apk_path_signed = apk_path.gsub(".apk", "-signed.apk")
90
- `rm -f #{apk_path_signed}`
91
- `#{build_tools_path}apksigner sign --ks #{keystore_path} --ks-key-alias '#{alias_name}' --ks-pass pass:'#{alias_password}' --key-pass pass:'#{key_password}' --v1-signing-enabled true --v2-signing-enabled true --out #{apk_path_signed} #{apk_path_aligned}`
92
-
93
- `#{build_tools_path}apksigner verify #{apk_path_signed}`
94
- `rm -f #{apk_path_aligned}`
112
+ `rm -f '#{apk_path_signed}'`
113
+ `#{build_tools_path}apksigner sign --ks '#{keystore_path}' --ks-key-alias '#{alias_name}' --ks-pass pass:'#{key_password}' --key-pass pass:'#{alias_password}' --v1-signing-enabled true --v2-signing-enabled true --out '#{apk_path_signed}' '#{apk_path_aligned}'`
114
+
115
+ `#{build_tools_path}apksigner verify '#{apk_path_signed}'`
116
+ `rm -f '#{apk_path_aligned}'`
95
117
 
96
118
  apk_path_signed
97
119
  end
@@ -103,6 +125,11 @@ module Fastlane
103
125
 
104
126
  def self.resolve_apk_path(apk_path)
105
127
 
128
+ # Set default APK path if not set:
129
+ if apk_path.to_s.strip.empty?
130
+ apk_path = '/app/build/outputs/apk/'
131
+ end
132
+
106
133
  if !apk_path.to_s.end_with?(".apk")
107
134
 
108
135
  if !File.directory?(apk_path)
@@ -130,15 +157,28 @@ module Fastlane
130
157
  apk_path
131
158
  end
132
159
 
160
+ def self.prompt2(params)
161
+ # UI.message("prompt2: #{params[:value]}")
162
+ if params[:value].to_s.empty?
163
+ return_value = other_action.prompt(text: params[:text], secure_text: params[:secure_text], ci_input: params[:ci_input])
164
+ else
165
+ return_value = params[:value]
166
+ end
167
+ return_value
168
+ end
169
+
133
170
  def self.run(params)
134
171
 
172
+ # Get input parameters:
135
173
  git_url = params[:git_url]
136
174
  package_name = params[:package_name]
137
175
  apk_path = params[:apk_path]
138
176
  existing_keystore = params[:existing_keystore]
139
- ci_password = params[:ci_password]
177
+ match_secret = params[:match_secret]
140
178
  override_keystore = params[:override_keystore]
179
+ keystore_data = params[:keystore_data]
141
180
 
181
+ # Init constants:
142
182
  keystore_name = 'keystore.jks'
143
183
  properties_name = 'keystore.properties'
144
184
  keystore_info_name = 'keystore.txt'
@@ -151,56 +191,86 @@ module Fastlane
151
191
  raise "The environment variable ANDROID_HOME is not defined, or Android SDK is not installed!"
152
192
  end
153
193
 
194
+ # Check OpenSSL:
195
+ self.check_openssl_version
196
+
197
+ # Init workign local directory:
154
198
  dir_name = ENV['HOME'] + '/.match_keystore'
155
199
  unless File.directory?(dir_name)
156
200
  UI.message("Creating '.match_keystore' working directory...")
157
201
  FileUtils.mkdir_p(dir_name)
158
202
  end
159
203
 
160
- UI.message("OpenSSL version: ")
161
- puts `openssl version`
162
-
163
- key_path = dir_name + '/key.hex'
204
+ # Init 'security password' for AES encryption:
205
+ key_name = "#{self.to_md5(git_url)}.hex"
206
+ key_path = File.join(dir_name, key_name)
207
+ # UI.message(key_path)
164
208
  if !File.file?(key_path)
165
- if ci_password.to_s.strip.empty?
166
- security_password = other_action.prompt(text: "Security password: ")
167
- else
168
- security_password = ci_password
209
+ security_password = self.prompt2(text: "Security password: ", secure_text: true, value: match_secret)
210
+ if security_password.to_s.strip.empty?
211
+ raise "Security password is not defined! Please use 'match_secret' parameter for CI."
169
212
  end
170
- UI.message "Generating security key..."
213
+ UI.message "Generating security key '#{key_name}'..."
171
214
  self.gen_key(key_path, security_password)
172
215
  end
173
216
 
217
+ # Check is 'security password' is well initialized:
174
218
  tmpkey = self.get_file_content(key_path).strip
175
219
  if tmpkey.length == 128
176
- UI.message "Security key initialized"
220
+ UI.message "Security key '#{key_name}' initialized"
177
221
  else
178
- raise "The security key format is invalid, or not initialized!"
222
+ raise "The security key '#{key_name}' is malformed, or not initialized!"
179
223
  end
180
224
 
181
- repo_dir = dir_name + '/repo'
225
+ # Create repo directory to sync remote Keystores repository:
226
+ repo_dir = File.join(dir_name, self.to_md5(git_url))
227
+ # UI.message(repo_dir)
182
228
  unless File.directory?(repo_dir)
183
229
  UI.message("Creating 'repo' directory...")
184
230
  FileUtils.mkdir_p(repo_dir)
185
231
  end
186
232
 
187
- gitDir = repo_dir + '/.git'
188
- unless File.directory?(gitDir)
233
+ # Cloning/pulling GIT remote repository:
234
+ gitDir = File.join(repo_dir, '/.git')
235
+ if !File.directory?(gitDir)
189
236
  UI.message("Cloning remote Keystores repository...")
190
237
  puts ''
191
238
  `git clone #{git_url} #{repo_dir}`
192
239
  puts ''
240
+ else
241
+ UI.message("Pulling remote Keystores repository...")
242
+ puts ''
243
+ `cd #{repo_dir} && git pull`
244
+ puts ''
193
245
  end
194
246
 
195
- keystoreAppDir = repo_dir + '/' + package_name
247
+ # Create sub-directory for Android app:
248
+ if package_name.to_s.strip.empty?
249
+ raise "Package name is not defined!"
250
+ end
251
+ keystoreAppDir = File.join(repo_dir, package_name)
196
252
  unless File.directory?(keystoreAppDir)
197
253
  UI.message("Creating '#{package_name}' keystore directory...")
198
254
  FileUtils.mkdir_p(keystoreAppDir)
199
255
  end
200
256
 
201
- keystore_path = keystoreAppDir + '/' + keystore_name
202
- properties_path = keystoreAppDir + '/' + properties_name
203
- properties_encrypt_path = keystoreAppDir + '/' + properties_encrypt_name
257
+ keystore_path = File.join(keystoreAppDir, keystore_name)
258
+ properties_path = File.join(keystoreAppDir, properties_name)
259
+ properties_encrypt_path = File.join(keystoreAppDir, properties_encrypt_name)
260
+
261
+ # Load parameters from JSON for CI or Unit Tests:
262
+ if keystore_data != nil && File.file?(keystore_data)
263
+ data_json = self.load_json(keystore_data)
264
+ data_key_password = data_json['key_password']
265
+ data_alias_name = data_json['alias_name']
266
+ data_alias_password = data_json['alias_password']
267
+ data_full_name = data_json['full_name']
268
+ data_org_unit = data_json['org_unit']
269
+ data_org = data_json['org']
270
+ data_city_locality = data_json['city_locality']
271
+ data_state_province = data_json['state_province']
272
+ data_country = data_json['country']
273
+ end
204
274
 
205
275
  # Create keystore with command
206
276
  override_keystore = !existing_keystore.to_s.strip.empty? && File.file?(existing_keystore)
@@ -210,28 +280,37 @@ module Fastlane
210
280
  FileUtils.remove_dir(keystore_path)
211
281
  end
212
282
 
213
- key_password = other_action.prompt(text: "Keystore Password: ")
214
- alias_name = other_action.prompt(text: "Keystore Alias name: ")
215
- alias_password = other_action.prompt(text: "Keystore Alias password: ")
283
+ key_password = self.prompt2(text: "Keystore Password: ", value: data_key_password)
284
+ if key_password.to_s.strip.empty?
285
+ raise "Keystore Password is not definined!"
286
+ end
287
+ alias_name = self.prompt2(text: "Keystore Alias name: ", value: data_alias_name)
288
+ if alias_name.to_s.strip.empty?
289
+ raise "Keystore Alias name is not definined!"
290
+ end
291
+ alias_password = self.prompt2(text: "Keystore Alias password: ", value: data_alias_password)
292
+ if alias_password.to_s.strip.empty?
293
+ raise "Keystore Alias password is not definined!"
294
+ end
216
295
 
217
296
  # https://developer.android.com/studio/publish/app-signing
218
297
  if !File.file?(existing_keystore)
219
298
  UI.message("Generating Android Keystore...")
220
299
 
221
- full_name = other_action.prompt(text: "Certificate First and Last Name: ")
222
- org_unit = other_action.prompt(text: "Certificate Organisation Unit: ")
223
- org = other_action.prompt(text: "Certificate Organisation: ")
224
- city_locality = other_action.prompt(text: "Certificate City or Locality: ")
225
- state_province = other_action.prompt(text: "Certificate State or Province: ")
226
- country = other_action.prompt(text: "Certificate Country Code (XX): ")
300
+ full_name = self.prompt2(text: "Certificate First and Last Name: ", value: data_full_name)
301
+ org_unit = self.prompt2(text: "Certificate Organisation Unit: ", value: data_org_unit)
302
+ org = self.prompt2(text: "Certificate Organisation: ", value: data_org)
303
+ city_locality = self.prompt2(text: "Certificate City or Locality: ", value: data_city_locality)
304
+ state_province = self.prompt2(text: "Certificate State or Province: ", value: data_state_province)
305
+ country = self.prompt2(text: "Certificate Country Code (XX): ", value: data_country)
227
306
 
228
307
  keytool_parts = [
229
308
  "keytool -genkey -v",
230
- "-keystore #{keystore_path}",
231
- "-alias #{alias_name}",
309
+ "-keystore '#{keystore_path}'",
310
+ "-alias '#{alias_name}'",
232
311
  "-keyalg RSA -keysize 2048 -validity 10000",
233
- "-storepass #{alias_password} ",
234
- "-keypass #{key_password}",
312
+ "-storepass '#{alias_password}'",
313
+ "-keypass '#{key_password}'",
235
314
  "-dname \"CN=#{full_name}, OU=#{org_unit}, O=#{org}, L=#{city_locality}, S=#{state_province}, C=#{country}\"",
236
315
  ]
237
316
  sh keytool_parts.join(" ")
@@ -246,6 +325,7 @@ module Fastlane
246
325
  FileUtils.remove_dir(properties_path)
247
326
  end
248
327
 
328
+ # Build URL:
249
329
  store_file = git_url + '/' + package_name + '/' + keystore_name
250
330
 
251
331
  out_file = File.new(properties_path, "w")
@@ -259,15 +339,17 @@ module Fastlane
259
339
  File.delete(properties_path)
260
340
 
261
341
  # Print Keystore data in repo:
262
- keystore_info_path = keystoreAppDir + '/' + keystore_info_name
263
- `yes "" | keytool -list -v -keystore #{keystore_path} > #{keystore_info_path}`
342
+ keystore_info_path = File.join(keystoreAppDir, keystore_info_name)
343
+ `yes "" | keytool -list -v -keystore '#{keystore_path}' -storepass '#{key_password}' > '#{keystore_info_path}'`
264
344
 
265
345
  UI.message("Upload new Keystore to remote repository...")
266
- `cd #{repo_dir} && git add .`
267
- `cd #{repo_dir} && git commit -m "[ADD] Keystore for app '#{package_name}'."`
268
- `cd #{repo_dir} && git push`
346
+ puts ''
347
+ `cd '#{repo_dir}' && git add .`
348
+ `cd '#{repo_dir}' && git commit -m "[ADD] Keystore for app '#{package_name}'."`
349
+ `cd '#{repo_dir}' && git push`
350
+ puts ''
269
351
 
270
- else
352
+ else
271
353
  UI.message "Keystore file already exists, continue..."
272
354
 
273
355
  self.decrypt_file(properties_encrypt_path, properties_path, key_path)
@@ -278,37 +360,40 @@ module Fastlane
278
360
  alias_password = properties['aliasPassword']
279
361
 
280
362
  File.delete(properties_path)
281
-
282
363
  end
283
364
 
365
+ # Resolve path to the APK to sign:
284
366
  output_signed_apk = ''
285
367
  apk_path = self.resolve_apk_path(apk_path)
286
368
 
369
+ # Sign APK:
287
370
  if File.file?(apk_path)
288
371
  UI.message("APK to sign: " + apk_path)
289
372
 
290
373
  if File.file?(keystore_path)
291
374
 
292
375
  UI.message("Signing the APK...")
376
+ puts ''
293
377
  output_signed_apk = self.sign_apk(
294
378
  apk_path,
295
379
  keystore_path,
296
380
  key_password,
297
381
  alias_name,
298
382
  alias_password,
299
- true
383
+ true # Zip align
300
384
  )
385
+ puts ''
301
386
  end
302
387
  else
303
388
  UI.message("No APK file found to sign!")
304
389
  end
305
390
 
391
+ # Prepare contect shared values for next lanes:
306
392
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
307
393
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
308
394
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_APK_SIGNED] = output_signed_apk
309
395
 
310
396
  output_signed_apk
311
-
312
397
  end
313
398
 
314
399
  def self.description
@@ -326,7 +411,8 @@ module Fastlane
326
411
  def self.output
327
412
  [
328
413
  ['MATCH_KEYSTORE_PATH', 'File path of the Keystore fot the App.'],
329
- ['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.']
414
+ ['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.'],
415
+ ['MATCH_KEYSTORE_APK_SIGNED', 'Path of the signed APK.']
330
416
  ]
331
417
  end
332
418
 
@@ -350,11 +436,11 @@ module Fastlane
350
436
  FastlaneCore::ConfigItem.new(key: :apk_path,
351
437
  env_name: "MATCH_KEYSTORE_APK_PATH",
352
438
  description: "Path of the APK file to sign",
353
- optional: false,
439
+ optional: true,
354
440
  type: String),
355
- FastlaneCore::ConfigItem.new(key: :ci_password,
356
- env_name: "MATCH_KEYSTORE_CI_PASSWORD",
357
- description: "Password to decrypt keystore.properties file (CI)",
441
+ FastlaneCore::ConfigItem.new(key: :match_secret,
442
+ env_name: "MATCH_KEYSTORE_SECRET",
443
+ description: "Secret to decrypt keystore.properties file (CI)",
358
444
  optional: true,
359
445
  type: String),
360
446
  FastlaneCore::ConfigItem.new(key: :existing_keystore,
@@ -366,7 +452,12 @@ module Fastlane
366
452
  env_name: "MATCH_KEYSTORE_OVERRIDE",
367
453
  description: "Override an existing Keystore (false by default)",
368
454
  optional: true,
369
- type: Boolean)
455
+ type: Boolean),
456
+ FastlaneCore::ConfigItem.new(key: :keystore_data,
457
+ env_name: "MATCH_KEYSTORE_JSON_PATH",
458
+ description: "Required data to import an existing keystore, or create a new one",
459
+ optional: true,
460
+ type: String)
370
461
  ]
371
462
  end
372
463
 
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module MatchKeystore
3
- VERSION = "0.1.8"
3
+ VERSION = "0.1.13"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-match_keystore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher NEY
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-31 00:00:00.000000000 Z
11
+ date: 2020-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -167,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
167
  - !ruby/object:Gem::Version
168
168
  version: '0'
169
169
  requirements: []
170
- rubygems_version: 3.0.6
170
+ rubygems_version: 3.0.3
171
171
  signing_key:
172
172
  specification_version: 4
173
173
  summary: Easily sync your Android keystores across your team