Skip to content

Commit 5e381d4

Browse files
authored
fix: ns publish, apple authentication, appstore list (#5820)
1 parent 661b653 commit 5e381d4

10 files changed

+444
-329
lines changed

lib/commands/appstore-upload.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ export class PublishIOS implements ICommand {
5454
const user = await this.$applePortalSessionService.createUserSession(
5555
{ username, password },
5656
{
57-
applicationSpecificPassword: this.$options
58-
.appleApplicationSpecificPassword,
57+
applicationSpecificPassword:
58+
this.$options.appleApplicationSpecificPassword,
5959
sessionBase64: this.$options.appleSessionBase64,
6060
requireInteractiveConsole: true,
6161
requireApplicationSpecificPassword: true,
@@ -91,14 +91,12 @@ export class PublishIOS implements ICommand {
9191
mobileProvisionIdentifier
9292
);
9393

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

9896
const buildData = new IOSBuildData(
9997
this.$projectData.projectDir,
10098
platform,
101-
{ ...this.$options.argv, watch: false }
99+
{ ...this.$options.argv, buildForAppStore: true, watch: false }
102100
);
103101
ipaFilePath = await this.$buildController.prepareAndBuild(buildData);
104102
} else {
@@ -118,8 +116,8 @@ export class PublishIOS implements ICommand {
118116
await this.$itmsTransporterService.upload({
119117
credentials: { username, password },
120118
user,
121-
applicationSpecificPassword: this.$options
122-
.appleApplicationSpecificPassword,
119+
applicationSpecificPassword:
120+
this.$options.appleApplicationSpecificPassword,
123121
ipaFilePath,
124122
shouldExtractIpa: !!this.$options.ipa,
125123
verboseLogging: this.$logger.getLevel() === "TRACE",

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
} from "./definitions";
1111

1212
export class ApplePortalApplicationService
13-
implements IApplePortalApplicationService {
13+
implements IApplePortalApplicationService
14+
{
1415
constructor(
1516
private $applePortalSessionService: IApplePortalSessionService,
1617
private $errors: IErrors,
@@ -36,13 +37,12 @@ export class ApplePortalApplicationService
3637
public async getApplicationsByProvider(
3738
contentProviderId: number
3839
): Promise<IApplePortalApplication> {
39-
const webSessionCookie = await this.$applePortalSessionService.createWebSession(
40-
contentProviderId
41-
);
40+
const webSessionCookie =
41+
await this.$applePortalSessionService.createWebSession(contentProviderId);
4242
const summaries: IApplePortalApplicationSummary[] = [];
4343
await this.getApplicationsByUrl(
4444
webSessionCookie,
45-
"https://appstoreconnect.apple.com/iris/v1/apps?include=appStoreVersions,prices",
45+
"https://appstoreconnect.apple.com/iris/v1/apps?include=appStoreVersions",
4646
summaries
4747
);
4848

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

+52-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
} from "./definitions";
1111
import * as crypto from "crypto";
1212

13+
import { GSASRPAuthenticator } from "./srp/srp-wrapper";
14+
1315
export class ApplePortalSessionService implements IApplePortalSessionService {
1416
private loginConfigEndpoint =
1517
"https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com";
@@ -174,29 +176,53 @@ For more details how to set up your environment, please execute "ns publish ios
174176
}
175177

176178
private async loginCore(credentials: ICredentials): Promise<void> {
179+
const wrapper = new GSASRPAuthenticator(credentials.username);
180+
const initData = await wrapper.getInit();
181+
177182
const loginConfig = await this.getLoginConfig();
178-
const loginUrl = `${loginConfig.authServiceUrl}/auth/signin`;
183+
const loginUrl = `${loginConfig.authServiceUrl}/auth/signin/init`;
179184
const headers = {
180185
"Content-Type": "application/json",
181186
"X-Requested-With": "XMLHttpRequest",
182187
"X-Apple-Widget-Key": loginConfig.authServiceKey,
183188
Accept: "application/json, text/javascript",
184189
};
185-
const body = {
186-
accountName: credentials.username,
187-
password: credentials.password,
188-
rememberMe: true,
189-
};
190190

191-
const loginResponse = await this.$httpClient.httpRequest({
191+
const initResponse = await this.$httpClient.httpRequest({
192192
url: loginUrl,
193193
method: "POST",
194-
body,
194+
body: initData,
195195
headers,
196196
});
197197

198+
const body = JSON.parse(initResponse.response.body);
199+
200+
const completeData = await wrapper.getComplete(credentials.password, body);
201+
202+
const hashcash = await this.fetchHashcash(
203+
loginConfig.authServiceUrl,
204+
loginConfig.authServiceKey
205+
);
206+
207+
const completeUrl = `${loginConfig.authServiceUrl}/auth/signin/complete?isRememberMeEnabled=false`;
208+
const completeHeaders = {
209+
"Content-Type": "application/json",
210+
"X-Requested-With": "XMLHttpRequest",
211+
"X-Apple-Widget-Key": loginConfig.authServiceKey,
212+
Accept: "application/json, text/javascript",
213+
"X-Apple-HC": hashcash || "",
214+
};
215+
216+
const completeResponse = await this.$httpClient.httpRequest({
217+
url: completeUrl,
218+
method: "POST",
219+
completeHeaders,
220+
body: completeData,
221+
headers: completeHeaders,
222+
});
223+
198224
this.$applePortalCookieService.updateUserSessionCookie(
199-
loginResponse.headers["set-cookie"]
225+
completeResponse.headers["set-cookie"]
200226
);
201227
}
202228

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

250+
private async fetchHashcash(
251+
authServiceUrl: string,
252+
authServiceKey: string
253+
): Promise<string> {
254+
const loginUrl = `${authServiceUrl}/auth/signin?widgetKey=${authServiceKey}`;
255+
const response = await this.$httpClient.httpRequest({
256+
url: loginUrl,
257+
method: "GET",
258+
});
259+
260+
const headers = response.headers;
261+
262+
const bits = headers["X-Apple-HC-Bits"];
263+
const challenge = headers["X-Apple-HC-Challenge"];
264+
return makeHashCash(bits, challenge);
265+
}
266+
224267
private async handleTwoFactorAuthentication(
225268
scnt: string,
226269
xAppleIdSessionId: string,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Client, Hash, Mode, Srp, util } from "@foxt/js-srp";
2+
import * as crypto from "crypto";
3+
4+
export type SRPProtocol = "s2k" | "s2k_fo";
5+
6+
export interface ServerSRPInitRequest {
7+
a: string;
8+
accountName: string;
9+
protocols: SRPProtocol[];
10+
}
11+
export interface ServerSRPInitResponse {
12+
iteration: number;
13+
salt: string;
14+
protocol: "s2k" | "s2k_fo";
15+
b: string;
16+
c: string;
17+
}
18+
export interface ServerSRPCompleteRequest {
19+
accountName: string;
20+
c: string;
21+
m1: string;
22+
m2: string;
23+
rememberMe: boolean;
24+
trustTokens: string[];
25+
}
26+
27+
let srp = new Srp(Mode.GSA, Hash.SHA256, 2048);
28+
const stringToU8Array = (str: string) => new TextEncoder().encode(str);
29+
const base64ToU8Array = (str: string) =>
30+
Uint8Array.from(Buffer.from(str, "base64"));
31+
export class GSASRPAuthenticator {
32+
constructor(private username: string) {}
33+
private srpClient?: Client = undefined;
34+
35+
private async derivePassword(
36+
protocol: "s2k" | "s2k_fo",
37+
password: string,
38+
salt: Uint8Array,
39+
iterations: number
40+
) {
41+
let passHash = new Uint8Array(
42+
await util.hash(srp.h, stringToU8Array(password))
43+
);
44+
if (protocol == "s2k_fo") {
45+
passHash = stringToU8Array(util.toHex(passHash));
46+
}
47+
48+
let imported = await crypto.subtle.importKey(
49+
"raw",
50+
passHash,
51+
{ name: "PBKDF2" },
52+
false,
53+
["deriveBits"]
54+
);
55+
let derived = await crypto.subtle.deriveBits(
56+
{
57+
name: "PBKDF2",
58+
hash: { name: "SHA-256" },
59+
iterations,
60+
salt,
61+
},
62+
imported,
63+
256
64+
);
65+
66+
return new Uint8Array(derived);
67+
}
68+
69+
async getInit(): Promise<ServerSRPInitRequest> {
70+
if (this.srpClient) throw new Error("Already initialized");
71+
this.srpClient = await srp.newClient(
72+
stringToU8Array(this.username),
73+
// provide fake passsword because we need to get data from server
74+
new Uint8Array()
75+
);
76+
let a = Buffer.from(util.bytesFromBigint(this.srpClient.A)).toString(
77+
"base64"
78+
);
79+
return {
80+
a,
81+
protocols: ["s2k", "s2k_fo"],
82+
accountName: this.username,
83+
};
84+
}
85+
async getComplete(
86+
password: string,
87+
serverData: ServerSRPInitResponse
88+
): Promise<
89+
Pick<ServerSRPCompleteRequest, "m1" | "m2" | "c" | "accountName">
90+
> {
91+
if (!this.srpClient) throw new Error("Not initialized");
92+
if (serverData.protocol != "s2k" && serverData.protocol != "s2k_fo")
93+
throw new Error("Unsupported protocol " + serverData.protocol);
94+
let salt = base64ToU8Array(serverData.salt);
95+
let serverPub = base64ToU8Array(serverData.b);
96+
let iterations = serverData.iteration;
97+
let derived = await this.derivePassword(
98+
serverData.protocol,
99+
password,
100+
salt,
101+
iterations
102+
);
103+
this.srpClient.p = derived;
104+
await this.srpClient.generate(salt, serverPub);
105+
let m1 = Buffer.from(this.srpClient._M).toString("base64");
106+
let M2 = await this.srpClient.generateM2();
107+
let m2 = Buffer.from(M2).toString("base64");
108+
return {
109+
accountName: this.username,
110+
m1,
111+
m2,
112+
c: serverData.c,
113+
};
114+
}
115+
}

lib/services/ios/export-options-plist-service.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,17 @@ export class ExportOptionsPlistService implements IExportOptionsPlistService {
112112
`;
113113
}
114114
if (provision) {
115-
plistTemplate += ` <key>provisioningProfiles</key>
115+
plistTemplate += ` <key>signingStyle</key>
116+
<string>manual</string>
117+
<key>provisioningProfiles</key>
116118
<dict>
117119
<key>${projectData.projectIdentifiers.ios}</key>
118120
<string>${provision}</string>
119121
${this.getExtensionProvisions()}
120122
</dict>`;
121123
}
122124
plistTemplate += ` <key>method</key>
123-
<string>app-store</string>
125+
<string>app-store-connect</string>
124126
<key>uploadBitcode</key>
125127
<false/>
126128
<key>compileBitcode</key>

lib/services/ios/xcodebuild-service.ts

+15-10
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ export class XcodebuildService implements IXcodebuildService {
7979
platformData.getBuildOutputPath(buildConfig),
8080
projectData.projectName + ".xcarchive"
8181
);
82-
const output = await this.$exportOptionsPlistService.createDevelopmentExportOptionsPlist(
83-
archivePath,
84-
projectData,
85-
buildConfig
86-
);
82+
const output =
83+
await this.$exportOptionsPlistService.createDevelopmentExportOptionsPlist(
84+
archivePath,
85+
projectData,
86+
buildConfig
87+
);
8788
const args = [
8889
"-exportArchive",
8990
"-archivePath",
@@ -110,11 +111,14 @@ export class XcodebuildService implements IXcodebuildService {
110111
platformData.getBuildOutputPath(buildConfig),
111112
projectData.projectName + ".xcarchive"
112113
);
113-
const output = await this.$exportOptionsPlistService.createDistributionExportOptionsPlist(
114-
archivePath,
115-
projectData,
116-
buildConfig
117-
);
114+
const output =
115+
await this.$exportOptionsPlistService.createDistributionExportOptionsPlist(
116+
archivePath,
117+
projectData,
118+
buildConfig
119+
);
120+
const provision =
121+
buildConfig.provision || buildConfig.mobileProvisionIdentifier;
118122
const args = [
119123
"-exportArchive",
120124
"-archivePath",
@@ -123,6 +127,7 @@ export class XcodebuildService implements IXcodebuildService {
123127
output.exportFileDir,
124128
"-exportOptionsPlist",
125129
output.exportOptionsPlistFilePath,
130+
provision ? "" : "-allowProvisioningUpdates", // no profiles specificed so let xcode decide.
126131
];
127132

128133
await this.$xcodebuildCommandService.executeCommand(args, {

0 commit comments

Comments
 (0)