Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(publish): Various apple publish/sign in fixes. #5718

Merged
merged 6 commits into from
Mar 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading