fastlane-plugin-match_keystore 0.1.16 → 0.2.1
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: 13f57b6db57c10e142d53885360cb1416f676402fd7f234b61a549720501d9c6
|
4
|
+
data.tar.gz: 2809854cf748f5ba4931fd4cbe8dfc6292cfea2f4e5d556a887ed422ebfcacc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97a47758d5a39ebf414ae7a232e8767cb0dfa6ac7c484f3180b8e3149d3e879fb3ce3b27d363eae444703e9f3a9bad5b4ddc5d66f2987ed5fa0ed29b04bd4c74
|
7
|
+
data.tar.gz: 2c27e6a39112bdef3c44c6ed8abd77a31e19c9ce52a002040562d9f4aee229b4cf0e60cbe868196493acc983ca61b39acdfadebd2914e5a9ba15d40dd1024751
|
data/README.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
[](https://rubygems.org/gems/fastlane-plugin-match_keystore)
|
4
4
|
|
5
|
+
## Machine requirements
|
6
|
+
|
7
|
+
* OpenSSL 1.1.1 min OR LibreSSL 2.9 min installed
|
8
|
+
* Git installed
|
9
|
+
* Android SDK & Build-tools installed
|
10
|
+
* ANDROID_HOME environment variable defined
|
11
|
+
|
5
12
|
## Getting Started
|
6
13
|
|
7
14
|
This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-match_keystore`, add it to your project by running:
|
@@ -43,6 +50,8 @@ The keystore properties are encrypted with AES in order to secure sensitive data
|
|
43
50
|
end
|
44
51
|
```
|
45
52
|
|
53
|
+
You can build aab files as well by providing an `aab_path` instead of an `apk_path`.
|
54
|
+
|
46
55
|
## Example
|
47
56
|
|
48
57
|
Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
|
@@ -2,6 +2,7 @@ require 'fastlane/action'
|
|
2
2
|
require 'fileutils'
|
3
3
|
require 'os'
|
4
4
|
require 'json'
|
5
|
+
require 'pry'
|
5
6
|
require 'digest'
|
6
7
|
require_relative '../helper/match_keystore_helper'
|
7
8
|
|
@@ -11,14 +12,13 @@ module Fastlane
|
|
11
12
|
MATCH_KEYSTORE_PATH = :MATCH_KEYSTORE_PATH
|
12
13
|
MATCH_KEYSTORE_ALIAS_NAME = :MATCH_KEYSTORE_ALIAS_NAME
|
13
14
|
MATCH_KEYSTORE_APK_SIGNED = :MATCH_KEYSTORE_APK_SIGNED
|
15
|
+
MATCH_KEYSTORE_AAB_SIGNED = :MATCH_KEYSTORE_AAB_SIGNED
|
14
16
|
end
|
15
17
|
|
16
18
|
class MatchKeystoreAction < Action
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
path
|
21
|
-
end
|
20
|
+
KEY_VERSION = "2"
|
21
|
+
OPENSSL_BIN_PATH_MAC = "/usr/local/opt/openssl@1.1/bin"
|
22
22
|
|
23
23
|
def self.to_md5(value)
|
24
24
|
hash_value = Digest::MD5.hexdigest value
|
@@ -119,8 +119,7 @@ module Fastlane
|
|
119
119
|
|
120
120
|
def self.openssl(forceOpenSSL)
|
121
121
|
if forceOpenSSL
|
122
|
-
|
123
|
-
output = "#{path}/openssl"
|
122
|
+
output = "#{self::OPENSSL_BIN_PATH_MAC}/openssl"
|
124
123
|
else
|
125
124
|
output = "openssl"
|
126
125
|
end
|
@@ -137,10 +136,15 @@ module Fastlane
|
|
137
136
|
result
|
138
137
|
end
|
139
138
|
|
140
|
-
def self.gen_key(key_path, password)
|
139
|
+
def self.gen_key(key_path, password, compat_key)
|
141
140
|
`rm -f '#{key_path}'`
|
142
141
|
shaValue = self.sha512(password)
|
143
|
-
|
142
|
+
# Backward-compatibility
|
143
|
+
if compat_key == "1"
|
144
|
+
`echo "#{password}" | openssl dgst -sha512 | awk '{print $2}' | cut -c1-128 > '#{key_path}'`
|
145
|
+
else
|
146
|
+
`echo "#{shaValue}" > '#{key_path}'`
|
147
|
+
end
|
144
148
|
end
|
145
149
|
|
146
150
|
def self.encrypt_file(clear_file, encrypt_file, key_path, forceOpenSSL)
|
@@ -190,7 +194,7 @@ module Fastlane
|
|
190
194
|
|
191
195
|
# Check SHA-512-File
|
192
196
|
key_path = File.join(Dir.pwd, '/temp/key.txt')
|
193
|
-
self.gen_key(key_path, fakeValue)
|
197
|
+
self.gen_key(key_path, fakeValue, false)
|
194
198
|
shaValue = self.get_file_content(key_path).strip!
|
195
199
|
excepted = "cc6a7b0d89cc61c053f7018a305672bdb82bc07e5015f64bb063d9662be4ec81ec8afa819b009de266482b6bd56b7068def2524c32f5b5d4d9db49ee4578499d"
|
196
200
|
self.assert_equals("SHA-512-File", excepted, shaValue)
|
@@ -250,49 +254,68 @@ module Fastlane
|
|
250
254
|
build_tools_path = self.get_build_tools(version_targeted)
|
251
255
|
UI.message("Build-tools path: #{build_tools_path}")
|
252
256
|
|
253
|
-
# https://developer.android.com/studio/command-line/
|
254
|
-
if zip_align == true
|
255
|
-
apk_path_aligned = apk_path.gsub(".apk", "-aligned.apk")
|
256
|
-
`rm -f '#{apk_path_aligned}'`
|
257
|
-
UI.message("Aligning APK (zipalign): #{apk_path}")
|
258
|
-
output = `#{build_tools_path}zipalign -v 4 '#{apk_path}' '#{apk_path_aligned}'`
|
259
|
-
puts ""
|
260
|
-
puts output
|
261
|
-
|
262
|
-
if !File.file?(apk_path_aligned)
|
263
|
-
raise "Aligned APK not exists!"
|
264
|
-
end
|
265
|
-
|
266
|
-
else
|
267
|
-
UI.message("No zip align!")
|
268
|
-
apk_path_aligned = apk_path
|
269
|
-
end
|
257
|
+
# https://developer.android.com/studio/command-line/apksigner
|
270
258
|
apk_path_signed = apk_path.gsub(".apk", "-signed.apk")
|
271
259
|
apk_path_signed = apk_path_signed.gsub("unsigned", "")
|
272
260
|
apk_path_signed = apk_path_signed.gsub("--", "-")
|
273
|
-
|
274
|
-
# https://developer.android.com/studio/command-line/apksigner
|
275
261
|
`rm -f '#{apk_path_signed}'`
|
276
|
-
|
262
|
+
|
263
|
+
UI.message("Signing APK (input): #{apk_path}")
|
277
264
|
apksigner_opts = ""
|
278
265
|
build_tools_version = self.get_build_tools_version(version_targeted)
|
279
266
|
UI.message("Build-tools version: #{build_tools_version}")
|
280
267
|
if Gem::Version.new(build_tools_version) >= Gem::Version.new('30')
|
281
268
|
apksigner_opts = "--v4-signing-enabled false "
|
282
269
|
end
|
283
|
-
output = `#{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 #{apksigner_opts}--out '#{apk_path_signed}' '#{
|
270
|
+
output = `#{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 #{apksigner_opts}--out '#{apk_path_signed}' '#{apk_path}'`
|
284
271
|
puts ""
|
285
272
|
puts output
|
286
273
|
|
287
|
-
UI.message("Verifing APK signature: #{apk_path_signed}")
|
274
|
+
UI.message("Verifing APK signature (output): #{apk_path_signed}")
|
288
275
|
output = `#{build_tools_path}apksigner verify '#{apk_path_signed}'`
|
289
276
|
puts ""
|
290
277
|
puts output
|
291
|
-
|
278
|
+
|
279
|
+
|
280
|
+
# https://developer.android.com/studio/command-line/zipalign
|
281
|
+
if zip_align != false
|
282
|
+
apk_path_aligned = apk_path_signed.gsub(".apk", "-aligned.apk")
|
283
|
+
`rm -f '#{apk_path_aligned}'`
|
284
|
+
UI.message("Aligning APK (zipalign): #{apk_path_signed}")
|
285
|
+
output = `#{build_tools_path}zipalign -v 4 '#{apk_path_signed}' '#{apk_path_aligned}'`
|
286
|
+
puts ""
|
287
|
+
puts output
|
288
|
+
|
289
|
+
if !File.file?(apk_path_aligned)
|
290
|
+
raise "Aligned APK not exists!"
|
291
|
+
end
|
292
|
+
|
293
|
+
`rm -f '#{apk_path_signed}'`
|
294
|
+
apk_path_signed = apk_path_aligned
|
295
|
+
|
296
|
+
else
|
297
|
+
UI.message("No zip align - deactivated via parameter!")
|
298
|
+
end
|
292
299
|
|
293
300
|
apk_path_signed
|
294
301
|
end
|
295
302
|
|
303
|
+
def self.sign_aab(aab_path, keystore_path, key_password, alias_name, alias_password)
|
304
|
+
|
305
|
+
aab_path_signed = aab_path.gsub('.aab', '-signed.aab')
|
306
|
+
aab_path_signed = aab_path_signed.gsub('unsigned', '')
|
307
|
+
aab_path_signed = aab_path_signed.gsub('--', '-')
|
308
|
+
`rm -f '#{aab_path_signed}'`
|
309
|
+
|
310
|
+
UI.message("Signing AAB (input): #{aab_path}")
|
311
|
+
aabsigner_opts = ""
|
312
|
+
output = `jarsigner -keystore '#{keystore_path}' -storepass '#{key_password}' -keypass '#{alias_password}' -signedjar '#{aab_path_signed}' '#{aab_path}' '#{alias_name}'`
|
313
|
+
puts ""
|
314
|
+
puts output
|
315
|
+
|
316
|
+
aab_path_signed
|
317
|
+
end
|
318
|
+
|
296
319
|
def self.resolve_dir(path)
|
297
320
|
if !File.directory?(path)
|
298
321
|
path = File.join(Dir.pwd, path)
|
@@ -316,6 +339,34 @@ module Fastlane
|
|
316
339
|
data
|
317
340
|
end
|
318
341
|
|
342
|
+
def self.resolve_aab_path(aab_path)
|
343
|
+
|
344
|
+
# Set default AAB path if not set:
|
345
|
+
if aab_path.to_s.strip.empty?
|
346
|
+
aab_path = '/app/build/outputs/bundle/release/'
|
347
|
+
end
|
348
|
+
|
349
|
+
if !aab_path.to_s.end_with?('.aab')
|
350
|
+
|
351
|
+
aab_path = self.resolve_dir(aab_path)
|
352
|
+
|
353
|
+
pattern = File.join(aab_path, '*.aab')
|
354
|
+
files = Dir[pattern]
|
355
|
+
|
356
|
+
for file in files
|
357
|
+
if file.to_s.end_with?('.aab') && !file.to_s.end_with?("-signed.aab")
|
358
|
+
apk_path = file
|
359
|
+
break
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
else
|
364
|
+
aab_path = self.resolve_file(aab_path)
|
365
|
+
end
|
366
|
+
|
367
|
+
aab_path
|
368
|
+
end
|
369
|
+
|
319
370
|
def self.resolve_apk_path(apk_path)
|
320
371
|
|
321
372
|
# Set default APK path if not set:
|
@@ -360,6 +411,7 @@ module Fastlane
|
|
360
411
|
git_url = params[:git_url]
|
361
412
|
package_name = params[:package_name]
|
362
413
|
apk_path = params[:apk_path]
|
414
|
+
aab_path = params[:aab_path]
|
363
415
|
existing_keystore = params[:existing_keystore]
|
364
416
|
match_secret = params[:match_secret]
|
365
417
|
override_keystore = params[:override_keystore]
|
@@ -367,6 +419,8 @@ module Fastlane
|
|
367
419
|
clear_keystore = params[:clear_keystore]
|
368
420
|
unit_test = params[:unit_test]
|
369
421
|
build_tools_version = params[:build_tools_version]
|
422
|
+
zip_align = params[:zip_align]
|
423
|
+
compat_key = params[:compat_key]
|
370
424
|
|
371
425
|
# Test OpenSSL/LibreSSL
|
372
426
|
if unit_test
|
@@ -390,6 +444,11 @@ module Fastlane
|
|
390
444
|
# Check OpenSSL:
|
391
445
|
self.check_ssl_version(false)
|
392
446
|
|
447
|
+
# Check is backward-compatibility is required:
|
448
|
+
if !compat_key.to_s.strip.empty?
|
449
|
+
UI.message("Compatiblity version: #{compat_key}")
|
450
|
+
end
|
451
|
+
|
393
452
|
# Init workign local directory:
|
394
453
|
dir_name = ENV['HOME'] + '/.match_keystore'
|
395
454
|
unless File.directory?(dir_name)
|
@@ -398,7 +457,11 @@ module Fastlane
|
|
398
457
|
end
|
399
458
|
|
400
459
|
# Init 'security password' for AES encryption:
|
401
|
-
|
460
|
+
if compat_key == "1"
|
461
|
+
key_name = "#{self.to_md5(git_url)}.hex"
|
462
|
+
else
|
463
|
+
key_name = "#{self.to_md5(git_url)}-#{self::KEY_VERSION}.hex"
|
464
|
+
end
|
402
465
|
key_path = File.join(dir_name, key_name)
|
403
466
|
# UI.message(key_path)
|
404
467
|
if !File.file?(key_path)
|
@@ -407,7 +470,7 @@ module Fastlane
|
|
407
470
|
raise "Security password is not defined! Please use 'match_secret' parameter for CI."
|
408
471
|
end
|
409
472
|
UI.message "Generating security key '#{key_name}'..."
|
410
|
-
self.gen_key(key_path, security_password)
|
473
|
+
self.gen_key(key_path, security_password, compat_key)
|
411
474
|
end
|
412
475
|
|
413
476
|
# Check is 'security password' is well initialized:
|
@@ -446,14 +509,10 @@ module Fastlane
|
|
446
509
|
gitDir = File.join(repo_dir, '/.git')
|
447
510
|
if !File.directory?(gitDir)
|
448
511
|
UI.message("Cloning remote Keystores repository...")
|
449
|
-
puts ''
|
450
512
|
`git clone #{git_url} #{repo_dir}`
|
451
|
-
puts ''
|
452
513
|
else
|
453
514
|
UI.message("Pulling remote Keystores repository...")
|
454
|
-
puts ''
|
455
515
|
`cd #{repo_dir} && git pull`
|
456
|
-
puts ''
|
457
516
|
end
|
458
517
|
|
459
518
|
# Load parameters from JSON for CI or Unit Tests:
|
@@ -554,6 +613,7 @@ module Fastlane
|
|
554
613
|
self.decrypt_file(properties_encrypt_path, properties_path, key_path, false)
|
555
614
|
|
556
615
|
properties = self.load_properties(properties_path)
|
616
|
+
# Pry::ColorPrinter.pp(properties)
|
557
617
|
key_password = properties['keyPassword']
|
558
618
|
alias_name = properties['aliasName']
|
559
619
|
alias_password = properties['aliasPassword']
|
@@ -561,13 +621,13 @@ module Fastlane
|
|
561
621
|
File.delete(properties_path)
|
562
622
|
end
|
563
623
|
|
564
|
-
# Resolve path to the APK to sign:
|
565
|
-
output_signed_apk = ''
|
566
|
-
apk_path = self.resolve_apk_path(apk_path)
|
567
|
-
|
568
624
|
# Sign APK:
|
569
|
-
if File.file?(apk_path)
|
625
|
+
if apk_path && File.file?(apk_path)
|
570
626
|
UI.message("APK to sign: " + apk_path)
|
627
|
+
|
628
|
+
# Resolve path to the APK to sign:
|
629
|
+
output_signed_apk = ''
|
630
|
+
apk_path = self.resolve_apk_path(apk_path)
|
571
631
|
|
572
632
|
if File.file?(keystore_path)
|
573
633
|
|
@@ -579,21 +639,49 @@ module Fastlane
|
|
579
639
|
key_password,
|
580
640
|
alias_name,
|
581
641
|
alias_password,
|
582
|
-
|
642
|
+
zip_align, # Zip align
|
583
643
|
build_tools_version # Buil-tools version
|
584
644
|
)
|
585
645
|
puts ''
|
586
646
|
end
|
587
|
-
else
|
588
|
-
UI.message("No APK file found to sign!")
|
589
|
-
end
|
590
647
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
648
|
+
# Prepare contect shared values for next lanes:
|
649
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
|
650
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
|
651
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_APK_SIGNED] = output_signed_apk
|
652
|
+
|
653
|
+
output_signed_apk
|
654
|
+
# Sign AAB
|
655
|
+
elsif aab_path && File.file?(aab_path)
|
656
|
+
UI.message('AAB to sign: '+ aab_path)
|
657
|
+
|
658
|
+
# Resolve path to the AAB to sign:
|
659
|
+
output_signed_aab = ''
|
660
|
+
aab_path = self.resolve_aab_path(aab_path)
|
661
|
+
|
662
|
+
if File.file?(keystore_path)
|
663
|
+
|
664
|
+
UI.message("Signing the AAB...")
|
665
|
+
puts ''
|
666
|
+
output_signed_aab = self.sign_aab(
|
667
|
+
aab_path,
|
668
|
+
keystore_path,
|
669
|
+
key_password,
|
670
|
+
alias_name,
|
671
|
+
alias_password
|
672
|
+
)
|
673
|
+
puts ''
|
674
|
+
end
|
675
|
+
|
676
|
+
# Prepare contect shared values for next lanes:
|
677
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
|
678
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
|
679
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_AAB_SIGNED] = output_signed_aab
|
595
680
|
|
596
|
-
|
681
|
+
output_signed_aab
|
682
|
+
else
|
683
|
+
UI.message("No APK or AAB file found")
|
684
|
+
end
|
597
685
|
end
|
598
686
|
|
599
687
|
def self.description
|
@@ -601,7 +689,7 @@ module Fastlane
|
|
601
689
|
end
|
602
690
|
|
603
691
|
def self.authors
|
604
|
-
["Christopher NEY"]
|
692
|
+
["Christopher NEY", "Simon Scherzinger"]
|
605
693
|
end
|
606
694
|
|
607
695
|
def self.return_value
|
@@ -612,7 +700,8 @@ module Fastlane
|
|
612
700
|
[
|
613
701
|
['MATCH_KEYSTORE_PATH', 'File path of the Keystore fot the App.'],
|
614
702
|
['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.'],
|
615
|
-
['MATCH_KEYSTORE_APK_SIGNED', 'Path of the signed APK.']
|
703
|
+
['MATCH_KEYSTORE_APK_SIGNED', 'Path of the signed APK.'],
|
704
|
+
['MATCH_KEYSTORE_AAB_SIGNED', 'Path of the signed AAB.']
|
616
705
|
]
|
617
706
|
end
|
618
707
|
|
@@ -638,6 +727,11 @@ module Fastlane
|
|
638
727
|
description: "Path of the APK file to sign",
|
639
728
|
optional: true,
|
640
729
|
type: String),
|
730
|
+
FastlaneCore::ConfigItem.new(key: :aab_path,
|
731
|
+
env_name: "MATCH_KEYSTORE_AAB_PATH",
|
732
|
+
description: "Path of the AAB file to sign",
|
733
|
+
optional: true,
|
734
|
+
type: String),
|
641
735
|
FastlaneCore::ConfigItem.new(key: :match_secret,
|
642
736
|
env_name: "MATCH_KEYSTORE_SECRET",
|
643
737
|
description: "Secret to decrypt keystore.properties file (CI)",
|
@@ -662,17 +756,27 @@ module Fastlane
|
|
662
756
|
env_name: "MATCH_KEYSTORE_BUILD_TOOLS_VERSION",
|
663
757
|
description: "Set built-tools version (by default latest available on machine)",
|
664
758
|
optional: true,
|
665
|
-
type: String),
|
759
|
+
type: String),
|
760
|
+
FastlaneCore::ConfigItem.new(key: :zip_align,
|
761
|
+
env_name: "MATCH_KEYSTORE_ZIPALIGN",
|
762
|
+
description: "Define if plugin will run zipalign on APK before sign it (true by default)",
|
763
|
+
optional: true,
|
764
|
+
type: Boolean),
|
765
|
+
FastlaneCore::ConfigItem.new(key: :compat_key,
|
766
|
+
env_name: "MATCH_KEYSTORE_COMPAT_KEY",
|
767
|
+
description: "Define the compatibility key version used on local machine (nil by default)",
|
768
|
+
optional: true,
|
769
|
+
type: String),
|
666
770
|
FastlaneCore::ConfigItem.new(key: :clear_keystore,
|
667
771
|
env_name: "MATCH_KEYSTORE_CLEAR",
|
668
772
|
description: "Clear the local keystore (false by default)",
|
669
773
|
optional: true,
|
670
774
|
type: Boolean),
|
671
775
|
FastlaneCore::ConfigItem.new(key: :unit_test,
|
672
|
-
|
776
|
+
env_name: "MATCH_KEYSTORE_UNIT_TESTS",
|
673
777
|
description: "launch Unit Tests (false by default)",
|
674
|
-
|
675
|
-
|
778
|
+
optional: true,
|
779
|
+
type: Boolean)
|
676
780
|
]
|
677
781
|
end
|
678
782
|
|
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.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher NEY
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-09-15 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.
|
170
|
+
rubygems_version: 3.2.17
|
171
171
|
signing_key:
|
172
172
|
specification_version: 4
|
173
173
|
summary: Easily sync your Android keystores across your team
|