Skip to content

Commit

Permalink
fix(publish): various apple publish/sign-in fixes (#5718)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcassidyav authored Mar 25, 2023
1 parent 1443240 commit 1c27c2b
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 28 deletions.
22 changes: 11 additions & 11 deletions lib/commands/appstore-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ export class PublishIOS implements ICommand {
}

public async execute(args: string[]): Promise<void> {
await this.$itmsTransporterService.validate();
await this.$itmsTransporterService.validate(
this.$options.appleApplicationSpecificPassword
);

const username =
args[0] ||
(await this.$prompter.getString("Apple ID", { allowEmpty: false }));

const password =
args[1] || (await this.$prompter.getPassword("Apple ID password"));
const mobileProvisionIdentifier = args[2];
const codeSignIdentity = args[3];

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

const mobileProvisionIdentifier = this.$options.provision ?? args[2];

let ipaFilePath = this.$options.ipa
? path.resolve(this.$options.ipa)
: null;
Expand All @@ -76,25 +79,21 @@ export class PublishIOS implements ICommand {
);
}

if (!codeSignIdentity && !ipaFilePath) {
this.$logger.warn(
"No code sign identity set. A default code sign identity will be used. You can set one in app/App_Resources/iOS/build.xcconfig"
);
}

this.$options.release = true;

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

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

const buildData = new IOSBuildData(
this.$projectData.projectDir,
Expand Down Expand Up @@ -124,6 +123,7 @@ export class PublishIOS implements ICommand {
ipaFilePath,
shouldExtractIpa: !!this.$options.ipa,
verboseLogging: this.$logger.getLevel() === "TRACE",
teamId: this.$options.teamId,
});
}

Expand Down
1 change: 1 addition & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export class ITMSConstants {
};
static iTMSExecutableName = "iTMSTransporter";
static iTMSDirectoryName = "itms";
static altoolExecutableName = "altool";
}

class ItunesConnectApplicationTypesClass
Expand Down
7 changes: 6 additions & 1 deletion lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,13 +765,18 @@ interface IITMSData {
* @type {string}
*/
verboseLogging?: boolean;
/**
* Specifies the team id
* @type {string}
*/
teamId?: string;
}

/**
* Used for communicating with Xcode iTMS Transporter tool.
*/
interface IITMSTransporterService {
validate(): Promise<void>;
validate(appSpecificPassword?: string): Promise<void>;
/**
* Uploads an .ipa package to iTunes Connect.
* @param {IITMSData} data Data needed to upload the package
Expand Down
111 changes: 101 additions & 10 deletions lib/services/apple-portal/apple-portal-session-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IAppleLoginResult,
IApplePortalSessionService,
} from "./definitions";
import * as crypto from "crypto";

export class ApplePortalSessionService implements IApplePortalSessionService {
private loginConfigEndpoint =
Expand Down Expand Up @@ -38,7 +39,8 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
await this.handleTwoFactorAuthentication(
loginResult.scnt,
loginResult.xAppleIdSessionId,
authServiceKey
authServiceKey,
loginResult.hashcash
);
}

Expand Down Expand Up @@ -114,6 +116,7 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
xAppleIdSessionId: <string>null,
isTwoFactorAuthenticationEnabled: false,
areCredentialsValid: true,
hashcash: <string>null,
};

if (opts && opts.sessionBase64) {
Expand All @@ -130,6 +133,12 @@ export class ApplePortalSessionService implements IApplePortalSessionService {
await this.loginCore(credentials);
} catch (err) {
const statusCode = err && err.response && err.response.status;

const bits = err?.response?.headers["x-apple-hc-bits"];
const challenge = err?.response?.headers["x-apple-hc-challenge"];
const hashcash = makeHashCash(bits, challenge);
result.hashcash = hashcash;

result.areCredentialsValid = statusCode !== 401 && statusCode !== 403;
result.isTwoFactorAuthenticationEnabled = statusCode === 409;

Expand Down Expand Up @@ -216,12 +225,14 @@ For more details how to set up your environment, please execute "tns publish ios
private async handleTwoFactorAuthentication(
scnt: string,
xAppleIdSessionId: string,
authServiceKey: string
authServiceKey: string,
hashcash: string
): Promise<void> {
const headers = {
scnt: scnt,
"X-Apple-Id-Session-Id": xAppleIdSessionId,
"X-Apple-Widget-Key": authServiceKey,
"X-Apple-HC": hashcash,
Accept: "application/json",
};
const authResponse = await this.$httpClient.httpRequest({
Expand All @@ -231,21 +242,48 @@ For more details how to set up your environment, please execute "tns publish ios
});

const data = JSON.parse(authResponse.body);
if (data.trustedPhoneNumbers && data.trustedPhoneNumbers.length) {

const isSMS =
data.trustedPhoneNumbers &&
data.trustedPhoneNumbers.length === 1 &&
data.noTrustedDevices; // 1 device and no trusted devices means sms was automatically sent.
const multiSMS =
data.trustedPhoneNumbers &&
data.trustedPhoneNumbers.length !== 1 &&
data.noTrustedDevices; // Not handling more than 1 sms device and no trusted devices.

let token: string;

if (
data.trustedPhoneNumbers &&
data.trustedPhoneNumbers.length &&
!multiSMS
) {
const parsedAuthResponse = JSON.parse(authResponse.body);
const token = await this.$prompter.getString(
token = await this.$prompter.getString(
`Please enter the ${parsedAuthResponse.securityCode.length} digit code`,
{ allowEmpty: false }
);
const body: any = {
securityCode: {
code: token.toString(),
},
};
let url = `https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode`;

if (isSMS) {
// No trusted devices means it must be sms.
body.mode = "sms";
body.phoneNumber = {
id: data.trustedPhoneNumbers[0].id,
};
url = `https://idmsa.apple.com/appleauth/auth/verify/phone/securitycode`;
}

await this.$httpClient.httpRequest({
url: `https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode`,
url,
method: "POST",
body: {
securityCode: {
code: token.toString(),
},
},
body,
headers: { ...headers, "Content-Type": "application/json" },
});

Expand All @@ -258,6 +296,10 @@ For more details how to set up your environment, please execute "tns publish ios
this.$applePortalCookieService.updateUserSessionCookie(
authTrustResponse.headers["set-cookie"]
);
} else if (multiSMS) {
this.$errors.fail(
`The NativeScript CLI does not support SMS authenticaton with multiple registered phone numbers.`
);
} else {
this.$errors.fail(
`Although response from Apple indicated activated Two-step Verification or Two-factor Authentication, NativeScript CLI don't know how to handle this response: ${data}`
Expand All @@ -266,3 +308,52 @@ For more details how to set up your environment, please execute "tns publish ios
}
}
injector.register("applePortalSessionService", ApplePortalSessionService);

function makeHashCash(bits: string, challenge: string): string {
const version = 1;

const dateString = getHashCanDateString();
let result: string;
for (let counter = 0; ; counter++) {
const hc = [version, bits, dateString, challenge, `:${counter}`].join(":");

const shasumData = crypto.createHash("sha1");

shasumData.update(hc);
const digest = shasumData.digest();
if (checkBits(+bits, digest)) {
result = hc;
break;
}
}
return result;
}

function getHashCanDateString(): string {
const now = new Date();

return `${now.getFullYear()}${padTo2Digits(now.getMonth() + 1)}${padTo2Digits(
now.getDate()
)}${padTo2Digits(now.getHours())}${padTo2Digits(
now.getMinutes()
)}${padTo2Digits(now.getSeconds())}`;
}
function padTo2Digits(num: number) {
return num.toString().padStart(2, "0");
}

function checkBits(bits: number, digest: Buffer) {
let result = true;
for (let i = 0; i < bits; ++i) {
result = checkBit(i, digest);
if (!result) break;
}
return result;
}

function checkBit(position: number, buffer: Buffer): boolean {
const bitOffset = position & 7; // in byte
const byteIndex = position >> 3; // in buffer
const bit = (buffer[byteIndex] >> bitOffset) & 1;
return bit === 0;
}
1 change: 1 addition & 0 deletions lib/services/apple-portal/definitions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface IAppleLoginResult {
xAppleIdSessionId: string;
isTwoFactorAuthenticationEnabled: boolean;
areCredentialsValid: boolean;
hashcash: string;
}

interface IApplePortalUserDetail extends IAppleLoginResult {
Expand Down
90 changes: 84 additions & 6 deletions lib/services/itmstransporter-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,38 @@ export class ITMSTransporterService implements IITMSTransporterService {
return this.$injector.resolve("projectData");
}

public async validate(): Promise<void> {
public async validate(appSpecificPassword?: string): Promise<void> {
const itmsTransporterPath = await this.getITMSTransporterPath();
if (!this.$fs.exists(itmsTransporterPath)) {
this.$errors.fail(
"iTMS Transporter not found on this machine - make sure your Xcode installation is not damaged."
);
const version = await this.$xcodeSelectService.getXcodeVersion();
if (+version.major < 14) {
if (!this.$fs.exists(itmsTransporterPath)) {
this.$errors.fail(
"iTMS Transporter not found on this machine - make sure your Xcode installation is not damaged."
);
}
} else {
const altoolPath = await this.getAltoolPath();
if (!this.$fs.exists(altoolPath)) {
this.$errors.fail(
"altool not found on this machine - make sure your Xcode installation is not damaged."
);
}
if (!appSpecificPassword) {
this.$errors.fail(
"An app-specific password is required from xCode versions 14 and above, Use the --appleApplicationSpecificPassword to supply it."
);
}
}
}

public async upload(data: IITMSData): Promise<void> {
const version = await this.$xcodeSelectService.getXcodeVersion();
if (+version.major < 14) {
await this.upload_iTMSTransporter(data);
} else {
await this.upload_altool(data);
}
}
public async upload_iTMSTransporter(data: IITMSData): Promise<void> {
const itmsTransporterPath = await this.getITMSTransporterPath();
const ipaFileName = "app.ipa";
const itmsDirectory = await this.$tempService.mkdirSync("itms-");
Expand Down Expand Up @@ -99,6 +121,46 @@ export class ITMSTransporterService implements IITMSTransporterService {
);
}

public async upload_altool(data: IITMSData): Promise<void> {
const altoolPath = await this.getAltoolPath();
const ipaFileName = "app.ipa";
const itmsDirectory = await this.$tempService.mkdirSync("itms-");
const innerDirectory = path.join(itmsDirectory, "mybundle.itmsp");
const ipaFileLocation = path.join(innerDirectory, ipaFileName);

this.$fs.createDirectory(innerDirectory);

this.$fs.copyFile(data.ipaFilePath, ipaFileLocation);

const password = data.applicationSpecificPassword;

const args = [
"--upload-app",
"-t",
"ios",
"-f",
ipaFileLocation,
"-u",
data.credentials.username,
"-p",
password,
"-k 100000",
];

if (data.teamId) {
args.push("--asc-provider");
args.push(data.teamId);
}

if (data.verboseLogging) {
args.push("--verbose");
}

await this.$childProcess.spawnFromEvent(altoolPath, args, "close", {
stdio: "inherit",
});
}

private async getBundleIdentifier(data: IITMSData): Promise<string> {
const { shouldExtractIpa, ipaFilePath } = data;

Expand Down Expand Up @@ -156,6 +218,22 @@ export class ITMSTransporterService implements IITMSTransporterService {
return this.$projectData.projectIdentifiers.ios;
}

@cache()
private async getAltoolPath(): Promise<string> {
const xcodePath = await this.$xcodeSelectService.getContentsDirectoryPath();
let itmsTransporterPath = path.join(
xcodePath,
"..",
"Contents",
"Developer",
"usr",
"bin",
ITMSConstants.altoolExecutableName
);

return itmsTransporterPath;
}

@cache()
private async getITMSTransporterPath(): Promise<string> {
const xcodePath = await this.$xcodeSelectService.getContentsDirectoryPath();
Expand Down

0 comments on commit 1c27c2b

Please sign in to comment.