8
8
IAppleLoginResult ,
9
9
IApplePortalSessionService ,
10
10
} from "./definitions" ;
11
+ import * as crypto from "crypto" ;
11
12
12
13
export class ApplePortalSessionService implements IApplePortalSessionService {
13
14
private loginConfigEndpoint =
@@ -38,7 +39,8 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
38
39
await this . handleTwoFactorAuthentication (
39
40
loginResult . scnt ,
40
41
loginResult . xAppleIdSessionId ,
41
- authServiceKey
42
+ authServiceKey ,
43
+ loginResult . hashcash
42
44
) ;
43
45
}
44
46
@@ -114,6 +116,7 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
114
116
xAppleIdSessionId : < string > null ,
115
117
isTwoFactorAuthenticationEnabled : false ,
116
118
areCredentialsValid : true ,
119
+ hashcash : < string > null ,
117
120
} ;
118
121
119
122
if ( opts && opts . sessionBase64 ) {
@@ -130,6 +133,12 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
130
133
await this . loginCore ( credentials ) ;
131
134
} catch ( err ) {
132
135
const statusCode = err && err . response && err . response . status ;
136
+
137
+ const bits = err ?. response ?. headers [ "x-apple-hc-bits" ] ;
138
+ const challenge = err ?. response ?. headers [ "x-apple-hc-challenge" ] ;
139
+ const hashcash = makeHashCash ( bits , challenge ) ;
140
+ result . hashcash = hashcash ;
141
+
133
142
result . areCredentialsValid = statusCode !== 401 && statusCode !== 403 ;
134
143
result . isTwoFactorAuthenticationEnabled = statusCode === 409 ;
135
144
@@ -216,12 +225,14 @@ For more details how to set up your environment, please execute "tns publish ios
216
225
private async handleTwoFactorAuthentication (
217
226
scnt : string ,
218
227
xAppleIdSessionId : string ,
219
- authServiceKey : string
228
+ authServiceKey : string ,
229
+ hashcash : string
220
230
) : Promise < void > {
221
231
const headers = {
222
232
scnt : scnt ,
223
233
"X-Apple-Id-Session-Id" : xAppleIdSessionId ,
224
234
"X-Apple-Widget-Key" : authServiceKey ,
235
+ "X-Apple-HC" : hashcash ,
225
236
Accept : "application/json" ,
226
237
} ;
227
238
const authResponse = await this . $httpClient . httpRequest ( {
@@ -231,21 +242,48 @@ For more details how to set up your environment, please execute "tns publish ios
231
242
} ) ;
232
243
233
244
const data = JSON . parse ( authResponse . body ) ;
234
- if ( data . trustedPhoneNumbers && data . trustedPhoneNumbers . length ) {
245
+
246
+ const isSMS =
247
+ data . trustedPhoneNumbers &&
248
+ data . trustedPhoneNumbers . length === 1 &&
249
+ data . noTrustedDevices ; // 1 device and no trusted devices means sms was automatically sent.
250
+ const multiSMS =
251
+ data . trustedPhoneNumbers &&
252
+ data . trustedPhoneNumbers . length !== 1 &&
253
+ data . noTrustedDevices ; // Not handling more than 1 sms device and no trusted devices.
254
+
255
+ let token : string ;
256
+
257
+ if (
258
+ data . trustedPhoneNumbers &&
259
+ data . trustedPhoneNumbers . length &&
260
+ ! multiSMS
261
+ ) {
235
262
const parsedAuthResponse = JSON . parse ( authResponse . body ) ;
236
- const token = await this . $prompter . getString (
263
+ token = await this . $prompter . getString (
237
264
`Please enter the ${ parsedAuthResponse . securityCode . length } digit code` ,
238
265
{ allowEmpty : false }
239
266
) ;
267
+ const body : any = {
268
+ securityCode : {
269
+ code : token . toString ( ) ,
270
+ } ,
271
+ } ;
272
+ let url = `https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode` ;
273
+
274
+ if ( isSMS ) {
275
+ // No trusted devices means it must be sms.
276
+ body . mode = "sms" ;
277
+ body . phoneNumber = {
278
+ id : data . trustedPhoneNumbers [ 0 ] . id ,
279
+ } ;
280
+ url = `https://idmsa.apple.com/appleauth/auth/verify/phone/securitycode` ;
281
+ }
240
282
241
283
await this . $httpClient . httpRequest ( {
242
- url : `https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode` ,
284
+ url,
243
285
method : "POST" ,
244
- body : {
245
- securityCode : {
246
- code : token . toString ( ) ,
247
- } ,
248
- } ,
286
+ body,
249
287
headers : { ...headers , "Content-Type" : "application/json" } ,
250
288
} ) ;
251
289
@@ -258,6 +296,10 @@ For more details how to set up your environment, please execute "tns publish ios
258
296
this . $applePortalCookieService . updateUserSessionCookie (
259
297
authTrustResponse . headers [ "set-cookie" ]
260
298
) ;
299
+ } else if ( multiSMS ) {
300
+ this . $errors . fail (
301
+ `The NativeScript CLI does not support SMS authenticaton with multiple registered phone numbers.`
302
+ ) ;
261
303
} else {
262
304
this . $errors . fail (
263
305
`Although response from Apple indicated activated Two-step Verification or Two-factor Authentication, NativeScript CLI don't know how to handle this response: ${ data } `
@@ -266,3 +308,52 @@ For more details how to set up your environment, please execute "tns publish ios
266
308
}
267
309
}
268
310
injector . register ( "applePortalSessionService" , ApplePortalSessionService ) ;
311
+
312
+ function makeHashCash ( bits : string , challenge : string ) : string {
313
+ const version = 1 ;
314
+
315
+ const dateString = getHashCanDateString ( ) ;
316
+ let result : string ;
317
+ for ( let counter = 0 ; ; counter ++ ) {
318
+ const hc = [ version , bits , dateString , challenge , `:${ counter } ` ] . join ( ":" ) ;
319
+
320
+ const shasumData = crypto . createHash ( "sha1" ) ;
321
+
322
+ shasumData . update ( hc ) ;
323
+ const digest = shasumData . digest ( ) ;
324
+ if ( checkBits ( + bits , digest ) ) {
325
+ result = hc ;
326
+ break ;
327
+ }
328
+ }
329
+ return result ;
330
+ }
331
+
332
+ function getHashCanDateString ( ) : string {
333
+ const now = new Date ( ) ;
334
+
335
+ return `${ now . getFullYear ( ) } ${ padTo2Digits ( now . getMonth ( ) + 1 ) } ${ padTo2Digits (
336
+ now . getDate ( )
337
+ ) } ${ padTo2Digits ( now . getHours ( ) ) } ${ padTo2Digits (
338
+ now . getMinutes ( )
339
+ ) } ${ padTo2Digits ( now . getSeconds ( ) ) } `;
340
+ }
341
+ function padTo2Digits ( num : number ) {
342
+ return num . toString ( ) . padStart ( 2 , "0" ) ;
343
+ }
344
+
345
+ function checkBits ( bits : number , digest : Buffer ) {
346
+ let result = true ;
347
+ for ( let i = 0 ; i < bits ; ++ i ) {
348
+ result = checkBit ( i , digest ) ;
349
+ if ( ! result ) break ;
350
+ }
351
+ return result ;
352
+ }
353
+
354
+ function checkBit ( position : number , buffer : Buffer ) : boolean {
355
+ const bitOffset = position & 7 ; // in byte
356
+ const byteIndex = position >> 3 ; // in buffer
357
+ const bit = ( buffer [ byteIndex ] >> bitOffset ) & 1 ;
358
+ return bit === 0 ;
359
+ }
0 commit comments