diff --git a/rollup.config.js b/rollup.config.js index 8364f6e..47fa623 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,7 +7,7 @@ import nodeResolve from '@rollup/plugin-node-resolve'; import nodePolyfill from 'rollup-plugin-polyfill-node'; import html from '@rollup/plugin-html'; -const preload = { +const app_preload = { input: 'src/app/preload/index.ts', output: { file: 'dist/app/bundle/preload.cjs', @@ -23,13 +23,17 @@ const preload = { extensions: ['.js', '.jsx', '.ts', '.tsx'], esmExternals: true, }), + nodeResolve({ + browser: true, + preferBuiltins: true, + }), ], external: [ 'electron', ], }; -const preload_webservice = { +const app_preload_webservice = { input: 'src/app/preload-webservice/index.ts', output: { file: 'dist/app/bundle/preload-webservice.cjs', @@ -45,17 +49,21 @@ const preload_webservice = { extensions: ['.js', '.jsx', '.ts', '.tsx'], esmExternals: true, }), + nodeResolve({ + browser: true, + preferBuiltins: true, + }), ], external: [ 'electron', ], }; -const browser = { +const app_browser = { input: 'src/app/browser/index.ts', output: { file: 'dist/app/bundle/browser.js', - format: 'es', + format: 'iife', }, plugins: [ html({ @@ -84,7 +92,7 @@ const browser = { }; export default [ - preload, - preload_webservice, - browser, + app_preload, + app_preload_webservice, + app_browser, ]; diff --git a/src/api/na.ts b/src/api/na.ts index 7e848ab..6d5b04f 100644 --- a/src/api/na.ts +++ b/src/api/na.ts @@ -1,6 +1,7 @@ import fetch from 'node-fetch'; import createDebug from 'debug'; -import { ErrorResponse, JwtPayload } from './util.js'; +import { ErrorResponse } from './util.js'; +import { JwtPayload } from '../util.js'; const debug = createDebug('api:na'); diff --git a/src/api/util.ts b/src/api/util.ts index 5ad2076..4819020 100644 --- a/src/api/util.ts +++ b/src/api/util.ts @@ -1,5 +1,4 @@ -import * as crypto from 'crypto'; -import { Response } from 'node-fetch'; +import { Response as NodeFetchResponse } from 'node-fetch'; export class ErrorResponse extends Error { readonly body: string | undefined; @@ -7,7 +6,7 @@ export class ErrorResponse extends Error { constructor( message: string, - readonly response: Response, + readonly response: Response | NodeFetchResponse, body?: string | T ) { super(message); @@ -28,116 +27,7 @@ Object.defineProperty(ErrorResponse, Symbol.hasInstance, { value: (instance: ErrorResponse) => { return instance instanceof Error && 'response' in instance && - instance.response instanceof Response && 'body' in instance && 'data' in instance; }, }); - -export interface JwtHeader { - typ?: 'JWT'; - alg: JwtAlgorithm; - /** Key ID */ - kid?: string; - /** JSON Web Key Set URL */ - jku?: string; -} -export enum JwtAlgorithm { - RS256 = 'RS256', -} - -export interface JwtPayload { - /** Audience */ - aud: string; - /** Expiration timestamp (seconds) */ - exp: number; - /** Issue timestamp (seconds) */ - iat: number; - /** Issuer */ - iss: string; - /** Token ID */ - jti: string; - /** Subject */ - sub: string | number; - /** Token type */ - typ: string; -} - -type JwtVerifier = (data: Buffer, signature: Buffer, key: string) => boolean; - -export class Jwt { - constructor( - readonly header: H, - readonly payload: T - ) {} - - static decode(token: string) { - const [header_str, payload_str, signature_str] = token.split('.', 3); - - const header = JSON.parse(Buffer.from(header_str, 'base64url').toString()); - const payload = JSON.parse(Buffer.from(payload_str, 'base64url').toString()); - const signature = Buffer.from(signature_str, 'base64url'); - - if ('typ' in header && header.typ !== 'JWT') { - throw new Error('Invalid JWT'); - } - - const jwt = new this(header, payload); - return [jwt, signature] as const; - } - - verify(signature: Buffer, key: string, verifier?: JwtVerifier) { - const header_str = Buffer.from(JSON.stringify(this.header)).toString('base64url'); - const payload_str = Buffer.from(JSON.stringify(this.payload)).toString('base64url'); - const sign_data = header_str + '.' + payload_str; - - if (!verifier) { - if (!(this.header.alg in Jwt.verifiers) || !Jwt.verifiers[this.header.alg]) { - throw new Error('Unknown algorithm'); - } - - verifier = Jwt.verifiers[this.header.alg]; - } - - return verifier.call(null, Buffer.from(sign_data), signature, key); - } - - static verifiers: Record = { - [JwtAlgorithm.RS256]: (data, signature, key) => { - const verify = crypto.createVerify('RSA-SHA256'); - verify.end(data); - return verify.verify(key, signature); - }, - }; -} - -export interface Jwks { - keys: Jwk[]; -} -export interface Jwk { - /** Key type */ - kty: string; - use?: JwkUse | string; - key_ops?: JwkKeyOperation | string; - alg?: JwtAlgorithm | string; - /** Key ID */ - kid?: string; - x5u?: string[]; - x5c?: string[]; - x5t?: string; - 'x5t#S256'?: string; -} -export enum JwkUse { - SIGNATURE = 'sig', - ENCRYPTION = 'enc', -} -export enum JwkKeyOperation { - SIGN = 'sign', - VERIFY = 'verify', - ENCRYPT = 'encrypt', - DECRYPT = 'decrypt', - WRAP_KEY = 'wrapKey', - UNWRAP_KEY = 'unwrapKey', - DERIVE_KEY = 'deriveKey', - DERIVE_BITS = 'deriveBits', -} diff --git a/src/api/znc.ts b/src/api/znc.ts index 3b55b52..c1790f5 100644 --- a/src/api/znc.ts +++ b/src/api/znc.ts @@ -4,7 +4,8 @@ import createDebug from 'debug'; import { flapg, FlapgIid, genfc } from './f.js'; import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, ZncResponse, ZncStatus } from './znc-types.js'; import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountUser } from './na.js'; -import { ErrorResponse, JwtPayload } from './util.js'; +import { ErrorResponse } from './util.js'; +import { JwtPayload } from '../util.js'; const debug = createDebug('api:znc'); diff --git a/src/app/browser/util.ts b/src/app/browser/util.ts index 876268a..2d9445c 100644 --- a/src/app/browser/util.ts +++ b/src/app/browser/util.ts @@ -1,5 +1,4 @@ import * as React from 'react'; -import fetch, { RequestInfo, RequestInit, Response } from 'node-fetch'; import { ErrorResponse } from '../../api/util.js'; export enum RequestState { diff --git a/src/app/main/na-auth.ts b/src/app/main/na-auth.ts index bbbccaa..32ecf53 100644 --- a/src/app/main/na-auth.ts +++ b/src/app/main/na-auth.ts @@ -4,7 +4,7 @@ import * as persist from 'node-persist'; import fetch from 'node-fetch'; import { BrowserWindow, nativeImage, Notification, session, shell } from '../electron.js'; import { getNintendoAccountSessionToken, NintendoAccountSessionToken } from '../../api/na.js'; -import { Jwt } from '../../api/util.js'; +import { Jwt } from '../../util.js'; import { ZNCA_CLIENT_ID } from '../../api/znc.js'; import { ZNMA_CLIENT_ID } from '../../api/moon.js'; import { getToken, SavedToken } from '../../cli/nso/util.js'; diff --git a/src/cli/android-znca-api-server-frida.ts b/src/cli/android-znca-api-server-frida.ts index cb7927d..811c62c 100644 --- a/src/cli/android-znca-api-server-frida.ts +++ b/src/cli/android-znca-api-server-frida.ts @@ -7,7 +7,7 @@ import express from 'express'; import bodyParser from 'body-parser'; import type { Arguments as ParentArguments } from '../cli.js'; import { ArgumentsCamelCase, Argv, dir, getJwks, initStorage, YargsArguments } from '../util.js'; -import { Jwt } from '../api/util.js'; +import { Jwt } from '../util.js'; import { NintendoAccountIdTokenJwtPayload } from '../api/na.js'; import { ZNCA_CLIENT_ID, ZncJwtPayload } from '../api/znc.js'; diff --git a/src/cli/nso/util.ts b/src/cli/nso/util.ts index a972070..a0028b2 100644 --- a/src/cli/nso/util.ts +++ b/src/cli/nso/util.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import * as persist from 'node-persist'; import { FlapgApiResponse } from '../../api/f.js'; import { NintendoAccountSessionTokenJwtPayload, NintendoAccountToken, NintendoAccountUser } from '../../api/na.js'; -import { Jwt } from '../../api/util.js'; +import { Jwt } from '../../util.js'; import { AccountLogin } from '../../api/znc-types.js'; import ZncApi, { ZNCA_CLIENT_ID } from '../../api/znc.js'; import ZncProxyApi from '../../api/znc-proxy.js'; diff --git a/src/cli/pctl/util.ts b/src/cli/pctl/util.ts index fb7200b..b30a1aa 100644 --- a/src/cli/pctl/util.ts +++ b/src/cli/pctl/util.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import * as persist from 'node-persist'; import { ZNMA_CLIENT_ID } from '../../api/moon.js'; import { NintendoAccountSessionTokenJwtPayload, NintendoAccountToken, NintendoAccountUser } from '../../api/na.js'; -import { Jwt } from '../../api/util.js'; +import { Jwt } from '../../util.js'; import MoonApi from '../../api/moon.js'; const debug = createDebug('cli:nso'); diff --git a/src/util.ts b/src/util.ts index 80f2fef..e4bc570 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,13 +1,12 @@ import * as path from 'path'; import * as fs from 'fs'; import * as child_process from 'child_process'; +import * as crypto from 'crypto'; import * as yargs from 'yargs'; -// import type * as yargstypes from '../node_modules/@types/yargs/index.js'; import createDebug from 'debug'; import persist from 'node-persist'; import getPaths from 'env-paths'; import fetch from 'node-fetch'; -import { Jwks } from './api/util.js'; const debug = createDebug('cli'); @@ -167,12 +166,124 @@ export enum LoopResult { OK_SKIP_INTERVAL = LoopRunOkSkipInterval as any, } +// +// JSON Web Tokens +// + +export interface JwtHeader { + typ?: 'JWT'; + alg: JwtAlgorithm; + /** Key ID */ + kid?: string; + /** JSON Web Key Set URL */ + jku?: string; +} +export enum JwtAlgorithm { + RS256 = 'RS256', +} + +export interface JwtPayload { + /** Audience */ + aud: string; + /** Expiration timestamp (seconds) */ + exp: number; + /** Issue timestamp (seconds) */ + iat: number; + /** Issuer */ + iss: string; + /** Token ID */ + jti: string; + /** Subject */ + sub: string | number; + /** Token type */ + typ: string; +} + +type JwtVerifier = (data: Buffer, signature: Buffer, key: string) => boolean; + +export class Jwt { + constructor( + readonly header: H, + readonly payload: T + ) {} + + static decode(token: string) { + const [header_str, payload_str, signature_str] = token.split('.', 3); + + const header = JSON.parse(Buffer.from(header_str, 'base64url').toString()); + const payload = JSON.parse(Buffer.from(payload_str, 'base64url').toString()); + const signature = Buffer.from(signature_str, 'base64url'); + + if ('typ' in header && header.typ !== 'JWT') { + throw new Error('Invalid JWT'); + } + + const jwt = new this(header, payload); + return [jwt, signature] as const; + } + + verify(signature: Buffer, key: string, verifier?: JwtVerifier) { + const header_str = Buffer.from(JSON.stringify(this.header)).toString('base64url'); + const payload_str = Buffer.from(JSON.stringify(this.payload)).toString('base64url'); + const sign_data = header_str + '.' + payload_str; + + if (!verifier) { + if (!(this.header.alg in Jwt.verifiers) || !Jwt.verifiers[this.header.alg]) { + throw new Error('Unknown algorithm'); + } + + verifier = Jwt.verifiers[this.header.alg]; + } + + return verifier.call(null, Buffer.from(sign_data), signature, key); + } + + static verifiers: Record = { + [JwtAlgorithm.RS256]: (data, signature, key) => { + const verify = crypto.createVerify('RSA-SHA256'); + verify.end(data); + return verify.verify(key, signature); + }, + }; +} + // // JSON Web Key Sets // // Used for verifying JSON Web Tokens // +export interface Jwks { + keys: Jwk[]; +} +export interface Jwk { + /** Key type */ + kty: string; + use?: JwkUse | string; + key_ops?: JwkKeyOperation | string; + alg?: JwtAlgorithm | string; + /** Key ID */ + kid?: string; + x5u?: string[]; + x5c?: string[]; + x5t?: string; + 'x5t#S256'?: string; +} +export enum JwkUse { + SIGNATURE = 'sig', + ENCRYPTION = 'enc', +} +export enum JwkKeyOperation { + SIGN = 'sign', + VERIFY = 'verify', + ENCRYPT = 'encrypt', + DECRYPT = 'decrypt', + WRAP_KEY = 'wrapKey', + UNWRAP_KEY = 'unwrapKey', + DERIVE_KEY = 'deriveKey', + DERIVE_BITS = 'deriveBits', +} + interface SavedJwks { jwks: Jwks; expires_at: number;