mysigner 0.1.2 → 0.1.4

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.githooks/pre-commit +15 -0
  3. data/.githooks/pre-push +21 -0
  4. data/.github/workflows/ci.yml +29 -0
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +55 -0
  7. data/.rubocop_todo.yml +126 -0
  8. data/CHANGELOG.md +96 -0
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +38 -8
  11. data/README.md +14 -16
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/bin/setup +3 -0
  15. data/certificate_.cer +0 -0
  16. data/exe/mysigner +19 -2
  17. data/iOS_App_Store_Profile.mobileprovision +1 -0
  18. data/iOS_Distribution_Certificate.cer +1 -0
  19. data/lib/mysigner/build/android_executor.rb +83 -63
  20. data/lib/mysigner/build/android_parser.rb +33 -40
  21. data/lib/mysigner/build/configurator.rb +17 -16
  22. data/lib/mysigner/build/detector.rb +39 -50
  23. data/lib/mysigner/build/error_analyzer.rb +70 -68
  24. data/lib/mysigner/build/executor.rb +30 -37
  25. data/lib/mysigner/build/parser.rb +18 -18
  26. data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
  27. data/lib/mysigner/cli/auth_commands.rb +771 -764
  28. data/lib/mysigner/cli/build_commands.rb +962 -796
  29. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
  30. data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
  31. data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
  32. data/lib/mysigner/cli/concerns/helpers.rb +44 -1
  33. data/lib/mysigner/cli/diagnostic_commands.rb +667 -636
  34. data/lib/mysigner/cli/resource_commands.rb +1153 -985
  35. data/lib/mysigner/cli/validate_commands.rb +25 -25
  36. data/lib/mysigner/cli.rb +11 -1
  37. data/lib/mysigner/client.rb +27 -19
  38. data/lib/mysigner/config.rb +161 -60
  39. data/lib/mysigner/export/exporter.rb +38 -37
  40. data/lib/mysigner/signing/certificate_checker.rb +18 -23
  41. data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
  42. data/lib/mysigner/signing/keystore_manager.rb +81 -61
  43. data/lib/mysigner/signing/validator.rb +38 -40
  44. data/lib/mysigner/signing/wizard.rb +329 -342
  45. data/lib/mysigner/upload/app_store_automation.rb +96 -49
  46. data/lib/mysigner/upload/app_store_submission.rb +87 -92
  47. data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
  48. data/lib/mysigner/upload/play_store_uploader.rb +164 -144
  49. data/lib/mysigner/upload/uploader.rb +136 -115
  50. data/lib/mysigner/version.rb +3 -1
  51. data/lib/mysigner.rb +13 -11
  52. data/mysigner.gemspec +36 -33
  53. data/profile_.mobileprovision +0 -0
  54. data/test_manual.rb +37 -36
  55. metadata +44 -17
  56. data/.DS_Store +0 -0
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
1
4
  require 'fileutils'
2
5
  require 'tmpdir'
3
6
 
@@ -9,31 +12,27 @@ module Mysigner
9
12
  def initialize(archive_path, output_dir: nil)
10
13
  @archive_path = File.expand_path(archive_path)
11
14
  @output_dir = output_dir || File.dirname(@archive_path)
12
-
15
+
13
16
  validate_archive!
14
17
  end
15
18
 
16
19
  def export!(method: :appstore, team_id: nil, signing_style: 'automatic')
17
20
  say_exporting(method)
18
-
21
+
19
22
  # Generate export options plist
20
23
  options_plist = generate_export_options(method, team_id, signing_style)
21
-
24
+
22
25
  begin
23
26
  # Run xcodebuild -exportArchive
24
27
  success = execute_export(options_plist)
25
-
26
- unless success
27
- raise ExportError, "Export failed. Check output above for errors."
28
- end
29
-
28
+
29
+ raise ExportError, 'Export failed. Check output above for errors.' unless success
30
+
30
31
  # Find the generated .ipa file
31
32
  ipa_path = find_ipa_file
32
-
33
- unless ipa_path
34
- raise ExportError, "Export reported success but .ipa file not found in: #{@output_dir}"
35
- end
36
-
33
+
34
+ raise ExportError, "Export reported success but .ipa file not found in: #{@output_dir}" unless ipa_path
35
+
37
36
  ipa_path
38
37
  ensure
39
38
  # Clean up temp plist
@@ -44,42 +43,40 @@ module Mysigner
44
43
  private
45
44
 
46
45
  def validate_archive!
47
- unless File.exist?(@archive_path)
48
- raise ExportError, "Archive not found: #{@archive_path}"
49
- end
50
-
51
- unless File.directory?(@archive_path) && @archive_path.end_with?('.xcarchive')
52
- raise ExportError, "Invalid archive: #{@archive_path} (must be a .xcarchive directory)"
53
- end
46
+ raise ExportError, "Archive not found: #{@archive_path}" unless File.exist?(@archive_path)
47
+
48
+ return if File.directory?(@archive_path) && @archive_path.end_with?('.xcarchive')
49
+
50
+ raise ExportError, "Invalid archive: #{@archive_path} (must be a .xcarchive directory)"
54
51
  end
55
52
 
56
53
  def generate_export_options(method, team_id, signing_style)
57
54
  require 'plist'
58
-
55
+
59
56
  options = {
60
57
  'method' => export_method_string(method),
61
58
  'uploadBitcode' => false,
62
59
  'uploadSymbols' => false,
63
60
  'compileBitcode' => false
64
61
  }
65
-
62
+
66
63
  # Add team ID if provided
67
64
  options['teamID'] = team_id if team_id
68
-
65
+
69
66
  # Signing style
70
67
  if signing_style.to_s.downcase == 'manual'
71
68
  options['signingStyle'] = 'manual'
72
- # Note: For manual signing, we'd need to specify provisioningProfiles
69
+ # NOTE: For manual signing, we'd need to specify provisioningProfiles
73
70
  # But since the archive was already signed during build, we can often omit this
74
71
  else
75
72
  options['signingStyle'] = 'automatic'
76
73
  options['signingCertificate'] = 'Apple Distribution'
77
74
  end
78
-
75
+
79
76
  # Create temp plist file
80
77
  plist_path = File.join(Dir.tmpdir, "exportOptions-#{Time.now.to_i}.plist")
81
78
  File.write(plist_path, options.to_plist)
82
-
79
+
83
80
  plist_path
84
81
  end
85
82
 
@@ -100,7 +97,7 @@ module Mysigner
100
97
 
101
98
  def execute_export(options_plist)
102
99
  FileUtils.mkdir_p(@output_dir)
103
- puts ""
100
+ puts ''
104
101
 
105
102
  cmd = [
106
103
  'xcodebuild',
@@ -111,40 +108,44 @@ module Mysigner
111
108
  '-allowProvisioningUpdates' # Allow Xcode to update profiles if needed
112
109
  ].join(' ')
113
110
 
114
- # Run command and capture output
115
- IO.popen(cmd, err: [:child, :out]) do |io|
111
+ # Run command and capture output. xcodebuild output can contain
112
+ # non-ASCII bytes (smart quotes in Apple error messages, emoji in
113
+ # file paths) — force UTF-8 + scrub so `.strip` / `.include?` don't
114
+ # raise Encoding::CompatibilityError under the default US-ASCII
115
+ # locale (e.g. on CI runners without LANG set).
116
+ IO.popen(cmd, err: %i[child out]) do |io|
116
117
  io.each_line do |line|
118
+ line = line.force_encoding('UTF-8').scrub
117
119
  next if line.strip.empty?
118
-
120
+
119
121
  # Show errors and warnings
120
122
  if line.include?('error:') || line.include?('warning:')
121
123
  puts line
122
124
  # Show progress markers
123
- elsif line.include?('Exporting') || line.include?('Processing') ||
125
+ elsif line.include?('Exporting') || line.include?('Processing') ||
124
126
  line.include?('Validating')
125
127
  print '.'
126
128
  end
127
129
  end
128
130
  end
129
131
 
130
- puts "" # New line after dots
131
-
132
- $?.success?
132
+ puts '' # New line after dots
133
+
134
+ $CHILD_STATUS.success?
133
135
  end
134
136
 
135
137
  def find_ipa_file
136
138
  # Look for .ipa files in output directory
137
139
  ipa_files = Dir.glob(File.join(@output_dir, '*.ipa'))
138
-
140
+
139
141
  # Return the most recently created one
140
142
  ipa_files.max_by { |f| File.mtime(f) }
141
143
  end
142
144
 
143
145
  def say_exporting(method)
144
146
  puts "📦 Exporting archive for #{method}..."
145
- puts ""
147
+ puts ''
146
148
  end
147
149
  end
148
150
  end
149
151
  end
150
-
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'open3'
2
4
  require 'time'
3
5
 
@@ -42,23 +44,21 @@ module Mysigner
42
44
  cmd = 'security find-identity -v -p codesigning'
43
45
  stdout, stderr, status = Open3.capture3(cmd)
44
46
 
45
- unless status.success?
46
- raise CheckError, "Failed to query certificates: #{stderr}"
47
- end
47
+ raise CheckError, "Failed to query certificates: #{stderr}" unless status.success?
48
48
 
49
49
  certificates = []
50
-
50
+
51
51
  # Parse output: " 1) HASH \"Certificate Name\""
52
52
  stdout.each_line do |line|
53
53
  next unless line =~ /\d+\)\s+([A-F0-9]+)\s+"([^"]+)"/
54
-
55
- hash = $1
56
- name = $2
57
-
54
+
55
+ hash = ::Regexp.last_match(1)
56
+ name = ::Regexp.last_match(2)
57
+
58
58
  # Get certificate details
59
59
  details = get_certificate_details(name)
60
60
  next unless details
61
-
61
+
62
62
  certificates << {
63
63
  hash: hash,
64
64
  name: name,
@@ -76,12 +76,10 @@ module Mysigner
76
76
  def get_certificate_details(name)
77
77
  # Get certificate in PEM format by name
78
78
  cmd = "security find-certificate -c \"#{name}\" -p"
79
- stdout, stderr, status = Open3.capture3(cmd)
79
+ stdout, _, status = Open3.capture3(cmd)
80
80
 
81
81
  # If not found, return nil
82
- if !status.success? || stdout.empty?
83
- return nil
84
- end
82
+ return nil if !status.success? || stdout.empty?
85
83
 
86
84
  # Save to temp file and use openssl to read expiry
87
85
  require 'tempfile'
@@ -92,12 +90,12 @@ module Mysigner
92
90
 
93
91
  # Get expiry date using openssl
94
92
  cmd = "openssl x509 -in #{temp.path} -noout -enddate"
95
- out, err, stat = Open3.capture3(cmd)
93
+ out, _, stat = Open3.capture3(cmd)
96
94
 
97
95
  if stat.success? && out =~ /notAfter=(.+)/
98
- expiry_str = $1.strip
96
+ expiry_str = ::Regexp.last_match(1).strip
99
97
  expires_at = Time.parse(expiry_str)
100
- days_until_expiry = ((expires_at - Time.now) / 86400).to_i
98
+ days_until_expiry = ((expires_at - Time.now) / 86_400).to_i
101
99
 
102
100
  return {
103
101
  expires_at: expires_at,
@@ -113,11 +111,9 @@ module Mysigner
113
111
 
114
112
  def extract_team_id(name)
115
113
  # Team ID is usually in parentheses at the end
116
- if name =~ /\(([A-Z0-9]{10})\)$/
117
- $1
118
- else
119
- nil
120
- end
114
+ return unless name =~ /\(([A-Z0-9]{10})\)$/
115
+
116
+ ::Regexp.last_match(1)
121
117
  end
122
118
 
123
119
  def determine_cert_type(name)
@@ -134,7 +130,7 @@ module Mysigner
134
130
  end
135
131
 
136
132
  def determine_status(days_until_expiry)
137
- if days_until_expiry < 0
133
+ if days_until_expiry.negative?
138
134
  STATUS_EXPIRED
139
135
  elsif days_until_expiry < 30
140
136
  STATUS_EXPIRING_SOON
@@ -145,4 +141,3 @@ module Mysigner
145
141
  end
146
142
  end
147
143
  end
148
-
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ module Mysigner
7
+ module Signing
8
+ class GradleSigningInjector
9
+ # Local variable is `aliasName` (not `keyAlias`) to avoid a Groovy
10
+ # `with { }` scoping ambiguity: `keyAlias = keyAlias` would resolve the
11
+ # RHS against the property being set (null at that point), silently
12
+ # signing with the wrong alias.
13
+ INIT_SCRIPT = <<~GROOVY
14
+ allprojects {
15
+ afterEvaluate { project ->
16
+ if (!project.hasProperty('android')) return
17
+ def storePw = System.getenv('MYSIGNER_STORE_PASSWORD')
18
+ def keyPw = System.getenv('MYSIGNER_KEY_PASSWORD')
19
+ def aliasName = System.getenv('MYSIGNER_KEY_ALIAS')
20
+ def ksPath = System.getenv('MYSIGNER_STORE_FILE')
21
+ if (!storePw || !ksPath) return
22
+
23
+ def existing = project.android.signingConfigs.findByName('release')
24
+ def alreadyConfigured = existing != null && existing.storeFile != null
25
+ if (alreadyConfigured) {
26
+ println "MySigner: release signingConfig already set; skipping override."
27
+ return
28
+ }
29
+ project.android.signingConfigs.maybeCreate('release').with {
30
+ storeFile = file(ksPath)
31
+ storePassword = storePw
32
+ keyAlias = aliasName
33
+ keyPassword = keyPw
34
+ }
35
+ project.android.buildTypes.findByName('release')?.signingConfig =
36
+ project.android.signingConfigs.getByName('release')
37
+ }
38
+ }
39
+ GROOVY
40
+
41
+ def initialize
42
+ @tmpdir = nil
43
+ end
44
+
45
+ def write_init_script!
46
+ @tmpdir = Dir.mktmpdir('mysigner-signing-')
47
+ path = File.join(@tmpdir, 'init.gradle')
48
+ File.write(path, INIT_SCRIPT)
49
+ path
50
+ end
51
+
52
+ def env_vars(keystore_path:, store_password:, key_password:, key_alias:)
53
+ {
54
+ 'MYSIGNER_STORE_FILE' => keystore_path,
55
+ 'MYSIGNER_STORE_PASSWORD' => store_password,
56
+ 'MYSIGNER_KEY_PASSWORD' => key_password,
57
+ 'MYSIGNER_KEY_ALIAS' => key_alias
58
+ }
59
+ end
60
+
61
+ def cleanup!
62
+ FileUtils.rm_rf(@tmpdir) if @tmpdir && Dir.exist?(@tmpdir)
63
+ @tmpdir = nil
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
1
4
  require 'fileutils'
2
5
  require 'base64'
6
+ require 'open3'
3
7
 
4
8
  module Mysigner
5
9
  module Signing
@@ -18,11 +22,13 @@ module Mysigner
18
22
 
19
23
  # List all keystores from API
20
24
  # @param android_app_id [Integer, nil] Filter by app ID
21
- # @param include_secrets [Boolean] Include passwords in response (only for build operations)
22
- def list(android_app_id: nil, include_secrets: false)
25
+ # @param include_secrets [Boolean] DEPRECATED and silently ignored. Kept
26
+ # for signature-compat during the 10.0 transition; will be removed in
27
+ # the next release. Passwords are now fetched via #fetch_secrets.
28
+ def list(android_app_id: nil, include_secrets: nil)
29
+ _ = include_secrets # intentionally unused — see note above
23
30
  params = {}
24
31
  params[:android_app_id] = android_app_id if android_app_id
25
- params[:include_secrets] = true if include_secrets
26
32
 
27
33
  response = @client.get(
28
34
  "/api/v1/organizations/#{@organization_id}/android_keystores",
@@ -32,33 +38,43 @@ module Mysigner
32
38
  end
33
39
 
34
40
  # Get active keystore for an app (or any active keystore if no app specified)
35
- # @param include_secrets [Boolean] Include passwords in response (only for build operations)
36
- def active_keystore(android_app_id: nil, include_secrets: false)
37
- keystores = list(android_app_id: android_app_id, include_secrets: include_secrets)
41
+ # @param include_secrets [Boolean] DEPRECATED see #list.
42
+ def active_keystore(android_app_id: nil, include_secrets: nil)
43
+ _ = include_secrets
44
+ keystores = list(android_app_id: android_app_id)
38
45
  keystores.find { |k| k['active'] }
39
46
  end
40
47
 
48
+ # Phase 0: narrow audit-logged endpoint that returns the keystore
49
+ # password + key password + key alias for a single keystore. Replaces
50
+ # the insecure ?include_secrets=true list flag.
51
+ # Returns a hash: { 'keystore_password' =>, 'key_password' =>, 'key_alias' => }
52
+ def fetch_secrets(keystore_id)
53
+ response = @client.post(
54
+ "/api/v1/organizations/#{@organization_id}/android_keystores/#{keystore_id}/secrets"
55
+ )
56
+ response[:data] || {}
57
+ end
58
+
41
59
  # Download a keystore from API and save locally
42
60
  # Returns: { path: String, password: String, alias: String, key_password: String }
43
61
  def download(keystore_id)
44
62
  # Get keystore details
45
63
  keystores = list
46
64
  keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
47
-
48
- unless keystore
49
- raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found"
50
- end
65
+
66
+ raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found" unless keystore
51
67
 
52
68
  # Download the keystore file
53
69
  download_url = "/api/v1/organizations/#{@organization_id}/android_keystores/#{keystore_id}/download"
54
-
70
+
55
71
  conn = build_download_connection
56
72
  response = conn.get(download_url)
57
-
73
+
58
74
  unless response.success?
59
75
  error_msg = begin
60
76
  JSON.parse(response.body)['message']
61
- rescue
77
+ rescue StandardError
62
78
  "HTTP #{response.status}"
63
79
  end
64
80
  raise DownloadError, "Failed to download keystore: #{error_msg}"
@@ -67,9 +83,9 @@ module Mysigner
67
83
  # Save to local file
68
84
  filename = "#{keystore['name'].gsub(/[^a-zA-Z0-9_.-]/, '_')}.jks"
69
85
  local_path = File.join(KEYSTORES_DIR, filename)
70
-
86
+
71
87
  File.binwrite(local_path, response.body)
72
- File.chmod(0600, local_path) # Secure permissions
88
+ File.chmod(0o600, local_path) # Secure permissions
73
89
 
74
90
  {
75
91
  path: local_path,
@@ -79,40 +95,45 @@ module Mysigner
79
95
  }
80
96
  end
81
97
 
82
- # Get or download keystore (uses cached version if available)
98
+ # Get or download keystore (uses cached version if available and fresh).
99
+ # Phase 0: cache has a TTL (default 24h, override via
100
+ # MYSIGNER_KEYSTORE_CACHE_HOURS). Stale files are deleted + re-downloaded.
83
101
  def get_or_download(keystore_id)
84
102
  keystores = list
85
103
  keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
86
-
87
- unless keystore
88
- raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found"
89
- end
90
104
 
91
- # Check if already cached locally
105
+ raise KeystoreNotFoundError, "Keystore with ID #{keystore_id} not found" unless keystore
106
+
92
107
  filename = "#{keystore['name'].gsub(/[^a-zA-Z0-9_.-]/, '_')}.jks"
93
108
  local_path = File.join(KEYSTORES_DIR, filename)
109
+ max_age_hours = (ENV['MYSIGNER_KEYSTORE_CACHE_HOURS'] || 24).to_i
94
110
 
95
111
  if File.exist?(local_path)
96
- return {
97
- path: local_path,
98
- name: keystore['name'],
99
- key_alias: keystore['key_alias'],
100
- id: keystore['id'],
101
- cached: true
102
- }
112
+ age_seconds = Time.now - File.mtime(local_path)
113
+ if age_seconds < (max_age_hours * 3600)
114
+ return {
115
+ path: local_path,
116
+ name: keystore['name'],
117
+ key_alias: keystore['key_alias'],
118
+ id: keystore['id'],
119
+ cached: true
120
+ }
121
+ else
122
+ # Stale cache — delete and re-download below
123
+ File.delete(local_path)
124
+ end
103
125
  end
104
126
 
105
- # Download if not cached
127
+ # Download if not cached (or stale)
106
128
  result = download(keystore_id)
107
129
  result[:cached] = false
108
130
  result
109
131
  end
110
132
 
111
133
  # Upload a keystore to API
112
- def upload(name:, keystore_path:, keystore_password:, key_alias:, key_password: nil, android_app_id: nil, active: true)
113
- unless File.exist?(keystore_path)
114
- raise KeystoreError, "Keystore file not found: #{keystore_path}"
115
- end
134
+ def upload(name:, keystore_path:, keystore_password:, key_alias:, key_password: nil, android_app_id: nil,
135
+ active: true)
136
+ raise KeystoreError, "Keystore file not found: #{keystore_path}" unless File.exist?(keystore_path)
116
137
 
117
138
  # Read and encode keystore
118
139
  keystore_content = File.binread(keystore_path)
@@ -141,14 +162,18 @@ module Mysigner
141
162
  # Delete a keystore
142
163
  def delete(keystore_id)
143
164
  @client.delete("/api/v1/organizations/#{@organization_id}/android_keystores/#{keystore_id}")
144
-
165
+
145
166
  # Also remove local cached file
146
- keystores = list rescue []
167
+ keystores = begin
168
+ list
169
+ rescue StandardError
170
+ []
171
+ end
147
172
  keystore = keystores.find { |k| k['id'].to_s == keystore_id.to_s }
148
173
  if keystore
149
174
  filename = "#{keystore['name'].gsub(/[^a-zA-Z0-9_.-]/, '_')}.jks"
150
175
  local_path = File.join(KEYSTORES_DIR, filename)
151
- File.delete(local_path) if File.exist?(local_path)
176
+ FileUtils.rm_f(local_path)
152
177
  end
153
178
 
154
179
  true
@@ -181,32 +206,32 @@ module Mysigner
181
206
  end
182
207
  end
183
208
 
184
- # Get keystore info using keytool
209
+ # Get keystore info using keytool.
210
+ # Phase 0: passes the password via a temp env var consumed by
211
+ # `-storepass:env` so it's never in argv/ps output.
185
212
  def keystore_info(keystore_path, password)
186
213
  return nil unless File.exist?(keystore_path)
187
214
  return nil unless system('which keytool > /dev/null 2>&1')
188
215
 
189
- output = `keytool -list -v -keystore #{shell_escape(keystore_path)} -storepass #{shell_escape(password)} 2>&1`
190
- return nil unless $?.success?
216
+ env = { 'MYSIGNER_KS_PW' => password.to_s }
217
+ output, status = Open3.capture2e(
218
+ env,
219
+ 'keytool', '-list', '-v',
220
+ '-keystore', keystore_path,
221
+ '-storepass:env', 'MYSIGNER_KS_PW'
222
+ )
223
+ return nil unless status.success?
191
224
 
192
225
  # Parse output
193
226
  info = {}
194
-
195
- if output =~ /Alias name: (.+)/
196
- info[:aliases] = output.scan(/Alias name: (.+)/).flatten
197
- end
198
-
199
- if output =~ /Valid from: .+ until: (.+)/
200
- info[:expires] = $1
201
- end
202
-
203
- if output =~ /SHA256: (.+)/
204
- info[:sha256] = $1.strip
205
- end
206
-
207
- if output =~ /SHA1: (.+)/
208
- info[:sha1] = $1.strip
209
- end
227
+
228
+ info[:aliases] = output.scan(/Alias name: (.+)/).flatten if output =~ /Alias name: (.+)/
229
+
230
+ info[:expires] = ::Regexp.last_match(1) if output =~ /Valid from: .+ until: (.+)/
231
+
232
+ info[:sha256] = ::Regexp.last_match(1).strip if output =~ /SHA256: (.+)/
233
+
234
+ info[:sha1] = ::Regexp.last_match(1).strip if output =~ /SHA1: (.+)/
210
235
 
211
236
  info
212
237
  end
@@ -215,7 +240,7 @@ module Mysigner
215
240
 
216
241
  def ensure_keystores_dir
217
242
  FileUtils.mkdir_p(KEYSTORES_DIR)
218
- File.chmod(0700, KEYSTORES_DIR) # Secure permissions
243
+ File.chmod(0o700, KEYSTORES_DIR) # Secure permissions
219
244
  end
220
245
 
221
246
  def build_download_connection
@@ -229,11 +254,6 @@ module Mysigner
229
254
  f.adapter Faraday.default_adapter
230
255
  end
231
256
  end
232
-
233
- def shell_escape(str)
234
- return "''" if str.nil? || str.empty?
235
- "'" + str.gsub("'", "'\\''") + "'"
236
- end
237
257
  end
238
258
  end
239
259
  end