You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 15:06:20 +00:00
🆙 CryptoV2
This commit is contained in:
@@ -3,19 +3,7 @@ import { GetConfiguration } from '@nitrots/configuration';
|
||||
import { GetEventDispatcher, NitroEvent, NitroEventType, ReconnectEvent } from '@nitrots/events';
|
||||
import { NitroLogger } from '@nitrots/utils';
|
||||
import { EvaWireFormat } from './codec';
|
||||
import {
|
||||
aesGcmDecrypt,
|
||||
aesGcmEncrypt,
|
||||
buildClientHello,
|
||||
deriveAesKey,
|
||||
deriveSharedSecret,
|
||||
exportPublicKeySpki,
|
||||
generateEphemeralKeyPair,
|
||||
importPublicKeySpki,
|
||||
NONCE_LEN,
|
||||
parseServerHello,
|
||||
randomNonce
|
||||
} from './crypto';
|
||||
import { aesGcmDecrypt, aesGcmEncrypt, buildClientHello, deriveAesKey, deriveSharedSecret, exportPublicKeySpki, generateEphemeralKeyPair, importPublicKeySpki, importSigningPublicKeyFromBase64, NONCE_LEN, parseServerHello, randomNonce, verifyEphemeralSignature } from './crypto';
|
||||
import { MessageClassManager } from './messages';
|
||||
|
||||
type CryptoState = 'disabled' | 'awaiting_server_hello' | 'ready' | 'error';
|
||||
@@ -40,7 +28,7 @@ export class SocketConnection implements IConnection
|
||||
private _isReconnecting: boolean = false;
|
||||
private _intentionalClose: boolean = false;
|
||||
private _wasAuthenticated: boolean = false;
|
||||
|
||||
|
||||
public static readonly MAX_RECONNECT_ATTEMPTS: number = 7;
|
||||
public static readonly BASE_RECONNECT_DELAY_MS: number = 1000;
|
||||
public static readonly MAX_RECONNECT_DELAY_MS: number = 30000;
|
||||
@@ -62,7 +50,6 @@ export class SocketConnection implements IConnection
|
||||
private createSocket(socketUrl: string): void
|
||||
{
|
||||
this._dataBuffer = new ArrayBuffer(0);
|
||||
|
||||
const cryptoEnabled = !!GetConfiguration().getValue<boolean>('crypto.ws.enabled', false);
|
||||
if(cryptoEnabled && !this.subtleCryptoAvailable())
|
||||
{
|
||||
@@ -84,7 +71,6 @@ export class SocketConnection implements IConnection
|
||||
this._onCloseCallback = (event: Event) => this.onSocketClosed(event as CloseEvent);
|
||||
this._onErrorCallback = () => this.onSocketError();
|
||||
this._onMessageCallback = (event: MessageEvent) => this.onSocketMessage(event.data as ArrayBuffer);
|
||||
|
||||
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_OPENED, this._onOpenCallback);
|
||||
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_CLOSED, this._onCloseCallback);
|
||||
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_ERROR, this._onErrorCallback);
|
||||
@@ -138,15 +124,23 @@ export class SocketConnection implements IConnection
|
||||
return;
|
||||
}
|
||||
|
||||
if(this._cryptoState === 'error') return;
|
||||
|
||||
this._dataBuffer = this.concatArrayBuffers(this._dataBuffer, data);
|
||||
this.processReceivedData();
|
||||
}
|
||||
|
||||
private async handleServerHello(frame: ArrayBuffer): Promise<void>
|
||||
{
|
||||
const serverPubkeySpki = parseServerHello(frame);
|
||||
const { pubkeySpki: serverPubkeySpki, signature } = parseServerHello(frame);
|
||||
const signingRequired = !!GetConfiguration().getValue<boolean>('crypto.ws.signing.enabled', false);
|
||||
if(signingRequired)
|
||||
{
|
||||
if(!signature) throw new Error('crypto.ws.signing.enabled=true but server_hello had no signature');
|
||||
|
||||
const signingPub = await this.getSigningPublicKey();
|
||||
const ok = await verifyEphemeralSignature(signingPub, signature, serverPubkeySpki);
|
||||
if(!ok) throw new Error('server_hello signature verification failed (MITM?)');
|
||||
}
|
||||
|
||||
const serverPubkey = await importPublicKeySpki(serverPubkeySpki);
|
||||
const ourKeys = await generateEphemeralKeyPair();
|
||||
const ourPubkeySpki = await exportPublicKeySpki(ourKeys.publicKey);
|
||||
@@ -163,6 +157,29 @@ export class SocketConnection implements IConnection
|
||||
}
|
||||
}
|
||||
|
||||
private _cachedSigningPublicKey: CryptoKey = null;
|
||||
private async getSigningPublicKey(): Promise<CryptoKey>
|
||||
{
|
||||
if(this._cachedSigningPublicKey) return this._cachedSigningPublicKey;
|
||||
|
||||
const pinned = GetConfiguration().getValue<string>('crypto.ws.signing.public_key', '');
|
||||
if(pinned)
|
||||
{
|
||||
this._cachedSigningPublicKey = await importSigningPublicKeyFromBase64(pinned);
|
||||
return this._cachedSigningPublicKey;
|
||||
}
|
||||
|
||||
const endpointTemplate = GetConfiguration().getValue<string>('login.server_key.endpoint', '/api/auth/server-key');
|
||||
const endpoint = GetConfiguration().interpolate(endpointTemplate);
|
||||
const resp = await fetch(endpoint, { credentials: 'include' });
|
||||
if(!resp.ok) throw new Error(`server-key fetch failed: HTTP ${ resp.status }`);
|
||||
const payload = await resp.json();
|
||||
const b64 = typeof payload?.publicKey === 'string' ? payload.publicKey : '';
|
||||
if(!b64) throw new Error('server-key response missing publicKey');
|
||||
this._cachedSigningPublicKey = await importSigningPublicKeyFromBase64(b64);
|
||||
return this._cachedSigningPublicKey;
|
||||
}
|
||||
|
||||
private async decryptFrame(frame: ArrayBuffer): Promise<ArrayBuffer>
|
||||
{
|
||||
if(frame.byteLength < NONCE_LEN + 16) throw new Error('encrypted frame too short');
|
||||
|
||||
@@ -66,7 +66,13 @@ export function buildClientHello(pubkeySpki: ArrayBuffer): ArrayBuffer
|
||||
return out.buffer;
|
||||
}
|
||||
|
||||
export function parseServerHello(frame: ArrayBuffer): ArrayBuffer
|
||||
export interface ParsedServerHello
|
||||
{
|
||||
pubkeySpki: ArrayBuffer;
|
||||
signature: ArrayBuffer | null;
|
||||
}
|
||||
|
||||
export function parseServerHello(frame: ArrayBuffer): ParsedServerHello
|
||||
{
|
||||
if (frame.byteLength < 7) throw new Error('server_hello frame too short');
|
||||
const dv = new DataView(frame);
|
||||
@@ -74,7 +80,40 @@ export function parseServerHello(frame: ArrayBuffer): ArrayBuffer
|
||||
if (magic >>> 0 !== (HANDSHAKE_MAGIC >>> 0)) throw new Error('server_hello magic mismatch');
|
||||
const type = dv.getUint8(4);
|
||||
if (type !== TYPE_SERVER_HELLO) throw new Error(`expected server_hello, got type=0x${ type.toString(16) }`);
|
||||
|
||||
const keyLen = dv.getUint16(5, false);
|
||||
if (keyLen <= 0 || keyLen > frame.byteLength - 7) throw new Error(`invalid server key length ${ keyLen }`);
|
||||
return frame.slice(7, 7 + keyLen);
|
||||
const pubkeySpki = frame.slice(7, 7 + keyLen);
|
||||
|
||||
const remaining = frame.byteLength - (7 + keyLen);
|
||||
if (remaining === 0) return { pubkeySpki, signature: null };
|
||||
if (remaining < 2) throw new Error('truncated signature trailer');
|
||||
const sigLen = dv.getUint16(7 + keyLen, false);
|
||||
if (sigLen <= 0 || 7 + keyLen + 2 + sigLen !== frame.byteLength) throw new Error(`invalid signature length ${ sigLen }`);
|
||||
const signature = frame.slice(7 + keyLen + 2, 7 + keyLen + 2 + sigLen);
|
||||
return { pubkeySpki, signature };
|
||||
}
|
||||
|
||||
export async function importSigningPublicKeyFromBase64(spkiBase64: string): Promise<CryptoKey>
|
||||
{
|
||||
const bin = atob(spkiBase64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
return window.crypto.subtle.importKey(
|
||||
'spki',
|
||||
bytes.buffer,
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
false,
|
||||
[ 'verify' ]
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyEphemeralSignature(signingKey: CryptoKey, signature: ArrayBuffer, signedBytes: ArrayBuffer): Promise<boolean>
|
||||
{
|
||||
return window.crypto.subtle.verify(
|
||||
{ name: 'ECDSA', hash: 'SHA-256' },
|
||||
signingKey,
|
||||
signature,
|
||||
signedBytes
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user