adsedare 0.0.1 → 0.0.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.
- checksums.yaml +4 -4
- data/adsedare.gemspec +1 -1
- data/lib/adsedare/capabilities.rb +43 -38
- data/lib/adsedare/export_options.rb +93 -0
- data/lib/adsedare/install_profiles.rb +76 -0
- data/lib/adsedare/keychain.rb +131 -0
- data/lib/adsedare/version.rb +1 -1
- data/lib/adsedare/xcodeproj.rb +76 -0
- data/lib/adsedare.rb +17 -348
- data/lib/appstoreconnect.rb +65 -65
- data/lib/starship/2fa_provider.rb +4 -4
- data/lib/starship/auth_helper.rb +87 -88
- data/lib/starship.rb +60 -60
- metadata +5 -1
data/lib/starship/auth_helper.rb
CHANGED
@@ -14,49 +14,49 @@ require_relative "../logging"
|
|
14
14
|
|
15
15
|
module Starship
|
16
16
|
class Error < StandardError; end
|
17
|
-
|
17
|
+
|
18
18
|
# AuthHelper handles authentication with Apple"s developer portal
|
19
19
|
class AuthHelper
|
20
20
|
include Logging
|
21
21
|
|
22
22
|
attr_reader :session, :csrf, :csrf_ts, :session_data
|
23
|
-
|
23
|
+
|
24
24
|
AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth"
|
25
25
|
WIDGET_KEY_URL = "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com"
|
26
|
-
|
26
|
+
|
27
27
|
def initialize(two_factor_provider = nil)
|
28
28
|
# Create session directory if it doesn't exist
|
29
|
-
@session_directory = File.expand_path(
|
29
|
+
@session_directory = File.expand_path("~/.starship")
|
30
30
|
FileUtils.mkdir_p(@session_directory)
|
31
|
-
|
31
|
+
|
32
32
|
@widget_key = nil
|
33
33
|
@csrf = nil
|
34
34
|
@csrf_ts = nil
|
35
35
|
@email = nil
|
36
36
|
@session_data = {}
|
37
37
|
@two_factor_provider = two_factor_provider || Starship::ManualTwoFactorProvider.new
|
38
|
-
|
38
|
+
|
39
39
|
# Initialize Faraday with cookie jar
|
40
40
|
@session = Faraday.new do |builder|
|
41
41
|
builder.use :cookie_jar, jar: HTTP::CookieJar.new
|
42
42
|
builder.adapter Faraday.default_adapter
|
43
43
|
end
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
# Sign in to Apple Developer Portal
|
47
47
|
# @return [Boolean] Whether authentication was successful
|
48
48
|
def sign_in
|
49
49
|
email = ENV["APPLE_DEVELOPER_USERNAME"]
|
50
50
|
password = ENV["APPLE_DEVELOPER_PASSWORD"]
|
51
|
-
|
51
|
+
|
52
52
|
if !email || !password
|
53
53
|
raise Error, "Email and password are required. Set APPLE_ID and APPLE_PASSWORD environment variables."
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
@email = email
|
57
57
|
@client_id = generate_session_id(email)
|
58
58
|
cookie_path, session_path = get_paths(email)
|
59
|
-
|
59
|
+
|
60
60
|
# Try to load existing session
|
61
61
|
if File.exist?(session_path)
|
62
62
|
load_session
|
@@ -64,13 +64,13 @@ module Starship
|
|
64
64
|
return true
|
65
65
|
end
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
logger.warn "Session invalid or expired. Starting authentication from scratch..."
|
69
69
|
@session_data = { "client_id" => @client_id, "email" => email }
|
70
|
-
|
70
|
+
|
71
71
|
# Start authentication process
|
72
72
|
auth_result = authenticate_with_srp(email, password)
|
73
|
-
|
73
|
+
|
74
74
|
if auth_result == :two_factor_required
|
75
75
|
logger.info "Two-factor authentication required. Requesting verification code..."
|
76
76
|
|
@@ -80,44 +80,44 @@ module Starship
|
|
80
80
|
response = @session.get("https://developer.apple.com/account")
|
81
81
|
if response.status == 200
|
82
82
|
extract_csrf_tokens(response)
|
83
|
-
|
83
|
+
|
84
84
|
if @csrf && @csrf_ts
|
85
85
|
save_session
|
86
86
|
return true
|
87
87
|
end
|
88
88
|
end
|
89
89
|
end
|
90
|
-
|
90
|
+
|
91
91
|
return auth_result
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
# Check if the current session is valid
|
95
95
|
# @return [Boolean] Whether the session is valid
|
96
96
|
def validate_token
|
97
97
|
return false unless @session_data["session_id"] && @session_data["scnt"]
|
98
|
-
|
98
|
+
|
99
99
|
begin
|
100
100
|
headers = {
|
101
101
|
"Accept" => "application/json, text/plain, */*",
|
102
102
|
"Content-Type" => "application/vnd.api+json",
|
103
103
|
"X-Requested-With" => "XMLHttpRequest",
|
104
104
|
"X-Apple-ID-Session-Id" => @session_data["session_id"],
|
105
|
-
"scnt" => @session_data["scnt"]
|
105
|
+
"scnt" => @session_data["scnt"],
|
106
106
|
}
|
107
|
-
|
107
|
+
|
108
108
|
response = @session.get(
|
109
|
-
"https://developer.apple.com/services-account/v1/certificates",
|
110
|
-
nil,
|
109
|
+
"https://developer.apple.com/services-account/v1/certificates",
|
110
|
+
nil,
|
111
111
|
headers
|
112
112
|
)
|
113
113
|
|
114
|
-
if response.status == 403
|
114
|
+
if response.status == 403
|
115
115
|
# Fetch CSRF tokens after confirming session is valid
|
116
116
|
csrf_response = @session.get("https://developer.apple.com/account/resources/certificates/list")
|
117
117
|
|
118
118
|
if csrf_response.status == 200
|
119
119
|
extract_csrf_tokens(csrf_response)
|
120
|
-
|
120
|
+
|
121
121
|
if @csrf && @csrf_ts
|
122
122
|
return true
|
123
123
|
else
|
@@ -135,14 +135,14 @@ module Starship
|
|
135
135
|
return false
|
136
136
|
end
|
137
137
|
end
|
138
|
-
|
138
|
+
|
139
139
|
def request(endpoint, method: :get, params: nil, body: nil, headers: nil)
|
140
140
|
default_headers = {
|
141
141
|
"Accept" => "application/json, text/plain, */*",
|
142
142
|
"X-Requested-With" => "XMLHttpRequest",
|
143
143
|
"X-HTTP-Method-Override" => "GET",
|
144
144
|
"csrf" => @csrf,
|
145
|
-
"csrf_ts" => @csrf_ts
|
145
|
+
"csrf_ts" => @csrf_ts,
|
146
146
|
}
|
147
147
|
|
148
148
|
if headers
|
@@ -150,38 +150,38 @@ module Starship
|
|
150
150
|
end
|
151
151
|
|
152
152
|
response = case method
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
153
|
+
when :get
|
154
|
+
@session.get(endpoint, params, default_headers)
|
155
|
+
when :post
|
156
|
+
@session.post(endpoint, body, default_headers)
|
157
|
+
when :put
|
158
|
+
@session.put(endpoint, body, default_headers)
|
159
|
+
when :delete
|
160
|
+
@session.delete(endpoint, default_headers)
|
161
|
+
when :patch
|
162
|
+
@session.patch(endpoint, body, default_headers)
|
163
|
+
end
|
164
|
+
|
165
165
|
if response.status == 401 || response.status == 403
|
166
166
|
logger.warn "Session invalid or expired. Starting authentication from scratch..."
|
167
167
|
|
168
168
|
self.sign_in
|
169
|
-
|
169
|
+
|
170
170
|
response = self.request(endpoint, method: method, params: params, body: body, headers: headers)
|
171
171
|
end
|
172
|
-
|
172
|
+
|
173
173
|
return response
|
174
174
|
end
|
175
|
-
|
175
|
+
|
176
176
|
private
|
177
|
-
|
177
|
+
|
178
178
|
# Generate a consistent session ID from email
|
179
179
|
# @param email [String] The email address
|
180
180
|
# @return [String] The session ID
|
181
181
|
def generate_session_id(email)
|
182
182
|
"auth-#{Digest::SHA256.hexdigest(email)[0...8]}"
|
183
183
|
end
|
184
|
-
|
184
|
+
|
185
185
|
# Get the paths for cookie and session files
|
186
186
|
# @param email [String] The email address
|
187
187
|
# @return [Array<String>] The cookie path and session path
|
@@ -191,21 +191,21 @@ module Starship
|
|
191
191
|
session_path = File.join(@session_directory, "#{session_id}.session")
|
192
192
|
[cookie_path, session_path]
|
193
193
|
end
|
194
|
-
|
194
|
+
|
195
195
|
# Get the cookie jar path
|
196
196
|
# @return [String] The cookie jar path
|
197
197
|
def cookiejar_path
|
198
198
|
raise Error, "Email not set" unless @email
|
199
199
|
get_paths(@email)[0]
|
200
200
|
end
|
201
|
-
|
201
|
+
|
202
202
|
# Get the session path
|
203
203
|
# @return [String] The session path
|
204
204
|
def session_path
|
205
205
|
raise Error, "Email not set" unless @email
|
206
206
|
get_paths(@email)[1]
|
207
207
|
end
|
208
|
-
|
208
|
+
|
209
209
|
# Load session data from file
|
210
210
|
# @return [Boolean] Whether the session was loaded successfully
|
211
211
|
def load_session
|
@@ -216,7 +216,7 @@ module Starship
|
|
216
216
|
# Create a new cookie jar for Faraday
|
217
217
|
jar = HTTP::CookieJar.new
|
218
218
|
jar.load(cookiejar_path, format: :cookiestxt)
|
219
|
-
|
219
|
+
|
220
220
|
# Recreate the Faraday connection with the loaded cookies
|
221
221
|
@session = Faraday.new do |builder|
|
222
222
|
builder.use :cookie_jar, jar: jar
|
@@ -233,17 +233,17 @@ module Starship
|
|
233
233
|
return false
|
234
234
|
end
|
235
235
|
end
|
236
|
-
|
236
|
+
|
237
237
|
# Save session data to file
|
238
238
|
def save_session
|
239
239
|
logger.info "Saving session to disk at #{session_path}"
|
240
240
|
File.write(session_path, JSON.pretty_generate(@session_data))
|
241
|
-
|
241
|
+
|
242
242
|
# Save cookies
|
243
243
|
cookie_path = get_paths(@email)[0]
|
244
|
-
|
244
|
+
|
245
245
|
# Get the cookie jar from Faraday
|
246
|
-
jar = @session.builder.app.instance_variable_get(
|
246
|
+
jar = @session.builder.app.instance_variable_get("@jar")
|
247
247
|
if jar
|
248
248
|
# Save all cookies, even if they're marked as discardable or expired
|
249
249
|
jar.save(cookie_path, format: :cookiestxt, session: true)
|
@@ -251,10 +251,10 @@ module Starship
|
|
251
251
|
else
|
252
252
|
logger.info "No cookies were found to save."
|
253
253
|
end
|
254
|
-
|
254
|
+
|
255
255
|
logger.info "Session saved successfully."
|
256
256
|
end
|
257
|
-
|
257
|
+
|
258
258
|
# Get the widget key used for authentication
|
259
259
|
# @return [String] The widget key
|
260
260
|
def widget_key
|
@@ -264,26 +264,26 @@ module Starship
|
|
264
264
|
end
|
265
265
|
@widget_key
|
266
266
|
end
|
267
|
-
|
267
|
+
|
268
268
|
# Get a cookie value by name
|
269
269
|
# @param name [String] The cookie name
|
270
270
|
# @return [String, nil] The cookie value or nil if not found
|
271
271
|
def get_cookie_value(name)
|
272
272
|
# Access the cookie jar middleware from Faraday
|
273
|
-
jar = @session.builder.app.instance_variable_get(
|
273
|
+
jar = @session.builder.app.instance_variable_get("@jar")
|
274
274
|
return nil unless jar
|
275
|
-
|
275
|
+
|
276
276
|
# Find the cookie by name
|
277
277
|
jar.cookies.find { |cookie| cookie.name == name }&.value
|
278
278
|
end
|
279
|
-
|
279
|
+
|
280
280
|
# Extract CSRF tokens from response
|
281
281
|
# @param response [Faraday::Response] The response object
|
282
282
|
def extract_csrf_tokens(response)
|
283
283
|
# Try cookies first
|
284
284
|
@csrf = get_cookie_value("csrf")
|
285
285
|
@csrf_ts = get_cookie_value("csrf_ts")
|
286
|
-
|
286
|
+
|
287
287
|
# If not in cookies, try response headers
|
288
288
|
unless @csrf
|
289
289
|
@csrf = response.headers["csrf"]
|
@@ -291,18 +291,18 @@ module Starship
|
|
291
291
|
unless @csrf_ts
|
292
292
|
@csrf_ts = response.headers["csrf_ts"]
|
293
293
|
end
|
294
|
-
|
294
|
+
|
295
295
|
# If still not found, try to extract from page content
|
296
296
|
if !@csrf || !@csrf_ts
|
297
|
-
if response.body =~ /csrf[""]\s*:\s*[""](.*?)[""]/
|
297
|
+
if response.body =~ /csrf[""]\s*:\s*[""](.*?)[""]/
|
298
298
|
@csrf = $1
|
299
299
|
end
|
300
|
-
if response.body =~ /csrf_ts[""]\s*:\s*[""](.*?)[""]/
|
300
|
+
if response.body =~ /csrf_ts[""]\s*:\s*[""](.*?)[""]/
|
301
301
|
@csrf_ts = $1
|
302
302
|
end
|
303
303
|
end
|
304
304
|
end
|
305
|
-
|
305
|
+
|
306
306
|
# Authenticate with Secure Remote Password protocol
|
307
307
|
# @param email [String] The email address
|
308
308
|
# @param password [String] The password
|
@@ -310,108 +310,108 @@ module Starship
|
|
310
310
|
def authenticate_with_srp(email, password)
|
311
311
|
# This is a simplified SRP implementation since Ruby doesn"t have a direct equivalent to Python"s srp library
|
312
312
|
# In a real implementation, you would use a proper SRP library or implement the full protocol
|
313
|
-
|
313
|
+
|
314
314
|
headers = {
|
315
315
|
"Accept" => "application/json, text/javascript",
|
316
316
|
"Content-Type" => "application/json",
|
317
317
|
"X-Requested-With" => "XMLHttpRequest",
|
318
|
-
"X-Apple-Widget-Key" => widget_key
|
318
|
+
"X-Apple-Widget-Key" => widget_key,
|
319
319
|
}
|
320
|
-
|
320
|
+
|
321
321
|
if @session_data["session_id"]
|
322
322
|
headers.update({
|
323
323
|
"X-Apple-ID-Session-Id" => @session_data["session_id"],
|
324
|
-
"scnt" => @session_data["scnt"]
|
324
|
+
"scnt" => @session_data["scnt"],
|
325
325
|
})
|
326
326
|
end
|
327
|
-
|
327
|
+
|
328
328
|
# For simplicity, we"ll use a direct password-based authentication approach
|
329
329
|
# In a real implementation, you would implement the full SRP protocol
|
330
330
|
auth_data = {
|
331
331
|
"accountName" => email,
|
332
332
|
"password" => password,
|
333
|
-
"rememberMe" => true
|
333
|
+
"rememberMe" => true,
|
334
334
|
}
|
335
|
-
|
335
|
+
|
336
336
|
logger.info "Initializing authentication request to Apple ID..."
|
337
337
|
response = @session.post(
|
338
338
|
"#{AUTH_ENDPOINT}/signin",
|
339
339
|
auth_data.to_json,
|
340
340
|
headers
|
341
341
|
)
|
342
|
-
|
342
|
+
|
343
343
|
# Handle 409 response (2FA required)
|
344
344
|
if response.status == 409
|
345
345
|
session_id = response.headers["X-Apple-ID-Session-Id"]
|
346
346
|
scnt = response.headers["scnt"]
|
347
|
-
|
347
|
+
|
348
348
|
if session_id && scnt
|
349
349
|
@session_data.update({
|
350
350
|
"session_id" => session_id,
|
351
351
|
"scnt" => scnt,
|
352
352
|
"client_id" => @client_id,
|
353
|
-
"email" => email
|
353
|
+
"email" => email,
|
354
354
|
})
|
355
355
|
save_session
|
356
356
|
end
|
357
|
-
|
357
|
+
|
358
358
|
return :two_factor_required
|
359
359
|
end
|
360
|
-
|
360
|
+
|
361
361
|
# Handle successful authentication
|
362
362
|
if response.status == 200 || response.status == 302
|
363
363
|
session_id = response.headers["X-Apple-ID-Session-Id"]
|
364
364
|
scnt = response.headers["scnt"]
|
365
|
-
|
365
|
+
|
366
366
|
if session_id && scnt
|
367
367
|
@session_data.update({
|
368
368
|
"session_id" => session_id,
|
369
369
|
"scnt" => scnt,
|
370
370
|
"client_id" => @client_id,
|
371
|
-
"email" => email
|
371
|
+
"email" => email,
|
372
372
|
})
|
373
373
|
save_session
|
374
374
|
logger.info "Session data saved after basic authentication."
|
375
375
|
return true
|
376
376
|
end
|
377
377
|
end
|
378
|
-
|
378
|
+
|
379
379
|
return false
|
380
380
|
end
|
381
|
-
|
381
|
+
|
382
382
|
# Handle two-factor authentication
|
383
383
|
# @return [Boolean] Whether 2FA was successful
|
384
384
|
def handle_two_factor_auth
|
385
385
|
session_id = @session_data["session_id"]
|
386
386
|
scnt = @session_data["scnt"]
|
387
|
-
|
387
|
+
|
388
388
|
unless session_id && scnt
|
389
389
|
logger.error "Missing session data. Cannot continue two-factor authentication."
|
390
390
|
return false
|
391
391
|
end
|
392
|
-
|
392
|
+
|
393
393
|
begin
|
394
394
|
# Get 2FA code from provider
|
395
395
|
code = @two_factor_provider.get_code(session_id, scnt)
|
396
|
-
|
396
|
+
|
397
397
|
verify_headers = {
|
398
398
|
"Accept" => "application/json, text/javascript",
|
399
399
|
"Content-Type" => "application/json",
|
400
400
|
"X-Requested-With" => "XMLHttpRequest",
|
401
401
|
"X-Apple-ID-Session-Id" => session_id,
|
402
402
|
"scnt" => scnt,
|
403
|
-
"X-Apple-Widget-Key" => widget_key
|
403
|
+
"X-Apple-Widget-Key" => widget_key,
|
404
404
|
}
|
405
|
-
|
405
|
+
|
406
406
|
verify_data = { "securityCode" => { "code" => code.strip } }
|
407
|
-
|
407
|
+
|
408
408
|
# First verify the security code
|
409
409
|
verify_response = @session.post(
|
410
410
|
"#{AUTH_ENDPOINT}/verify/trusteddevice/securitycode",
|
411
411
|
verify_data.to_json,
|
412
412
|
verify_headers
|
413
413
|
)
|
414
|
-
|
414
|
+
|
415
415
|
if verify_response.status == 204
|
416
416
|
logger.info "Two-factor code verified successfully."
|
417
417
|
logger.info "Trusting the session after 2FA verification..."
|
@@ -422,15 +422,14 @@ module Starship
|
|
422
422
|
nil,
|
423
423
|
verify_headers
|
424
424
|
)
|
425
|
-
|
426
|
-
|
425
|
+
|
427
426
|
if trust_response.status == 204
|
428
427
|
# Store all relevant session data
|
429
428
|
@session_data.update({
|
430
429
|
"session_id" => session_id,
|
431
430
|
"scnt" => scnt,
|
432
431
|
"client_id" => @client_id,
|
433
|
-
"email" => @email
|
432
|
+
"email" => @email,
|
434
433
|
})
|
435
434
|
logger.info "Session trusted and fully authenticated. Saving final session data."
|
436
435
|
save_session
|
@@ -443,7 +442,7 @@ module Starship
|
|
443
442
|
rescue => e
|
444
443
|
logger.error "Two-factor verification failed: #{e.message}"
|
445
444
|
end
|
446
|
-
|
445
|
+
|
447
446
|
return false
|
448
447
|
end
|
449
448
|
end
|