Fix f parameter generation

This fixes `f` parameter generation with nxapi's custom server. This will also work with imink/flapg *if the system time exactly matches the imink/flapg Android device's time*. This commit also makes nxapi's API compatible with imink's API and adds some information about the Android device and NSO app to response headers.
This commit is contained in:
Samuel Elliott 2022-08-23 16:38:08 +01:00
parent 7a37d7f7df
commit 428f7131a1
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
4 changed files with 259 additions and 97 deletions

View File

@ -60,7 +60,7 @@ export interface AccountLoginParameter {
naBirthday: string;
naCountry: string;
language: string;
timestamp: string;
timestamp: number;
requestId: string;
f: string;
}
@ -84,7 +84,7 @@ export type AccountToken = AccountLogin;
export interface AccountTokenParameter {
naIdToken: string;
naBirthday: string;
timestamp: string;
timestamp: number;
requestId: string;
f: string;
}
@ -188,6 +188,20 @@ export interface WebServiceAttribute {
attrKey: string;
}
export interface WebServiceTokenParameter {
id: number;
registrationToken: string;
timestamp: number;
requestId: string;
f: string;
}
/** /v2/Game/GetWebServiceToken */
export interface WebServiceToken {
accessToken: string;
expiresIn: number;
}
/** /v1/Event/GetActiveEvent */
export type GetActiveEventResult = ActiveEvent | {};
@ -281,9 +295,3 @@ export interface UpdateCurrentUserPermissionsParameter {
};
etag: string;
}
/** /v2/Game/GetWebServiceToken */
export interface WebServiceToken {
accessToken: string;
expiresIn: number;
}

View File

@ -1,7 +1,7 @@
import fetch, { Response } from 'node-fetch';
import { v4 as uuidgen } from 'uuid';
import createDebug from 'debug';
import { f, FlapgIid, FResult } from './f.js';
import { f, FResult } from './f.js';
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl, AccountTokenParameter, AccountLoginParameter } from './coral-types.js';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
@ -178,17 +178,14 @@ export default class CoralApi {
}
async getWebServiceToken(id: string) {
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const data = await f(this.token, timestamp, uuid, FlapgIid.APP, this.useragent ?? getAdditionalUserAgents());
const data = await f(this.token, '2', this.useragent ?? getAdditionalUserAgents());
const req = {
id,
registrationToken: this.token,
registrationToken: '',
f: data.f,
requestId: uuid,
timestamp,
requestId: data.request_id,
timestamp: data.timestamp,
};
return this.call<WebServiceToken>('/v2/Game/GetWebServiceToken', req);
@ -198,27 +195,19 @@ export default class CoralApi {
// Nintendo Account token
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const fdata = await f(
nintendoAccountToken.id_token, timestamp, uuid,
FlapgIid.NSO, this.useragent ?? getAdditionalUserAgents()
);
const fdata = await f(nintendoAccountToken.id_token, '1', this.useragent ?? getAdditionalUserAgents());
const req: AccountTokenParameter = {
naBirthday: user.birthday,
timestamp,
timestamp: fdata.timestamp,
f: fdata.f,
requestId: uuid,
requestId: fdata.request_id,
naIdToken: nintendoAccountToken.id_token,
};
const data = await this.call<AccountToken>('/v3/Account/GetToken', req, false);
return {
uuid,
timestamp,
nintendoAccountToken,
// user,
f: fdata,
@ -261,13 +250,7 @@ export default class CoralApi {
// Nintendo Account user data
const user = await getNintendoAccountUser(nintendoAccountToken);
const uuid = uuidgen();
const timestamp = '' + Math.floor(Date.now() / 1000);
const fdata = await f(
nintendoAccountToken.id_token, timestamp, uuid,
FlapgIid.NSO, useragent
);
const fdata = await f(nintendoAccountToken.id_token, '1', useragent);
debug('Getting Nintendo Switch Online app token');
@ -276,8 +259,8 @@ export default class CoralApi {
naBirthday: user.birthday,
naCountry: user.country,
language: user.language,
timestamp,
requestId: uuid,
timestamp: fdata.timestamp,
requestId: fdata.request_id,
f: fdata.f,
};
@ -314,8 +297,6 @@ export default class CoralApi {
debug('Got Nintendo Switch Online app token', data);
return {
uuid,
timestamp,
nintendoAccountToken,
user,
f: fdata,
@ -328,8 +309,6 @@ export default class CoralApi {
}
export interface CoralAuthData {
uuid: string;
timestamp: string;
nintendoAccountToken: NintendoAccountToken;
user: NintendoAccountUser;
f: FResult;
@ -340,7 +319,7 @@ export interface CoralAuthData {
}
export type PartialCoralAuthData =
Pick<CoralAuthData, 'uuid' | 'timestamp' | 'nintendoAccountToken' | 'f' | 'nsoAccount' | 'credential'>;
Pick<CoralAuthData, 'nintendoAccountToken' | 'f' | 'nsoAccount' | 'credential'>;
export interface CoralJwtPayload extends JwtPayload {
isChildRestricted: boolean;

View File

@ -1,6 +1,7 @@
import process from 'node:process';
import fetch from 'node-fetch';
import createDebug from 'debug';
import { v4 as uuidgen } from 'uuid';
import { ErrorResponse } from './util.js';
import { timeoutSignal } from '../util/misc.js';
import { getUserAgent } from '../util/useragent.js';
@ -15,7 +16,7 @@ export abstract class ZncaApi {
public useragent?: string
) {}
abstract genf(token: string, timestamp: string, uuid: string, type: FlapgIid): Promise<FResult>;
abstract genf(token: string, hash_method: '1' | '2'): Promise<FResult>;
}
//
@ -123,12 +124,16 @@ export class ZncaApiFlapg extends ZncaApi {
return getLoginHash(id_token, timestamp, this.useragent);
}
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
const result = await flapg(token, timestamp, uuid, type, this.useragent);
async genf(token: string, hash_method: '1' | '2') {
const timestamp = Date.now();
const request_id = uuidgen();
const type = hash_method === '2' ? FlapgIid.APP : FlapgIid.NSO;
const result = await flapg(token, timestamp, request_id, type, this.useragent);
return {
provider: 'flapg' as const,
token, timestamp, uuid, type,
token, timestamp, request_id, hash_method, type,
f: result.result.f,
result,
};
@ -198,12 +203,15 @@ export interface IminkFError {
}
export class ZncaApiImink extends ZncaApi {
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
const result = await iminkf(token, timestamp, uuid, type === FlapgIid.APP ? '2' : '1', this.useragent);
async genf(token: string, hash_method: '1' | '2') {
const timestamp = Date.now();
const request_id = uuidgen();
const result = await iminkf(token, timestamp, request_id, hash_method, this.useragent);
return {
provider: 'imink' as const,
token, timestamp, uuid, type,
token, timestamp, request_id, hash_method,
f: result.f,
result,
};
@ -215,18 +223,19 @@ export class ZncaApiImink extends ZncaApi {
//
export async function genf(
url: string, token: string, timestamp: string | number, uuid: string, type: FlapgIid,
url: string, hash_method: '1' | '2',
token: string, timestamp?: number, request_id?: string,
useragent?: string
) {
debugZncaApi('Getting f parameter', {
url, token, timestamp, uuid,
url, token, timestamp, request_id,
});
const req: AndroidZncaFRequest = {
type,
hash_method,
token,
timestamp: '' + timestamp,
uuid,
timestamp,
request_id,
};
const [signal, cancel] = timeoutSignal();
@ -251,19 +260,21 @@ export async function genf(
throw new ErrorResponse('[znca-api] ' + data.error, response, data);
}
debugZncaApi('Got f parameter "%s"', data.f);
debugZncaApi('Got f parameter', data, response.headers);
return data;
}
export interface AndroidZncaFRequest {
type: FlapgIid;
hash_method: '1' | '2';
token: string;
timestamp: string;
uuid: string;
timestamp?: string | number;
request_id?: string;
}
export interface AndroidZncaFResponse {
f: string;
timestamp?: number;
request_id?: string;
}
export interface AndroidZncaFError {
error: string;
@ -274,38 +285,39 @@ export class ZncaApiNxapi extends ZncaApi {
super(useragent);
}
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
const result = await genf(this.url + '/f', token, timestamp, uuid, type, this.useragent);
async genf(token: string, hash_method: '1' | '2') {
const result = await genf(this.url + '/f', hash_method, token, undefined, undefined, this.useragent);
return {
provider: 'nxapi' as const,
url: this.url + '/f',
token, timestamp, uuid, type,
token,
timestamp: result.timestamp!, // will be included as not sent in request
request_id: result.request_id!,
hash_method,
f: result.f,
result,
};
}
}
export async function f(
token: string, timestamp: string | number, uuid: string, type: FlapgIid,
useragent?: string
): Promise<FResult> {
export async function f(token: string, hash_method: '1' | '2', useragent?: string): Promise<FResult> {
const provider = getPreferredZncaApiFromEnvironment(useragent) ?? await getDefaultZncaApi(useragent);
return provider.genf(token, '' + timestamp, uuid, type);
return provider.genf(token, hash_method);
}
export type FResult = {
provider: string;
token: string;
timestamp: string;
uuid: string;
type: FlapgIid;
timestamp: number;
request_id: string;
hash_method: '1' | '2';
f: string;
result: unknown;
} & ({
provider: 'flapg';
type: FlapgIid;
result: FlapgApiResponse;
} | {
provider: 'imink';

View File

@ -5,6 +5,7 @@ import * as net from 'node:net';
import * as fs from 'node:fs/promises';
import * as crypto from 'node:crypto';
import createDebug from 'debug';
import { v4 as uuidgen } from 'uuid';
import express from 'express';
import bodyParser from 'body-parser';
import mkdirp from 'mkdirp';
@ -50,6 +51,45 @@ export function builder(yargs: Argv<ParentArguments>) {
type Arguments = YargsArguments<ReturnType<typeof builder>>;
interface PackageInfo {
name: string;
version: string;
build: number;
}
interface SystemInfo {
board: string;
bootloader: string;
brand: string;
abis: string[];
device: string;
display: string;
fingerprint: string;
hardware: string;
host: string;
id: string;
manufacturer: string;
model: string;
product: string;
tags: string;
time: string;
type: string;
user: string;
version: {
codename: string;
release: string;
// release_display: string;
sdk: string;
sdk_int: number;
security_patch: string;
};
}
interface FResult {
f: string;
timestamp: string;
}
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
await mkdirp(script_dir);
@ -61,10 +101,15 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
let api: {
ping(): Promise<true>;
genAudioH(token: string, timestamp: string, uuid: string): Promise<string>;
genAudioH2(token: string, timestamp: string, uuid: string): Promise<string>;
getPackageInfo(): Promise<PackageInfo>;
getSystemInfo(): Promise<SystemInfo>;
genAudioH(token: string, timestamp: string | number | undefined, request_id: string): Promise<FResult>;
genAudioH2(token: string, timestamp: string | number | undefined, request_id: string): Promise<FResult>;
} = script.exports as any;
let system_info = await api.getSystemInfo();
let package_info = await api.getPackageInfo();
const onexit = (code: number | NodeJS.Signals) => {
// @ts-expect-error
process.removeListener('exit', onexit);
@ -89,11 +134,24 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
debug('Attempting to reconnect to the device');
ready = attach(argv).then(a => {
ready = attach(argv).then(async a => {
ready = null;
session = a.session;
script = a.script;
api = script.exports as any;
const new_system_info = await api.getSystemInfo();
const new_package_info = await api.getPackageInfo();
if (system_info.version.sdk_int !== new_system_info.version.sdk_int) {
debug('Android system version updated while disconnected');
}
if (package_info.build !== new_package_info.build) {
debug('znca version updated while disconnected');
}
system_info = new_system_info;
package_info = new_package_info;
}).catch(err => {
console.error('Reattach failed', err);
process.exit(1);
@ -110,6 +168,12 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
req.headers['user-agent']);
res.setHeader('Server', product + ' android-znca-api-frida');
res.setHeader('X-Android-Build-Type', system_info.type);
res.setHeader('X-Android-Release', system_info.version.release);
res.setHeader('X-Android-Platform-Version', system_info.version.sdk_int);
res.setHeader('X-znca-Platform', 'Android');
res.setHeader('X-znca-Version', package_info.version);
res.setHeader('X-znca-Build', package_info.build);
next();
});
@ -118,19 +182,34 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
try {
await ready;
const data: {
let data: {
hash_method: '1' | '2';
token: string;
timestamp?: string | number;
request_id?: string;
} | {
type: 'nso' | 'app';
token: string;
timestamp: string;
uuid: string;
timestamp?: string;
uuid?: string;
} = req.body;
if (data && 'type' in data) data = {
hash_method:
data.type === 'nso' ? '1' :
data.type === 'app' ? '2' : null!,
token: data.token,
timestamp: '' + data.timestamp,
request_id: data.uuid,
};
if (
!data ||
typeof data !== 'object' ||
(data.type !== 'nso' && data.type !== 'app') ||
(data.hash_method !== '1' && data.hash_method !== '2') ||
typeof data.token !== 'string' ||
typeof data.timestamp !== 'string' ||
typeof data.uuid !== 'string'
(data.timestamp && typeof data.timestamp !== 'string' && typeof data.timestamp !== 'number') ||
(data.request_id && typeof data.request_id !== 'string')
) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
@ -143,13 +222,13 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const check_signature = jwt.payload.iss === 'https://accounts.nintendo.com';
if (data.type === 'nso' && jwt.payload.iss !== 'https://accounts.nintendo.com') {
if (data.hash_method === '1' && jwt.payload.iss !== 'https://accounts.nintendo.com') {
throw new Error('Invalid token issuer');
}
if (data.type === 'nso' && jwt.payload.aud !== ZNCA_CLIENT_ID) {
if (data.hash_method === '1' && jwt.payload.aud !== ZNCA_CLIENT_ID) {
throw new Error('Invalid token audience');
}
if (data.type === 'app' && jwt.payload.iss !== 'api-lp1.znc.srv.nintendo.net') {
if (data.hash_method === '2' && jwt.payload.iss !== 'api-lp1.znc.srv.nintendo.net') {
throw new Error('Invalid token issuer');
}
@ -197,16 +276,21 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}
}
debugApi('Calling %s', data.type === 'app' ? 'genAudioH2' : 'genAudioH');
const timestamp = data.timestamp ? '' + data.timestamp : undefined;
const request_id = data.request_id ? data.request_id : uuidgen();
const result = data.type === 'app' ?
await api.genAudioH2(data.token, data.timestamp, data.uuid) :
await api.genAudioH(data.token, data.timestamp, data.uuid);
debugApi('Calling %s', data.hash_method === '2' ? 'genAudioH2' : 'genAudioH');
const result = data.hash_method === '2' ?
await api.genAudioH2(data.token, timestamp, request_id) :
await api.genAudioH(data.token, timestamp, request_id);
debugApi('Returned %s', result);
const response = {
f: result,
f: result.f,
timestamp: data.timestamp ? undefined : result.timestamp,
request_id: data.request_id ? undefined : request_id,
};
res.setHeader('Content-Type', 'application/json');
@ -245,29 +329,108 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
throw err;
}
}, 5000);
debug('System info', system_info);
debug('Package info', package_info);
try {
debug('Test gen_audio_h');
const result = await api.genAudioH('id_token', 'timestamp', 'request_id');
debug('Test returned', result);
} catch (err) {
debug('Test failed', err);
}
}
const frida_script = `
const perform = callback => new Promise((rs, rj) => {
Java.scheduleOnMainThread(() => {
try {
rs(callback());
} catch (err) {
rj(err);
}
});
});
rpc.exports = {
ping() {
return true;
},
genAudioH(token, timestamp, uuid) {
return new Promise(resolve => {
Java.perform(() => {
const libvoip = Java.use('com.nintendo.coral.core.services.voip.LibvoipJni');
getPackageInfo() {
return perform(() => {
const context = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();
resolve(libvoip.genAudioH(token, timestamp, uuid));
});
const info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return {
name: info.packageName.value,
version: info.versionName.value,
build: info.versionCode.value,
// build: info.getLongVersionCode(),
};
});
},
genAudioH2(token, timestamp, uuid) {
return new Promise(resolve => {
Java.perform(() => {
const libvoip = Java.use('com.nintendo.coral.core.services.voip.LibvoipJni');
getSystemInfo() {
return perform(() => {
const Build = Java.use('android.os.Build');
const Version = Java.use('android.os.Build$VERSION');
resolve(libvoip.genAudioH2(token, timestamp, uuid));
});
return {
board: Build.BOARD.value,
bootloader: Build.BOOTLOADER.value,
brand: Build.BRAND.value,
abis: Build.SUPPORTED_ABIS.value,
device: Build.DEVICE.value,
display: Build.DISPLAY.value,
fingerprint: Build.FINGERPRINT.value,
hardware: Build.HARDWARE.value,
host: Build.HOST.value,
id: Build.ID.value,
manufacturer: Build.MANUFACTURER.value,
model: Build.MODEL.value,
product: Build.PRODUCT.value,
tags: Build.TAGS.value,
time: Build.TIME.value,
type: Build.TYPE.value,
user: Build.USER.value,
version: {
codename: Version.CODENAME.value,
release: Version.RELEASE.value,
sdk: Version.SDK.value,
sdk_int: Version.SDK_INT.value,
security_patch: Version.SECURITY_PATCH.value,
},
};
});
},
genAudioH(token, timestamp, request_id) {
return perform(() => {
const libvoip = Java.use('com.nintendo.coral.core.services.voip.LibvoipJni');
const context = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();
libvoip.init(context);
if (!timestamp) timestamp = Date.now();
return {
f: libvoip.genAudioH(token, '' + timestamp, request_id),
timestamp,
};
});
},
genAudioH2(token, timestamp, request_id) {
return perform(() => {
const libvoip = Java.use('com.nintendo.coral.core.services.voip.LibvoipJni');
const context = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();
libvoip.init(context);
if (!timestamp) timestamp = Date.now();
return {
f: libvoip.genAudioH2(token, '' + timestamp, request_id),
timestamp,
};
});
},
};