mysigner 0.1.1 → 0.1.3

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 (48) 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 +1 -0
  6. data/.rubocop.yml +55 -0
  7. data/.rubocop_todo.yml +112 -0
  8. data/CHANGELOG.md +96 -0
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +38 -8
  11. data/README.md +87 -17
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/bin/setup +3 -0
  15. data/exe/mysigner +2 -1
  16. data/lib/mysigner/build/android_executor.rb +46 -52
  17. data/lib/mysigner/build/android_parser.rb +33 -40
  18. data/lib/mysigner/build/configurator.rb +17 -16
  19. data/lib/mysigner/build/detector.rb +39 -50
  20. data/lib/mysigner/build/error_analyzer.rb +70 -68
  21. data/lib/mysigner/build/executor.rb +30 -37
  22. data/lib/mysigner/build/parser.rb +18 -18
  23. data/lib/mysigner/cli/auth_commands.rb +735 -752
  24. data/lib/mysigner/cli/build_commands.rb +697 -721
  25. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
  26. data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
  27. data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
  28. data/lib/mysigner/cli/concerns/helpers.rb +12 -1
  29. data/lib/mysigner/cli/diagnostic_commands.rb +659 -635
  30. data/lib/mysigner/cli/resource_commands.rb +1266 -822
  31. data/lib/mysigner/cli/validate_commands.rb +161 -0
  32. data/lib/mysigner/cli.rb +5 -1
  33. data/lib/mysigner/client.rb +27 -19
  34. data/lib/mysigner/config.rb +93 -56
  35. data/lib/mysigner/export/exporter.rb +32 -36
  36. data/lib/mysigner/signing/certificate_checker.rb +18 -23
  37. data/lib/mysigner/signing/keystore_manager.rb +34 -39
  38. data/lib/mysigner/signing/validator.rb +38 -40
  39. data/lib/mysigner/signing/wizard.rb +329 -342
  40. data/lib/mysigner/upload/app_store_automation.rb +51 -49
  41. data/lib/mysigner/upload/app_store_submission.rb +87 -92
  42. data/lib/mysigner/upload/play_store_uploader.rb +98 -115
  43. data/lib/mysigner/upload/uploader.rb +101 -109
  44. data/lib/mysigner/version.rb +3 -1
  45. data/lib/mysigner.rb +13 -11
  46. data/mysigner.gemspec +36 -33
  47. data/test_manual.rb +37 -36
  48. metadata +38 -16
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mysigner
4
+ class CLI < Thor
5
+ module ValidateCommands
6
+ def self.included(base)
7
+ base.class_eval do
8
+ desc 'validate', 'Validate signing configuration on the server'
9
+ long_desc <<~DESC
10
+ Check if your bundle ID, certificate, and provisioning profile exist and
11
+ are valid on the My Signer server.
12
+
13
+ WHY VALIDATE?
14
+
15
+ The CLI does local keychain/certificate validation, but doesn't check if
16
+ your signing assets exist on the server. This catches "forgot to sync"
17
+ or "profile expired" errors before a build starts.
18
+
19
+ OPTIONS:
20
+
21
+ --bundle-id / -b Bundle identifier (e.g., com.example.app)
22
+ Auto-detected from Xcode project if not provided
23
+
24
+ --type / -t Signing type: development, appstore, adhoc, inhouse
25
+
26
+ EXAMPLES:
27
+
28
+ # Validate development signing for an app
29
+ mysigner validate --bundle-id com.example.app --type development
30
+
31
+ # Validate App Store signing
32
+ mysigner validate -b com.example.app -t appstore
33
+
34
+ # Auto-detect bundle ID from current project
35
+ mysigner validate --type development
36
+ DESC
37
+ method_option :bundle_id, type: :string, aliases: '-b', desc: 'Bundle identifier (e.g., com.example.app)'
38
+ method_option :type, type: :string, aliases: '-t', desc: 'Signing type: development, appstore, adhoc, inhouse'
39
+ def validate
40
+ config = load_config
41
+ client = create_client(config)
42
+
43
+ bundle_id = options[:bundle_id] || detect_bundle_id_from_project
44
+ signing_type = options[:type]
45
+
46
+ unless bundle_id
47
+ error 'Bundle ID is required. Use --bundle-id or run from an Xcode project directory.'
48
+ say ''
49
+ say 'Example: mysigner validate --bundle-id com.example.app --type development', :yellow
50
+ exit 1
51
+ end
52
+
53
+ unless signing_type
54
+ error 'Signing type is required. Use --type with one of: development, appstore, adhoc, inhouse'
55
+ say ''
56
+ say "Example: mysigner validate --bundle-id #{bundle_id} --type development", :yellow
57
+ exit 1
58
+ end
59
+
60
+ valid_types = %w[development appstore adhoc inhouse]
61
+ unless valid_types.include?(signing_type)
62
+ error "Invalid signing type: #{signing_type}"
63
+ say "Valid types: #{valid_types.join(', ')}", :yellow
64
+ exit 1
65
+ end
66
+
67
+ say '🔍 Validating signing configuration...', :cyan
68
+ say ''
69
+ say " Bundle ID: #{bundle_id}", :white
70
+ say " Type: #{signing_type}", :white
71
+ say ''
72
+
73
+ begin
74
+ response = client.post(
75
+ "/api/v1/organizations/#{config.current_organization_id}/validate",
76
+ body: {
77
+ bundle_id: bundle_id,
78
+ type: signing_type
79
+ }
80
+ )
81
+
82
+ result = response[:data]
83
+ checks = result['checks'] || {}
84
+ valid = result['valid']
85
+
86
+ # Display each check
87
+ %w[bundle_id certificate profile].each do |check_name|
88
+ check = checks[check_name]
89
+ next unless check
90
+
91
+ if check['status'] == 'pass'
92
+ say " ✓ #{check_name.tr('_', ' ').capitalize}: #{check['message']}", :green
93
+ else
94
+ say " ✗ #{check_name.tr('_', ' ').capitalize}: #{check['message']}", :red
95
+ end
96
+ end
97
+
98
+ say ''
99
+
100
+ if valid
101
+ say '✓ All checks passed! Signing configuration is valid.', :green
102
+ else
103
+ say '✗ Validation failed. Some checks did not pass.', :red
104
+
105
+ suggestions = result['suggestions'] || []
106
+ if suggestions.any?
107
+ say ''
108
+ say '💡 Suggestions:', :cyan
109
+ suggestions.each do |suggestion|
110
+ say " → #{suggestion}", :yellow
111
+ end
112
+ end
113
+
114
+ exit 1
115
+ end
116
+ rescue Mysigner::NotFoundError => e
117
+ error "Not found: #{e.message}"
118
+ say ''
119
+ say '💡 Make sure your bundle ID is synced:', :cyan
120
+ say " → Run 'mysigner sync ios' to sync from Apple Developer Portal", :yellow
121
+ say " → Run 'mysigner bundleid list' to list registered bundle IDs", :yellow
122
+ exit 1
123
+ rescue Mysigner::ValidationError => e
124
+ error "Validation error: #{e.message}"
125
+ e.details&.each do |field, errors|
126
+ errors_text = errors.is_a?(Array) ? errors.join(', ') : errors.to_s
127
+ say " #{field}: #{errors_text}", :red
128
+ end
129
+ exit 1
130
+ rescue Mysigner::ClientError => e
131
+ error "Validation request failed: #{e.message}"
132
+ say ''
133
+ say '💡 Try these steps:', :cyan
134
+ say ' → Check your network connection', :yellow
135
+ say ' → Verify API token: mysigner status', :yellow
136
+ exit 1
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def detect_bundle_id_from_project
143
+ # Try to find bundle ID from Xcode project in current directory
144
+ pbxproj_files = Dir.glob('**/*.pbxproj')
145
+ return nil if pbxproj_files.empty?
146
+
147
+ pbxproj_files.each do |file|
148
+ content = File.read(file)
149
+ match = content.match(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^;"]+)"?/)
150
+ return match[1].strip if match
151
+ end
152
+
153
+ nil
154
+ rescue StandardError
155
+ nil
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
data/lib/mysigner/cli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
  require 'time'
@@ -11,6 +13,7 @@ require_relative 'cli/auth_commands'
11
13
  require_relative 'cli/diagnostic_commands'
12
14
  require_relative 'cli/build_commands'
13
15
  require_relative 'cli/resource_commands'
16
+ require_relative 'cli/validate_commands'
14
17
 
15
18
  module Mysigner
16
19
  class CLI < Thor
@@ -31,6 +34,7 @@ module Mysigner
31
34
  include DiagnosticCommands
32
35
  include BuildCommands
33
36
  include ResourceCommands
37
+ include ValidateCommands
34
38
 
35
39
  # Command aliases for power users
36
40
  map 's' => :ship
@@ -40,4 +44,4 @@ module Mysigner
40
44
  map 'st' => :status
41
45
  map 'd' => :doctor
42
46
  end
43
- end
47
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'faraday'
2
4
  require 'faraday/retry'
3
5
  require 'json'
@@ -57,7 +59,7 @@ module Mysigner
57
59
  get('/api/v1/status')
58
60
  rescue ClientError => e
59
61
  raise e
60
- rescue => e
62
+ rescue StandardError => e
61
63
  raise ConnectionError, "Failed to connect: #{e.message}"
62
64
  end
63
65
 
@@ -69,9 +71,7 @@ module Mysigner
69
71
  f.request :json
70
72
 
71
73
  # Add X-User-Email header if email is present
72
- if @user_email
73
- f.headers['X-User-Email'] = @user_email
74
- end
74
+ f.headers['X-User-Email'] = @user_email if @user_email
75
75
 
76
76
  # Retry failed requests
77
77
  f.request :retry, {
@@ -80,7 +80,7 @@ module Mysigner
80
80
  interval_randomness: 0.5,
81
81
  backoff_factor: 2,
82
82
  retry_statuses: [429, 502, 503, 504],
83
- methods: [:get, :post, :patch, :delete]
83
+ methods: %i[get post patch delete]
84
84
  }
85
85
 
86
86
  # Response middleware
@@ -120,21 +120,28 @@ module Mysigner
120
120
 
121
121
  case response.status
122
122
  when 401
123
- raise UnauthorizedError.new("Unauthorized: #{error_message}", error_code: error_code, suggestion: suggestion, details: error_data['details'], timestamp: timestamp)
123
+ raise UnauthorizedError.new("Unauthorized: #{error_message}", error_code: error_code, suggestion: suggestion,
124
+ details: error_data['details'], timestamp: timestamp)
124
125
  when 403
125
- raise ForbiddenError.new("Forbidden: #{error_message}", error_code: error_code, suggestion: suggestion, details: error_data['details'], timestamp: timestamp)
126
+ raise ForbiddenError.new("Forbidden: #{error_message}", error_code: error_code, suggestion: suggestion,
127
+ details: error_data['details'], timestamp: timestamp)
126
128
  when 404
127
- raise NotFoundError.new("Not found: #{error_message}", error_code: error_code, suggestion: suggestion, details: error_data['details'], timestamp: timestamp)
129
+ raise NotFoundError.new("Not found: #{error_message}", error_code: error_code, suggestion: suggestion,
130
+ details: error_data['details'], timestamp: timestamp)
128
131
  when 409
129
- raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code, timestamp: timestamp)
132
+ raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code,
133
+ timestamp: timestamp)
130
134
  when 422
131
- raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code, timestamp: timestamp)
135
+ raise ValidationError.new(error_message, error_data['details'], suggestion: suggestion, error_code: error_code,
136
+ timestamp: timestamp)
132
137
  when 429
133
138
  raise RateLimitError.new(error_message, error_data['retry_after'])
134
139
  when 500..599
135
- raise ServerError.new("Server error (#{response.status}): #{error_message}", error_code: error_code, timestamp: timestamp)
140
+ raise ServerError.new("Server error (#{response.status}): #{error_message}", error_code: error_code,
141
+ timestamp: timestamp)
136
142
  else
137
- raise ClientError.new("Request failed (#{response.status}): #{error_message}", error_code: error_code, timestamp: timestamp)
143
+ raise ClientError.new("Request failed (#{response.status}): #{error_message}", error_code: error_code,
144
+ timestamp: timestamp)
138
145
  end
139
146
  end
140
147
 
@@ -146,15 +153,16 @@ module Mysigner
146
153
  # Check if it's a wrapped timeout error
147
154
  if error.wrapped_exception.is_a?(Net::OpenTimeout) || error.wrapped_exception.is_a?(Net::ReadTimeout)
148
155
  raise TimeoutError, "Request timeout: #{error.message}"
149
- else
150
- raise ConnectionError, "Connection failed: #{error.message}"
151
156
  end
157
+
158
+ raise ConnectionError, "Connection failed: #{error.message}"
159
+
152
160
  when Faraday::UnauthorizedError
153
- raise UnauthorizedError, "Invalid or missing API token"
161
+ raise UnauthorizedError, 'Invalid or missing API token'
154
162
  when Faraday::ForbiddenError
155
- raise ForbiddenError, "Access forbidden"
163
+ raise ForbiddenError, 'Access forbidden'
156
164
  when Faraday::ResourceNotFound
157
- raise NotFoundError, "Resource not found"
165
+ raise NotFoundError, 'Resource not found'
158
166
  when Faraday::ClientError
159
167
  raise ClientError, "Client error: #{error.message}"
160
168
  when Faraday::ServerError
@@ -177,13 +185,14 @@ module Mysigner
177
185
  @timestamp = timestamp
178
186
  end
179
187
  end
188
+
180
189
  class ConnectionError < ClientError; end
181
190
  class TimeoutError < ClientError; end
182
191
  class UnauthorizedError < ClientError; end
183
192
  class ForbiddenError < ClientError; end
184
193
  class NotFoundError < ClientError; end
185
194
  class ServerError < ClientError; end
186
-
195
+
187
196
  class ValidationError < ClientError
188
197
  def initialize(message, details = nil, suggestion: nil, error_code: nil, timestamp: nil)
189
198
  super(message, error_code: error_code, suggestion: suggestion, details: details, timestamp: timestamp)
@@ -199,4 +208,3 @@ module Mysigner
199
208
  end
200
209
  end
201
210
  end
202
-
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
1
5
  require 'yaml'
2
6
  require 'fileutils'
3
7
  require 'openssl'
@@ -7,33 +11,68 @@ require 'securerandom'
7
11
 
8
12
  module Mysigner
9
13
  class Config
10
- CONFIG_DIR = File.expand_path("~/.mysigner").freeze
11
- CONFIG_FILE = File.join(CONFIG_DIR, "config.yml").freeze
12
- KEYCHAIN_SERVICE = "com.mysigner.cli".freeze
13
- KEYCHAIN_ACCOUNT = "config_encryption_key".freeze
14
-
15
- attr_accessor :api_url, :user_email, :current_organization_id
14
+ CONFIG_DIR = File.expand_path('~/.mysigner').freeze
15
+ CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml').freeze
16
+ KEYCHAIN_SERVICE = 'com.mysigner.cli'
17
+ KEYCHAIN_ACCOUNT = 'config_encryption_key'
18
+
19
+ # Environment variable names for CI/CD support
20
+ ENV_API_TOKEN = 'MYSIGNER_API_TOKEN'
21
+ ENV_API_URL = 'MYSIGNER_API_URL'
22
+ ENV_EMAIL = 'MYSIGNER_EMAIL'
23
+ ENV_ORG_ID = 'MYSIGNER_ORG_ID'
24
+
25
+ attr_accessor :api_url, :user_email, :current_organization_id, :encryption_enabled
16
26
  attr_reader :organizations
17
- attr_accessor :encryption_enabled
18
27
 
19
28
  def initialize
20
29
  @api_url = nil
21
30
  @user_email = nil
22
31
  @current_organization_id = nil
23
32
  @organizations = {}
24
- @encryption_enabled = true # Enable by default for security
33
+ @encryption_enabled = true # Enable by default for security
34
+ @from_env = false
25
35
  load if exists?
26
36
  end
27
37
 
38
+ # Check if all required env vars are set for CI/CD mode
39
+ def self.env_configured?
40
+ ENV.fetch(ENV_API_TOKEN, nil) && !ENV[ENV_API_TOKEN].empty? &&
41
+ ENV.fetch(ENV_ORG_ID, nil) && !ENV[ENV_ORG_ID].empty?
42
+ end
43
+
44
+ # Create a Config from environment variables (for CI/CD)
45
+ def self.from_env
46
+ config = allocate
47
+ config.instance_variable_set(:@encryption_enabled, false)
48
+ config.instance_variable_set(:@from_env, true)
49
+
50
+ org_id = ENV.fetch(ENV_ORG_ID, nil)
51
+ token = ENV.fetch(ENV_API_TOKEN, nil)
52
+ config.instance_variable_set(:@api_url, ENV[ENV_API_URL] || 'https://mysigner.dev')
53
+ config.instance_variable_set(:@user_email, ENV.fetch(ENV_EMAIL, nil))
54
+ config.instance_variable_set(:@current_organization_id, org_id.to_i)
55
+ config.instance_variable_set(:@organizations, {
56
+ org_id.to_s => { 'name' => 'CI', 'token' => token }
57
+ })
58
+
59
+ config
60
+ end
61
+
62
+ # Whether this config was loaded from environment variables
63
+ def from_env?
64
+ @from_env
65
+ end
66
+
28
67
  # Get API token for current organization (or specific org)
29
68
  def api_token(org_id = nil)
30
69
  org_id ||= @current_organization_id
31
70
  return nil if org_id.nil?
32
-
71
+
33
72
  org_data = @organizations[org_id.to_s]
34
73
  token = org_data&.dig('token')
35
74
  return nil if token.nil?
36
-
75
+
37
76
  # Decrypt if encrypted
38
77
  encrypted?(token) ? decrypt_token(token) : token
39
78
  end
@@ -57,7 +96,7 @@ module Mysigner
57
96
  def org_name(org_id = nil)
58
97
  org_id ||= @current_organization_id
59
98
  return nil if org_id.nil?
60
-
99
+
61
100
  org_data = @organizations[org_id.to_s]
62
101
  org_data&.dig('name')
63
102
  end
@@ -81,17 +120,17 @@ module Mysigner
81
120
  return false unless exists?
82
121
 
83
122
  data = YAML.load_file(CONFIG_FILE)
84
-
123
+
85
124
  @api_url = data['api_url']
86
125
  @user_email = data['user_email']
87
126
  @current_organization_id = data['current_organization_id']
88
127
  @organizations = data['organizations'] || {}
89
-
128
+
90
129
  # Auto-detect encryption from config
91
130
  @encryption_enabled = encrypted_config?
92
-
131
+
93
132
  true
94
- rescue => e
133
+ rescue StandardError => e
95
134
  raise ConfigError, "Failed to load config: #{e.message}"
96
135
  end
97
136
 
@@ -107,9 +146,9 @@ module Mysigner
107
146
  }
108
147
 
109
148
  File.write(CONFIG_FILE, data.to_yaml)
110
- File.chmod(0600, CONFIG_FILE) # Make file readable only by owner
149
+ File.chmod(0o600, CONFIG_FILE) # Make file readable only by owner
111
150
  true
112
- rescue => e
151
+ rescue StandardError => e
113
152
  raise ConfigError, "Failed to save config: #{e.message}"
114
153
  end
115
154
 
@@ -119,12 +158,10 @@ module Mysigner
119
158
  @user_email = nil
120
159
  @current_organization_id = nil
121
160
  @organizations = {}
122
-
123
- if exists?
124
- File.delete(CONFIG_FILE)
125
- end
161
+
162
+ File.delete(CONFIG_FILE) if exists?
126
163
  true
127
- rescue => e
164
+ rescue StandardError => e
128
165
  raise ConfigError, "Failed to clear config: #{e.message}"
129
166
  end
130
167
 
@@ -136,8 +173,8 @@ module Mysigner
136
173
  # Check if configuration is complete (has required fields)
137
174
  def valid?
138
175
  !@api_url.nil? && !@api_url.empty? &&
139
- !@current_organization_id.nil? &&
140
- has_token_for_org?(@current_organization_id)
176
+ !@current_organization_id.nil? &&
177
+ has_token_for_org?(@current_organization_id)
141
178
  end
142
179
 
143
180
  # Get config as hash
@@ -153,14 +190,14 @@ module Mysigner
153
190
  def display
154
191
  current_org_name = org_name(@current_organization_id) || '(not set)'
155
192
  current_token = api_token(@current_organization_id)
156
-
193
+
157
194
  display_data = {
158
195
  api_url: @api_url || '(not set)',
159
196
  user_email: @user_email || '(not set)',
160
197
  current_organization: "#{current_org_name} (ID: #{@current_organization_id || 'not set'})",
161
198
  current_token: current_token ? mask_token(current_token) : '(not set)'
162
199
  }
163
-
200
+
164
201
  # Show all organizations
165
202
  if @organizations.any?
166
203
  display_data[:all_organizations] = @organizations.map do |org_id, org_data|
@@ -168,24 +205,24 @@ module Mysigner
168
205
  "#{org_data['name']} (ID: #{org_id}) #{token_status}"
169
206
  end.join(', ')
170
207
  end
171
-
208
+
172
209
  display_data
173
210
  end
174
211
 
175
212
  # Enable encryption and re-encrypt all tokens
176
213
  def enable_encryption!
177
214
  return true if @encryption_enabled
178
-
215
+
179
216
  @encryption_enabled = true
180
-
217
+
181
218
  # Re-encrypt all existing tokens
182
- @organizations.each do |org_id, org_data|
219
+ @organizations.each_value do |org_data|
183
220
  token = org_data['token']
184
221
  next if token.nil? || encrypted?(token)
185
-
222
+
186
223
  org_data['token'] = encrypt_token(token)
187
224
  end
188
-
225
+
189
226
  save
190
227
  true
191
228
  end
@@ -193,15 +230,15 @@ module Mysigner
193
230
  # Disable encryption and decrypt all tokens
194
231
  def disable_encryption!
195
232
  return true unless @encryption_enabled
196
-
233
+
197
234
  # Decrypt all tokens first
198
- @organizations.each do |org_id, org_data|
235
+ @organizations.each_value do |org_data|
199
236
  token = org_data['token']
200
237
  next if token.nil? || !encrypted?(token)
201
-
238
+
202
239
  org_data['token'] = decrypt_token(token)
203
240
  end
204
-
241
+
205
242
  @encryption_enabled = false
206
243
  save
207
244
  true
@@ -215,12 +252,13 @@ module Mysigner
215
252
  private
216
253
 
217
254
  def ensure_config_dir_exists
218
- FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
255
+ FileUtils.mkdir_p(CONFIG_DIR)
219
256
  end
220
257
 
221
258
  def mask_token(token)
222
259
  return token if token.length < 8
223
- "#{token[0..3]}...#{token[-4..-1]}"
260
+
261
+ "#{token[0..3]}...#{token[-4..]}"
224
262
  end
225
263
 
226
264
  # Encryption methods
@@ -230,34 +268,34 @@ module Mysigner
230
268
  cipher.encrypt
231
269
  cipher.key = key
232
270
  iv = cipher.random_iv
233
-
271
+
234
272
  encrypted = cipher.update(token) + cipher.final
235
273
  auth_tag = cipher.auth_tag
236
-
274
+
237
275
  # Format: encrypted:base64(iv):base64(auth_tag):base64(encrypted_data)
238
276
  "encrypted:#{Base64.strict_encode64(iv)}:#{Base64.strict_encode64(auth_tag)}:#{Base64.strict_encode64(encrypted)}"
239
277
  end
240
278
 
241
279
  def decrypt_token(encrypted_token)
242
280
  return encrypted_token unless encrypted?(encrypted_token)
243
-
281
+
244
282
  # Parse format
245
283
  parts = encrypted_token.split(':', 4)
246
284
  return encrypted_token if parts.length != 4 || parts[0] != 'encrypted'
247
-
285
+
248
286
  iv = Base64.strict_decode64(parts[1])
249
287
  auth_tag = Base64.strict_decode64(parts[2])
250
288
  encrypted_data = Base64.strict_decode64(parts[3])
251
-
289
+
252
290
  key = get_or_create_encryption_key
253
291
  decipher = OpenSSL::Cipher.new('aes-256-gcm')
254
292
  decipher.decrypt
255
293
  decipher.key = key
256
294
  decipher.iv = iv
257
295
  decipher.auth_tag = auth_tag
258
-
296
+
259
297
  decipher.update(encrypted_data) + decipher.final
260
- rescue => e
298
+ rescue StandardError => e
261
299
  raise ConfigError, "Failed to decrypt token: #{e.message}"
262
300
  end
263
301
 
@@ -269,7 +307,7 @@ module Mysigner
269
307
  # Try to get key from keychain
270
308
  key = get_key_from_keychain
271
309
  return key if key
272
-
310
+
273
311
  # Generate new key and store in keychain
274
312
  new_key = SecureRandom.bytes(32) # 256-bit key
275
313
  store_key_in_keychain(new_key)
@@ -280,32 +318,31 @@ module Mysigner
280
318
  # Use macOS security command to get key from keychain
281
319
  cmd = "security find-generic-password -s '#{KEYCHAIN_SERVICE}' -a '#{KEYCHAIN_ACCOUNT}' -w 2>/dev/null"
282
320
  result = `#{cmd}`.strip
283
-
284
- return nil if result.empty? || $?.exitstatus != 0
285
-
321
+
322
+ return nil if result.empty? || $CHILD_STATUS.exitstatus != 0
323
+
286
324
  # Decode from base64
287
325
  Base64.strict_decode64(result)
288
- rescue => e
326
+ rescue StandardError
289
327
  nil
290
328
  end
291
329
 
292
330
  def store_key_in_keychain(key)
293
331
  # Encode key as base64
294
332
  encoded_key = Base64.strict_encode64(key)
295
-
333
+
296
334
  # Delete existing key if present
297
335
  `security delete-generic-password -s '#{KEYCHAIN_SERVICE}' -a '#{KEYCHAIN_ACCOUNT}' 2>/dev/null`
298
-
336
+
299
337
  # Add new key to keychain
300
338
  cmd = "security add-generic-password -s '#{KEYCHAIN_SERVICE}' -a '#{KEYCHAIN_ACCOUNT}' -w '#{encoded_key}'"
301
339
  system(cmd)
302
-
303
- $?.exitstatus == 0
304
- rescue => e
340
+
341
+ $CHILD_STATUS.exitstatus.zero?
342
+ rescue StandardError => e
305
343
  raise ConfigError, "Failed to store encryption key in keychain: #{e.message}"
306
344
  end
307
345
  end
308
346
 
309
347
  class ConfigError < StandardError; end
310
348
  end
311
-