Skip to content

Commit

Permalink
fix: ns publish, apple authentication, appstore list (#5820)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcassidyav authored Dec 27, 2024
1 parent 661b653 commit 5e381d4
Show file tree
Hide file tree
Showing 10 changed files with 444 additions and 329 deletions.
12 changes: 5 additions & 7 deletions lib/commands/appstore-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export class PublishIOS implements ICommand {
const user = await this.$applePortalSessionService.createUserSession(
{ username, password },
{
applicationSpecificPassword: this.$options
.appleApplicationSpecificPassword,
applicationSpecificPassword:
this.$options.appleApplicationSpecificPassword,
sessionBase64: this.$options.appleSessionBase64,
requireInteractiveConsole: true,
requireApplicationSpecificPassword: true,
Expand Down Expand Up @@ -91,14 +91,12 @@ export class PublishIOS implements ICommand {
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,
platform,
{ ...this.$options.argv, watch: false }
{ ...this.$options.argv, buildForAppStore: true, watch: false }
);
ipaFilePath = await this.$buildController.prepareAndBuild(buildData);
} else {
Expand All @@ -118,8 +116,8 @@ export class PublishIOS implements ICommand {
await this.$itmsTransporterService.upload({
credentials: { username, password },
user,
applicationSpecificPassword: this.$options
.appleApplicationSpecificPassword,
applicationSpecificPassword:
this.$options.appleApplicationSpecificPassword,
ipaFilePath,
shouldExtractIpa: !!this.$options.ipa,
verboseLogging: this.$logger.getLevel() === "TRACE",
Expand Down
10 changes: 5 additions & 5 deletions lib/services/apple-portal/apple-portal-application-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
} from "./definitions";

export class ApplePortalApplicationService
implements IApplePortalApplicationService {
implements IApplePortalApplicationService
{
constructor(
private $applePortalSessionService: IApplePortalSessionService,
private $errors: IErrors,
Expand All @@ -36,13 +37,12 @@ export class ApplePortalApplicationService
public async getApplicationsByProvider(
contentProviderId: number
): Promise<IApplePortalApplication> {
const webSessionCookie = await this.$applePortalSessionService.createWebSession(
contentProviderId
);
const webSessionCookie =
await this.$applePortalSessionService.createWebSession(contentProviderId);
const summaries: IApplePortalApplicationSummary[] = [];
await this.getApplicationsByUrl(
webSessionCookie,
"https://appstoreconnect.apple.com/iris/v1/apps?include=appStoreVersions,prices",
"https://appstoreconnect.apple.com/iris/v1/apps?include=appStoreVersions",
summaries
);

Expand Down
61 changes: 52 additions & 9 deletions lib/services/apple-portal/apple-portal-session-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
} from "./definitions";
import * as crypto from "crypto";

import { GSASRPAuthenticator } from "./srp/srp-wrapper";

export class ApplePortalSessionService implements IApplePortalSessionService {
private loginConfigEndpoint =
"https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com";
Expand Down Expand Up @@ -174,29 +176,53 @@ For more details how to set up your environment, please execute "ns publish ios
}

private async loginCore(credentials: ICredentials): Promise<void> {
const wrapper = new GSASRPAuthenticator(credentials.username);
const initData = await wrapper.getInit();

const loginConfig = await this.getLoginConfig();
const loginUrl = `${loginConfig.authServiceUrl}/auth/signin`;
const loginUrl = `${loginConfig.authServiceUrl}/auth/signin/init`;
const headers = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-Apple-Widget-Key": loginConfig.authServiceKey,
Accept: "application/json, text/javascript",
};
const body = {
accountName: credentials.username,
password: credentials.password,
rememberMe: true,
};

const loginResponse = await this.$httpClient.httpRequest({
const initResponse = await this.$httpClient.httpRequest({
url: loginUrl,
method: "POST",
body,
body: initData,
headers,
});

const body = JSON.parse(initResponse.response.body);

const completeData = await wrapper.getComplete(credentials.password, body);

const hashcash = await this.fetchHashcash(
loginConfig.authServiceUrl,
loginConfig.authServiceKey
);

const completeUrl = `${loginConfig.authServiceUrl}/auth/signin/complete?isRememberMeEnabled=false`;
const completeHeaders = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-Apple-Widget-Key": loginConfig.authServiceKey,
Accept: "application/json, text/javascript",
"X-Apple-HC": hashcash || "",
};

const completeResponse = await this.$httpClient.httpRequest({
url: completeUrl,
method: "POST",
completeHeaders,
body: completeData,
headers: completeHeaders,
});

this.$applePortalCookieService.updateUserSessionCookie(
loginResponse.headers["set-cookie"]
completeResponse.headers["set-cookie"]
);
}

Expand All @@ -221,6 +247,23 @@ For more details how to set up your environment, please execute "ns publish ios
return config || this.defaultLoginConfig;
}

private async fetchHashcash(
authServiceUrl: string,
authServiceKey: string
): Promise<string> {
const loginUrl = `${authServiceUrl}/auth/signin?widgetKey=${authServiceKey}`;
const response = await this.$httpClient.httpRequest({
url: loginUrl,
method: "GET",
});

const headers = response.headers;

const bits = headers["X-Apple-HC-Bits"];
const challenge = headers["X-Apple-HC-Challenge"];
return makeHashCash(bits, challenge);
}

private async handleTwoFactorAuthentication(
scnt: string,
xAppleIdSessionId: string,
Expand Down
115 changes: 115 additions & 0 deletions lib/services/apple-portal/srp/srp-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Client, Hash, Mode, Srp, util } from "@foxt/js-srp";
import * as crypto from "crypto";

export type SRPProtocol = "s2k" | "s2k_fo";

export interface ServerSRPInitRequest {
a: string;
accountName: string;
protocols: SRPProtocol[];
}
export interface ServerSRPInitResponse {
iteration: number;
salt: string;
protocol: "s2k" | "s2k_fo";
b: string;
c: string;
}
export interface ServerSRPCompleteRequest {
accountName: string;
c: string;
m1: string;
m2: string;
rememberMe: boolean;
trustTokens: string[];
}

let srp = new Srp(Mode.GSA, Hash.SHA256, 2048);
const stringToU8Array = (str: string) => new TextEncoder().encode(str);
const base64ToU8Array = (str: string) =>
Uint8Array.from(Buffer.from(str, "base64"));
export class GSASRPAuthenticator {
constructor(private username: string) {}
private srpClient?: Client = undefined;

private async derivePassword(
protocol: "s2k" | "s2k_fo",
password: string,
salt: Uint8Array,
iterations: number
) {
let passHash = new Uint8Array(
await util.hash(srp.h, stringToU8Array(password))
);
if (protocol == "s2k_fo") {
passHash = stringToU8Array(util.toHex(passHash));
}

let imported = await crypto.subtle.importKey(
"raw",
passHash,
{ name: "PBKDF2" },
false,
["deriveBits"]
);
let derived = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
hash: { name: "SHA-256" },
iterations,
salt,
},
imported,
256
);

return new Uint8Array(derived);
}

async getInit(): Promise<ServerSRPInitRequest> {
if (this.srpClient) throw new Error("Already initialized");
this.srpClient = await srp.newClient(
stringToU8Array(this.username),
// provide fake passsword because we need to get data from server
new Uint8Array()
);
let a = Buffer.from(util.bytesFromBigint(this.srpClient.A)).toString(
"base64"
);
return {
a,
protocols: ["s2k", "s2k_fo"],
accountName: this.username,
};
}
async getComplete(
password: string,
serverData: ServerSRPInitResponse
): Promise<
Pick<ServerSRPCompleteRequest, "m1" | "m2" | "c" | "accountName">
> {
if (!this.srpClient) throw new Error("Not initialized");
if (serverData.protocol != "s2k" && serverData.protocol != "s2k_fo")
throw new Error("Unsupported protocol " + serverData.protocol);
let salt = base64ToU8Array(serverData.salt);
let serverPub = base64ToU8Array(serverData.b);
let iterations = serverData.iteration;
let derived = await this.derivePassword(
serverData.protocol,
password,
salt,
iterations
);
this.srpClient.p = derived;
await this.srpClient.generate(salt, serverPub);
let m1 = Buffer.from(this.srpClient._M).toString("base64");
let M2 = await this.srpClient.generateM2();
let m2 = Buffer.from(M2).toString("base64");
return {
accountName: this.username,
m1,
m2,
c: serverData.c,
};
}
}
6 changes: 4 additions & 2 deletions lib/services/ios/export-options-plist-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,17 @@ export class ExportOptionsPlistService implements IExportOptionsPlistService {
`;
}
if (provision) {
plistTemplate += ` <key>provisioningProfiles</key>
plistTemplate += ` <key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>${projectData.projectIdentifiers.ios}</key>
<string>${provision}</string>
${this.getExtensionProvisions()}
</dict>`;
}
plistTemplate += ` <key>method</key>
<string>app-store</string>
<string>app-store-connect</string>
<key>uploadBitcode</key>
<false/>
<key>compileBitcode</key>
Expand Down
25 changes: 15 additions & 10 deletions lib/services/ios/xcodebuild-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,12 @@ export class XcodebuildService implements IXcodebuildService {
platformData.getBuildOutputPath(buildConfig),
projectData.projectName + ".xcarchive"
);
const output = await this.$exportOptionsPlistService.createDevelopmentExportOptionsPlist(
archivePath,
projectData,
buildConfig
);
const output =
await this.$exportOptionsPlistService.createDevelopmentExportOptionsPlist(
archivePath,
projectData,
buildConfig
);
const args = [
"-exportArchive",
"-archivePath",
Expand All @@ -110,11 +111,14 @@ export class XcodebuildService implements IXcodebuildService {
platformData.getBuildOutputPath(buildConfig),
projectData.projectName + ".xcarchive"
);
const output = await this.$exportOptionsPlistService.createDistributionExportOptionsPlist(
archivePath,
projectData,
buildConfig
);
const output =
await this.$exportOptionsPlistService.createDistributionExportOptionsPlist(
archivePath,
projectData,
buildConfig
);
const provision =
buildConfig.provision || buildConfig.mobileProvisionIdentifier;
const args = [
"-exportArchive",
"-archivePath",
Expand All @@ -123,6 +127,7 @@ export class XcodebuildService implements IXcodebuildService {
output.exportFileDir,
"-exportOptionsPlist",
output.exportOptionsPlistFilePath,
provision ? "" : "-allowProvisioningUpdates", // no profiles specificed so let xcode decide.
];

await this.$xcodebuildCommandService.executeCommand(args, {
Expand Down
Loading

0 comments on commit 5e381d4

Please sign in to comment.