Skip to content

Commit 1c27c2b

Browse files
authored
fix(publish): various apple publish/sign-in fixes (#5718)
1 parent 1443240 commit 1c27c2b

File tree

6 files changed

+204
-28
lines changed

6 files changed

+204
-28
lines changed

lib/commands/appstore-upload.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,16 @@ export class PublishIOS implements ICommand {
4040
}
4141

4242
public async execute(args: string[]): Promise<void> {
43-
await this.$itmsTransporterService.validate();
43+
await this.$itmsTransporterService.validate(
44+
this.$options.appleApplicationSpecificPassword
45+
);
4446

4547
const username =
4648
args[0] ||
4749
(await this.$prompter.getString("Apple ID", { allowEmpty: false }));
50+
4851
const password =
4952
args[1] || (await this.$prompter.getPassword("Apple ID password"));
50-
const mobileProvisionIdentifier = args[2];
51-
const codeSignIdentity = args[3];
5253

5354
const user = await this.$applePortalSessionService.createUserSession(
5455
{ username, password },
@@ -66,6 +67,8 @@ export class PublishIOS implements ICommand {
6667
);
6768
}
6869

70+
const mobileProvisionIdentifier = this.$options.provision ?? args[2];
71+
6972
let ipaFilePath = this.$options.ipa
7073
? path.resolve(this.$options.ipa)
7174
: null;
@@ -76,25 +79,21 @@ export class PublishIOS implements ICommand {
7679
);
7780
}
7881

79-
if (!codeSignIdentity && !ipaFilePath) {
80-
this.$logger.warn(
81-
"No code sign identity set. A default code sign identity will be used. You can set one in app/App_Resources/iOS/build.xcconfig"
82-
);
83-
}
84-
8582
this.$options.release = true;
8683

8784
if (!ipaFilePath) {
8885
const platform = this.$devicePlatformsConstants.iOS.toLowerCase();
8986
// No .ipa path provided, build .ipa on out own.
90-
if (mobileProvisionIdentifier || codeSignIdentity) {
87+
if (mobileProvisionIdentifier) {
9188
// This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here.
9289
this.$logger.info(
93-
"Building .ipa with the selected mobile provision and/or certificate."
90+
"Building .ipa with the selected mobile provision and/or certificate. " +
91+
mobileProvisionIdentifier
9492
);
9593

9694
// As we need to build the package for device
9795
this.$options.forDevice = true;
96+
this.$options.provision = mobileProvisionIdentifier;
9897

9998
const buildData = new IOSBuildData(
10099
this.$projectData.projectDir,
@@ -124,6 +123,7 @@ export class PublishIOS implements ICommand {
124123
ipaFilePath,
125124
shouldExtractIpa: !!this.$options.ipa,
126125
verboseLogging: this.$logger.getLevel() === "TRACE",
126+
teamId: this.$options.teamId,
127127
});
128128
}
129129

lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export class ITMSConstants {
158158
};
159159
static iTMSExecutableName = "iTMSTransporter";
160160
static iTMSDirectoryName = "itms";
161+
static altoolExecutableName = "altool";
161162
}
162163

163164
class ItunesConnectApplicationTypesClass

lib/declarations.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -765,13 +765,18 @@ interface IITMSData {
765765
* @type {string}
766766
*/
767767
verboseLogging?: boolean;
768+
/**
769+
* Specifies the team id
770+
* @type {string}
771+
*/
772+
teamId?: string;
768773
}
769774

770775
/**
771776
* Used for communicating with Xcode iTMS Transporter tool.
772777
*/
773778
interface IITMSTransporterService {
774-
validate(): Promise<void>;
779+
validate(appSpecificPassword?: string): Promise<void>;
775780
/**
776781
* Uploads an .ipa package to iTunes Connect.
777782
* @param {IITMSData} data Data needed to upload the package

lib/services/apple-portal/apple-portal-session-service.ts

+101-10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
IAppleLoginResult,
99
IApplePortalSessionService,
1010
} from "./definitions";
11+
import * as crypto from "crypto";
1112

1213
export 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
}
268310
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+
}

lib/services/apple-portal/definitions.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface IAppleLoginResult {
3939
xAppleIdSessionId: string;
4040
isTwoFactorAuthenticationEnabled: boolean;
4141
areCredentialsValid: boolean;
42+
hashcash: string;
4243
}
4344

4445
interface IApplePortalUserDetail extends IAppleLoginResult {

lib/services/itmstransporter-service.ts

+84-6
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,38 @@ export class ITMSTransporterService implements IITMSTransporterService {
3333
return this.$injector.resolve("projectData");
3434
}
3535

36-
public async validate(): Promise<void> {
36+
public async validate(appSpecificPassword?: string): Promise<void> {
3737
const itmsTransporterPath = await this.getITMSTransporterPath();
38-
if (!this.$fs.exists(itmsTransporterPath)) {
39-
this.$errors.fail(
40-
"iTMS Transporter not found on this machine - make sure your Xcode installation is not damaged."
41-
);
38+
const version = await this.$xcodeSelectService.getXcodeVersion();
39+
if (+version.major < 14) {
40+
if (!this.$fs.exists(itmsTransporterPath)) {
41+
this.$errors.fail(
42+
"iTMS Transporter not found on this machine - make sure your Xcode installation is not damaged."
43+
);
44+
}
45+
} else {
46+
const altoolPath = await this.getAltoolPath();
47+
if (!this.$fs.exists(altoolPath)) {
48+
this.$errors.fail(
49+
"altool not found on this machine - make sure your Xcode installation is not damaged."
50+
);
51+
}
52+
if (!appSpecificPassword) {
53+
this.$errors.fail(
54+
"An app-specific password is required from xCode versions 14 and above, Use the --appleApplicationSpecificPassword to supply it."
55+
);
56+
}
4257
}
4358
}
44-
4559
public async upload(data: IITMSData): Promise<void> {
60+
const version = await this.$xcodeSelectService.getXcodeVersion();
61+
if (+version.major < 14) {
62+
await this.upload_iTMSTransporter(data);
63+
} else {
64+
await this.upload_altool(data);
65+
}
66+
}
67+
public async upload_iTMSTransporter(data: IITMSData): Promise<void> {
4668
const itmsTransporterPath = await this.getITMSTransporterPath();
4769
const ipaFileName = "app.ipa";
4870
const itmsDirectory = await this.$tempService.mkdirSync("itms-");
@@ -99,6 +121,46 @@ export class ITMSTransporterService implements IITMSTransporterService {
99121
);
100122
}
101123

124+
public async upload_altool(data: IITMSData): Promise<void> {
125+
const altoolPath = await this.getAltoolPath();
126+
const ipaFileName = "app.ipa";
127+
const itmsDirectory = await this.$tempService.mkdirSync("itms-");
128+
const innerDirectory = path.join(itmsDirectory, "mybundle.itmsp");
129+
const ipaFileLocation = path.join(innerDirectory, ipaFileName);
130+
131+
this.$fs.createDirectory(innerDirectory);
132+
133+
this.$fs.copyFile(data.ipaFilePath, ipaFileLocation);
134+
135+
const password = data.applicationSpecificPassword;
136+
137+
const args = [
138+
"--upload-app",
139+
"-t",
140+
"ios",
141+
"-f",
142+
ipaFileLocation,
143+
"-u",
144+
data.credentials.username,
145+
"-p",
146+
password,
147+
"-k 100000",
148+
];
149+
150+
if (data.teamId) {
151+
args.push("--asc-provider");
152+
args.push(data.teamId);
153+
}
154+
155+
if (data.verboseLogging) {
156+
args.push("--verbose");
157+
}
158+
159+
await this.$childProcess.spawnFromEvent(altoolPath, args, "close", {
160+
stdio: "inherit",
161+
});
162+
}
163+
102164
private async getBundleIdentifier(data: IITMSData): Promise<string> {
103165
const { shouldExtractIpa, ipaFilePath } = data;
104166

@@ -156,6 +218,22 @@ export class ITMSTransporterService implements IITMSTransporterService {
156218
return this.$projectData.projectIdentifiers.ios;
157219
}
158220

221+
@cache()
222+
private async getAltoolPath(): Promise<string> {
223+
const xcodePath = await this.$xcodeSelectService.getContentsDirectoryPath();
224+
let itmsTransporterPath = path.join(
225+
xcodePath,
226+
"..",
227+
"Contents",
228+
"Developer",
229+
"usr",
230+
"bin",
231+
ITMSConstants.altoolExecutableName
232+
);
233+
234+
return itmsTransporterPath;
235+
}
236+
159237
@cache()
160238
private async getITMSTransporterPath(): Promise<string> {
161239
const xcodePath = await this.$xcodeSelectService.getContentsDirectoryPath();

0 commit comments

Comments
 (0)