fastlane-plugin-match_keystore 0.1.8 → 0.1.13

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: 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