fastlane-plugin-match_keystore 0.1.9 → 0.1.14

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: 42c7915b7d46b87470d41acfd251257c04ce89460ac60a08318404530a1fbd24
4
- data.tar.gz: 07f280f892d41adee35f025218da2a1eeb345f17c9cf27d2d027246f4b248940
3
+ metadata.gz: d5b1878b6e0f6453f4c62eab321b2080210dcaa93fafcb04b14ac9819e7f996e
4
+ data.tar.gz: 18748d40e3d7c6dbe63179c8bbc726b5284049c25349d28c5ad1e76d529c21de
5
5
  SHA512:
6
- metadata.gz: 85ff5b2dda6f5a38714a09aea05fb279ec87015e970860ac3238f10280d233195936b84ef617d6bfafca36ee9d164f4a1c6267d8ea76e75305a40f4d44b55627
7
- data.tar.gz: f8e3fba2336ef34b35300d38535d2f5fbb5798af9efc50f268fa33a7a05282157f1a46755953eac2d99b362642560b8dc9e7b6255377a13e0fae9b5ebcfcd60d
6
+ metadata.gz: 31e657e39efa81b9141182a75f8df3ee5cc1704a6b1ed2b8526d50ee1e361aaf7d74ed3b8a04771e323a215850051eba4b3f6f932c9808ec8cf57bc70ad894f7
7
+ data.tar.gz: 1dce9a11c73f7921cea57a5953b7b64b2497bb401864cc906386617f342f3792a8c360d81d6709c3538b4282b6e9462c3fb7d5a87b215cebaffc950448bf6d76
data/README.md CHANGED
@@ -24,13 +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
- gradle(task: "clean")
27
+ gradle(task: "clean")
28
28
  gradle(task: 'assemble', build_type: 'Release')
29
29
 
30
30
  signed_apk_path = match_keystore(
31
31
  git_url: "https://github.com/<GITHUB_USERNAME>/keystores.git", # Please use a private Git repository !
32
32
  package_name: "com.your.package.name",
33
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)
34
39
  )
35
40
 
36
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 = ''
@@ -62,40 +75,45 @@ module Fastlane
62
75
  end
63
76
 
64
77
  def self.gen_key(key_path, password)
65
- `rm -f #{key_path}`
66
- `echo "#{password}" | openssl dgst -sha512 | awk '{print $2}' | cut -c1-128 > #{key_path}`
78
+ `rm -f '#{key_path}'`
79
+ `echo "#{password}" | openssl dgst -sha512 | awk '{print $2}' | cut -c1-128 > '#{key_path}'`
67
80
  end
68
81
 
69
82
  def self.encrypt_file(clear_file, encrypt_file, key_path)
70
- `rm -f #{encrypt_file}`
71
- `openssl enc -aes-256-cbc -salt -pbkdf2 -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}'`
72
85
  end
73
86
 
74
87
  def self.decrypt_file(encrypt_file, clear_file, key_path)
75
- `rm -f #{clear_file}`
76
- `openssl enc -d -aes-256-cbc -pbkdf2 -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}'`
77
90
  end
78
91
 
79
92
  def self.sign_apk(apk_path, keystore_path, key_password, alias_name, alias_password, zip_align)
80
93
 
81
94
  build_tools_path = self.get_build_tools()
95
+ UI.message("Build-tools path: #{build_tools_path}")
82
96
 
83
97
  # https://developer.android.com/studio/command-line/zipalign
84
98
  if zip_align == true
85
99
  apk_path_aligned = apk_path.gsub(".apk", "-aligned.apk")
86
- `rm -f #{apk_path_aligned}`
87
- `#{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 -v 4 '#{apk_path}' '#{apk_path_aligned}'`
88
103
  else
104
+ UI.message("No zip align!")
89
105
  apk_path_aligned = apk_path
90
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("--", "-")
91
110
 
92
111
  # https://developer.android.com/studio/command-line/apksigner
93
- apk_path_signed = apk_path.gsub(".apk", "-signed.apk")
94
- `rm -f #{apk_path_signed}`
95
- `#{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}`
96
-
97
- `#{build_tools_path}apksigner verify #{apk_path_signed}`
98
- `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}'`
99
117
 
100
118
  apk_path_signed
101
119
  end
@@ -107,6 +125,11 @@ module Fastlane
107
125
 
108
126
  def self.resolve_apk_path(apk_path)
109
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
+
110
133
  if !apk_path.to_s.end_with?(".apk")
111
134
 
112
135
  if !File.directory?(apk_path)
@@ -134,15 +157,28 @@ module Fastlane
134
157
  apk_path
135
158
  end
136
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
+
137
170
  def self.run(params)
138
171
 
172
+ # Get input parameters:
139
173
  git_url = params[:git_url]
140
174
  package_name = params[:package_name]
141
175
  apk_path = params[:apk_path]
142
176
  existing_keystore = params[:existing_keystore]
143
- ci_password = params[:ci_password]
177
+ match_secret = params[:match_secret]
144
178
  override_keystore = params[:override_keystore]
179
+ keystore_data = params[:keystore_data]
145
180
 
181
+ # Init constants:
146
182
  keystore_name = 'keystore.jks'
147
183
  properties_name = 'keystore.properties'
148
184
  keystore_info_name = 'keystore.txt'
@@ -158,52 +194,83 @@ module Fastlane
158
194
  # Check OpenSSL:
159
195
  self.check_openssl_version
160
196
 
197
+ # Init workign local directory:
161
198
  dir_name = ENV['HOME'] + '/.match_keystore'
162
199
  unless File.directory?(dir_name)
163
200
  UI.message("Creating '.match_keystore' working directory...")
164
201
  FileUtils.mkdir_p(dir_name)
165
202
  end
166
203
 
167
- 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)
168
208
  if !File.file?(key_path)
169
- security_password = other_action.prompt(text: "Security password: ", secure_text: true, ci_input: ci_password)
209
+ security_password = self.prompt2(text: "Security password: ", secure_text: true, value: match_secret)
170
210
  if security_password.to_s.strip.empty?
171
- raise "Security password is not defined! Please use 'ci_password' parameter for CI."
211
+ raise "Security password is not defined! Please use 'match_secret' parameter for CI."
172
212
  end
173
- UI.message "Generating security key..."
213
+ UI.message "Generating security key '#{key_name}'..."
174
214
  self.gen_key(key_path, security_password)
175
215
  end
176
216
 
217
+ # Check is 'security password' is well initialized:
177
218
  tmpkey = self.get_file_content(key_path).strip
178
219
  if tmpkey.length == 128
179
- UI.message "Security key initialized"
220
+ UI.message "Security key '#{key_name}' initialized"
180
221
  else
181
- raise "The security key format is invalid, or not initialized!"
222
+ raise "The security key '#{key_name}' is malformed, or not initialized!"
182
223
  end
183
224
 
184
- 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)
185
228
  unless File.directory?(repo_dir)
186
229
  UI.message("Creating 'repo' directory...")
187
230
  FileUtils.mkdir_p(repo_dir)
188
231
  end
189
232
 
190
- gitDir = repo_dir + '/.git'
191
- unless File.directory?(gitDir)
233
+ # Cloning/pulling GIT remote repository:
234
+ gitDir = File.join(repo_dir, '/.git')
235
+ if !File.directory?(gitDir)
192
236
  UI.message("Cloning remote Keystores repository...")
193
237
  puts ''
194
238
  `git clone #{git_url} #{repo_dir}`
195
239
  puts ''
240
+ else
241
+ UI.message("Pulling remote Keystores repository...")
242
+ puts ''
243
+ `cd #{repo_dir} && git pull`
244
+ puts ''
196
245
  end
197
246
 
198
- 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)
199
252
  unless File.directory?(keystoreAppDir)
200
253
  UI.message("Creating '#{package_name}' keystore directory...")
201
254
  FileUtils.mkdir_p(keystoreAppDir)
202
255
  end
203
256
 
204
- keystore_path = keystoreAppDir + '/' + keystore_name
205
- properties_path = keystoreAppDir + '/' + properties_name
206
- 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
207
274
 
208
275
  # Create keystore with command
209
276
  override_keystore = !existing_keystore.to_s.strip.empty? && File.file?(existing_keystore)
@@ -213,15 +280,15 @@ module Fastlane
213
280
  FileUtils.remove_dir(keystore_path)
214
281
  end
215
282
 
216
- key_password = other_action.prompt(text: "Keystore Password: ")
283
+ key_password = self.prompt2(text: "Keystore Password: ", value: data_key_password)
217
284
  if key_password.to_s.strip.empty?
218
285
  raise "Keystore Password is not definined!"
219
286
  end
220
- alias_name = other_action.prompt(text: "Keystore Alias name: ")
287
+ alias_name = self.prompt2(text: "Keystore Alias name: ", value: data_alias_name)
221
288
  if alias_name.to_s.strip.empty?
222
289
  raise "Keystore Alias name is not definined!"
223
290
  end
224
- alias_password = other_action.prompt(text: "Keystore Alias password: ")
291
+ alias_password = self.prompt2(text: "Keystore Alias password: ", value: data_alias_password)
225
292
  if alias_password.to_s.strip.empty?
226
293
  raise "Keystore Alias password is not definined!"
227
294
  end
@@ -230,20 +297,20 @@ module Fastlane
230
297
  if !File.file?(existing_keystore)
231
298
  UI.message("Generating Android Keystore...")
232
299
 
233
- full_name = other_action.prompt(text: "Certificate First and Last Name: ")
234
- org_unit = other_action.prompt(text: "Certificate Organisation Unit: ")
235
- org = other_action.prompt(text: "Certificate Organisation: ")
236
- city_locality = other_action.prompt(text: "Certificate City or Locality: ")
237
- state_province = other_action.prompt(text: "Certificate State or Province: ")
238
- 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)
239
306
 
240
307
  keytool_parts = [
241
308
  "keytool -genkey -v",
242
- "-keystore #{keystore_path}",
243
- "-alias #{alias_name}",
309
+ "-keystore '#{keystore_path}'",
310
+ "-alias '#{alias_name}'",
244
311
  "-keyalg RSA -keysize 2048 -validity 10000",
245
- "-storepass #{alias_password} ",
246
- "-keypass #{key_password}",
312
+ "-storepass '#{alias_password}'",
313
+ "-keypass '#{key_password}'",
247
314
  "-dname \"CN=#{full_name}, OU=#{org_unit}, O=#{org}, L=#{city_locality}, S=#{state_province}, C=#{country}\"",
248
315
  ]
249
316
  sh keytool_parts.join(" ")
@@ -258,6 +325,7 @@ module Fastlane
258
325
  FileUtils.remove_dir(properties_path)
259
326
  end
260
327
 
328
+ # Build URL:
261
329
  store_file = git_url + '/' + package_name + '/' + keystore_name
262
330
 
263
331
  out_file = File.new(properties_path, "w")
@@ -271,15 +339,17 @@ module Fastlane
271
339
  File.delete(properties_path)
272
340
 
273
341
  # Print Keystore data in repo:
274
- keystore_info_path = keystoreAppDir + '/' + keystore_info_name
275
- `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}'`
276
344
 
277
345
  UI.message("Upload new Keystore to remote repository...")
278
- `cd #{repo_dir} && git add .`
279
- `cd #{repo_dir} && git commit -m "[ADD] Keystore for app '#{package_name}'."`
280
- `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 ''
281
351
 
282
- else
352
+ else
283
353
  UI.message "Keystore file already exists, continue..."
284
354
 
285
355
  self.decrypt_file(properties_encrypt_path, properties_path, key_path)
@@ -290,37 +360,40 @@ module Fastlane
290
360
  alias_password = properties['aliasPassword']
291
361
 
292
362
  File.delete(properties_path)
293
-
294
363
  end
295
364
 
365
+ # Resolve path to the APK to sign:
296
366
  output_signed_apk = ''
297
367
  apk_path = self.resolve_apk_path(apk_path)
298
368
 
369
+ # Sign APK:
299
370
  if File.file?(apk_path)
300
371
  UI.message("APK to sign: " + apk_path)
301
372
 
302
373
  if File.file?(keystore_path)
303
374
 
304
375
  UI.message("Signing the APK...")
376
+ puts ''
305
377
  output_signed_apk = self.sign_apk(
306
378
  apk_path,
307
379
  keystore_path,
308
380
  key_password,
309
381
  alias_name,
310
382
  alias_password,
311
- true
383
+ true # Zip align
312
384
  )
385
+ puts ''
313
386
  end
314
387
  else
315
388
  UI.message("No APK file found to sign!")
316
389
  end
317
390
 
391
+ # Prepare contect shared values for next lanes:
318
392
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
319
393
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
320
394
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_APK_SIGNED] = output_signed_apk
321
395
 
322
396
  output_signed_apk
323
-
324
397
  end
325
398
 
326
399
  def self.description
@@ -363,11 +436,11 @@ module Fastlane
363
436
  FastlaneCore::ConfigItem.new(key: :apk_path,
364
437
  env_name: "MATCH_KEYSTORE_APK_PATH",
365
438
  description: "Path of the APK file to sign",
366
- optional: false,
439
+ optional: true,
367
440
  type: String),
368
- FastlaneCore::ConfigItem.new(key: :ci_password,
369
- env_name: "MATCH_KEYSTORE_CI_PASSWORD",
370
- 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)",
371
444
  optional: true,
372
445
  type: String),
373
446
  FastlaneCore::ConfigItem.new(key: :existing_keystore,
@@ -379,7 +452,12 @@ module Fastlane
379
452
  env_name: "MATCH_KEYSTORE_OVERRIDE",
380
453
  description: "Override an existing Keystore (false by default)",
381
454
  optional: true,
382
- 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)
383
461
  ]
384
462
  end
385
463
 
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module MatchKeystore
3
- VERSION = "0.1.9"
3
+ VERSION = "0.1.14"
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.9
4
+ version: 0.1.14
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-04-11 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