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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5b1878b6e0f6453f4c62eab321b2080210dcaa93fafcb04b14ac9819e7f996e
|
4
|
+
data.tar.gz: 18748d40e3d7c6dbe63179c8bbc726b5284049c25349d28c5ad1e76d529c21de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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 '
|
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
|
222
|
+
raise "The security key '#{key_name}' is malformed, or not initialized!"
|
182
223
|
end
|
183
224
|
|
184
|
-
|
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
|
-
|
191
|
-
|
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
|
-
|
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
|
205
|
-
properties_path = keystoreAppDir
|
206
|
-
properties_encrypt_path = keystoreAppDir
|
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 =
|
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 =
|
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 =
|
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 =
|
234
|
-
org_unit =
|
235
|
-
org =
|
236
|
-
city_locality =
|
237
|
-
state_province =
|
238
|
-
country =
|
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
|
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
|
-
|
279
|
-
`cd #{repo_dir} && git
|
280
|
-
`cd #{repo_dir} && git
|
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:
|
439
|
+
optional: true,
|
367
440
|
type: String),
|
368
|
-
FastlaneCore::ConfigItem.new(key: :
|
369
|
-
env_name: "
|
370
|
-
description: "
|
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
|
|
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.
|
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-
|
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.
|
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
|