fastlane-plugin-match_keystore 0.1.7 → 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cf1cf5e0df1d941bd776ab65ad5bccf1ef4059a07fcbd31040ccc0cd988332e
4
- data.tar.gz: 9bd6479f34526a30974d7a4a65874be1a9232c967facff8b0f09b8f624c91314
3
+ metadata.gz: 91d65c828bc6bc3ddd54e953955b1c01545fbd62481cc9aec4322f55aa9e44f8
4
+ data.tar.gz: d60ed3f2b4f0df42f3b09fd2c0bad9990732f1ea609fe88fca0f448dc1740fd8
5
5
  SHA512:
6
- metadata.gz: 5b20f9c4d03b4a6ba7e733007bdc5d55103c9bce852c83e438bb0177ed30636b9feaa28fee2da68e80a4ab2bc95a47c574e41fafc56915383926e451b2fb46ee
7
- data.tar.gz: b25983d059e338fd93d582292e02c87ff154179872a1944dd9b465365fbd181bb26d8e02e409b204a2bba5bd322985acd58fc92983a22e6e153a9ea1df748997
6
+ metadata.gz: 6796ac4aed31fa6d4dee60fb649a21c0c185b6f19734b01256181c4949627f1ec4fe331c27683262f128ab243ed820e8a728d04166dda21390fd212227b6941e
7
+ data.tar.gz: 7ea032e3fdaac9da9281acff0f409ab2f5abf0b1e87e58f5765b853308ddfafcbe3f2efd8530a29245db38e8fa18b740736d7c2311fdb6e4a28142f93fec8416
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,24 +65,28 @@ 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)
@@ -79,19 +96,23 @@ module Fastlane
79
96
  # https://developer.android.com/studio/command-line/zipalign
80
97
  if zip_align == true
81
98
  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}`
99
+ `rm -f '#{apk_path_aligned}'`
100
+ UI.message("Aligning APK (zipalign): #{apk_path_aligned}")
101
+ `#{build_tools_path}zipalign 4 '#{apk_path}' '#{apk_path_aligned}'`
84
102
  else
103
+ UI.message("No zip align!")
85
104
  apk_path_aligned = apk_path
86
105
  end
106
+ apk_path_signed = apk_path.gsub(".apk", "-signed.apk")
107
+ apk_path_signed = apk_path_signed.gsub("unsigned", "")
108
+ apk_path_signed = apk_path_signed.gsub("--", "-")
87
109
 
88
110
  # 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}`
111
+ `rm -f '#{apk_path_signed}'`
112
+ `#{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}'`
113
+
114
+ `#{build_tools_path}apksigner verify '#{apk_path_signed}'`
115
+ `rm -f '#{apk_path_aligned}'`
95
116
 
96
117
  apk_path_signed
97
118
  end
@@ -103,6 +124,11 @@ module Fastlane
103
124
 
104
125
  def self.resolve_apk_path(apk_path)
105
126
 
127
+ # Set default APK path if not set:
128
+ if apk_path.to_s.strip.empty?
129
+ apk_path = '/app/build/outputs/apk/'
130
+ end
131
+
106
132
  if !apk_path.to_s.end_with?(".apk")
107
133
 
108
134
  if !File.directory?(apk_path)
@@ -130,15 +156,28 @@ module Fastlane
130
156
  apk_path
131
157
  end
132
158
 
159
+ def self.prompt2(params)
160
+ # UI.message("prompt2: #{params[:value]}")
161
+ if params[:value].to_s.empty?
162
+ return_value = other_action.prompt(text: params[:text], secure_text: params[:secure_text], ci_input: params[:ci_input])
163
+ else
164
+ return_value = params[:value]
165
+ end
166
+ return_value
167
+ end
168
+
133
169
  def self.run(params)
134
170
 
171
+ # Get input parameters:
135
172
  git_url = params[:git_url]
136
173
  package_name = params[:package_name]
137
174
  apk_path = params[:apk_path]
138
175
  existing_keystore = params[:existing_keystore]
139
- ci_password = params[:ci_password]
176
+ match_secret = params[:match_secret]
140
177
  override_keystore = params[:override_keystore]
178
+ keystore_data = params[:keystore_data]
141
179
 
180
+ # Init constants:
142
181
  keystore_name = 'keystore.jks'
143
182
  properties_name = 'keystore.properties'
144
183
  keystore_info_name = 'keystore.txt'
@@ -151,50 +190,86 @@ module Fastlane
151
190
  raise "The environment variable ANDROID_HOME is not defined, or Android SDK is not installed!"
152
191
  end
153
192
 
193
+ # Check OpenSSL:
194
+ self.check_openssl_version
195
+
196
+ # Init workign local directory:
154
197
  dir_name = ENV['HOME'] + '/.match_keystore'
155
198
  unless File.directory?(dir_name)
156
199
  UI.message("Creating '.match_keystore' working directory...")
157
200
  FileUtils.mkdir_p(dir_name)
158
201
  end
159
202
 
160
- key_path = dir_name + '/key.hex'
203
+ # Init 'security password' for AES encryption:
204
+ key_name = "#{self.to_md5(git_url)}.hex"
205
+ key_path = File.join(dir_name, key_name)
206
+ # UI.message(key_path)
161
207
  if !File.file?(key_path)
162
- if ci_password.to_s.strip.empty?
163
- security_password = other_action.prompt(text: "Security password: ")
164
- else
165
- security_password = ci_password
208
+ security_password = self.prompt2(text: "Security password: ", secure_text: true, value: match_secret)
209
+ if security_password.to_s.strip.empty?
210
+ raise "Security password is not defined! Please use 'match_secret' parameter for CI."
166
211
  end
167
- UI.message "Generating security key..."
212
+ UI.message "Generating security key '#{key_name}'..."
168
213
  self.gen_key(key_path, security_password)
169
- else
170
- UI.message "Security key already exists"
171
214
  end
215
+
216
+ # Check is 'security password' is well initialized:
172
217
  tmpkey = self.get_file_content(key_path).strip
173
- UI.message "Key: '#{tmpkey}'"
218
+ if tmpkey.length == 128
219
+ UI.message "Security key '#{key_name}' initialized"
220
+ else
221
+ raise "The security key '#{key_name}' is malformed, or not initialized!"
222
+ end
174
223
 
175
- repo_dir = dir_name + '/repo'
224
+ # Create repo directory to sync remote Keystores repository:
225
+ repo_dir = File.join(dir_name, self.to_md5(git_url))
226
+ # UI.message(repo_dir)
176
227
  unless File.directory?(repo_dir)
177
228
  UI.message("Creating 'repo' directory...")
178
229
  FileUtils.mkdir_p(repo_dir)
179
230
  end
180
231
 
181
- gitDir = repo_dir + '/.git'
182
- unless File.directory?(gitDir)
232
+ # Cloning/pulling GIT remote repository:
233
+ gitDir = File.join(repo_dir, '/.git')
234
+ if !File.directory?(gitDir)
183
235
  UI.message("Cloning remote Keystores repository...")
184
236
  puts ''
185
237
  `git clone #{git_url} #{repo_dir}`
186
238
  puts ''
239
+ else
240
+ UI.message("Pulling remote Keystores repository...")
241
+ puts ''
242
+ `cd #{repo_dir} && git pull`
243
+ puts ''
187
244
  end
188
245
 
189
- keystoreAppDir = repo_dir + '/' + package_name
246
+ # Create sub-directory for Android app:
247
+ if package_name.to_s.strip.empty?
248
+ raise "Package name is not defined!"
249
+ end
250
+ keystoreAppDir = File.join(repo_dir, package_name)
190
251
  unless File.directory?(keystoreAppDir)
191
252
  UI.message("Creating '#{package_name}' keystore directory...")
192
253
  FileUtils.mkdir_p(keystoreAppDir)
193
254
  end
194
255
 
195
- keystore_path = keystoreAppDir + '/' + keystore_name
196
- properties_path = keystoreAppDir + '/' + properties_name
197
- properties_encrypt_path = keystoreAppDir + '/' + properties_encrypt_name
256
+ keystore_path = File.join(keystoreAppDir, keystore_name)
257
+ properties_path = File.join(keystoreAppDir, properties_name)
258
+ properties_encrypt_path = File.join(keystoreAppDir, properties_encrypt_name)
259
+
260
+ # Load parameters from JSON for CI or Unit Tests:
261
+ if keystore_data != nil && File.file?(keystore_data)
262
+ data_json = self.load_json(keystore_data)
263
+ data_key_password = data_json['key_password']
264
+ data_alias_name = data_json['alias_name']
265
+ data_alias_password = data_json['alias_password']
266
+ data_full_name = data_json['full_name']
267
+ data_org_unit = data_json['org_unit']
268
+ data_org = data_json['org']
269
+ data_city_locality = data_json['city_locality']
270
+ data_state_province = data_json['state_province']
271
+ data_country = data_json['country']
272
+ end
198
273
 
199
274
  # Create keystore with command
200
275
  override_keystore = !existing_keystore.to_s.strip.empty? && File.file?(existing_keystore)
@@ -204,28 +279,37 @@ module Fastlane
204
279
  FileUtils.remove_dir(keystore_path)
205
280
  end
206
281
 
207
- key_password = other_action.prompt(text: "Keystore Password: ")
208
- alias_name = other_action.prompt(text: "Keystore Alias name: ")
209
- alias_password = other_action.prompt(text: "Keystore Alias password: ")
282
+ key_password = self.prompt2(text: "Keystore Password: ", value: data_key_password)
283
+ if key_password.to_s.strip.empty?
284
+ raise "Keystore Password is not definined!"
285
+ end
286
+ alias_name = self.prompt2(text: "Keystore Alias name: ", value: data_alias_name)
287
+ if alias_name.to_s.strip.empty?
288
+ raise "Keystore Alias name is not definined!"
289
+ end
290
+ alias_password = self.prompt2(text: "Keystore Alias password: ", value: data_alias_password)
291
+ if alias_password.to_s.strip.empty?
292
+ raise "Keystore Alias password is not definined!"
293
+ end
210
294
 
211
295
  # https://developer.android.com/studio/publish/app-signing
212
296
  if !File.file?(existing_keystore)
213
297
  UI.message("Generating Android Keystore...")
214
298
 
215
- full_name = other_action.prompt(text: "Certificate First and Last Name: ")
216
- org_unit = other_action.prompt(text: "Certificate Organisation Unit: ")
217
- org = other_action.prompt(text: "Certificate Organisation: ")
218
- city_locality = other_action.prompt(text: "Certificate City or Locality: ")
219
- state_province = other_action.prompt(text: "Certificate State or Province: ")
220
- country = other_action.prompt(text: "Certificate Country Code (XX): ")
299
+ full_name = self.prompt2(text: "Certificate First and Last Name: ", value: data_full_name)
300
+ org_unit = self.prompt2(text: "Certificate Organisation Unit: ", value: data_org_unit)
301
+ org = self.prompt2(text: "Certificate Organisation: ", value: data_org)
302
+ city_locality = self.prompt2(text: "Certificate City or Locality: ", value: data_city_locality)
303
+ state_province = self.prompt2(text: "Certificate State or Province: ", value: data_state_province)
304
+ country = self.prompt2(text: "Certificate Country Code (XX): ", value: data_country)
221
305
 
222
306
  keytool_parts = [
223
307
  "keytool -genkey -v",
224
- "-keystore #{keystore_path}",
225
- "-alias #{alias_name}",
308
+ "-keystore '#{keystore_path}'",
309
+ "-alias '#{alias_name}'",
226
310
  "-keyalg RSA -keysize 2048 -validity 10000",
227
- "-storepass #{alias_password} ",
228
- "-keypass #{key_password}",
311
+ "-storepass '#{alias_password}'",
312
+ "-keypass '#{key_password}'",
229
313
  "-dname \"CN=#{full_name}, OU=#{org_unit}, O=#{org}, L=#{city_locality}, S=#{state_province}, C=#{country}\"",
230
314
  ]
231
315
  sh keytool_parts.join(" ")
@@ -240,6 +324,7 @@ module Fastlane
240
324
  FileUtils.remove_dir(properties_path)
241
325
  end
242
326
 
327
+ # Build URL:
243
328
  store_file = git_url + '/' + package_name + '/' + keystore_name
244
329
 
245
330
  out_file = File.new(properties_path, "w")
@@ -253,15 +338,17 @@ module Fastlane
253
338
  File.delete(properties_path)
254
339
 
255
340
  # Print Keystore data in repo:
256
- keystore_info_path = keystoreAppDir + '/' + keystore_info_name
257
- `yes "" | keytool -list -v -keystore #{keystore_path} > #{keystore_info_path}`
341
+ keystore_info_path = File.join(keystoreAppDir, keystore_info_name)
342
+ `yes "" | keytool -list -v -keystore '#{keystore_path}' -storepass '#{key_password}' > '#{keystore_info_path}'`
258
343
 
259
344
  UI.message("Upload new Keystore to remote repository...")
260
- `cd #{repo_dir} && git add .`
261
- `cd #{repo_dir} && git commit -m "[ADD] Keystore for app '#{package_name}'."`
262
- `cd #{repo_dir} && git push`
345
+ puts ''
346
+ `cd '#{repo_dir}' && git add .`
347
+ `cd '#{repo_dir}' && git commit -m "[ADD] Keystore for app '#{package_name}'."`
348
+ `cd '#{repo_dir}' && git push`
349
+ puts ''
263
350
 
264
- else
351
+ else
265
352
  UI.message "Keystore file already exists, continue..."
266
353
 
267
354
  self.decrypt_file(properties_encrypt_path, properties_path, key_path)
@@ -272,37 +359,40 @@ module Fastlane
272
359
  alias_password = properties['aliasPassword']
273
360
 
274
361
  File.delete(properties_path)
275
-
276
362
  end
277
363
 
364
+ # Resolve path to the APK to sign:
278
365
  output_signed_apk = ''
279
366
  apk_path = self.resolve_apk_path(apk_path)
280
367
 
368
+ # Sign APK:
281
369
  if File.file?(apk_path)
282
370
  UI.message("APK to sign: " + apk_path)
283
371
 
284
372
  if File.file?(keystore_path)
285
373
 
286
374
  UI.message("Signing the APK...")
375
+ puts ''
287
376
  output_signed_apk = self.sign_apk(
288
377
  apk_path,
289
378
  keystore_path,
290
379
  key_password,
291
380
  alias_name,
292
381
  alias_password,
293
- true
382
+ true # Zip align
294
383
  )
384
+ puts ''
295
385
  end
296
386
  else
297
387
  UI.message("No APK file found to sign!")
298
388
  end
299
389
 
390
+ # Prepare contect shared values for next lanes:
300
391
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
301
392
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
302
393
  Actions.lane_context[SharedValues::MATCH_KEYSTORE_APK_SIGNED] = output_signed_apk
303
394
 
304
395
  output_signed_apk
305
-
306
396
  end
307
397
 
308
398
  def self.description
@@ -320,7 +410,8 @@ module Fastlane
320
410
  def self.output
321
411
  [
322
412
  ['MATCH_KEYSTORE_PATH', 'File path of the Keystore fot the App.'],
323
- ['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.']
413
+ ['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.'],
414
+ ['MATCH_KEYSTORE_APK_SIGNED', 'Path of the signed APK.']
324
415
  ]
325
416
  end
326
417
 
@@ -344,11 +435,11 @@ module Fastlane
344
435
  FastlaneCore::ConfigItem.new(key: :apk_path,
345
436
  env_name: "MATCH_KEYSTORE_APK_PATH",
346
437
  description: "Path of the APK file to sign",
347
- optional: false,
438
+ optional: true,
348
439
  type: String),
349
- FastlaneCore::ConfigItem.new(key: :ci_password,
350
- env_name: "MATCH_KEYSTORE_CI_PASSWORD",
351
- description: "Password to decrypt keystore.properties file (CI)",
440
+ FastlaneCore::ConfigItem.new(key: :match_secret,
441
+ env_name: "MATCH_KEYSTORE_SECRET",
442
+ description: "Secret to decrypt keystore.properties file (CI)",
352
443
  optional: true,
353
444
  type: String),
354
445
  FastlaneCore::ConfigItem.new(key: :existing_keystore,
@@ -360,7 +451,12 @@ module Fastlane
360
451
  env_name: "MATCH_KEYSTORE_OVERRIDE",
361
452
  description: "Override an existing Keystore (false by default)",
362
453
  optional: true,
363
- type: Boolean)
454
+ type: Boolean),
455
+ FastlaneCore::ConfigItem.new(key: :keystore_data,
456
+ env_name: "MATCH_KEYSTORE_JSON_PATH",
457
+ description: "Required data to import an existing keystore, or create a new one",
458
+ optional: true,
459
+ type: String)
364
460
  ]
365
461
  end
366
462
 
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module MatchKeystore
3
- VERSION = "0.1.7"
3
+ VERSION = "0.1.12"
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.7
4
+ version: 0.1.12
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