mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-07-18 01:09:52 -05:00
Add server to generate f parameters for NSO app authentication
This commit is contained in:
parent
4de8373e0b
commit
2005f34780
35
README.md
35
README.md
|
|
@ -171,6 +171,8 @@ curl --no-buffer "http://[::1]:12345/api/znc/presence/events?user=0123456789abcd
|
|||
|
||||
The splatnet2statink and flapg APIs are used by default to automate authenticating to the Nintendo Switch Online app's API and authenticating to web services. An access token (`id_token`) created by Nintendo must be sent to these APIs to generate some data that is required to authenticate the app. These APIs run the Nintendo Switch Online app in an Android emulator to generate this data. The access token sent includes some information about the authenticated Nintendo Account and can be used to authenticate to the Nintendo Switch Online app and web services.
|
||||
|
||||
Alternatively nxapi includes a custom server using Frida on an Android device/emulator that can be used instead of these.
|
||||
|
||||
This is only required for Nintendo Switch Online app data. Nintendo Switch Parental Controls data can be fetched without sending an access token to a third-party API.
|
||||
|
||||
### Nintendo Switch Parental Controls
|
||||
|
|
@ -290,6 +292,39 @@ DEBUG=api:* nxapi ...
|
|||
DEBUG=* nxapi ...
|
||||
```
|
||||
|
||||
#### znca API server
|
||||
|
||||
A server for controlling the Nintendo Switch Online app on an Android device/emulator using Frida can be used instead of the splatnet2statink and flapg APIs.
|
||||
|
||||
This requires:
|
||||
|
||||
- adb is installed on the computer running nxapi
|
||||
- The Android device is running adbd as root or a su-like command can be used to escalate to root
|
||||
- The frida-server executable to be located at `/data/local/tmp/frida-server` on the Android device
|
||||
- The Nintendo Switch Online app is installed on the Android device
|
||||
|
||||
The Android device must be constantly reachable using ADB. The server will exit if the device is unreachable.
|
||||
|
||||
```sh
|
||||
# Start the server using the ADB server "android.local:5555" listening on all interfaces on a random port
|
||||
nxapi android-znca-api-server-frida android.local:5555
|
||||
|
||||
# Start the server listening on a specific address/port
|
||||
# The `--listen` option can be used multiple times
|
||||
nxapi android-znca-api-server-frida android.local:5555 --listen "[::1]:12345"
|
||||
|
||||
# Use a command to escalate to root to start frida-server and the Nintendo Switch Online app
|
||||
# "{cmd}" will be replaced with the path to a temporary script in double quotes
|
||||
nxapi android-znca-api-server-frida android.local:5555 --exec-command "/system/bin/su -c {cmd}"
|
||||
|
||||
# Make API requests using curl
|
||||
curl --header "Content-Type: application/json" --data '{"type": "nso", "token": "...", "timestamp": "...", "uuid": "..."}' "http://[::1]:12345/api/znca/f"
|
||||
|
||||
# Use the znca API server in other commands
|
||||
# This should be set when running any nso commands as the access token will be refreshed automatically when it expires
|
||||
ZNCA_API_URL=http://[::1]:12345/api/znca nxapi nso ...
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
- Nintendo Switch Online app API docs
|
||||
|
|
|
|||
776
package-lock.json
generated
776
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -21,11 +21,13 @@
|
|||
"start": "node bin/nxapi.js presence"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.2",
|
||||
"cli-table": "^0.3.11",
|
||||
"debug": "^4.3.3",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"env-paths": "^3.0.0",
|
||||
"express": "^4.17.3",
|
||||
"frida": "^15.1.17",
|
||||
"mkdirp": "^1.0.4",
|
||||
"node-fetch": "^3.2.2",
|
||||
"node-notifier": "^10.0.1",
|
||||
|
|
|
|||
26
resources/android-znca-api-server.sh
Normal file
26
resources/android-znca-api-server.sh
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/system/bin/sh
|
||||
|
||||
# Ensure frida-server is running
|
||||
echo "Running frida-server"
|
||||
killall frida-server
|
||||
nohup /data/local/tmp/frida-server >/dev/null 2>&1 &
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Failed to start frida-server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
# Ensure the app is running
|
||||
echo "Starting com.nintendo.znca"
|
||||
am start-foreground-service com.nintendo.znca/com.google.firebase.messaging.FirebaseMessagingService
|
||||
am start-service com.nintendo.znca/com.google.firebase.messaging.FirebaseMessagingService
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Failed to start com.nintendo.znca"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Acquiring wake lock"
|
||||
echo androidzncaapiserver > /sys/power/wake_lock
|
||||
59
src/api/f.ts
59
src/api/f.ts
|
|
@ -4,6 +4,7 @@ import { ErrorResponse } from './util.js';
|
|||
|
||||
const debugS2s = createDebug('api:s2s');
|
||||
const debugFlapg = createDebug('api:flapg');
|
||||
const debugZncaApi = createDebug('api:znca-api');
|
||||
|
||||
export async function getLoginHash(token: string, timestamp: string | number) {
|
||||
debugS2s('Getting login hash');
|
||||
|
|
@ -56,6 +57,51 @@ export async function flapg(token: string, timestamp: string | number, guid: str
|
|||
return data.result;
|
||||
}
|
||||
|
||||
export async function genf(url: string, token: string, timestamp: string | number, uuid: string, type: FlapgIid) {
|
||||
debugZncaApi('Getting f parameter', {
|
||||
url, token, timestamp, uuid,
|
||||
});
|
||||
|
||||
const req: AndroidZncaFRequest = {
|
||||
type,
|
||||
token,
|
||||
timestamp: '' + timestamp,
|
||||
uuid,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
|
||||
const data = await response.json() as AndroidZncaFResponse | AndroidZncaFError;
|
||||
|
||||
if ('error' in data) {
|
||||
debugZncaApi('Error getting f parameter "%s"', data.error);
|
||||
throw new ErrorResponse('[znca-api] ' + data.error, response, data);
|
||||
}
|
||||
|
||||
debugZncaApi('Got f parameter "%s"', data.f);
|
||||
|
||||
return data.f;
|
||||
}
|
||||
|
||||
export async function genfc(url: string, token: string, timestamp: string | number, uuid: string, type: FlapgIid) {
|
||||
const f = await genf(url, token, timestamp, uuid, type);
|
||||
|
||||
return {
|
||||
f,
|
||||
p1: token,
|
||||
p2: '' + timestamp,
|
||||
p3: uuid,
|
||||
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoginHashApiResponse {
|
||||
hash: string;
|
||||
}
|
||||
|
|
@ -78,3 +124,16 @@ export interface FlapgApiResponse {
|
|||
p3: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AndroidZncaFRequest {
|
||||
type: FlapgIid;
|
||||
token: string;
|
||||
timestamp: string;
|
||||
uuid: string;
|
||||
}
|
||||
export interface AndroidZncaFResponse {
|
||||
f: string;
|
||||
}
|
||||
export interface AndroidZncaFError {
|
||||
error: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import fetch from 'node-fetch';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import createDebug from 'debug';
|
||||
import { flapg, FlapgIid } from './f.js';
|
||||
import { flapg, FlapgIid, genfc } from './f.js';
|
||||
import { AccountLogin, ActiveEvent, Announcement, CurrentUser, Friends, WebService, WebServiceToken, ZncResponse } from './znc-types.js';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
|
|
@ -76,14 +76,16 @@ export default class ZncApi {
|
|||
const uuid = uuidgen();
|
||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||
|
||||
const data = await flapg(nintendoAccountToken, timestamp, uuid, FlapgIid.APP);
|
||||
const data = process.env.ZNCA_API_URL ?
|
||||
await genfc(process.env.ZNCA_API_URL + '/f', nintendoAccountToken, timestamp, uuid, FlapgIid.APP) :
|
||||
await flapg(nintendoAccountToken, timestamp, uuid, FlapgIid.APP);
|
||||
|
||||
const req = {
|
||||
id,
|
||||
registrationToken: nintendoAccountToken,
|
||||
f: data.f,
|
||||
requestId: data.p3,
|
||||
timestamp: data.p2,
|
||||
requestId: uuid,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
return this.fetch<WebServiceToken>('/v2/Game/GetWebServiceToken', 'POST', JSON.stringify({
|
||||
|
|
@ -118,7 +120,9 @@ export default class ZncApi {
|
|||
// Nintendo Account user data
|
||||
const user = await getNintendoAccountUser(nintendoAccountToken);
|
||||
|
||||
const flapgdata = await flapg(nintendoAccountToken.id_token, timestamp, uuid, FlapgIid.NSO);
|
||||
const flapgdata = process.env.ZNCA_API_URL ?
|
||||
await genfc(process.env.ZNCA_API_URL + '/f', nintendoAccountToken.id_token, timestamp, uuid, FlapgIid.NSO) :
|
||||
await flapg(nintendoAccountToken.id_token, timestamp, uuid, FlapgIid.NSO);
|
||||
|
||||
debug('Getting Nintendo Switch Online app token');
|
||||
|
||||
|
|
@ -132,12 +136,12 @@ export default class ZncApi {
|
|||
},
|
||||
body: JSON.stringify({
|
||||
parameter: {
|
||||
naIdToken: flapgdata.p1,
|
||||
naIdToken: nintendoAccountToken.id_token,
|
||||
naBirthday: user.birthday,
|
||||
naCountry: user.country,
|
||||
language: user.language,
|
||||
timestamp: flapgdata.p2,
|
||||
requestId: flapgdata.p3,
|
||||
timestamp,
|
||||
requestId: uuid,
|
||||
f: flapgdata.f,
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
265
src/cli/android-znca-api-server-frida.ts
Normal file
265
src/cli/android-znca-api-server-frida.ts
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
import * as path from 'path';
|
||||
import createDebug from 'debug';
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as net from 'net';
|
||||
import frida, { Session } from 'frida';
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:android-znca-api-server-frida');
|
||||
const debugApi = createDebug('cli:android-znca-api-server-frida:api');
|
||||
|
||||
export const command = 'android-znca-api-server-frida <device>';
|
||||
export const desc = 'Connect to a rooted Android device with frida-server over ADB running the Nintendo Switch Online app and start a HTTP server to generate f parameters';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('device', {
|
||||
describe: 'ADB server address/port',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).option('exec-command', {
|
||||
describe: 'Command to use to run a file on the device',
|
||||
type: 'string',
|
||||
}).option('listen', {
|
||||
describe: 'Server address and port',
|
||||
type: 'array',
|
||||
default: ['[::]:0'],
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
await setup(argv);
|
||||
|
||||
let {session, script} = await attach(argv);
|
||||
let ready: Promise<void> | null = null;
|
||||
|
||||
let api: {
|
||||
ping(): Promise<true>;
|
||||
genAudioH(token: string, timestamp: string, uuid: string): Promise<string>;
|
||||
genAudioH2(token: string, timestamp: string, uuid: string): Promise<string>;
|
||||
} = script.exports as any;
|
||||
|
||||
process.on('beforeExit', () => {
|
||||
debug('Releasing wake lock', argv.device);
|
||||
execFileSync('adb', [
|
||||
'-s',
|
||||
argv.device,
|
||||
'shell',
|
||||
'sh -c "echo androidzncaapiserver > /sys/power/wake_unlock"',
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
});
|
||||
|
||||
function reattach() {
|
||||
// Already attempting to reattach
|
||||
if (ready) return;
|
||||
|
||||
debug('Attempting to reconnect to the device');
|
||||
|
||||
ready = attach(argv).then(a => {
|
||||
ready = null;
|
||||
session = a.session;
|
||||
script = a.script;
|
||||
api = script.exports as any;
|
||||
}).catch(err => {
|
||||
console.error('Reattach failed', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
app.post('/api/znca/f', bodyParser.json(), async (req, res) => {
|
||||
try {
|
||||
console.log('Received request from %s, port %d', req.socket.remoteAddress, req.socket.remotePort);
|
||||
await ready;
|
||||
|
||||
const data: {
|
||||
type: 'nso' | 'app';
|
||||
token: string;
|
||||
timestamp: string;
|
||||
uuid: string;
|
||||
} = req.body;
|
||||
|
||||
if (
|
||||
typeof data !== 'object' ||
|
||||
(data.type !== 'nso' && data.type !== 'app') ||
|
||||
typeof data.token !== 'string' ||
|
||||
typeof data.timestamp !== 'string' ||
|
||||
typeof data.uuid !== 'string'
|
||||
) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({error: 'invalid_request'}));
|
||||
return;
|
||||
}
|
||||
|
||||
debugApi('Calling %s', data.type === 'app' ? 'genAudioH2' : 'genAudioH');
|
||||
|
||||
const result = data.type === 'app' ?
|
||||
await api.genAudioH2(data.token, data.timestamp, data.uuid) :
|
||||
await api.genAudioH(data.token, data.timestamp, data.uuid);
|
||||
|
||||
debugApi('Returned %s', result);
|
||||
|
||||
const response = {
|
||||
f: result,
|
||||
};
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify(response));
|
||||
} catch (err) {
|
||||
debugApi('Error in request from %s', req.ip, err);
|
||||
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({error: 'unknown'}));
|
||||
|
||||
if ((err as any)?.message === 'Script is destroyed') {
|
||||
reattach();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const address of argv.listen) {
|
||||
const match = address.match(/^(?:((?:\d+\.){3}\d+)|\[(.*)\]):(\d+)$/);
|
||||
if (!match || !net.isIP(match[1] || match[2])) throw new Error('Not a valid address/port');
|
||||
|
||||
const server = app.listen(parseInt(match[3]), match[1] || match[2]);
|
||||
server.on('listening', () => {
|
||||
const address = server.address() as net.AddressInfo;
|
||||
console.log('Listening on %s, port %d', address.address, address.port);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await api.ping();
|
||||
} catch (err) {
|
||||
if ((err as any)?.message === 'Script is destroyed') {
|
||||
reattach();
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
const frida_script = `
|
||||
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');
|
||||
|
||||
resolve(libvoip.genAudioH(token, timestamp, uuid));
|
||||
});
|
||||
});
|
||||
},
|
||||
genAudioH2(token, timestamp, uuid) {
|
||||
return new Promise(resolve => {
|
||||
Java.perform(() => {
|
||||
const libvoip = Java.use('com.nintendo.coral.core.services.voip.LibvoipJni');
|
||||
|
||||
resolve(libvoip.genAudioH2(token, timestamp, uuid));
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
`;
|
||||
|
||||
async function setup(argv: ArgumentsCamelCase<Arguments>) {
|
||||
debug('Connecting to device %s', argv.device);
|
||||
let co = execFileSync('adb', [
|
||||
'connect',
|
||||
argv.device,
|
||||
]);
|
||||
|
||||
while (co.toString().includes('failed to authenticate')) {
|
||||
console.log('');
|
||||
console.log('-- Allow this computer to connect to the device. --');
|
||||
console.log('');
|
||||
await new Promise(rs => setTimeout(rs, 5 * 1000));
|
||||
|
||||
co = execFileSync('adb', [
|
||||
'disconnect',
|
||||
argv.device,
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
debug('Connecting to device %s', argv.device);
|
||||
co = execFileSync('adb', [
|
||||
'connect',
|
||||
argv.device,
|
||||
]);
|
||||
}
|
||||
|
||||
debug('Pushing scripts');
|
||||
execFileSync('adb', [
|
||||
'-s',
|
||||
argv.device,
|
||||
'push',
|
||||
path.join(import.meta.url.substr(7), '..', '..', '..', 'resources', 'android-znca-api-server.sh'),
|
||||
'/data/local/tmp/android-znca-api-server.sh',
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
execFileSync('adb', [
|
||||
'-s',
|
||||
argv.device,
|
||||
'shell',
|
||||
'chmod 755 /data/local/tmp/android-znca-api-server.sh',
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
async function attach(argv: ArgumentsCamelCase<Arguments>) {
|
||||
debug('Running scripts');
|
||||
execFileSync('adb', [
|
||||
'-s',
|
||||
argv.device,
|
||||
'shell',
|
||||
argv.execCommand ?
|
||||
argv.execCommand.replace('{cmd}', JSON.stringify('/data/local/tmp/android-znca-api-server.sh')) :
|
||||
'/data/local/tmp/android-znca-api-server.sh',
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
debug('Done');
|
||||
|
||||
const device = await frida.getDevice(argv.device);
|
||||
debug('Connected to frida device %s', device.name);
|
||||
|
||||
let session: Session;
|
||||
|
||||
try {
|
||||
const process = await device.getProcess('Nintendo Switch Online');
|
||||
|
||||
debug('process', process);
|
||||
|
||||
session = await device.attach(process.pid);
|
||||
} catch (err) {
|
||||
debug('Could not attach to process', err);
|
||||
throw new Error('Failed to attach to process');
|
||||
}
|
||||
|
||||
debug('Attached to app process, pid %d', session.pid);
|
||||
|
||||
const script = await session.createScript(frida_script);
|
||||
await script.load();
|
||||
|
||||
return {session, script};
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * as users from './users.js';
|
||||
export * as nso from './nso.js';
|
||||
export * as pctl from './pctl.js';
|
||||
export * as androidZncaApiServerFrida from './android-znca-api-server-frida.js';
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user