88 IAppleLoginResult ,
99 IApplePortalSessionService ,
1010} from "./definitions" ;
11+ import * as crypto from "crypto" ;
1112
1213export class ApplePortalSessionService implements IApplePortalSessionService {
1314 private loginConfigEndpoint =
@@ -38,7 +39,8 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
3839 await this . handleTwoFactorAuthentication (
3940 loginResult . scnt ,
4041 loginResult . xAppleIdSessionId ,
41- authServiceKey
42+ authServiceKey ,
43+ loginResult . hashcash
4244 ) ;
4345 }
4446
@@ -114,6 +116,7 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
114116 xAppleIdSessionId : < string > null ,
115117 isTwoFactorAuthenticationEnabled : false ,
116118 areCredentialsValid : true ,
119+ hashcash : < string > null ,
117120 } ;
118121
119122 if ( opts && opts . sessionBase64 ) {
@@ -130,6 +133,12 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
130133 await this . loginCore ( credentials ) ;
131134 } catch ( err ) {
132135 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+
133142 result . areCredentialsValid = statusCode !== 401 && statusCode !== 403 ;
134143 result . isTwoFactorAuthenticationEnabled = statusCode === 409 ;
135144
@@ -216,12 +225,14 @@ For more details how to set up your environment, please execute "tns publish ios
216225 private async handleTwoFactorAuthentication (
217226 scnt : string ,
218227 xAppleIdSessionId : string ,
219- authServiceKey : string
228+ authServiceKey : string ,
229+ hashcash : string
220230 ) : Promise < void > {
221231 const headers = {
222232 scnt : scnt ,
223233 "X-Apple-Id-Session-Id" : xAppleIdSessionId ,
224234 "X-Apple-Widget-Key" : authServiceKey ,
235+ "X-Apple-HC" : hashcash ,
225236 Accept : "application/json" ,
226237 } ;
227238 const authResponse = await this . $httpClient . httpRequest ( {
@@ -231,21 +242,48 @@ For more details how to set up your environment, please execute "tns publish ios
231242 } ) ;
232243
233244 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+ ) {
235262 const parsedAuthResponse = JSON . parse ( authResponse . body ) ;
236- const token = await this . $prompter . getString (
263+ token = await this . $prompter . getString (
237264 `Please enter the ${ parsedAuthResponse . securityCode . length } digit code` ,
238265 { allowEmpty : false }
239266 ) ;
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+ }
240282
241283 await this . $httpClient . httpRequest ( {
242- url : `https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode` ,
284+ url,
243285 method : "POST" ,
244- body : {
245- securityCode : {
246- code : token . toString ( ) ,
247- } ,
248- } ,
286+ body,
249287 headers : { ...headers , "Content-Type" : "application/json" } ,
250288 } ) ;
251289
@@ -258,6 +296,10 @@ For more details how to set up your environment, please execute "tns publish ios
258296 this . $applePortalCookieService . updateUserSessionCookie (
259297 authTrustResponse . headers [ "set-cookie" ]
260298 ) ;
299+ } else if ( multiSMS ) {
300+ this . $errors . fail (
301+ `The NativeScript CLI does not support SMS authenticaton with multiple registered phone numbers.`
302+ ) ;
261303 } else {
262304 this . $errors . fail (
263305 `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
266308 }
267309}
268310injector . 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