mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
Update secure login flow and login view
This commit is contained in:
+80
-6
@@ -65,6 +65,13 @@ const textDecoder = new TextDecoder();
|
||||
let secureSessionPromise: Promise<SecureSession> = null;
|
||||
let installed = false;
|
||||
const secureResponseCache = new Map<string, Promise<Response>>();
|
||||
let secureSessionCreatedAt = 0;
|
||||
const SECURE_SESSION_TTL_MS = 5 * 60 * 1000;
|
||||
const REKEY_ENDPOINTS = new Set([
|
||||
'/api/auth/login',
|
||||
'/api/auth/remember',
|
||||
'/api/auth/logout'
|
||||
]);
|
||||
|
||||
const bytesToBase64 = (bytes: ArrayBuffer): string =>
|
||||
{
|
||||
@@ -76,6 +83,13 @@ const bytesToBase64 = (bytes: ArrayBuffer): string =>
|
||||
return btoa(binary);
|
||||
};
|
||||
|
||||
const randomHex = (byteLength: number): string =>
|
||||
{
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(byteLength));
|
||||
|
||||
return Array.from(bytes).map(value => value.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
const hexValue = (code: number): number =>
|
||||
{
|
||||
if(code >= 48 && code <= 57) return code - 48;
|
||||
@@ -139,14 +153,15 @@ const getApiBase = (): string =>
|
||||
|
||||
if(typeof configured === 'string' && configured.length) return configured.replace(/\/$/, '');
|
||||
|
||||
return 'https://nitro.slogga.it:2096';
|
||||
return 'http://localhost:8443/';
|
||||
};
|
||||
|
||||
export const secureUrl = (kind: 'config' | 'gamedata', file: string): string =>
|
||||
export const secureUrl = (kind: 'config' | 'gamedata', file: string, cacheBust = false): string =>
|
||||
{
|
||||
const base = getApiBase();
|
||||
const version = cacheBust ? `&v=${ encodeURIComponent(Date.now().toString(36)) }` : '';
|
||||
|
||||
return `${ base }/nitro-sec/file?kind=${ encodeURIComponent(kind) }&file=${ encodeURIComponent(file) }`;
|
||||
return `${ base }/nitro-sec/file?kind=${ encodeURIComponent(kind) }&file=${ encodeURIComponent(file) }${ version }`;
|
||||
};
|
||||
|
||||
const createSecureSession = async (): Promise<SecureSession> =>
|
||||
@@ -178,11 +193,26 @@ const createSecureSession = async (): Promise<SecureSession> =>
|
||||
|
||||
const derived = await deriveAesKey(pair.privateKey, serverKey);
|
||||
|
||||
secureSessionCreatedAt = Date.now();
|
||||
|
||||
return { publicKey: clientPublicKey, key: derived.key, fingerprint: derived.fingerprint };
|
||||
};
|
||||
|
||||
const clearSecureSession = (clearCache = false): void =>
|
||||
{
|
||||
secureSessionPromise = null;
|
||||
secureSessionCreatedAt = 0;
|
||||
if(clearCache) secureResponseCache.clear();
|
||||
};
|
||||
|
||||
export const getSecureSession = (): Promise<SecureSession> =>
|
||||
{
|
||||
if(secureSessionPromise && secureSessionCreatedAt && ((Date.now() - secureSessionCreatedAt) > SECURE_SESSION_TTL_MS))
|
||||
{
|
||||
setDebugState('secure: session expired, rotating');
|
||||
clearSecureSession();
|
||||
}
|
||||
|
||||
if(!secureSessionPromise) secureSessionPromise = createSecureSession();
|
||||
|
||||
return secureSessionPromise;
|
||||
@@ -229,6 +259,8 @@ const normalizeSecureCacheKey = (requestUrl: string): string =>
|
||||
if(!url.pathname.includes('/nitro-sec/file')) return requestUrl;
|
||||
|
||||
const kind = url.searchParams.get('kind') || '';
|
||||
if(kind === 'config') return requestUrl;
|
||||
|
||||
const file = (url.searchParams.get('file') || '')
|
||||
.replace(/^[\\/]+/, '')
|
||||
.split('?')[0]
|
||||
@@ -291,6 +323,30 @@ const readRequestBody = async (input: RequestInfo | URL, init: RequestInit | und
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildSecureApiEnvelope = (requestUrl: string, method: string, clearBody: ArrayBuffer | null): ArrayBuffer | null =>
|
||||
{
|
||||
if(!clearBody) return null;
|
||||
|
||||
const url = new URL(requestUrl, window.location.href);
|
||||
const envelope = {
|
||||
ts: Date.now(),
|
||||
nonce: randomHex(16),
|
||||
method,
|
||||
path: `${ url.pathname }${ url.search }`,
|
||||
body: bytesToBase64(clearBody)
|
||||
};
|
||||
|
||||
return textEncoder.encode(JSON.stringify(envelope)).buffer;
|
||||
};
|
||||
|
||||
const scheduleSecureRekey = (): void =>
|
||||
{
|
||||
queueMicrotask(() =>
|
||||
{
|
||||
clearSecureSession();
|
||||
});
|
||||
};
|
||||
|
||||
export const installSecureFetch = (): void =>
|
||||
{
|
||||
if(installed) return;
|
||||
@@ -355,20 +411,38 @@ export const installSecureFetch = (): void =>
|
||||
const session = await getSecureSession();
|
||||
const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : undefined));
|
||||
const clearBody = await readRequestBody(input, init, method);
|
||||
const secureBody = buildSecureApiEnvelope(requestUrl, method, clearBody);
|
||||
const encryptedInit: RequestInit = { ...init, method, headers };
|
||||
|
||||
headers.set('X-Nitro-Key', session.publicKey);
|
||||
headers.set('X-Nitro-Api', '1');
|
||||
|
||||
if(clearBody)
|
||||
if(secureBody)
|
||||
{
|
||||
encryptedInit.body = await encryptBytes(session, clearBody);
|
||||
encryptedInit.body = await encryptBytes(session, secureBody);
|
||||
headers.set('Content-Type', 'text/plain; charset=utf-8');
|
||||
}
|
||||
|
||||
const response = await nativeFetch(input, encryptedInit);
|
||||
|
||||
if(response.headers.get('X-Nitro-Sec') === '1') return decryptResponse(session, response);
|
||||
if(response.headers.get('X-Nitro-Sec') === '1')
|
||||
{
|
||||
const decrypted = await decryptResponse(session, response);
|
||||
|
||||
try
|
||||
{
|
||||
const pathname = new URL(requestUrl, window.location.href).pathname;
|
||||
|
||||
if(response.ok && REKEY_ENDPOINTS.has(pathname))
|
||||
{
|
||||
setDebugState(`secure: rekey after ${ pathname }`);
|
||||
scheduleSecureRekey();
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user