Documentazione
/
Ricette
/

Recuperare la chiave del bytecode array dal backend

Recuperare la chiave del bytecode array dal backend

Pro
v7.3.0+

Mantenere la chiave di decrittazione del bytecode VM fuori dal client, recuperandola dal proprio backend al momento del caricamento con un key getter asincrono protetto da autenticazione, così che un bundle sottratto risulti inerte.

Il problema

vmBytecodeArrayEncoding cifra il bytecode della VM con una chiave. Se tale chiave viene distribuita all'interno del bundle, chiunque disponga del file possiede tutto il necessario per decifrarlo. Un vmBytecodeArrayEncodingKeyGetter consente di collocare la chiave altrove e di produrla a runtime — ma un getter sincrono può leggere soltanto ciò che è già presente sul client (una variabile globale, un cookie, localStorage). Per recuperare la chiave dal proprio server — così da poterla proteggere con l'autenticazione e revocarla — il getter deve essere asincrono.

La soluzione

Attivare vmAsyncExecutor (v7.3.0+). Con l'executor asincrono il key getter può restituire una Promise ed è quindi in grado di recuperare la chiave dal backend tramite fetch prima che la VM entri in esecuzione. Tre opzioni operano di concerto:

  • vmBytecodeArrayEncoding: true — cifra il bytecode array.
  • vmBytecodeArrayEncodingKey — la chiave utilizzata in fase di compilazione (conservata sul proprio server e non nel bundle).
  • vmBytecodeArrayEncodingKeyGetter — un'espressione JS che restituisce la stessa chiave a runtime. Con vmAsyncExecutor attivo può restituire una Promise; in caso contrario il getter deve restituire la chiave in modo sincrono.

Configurazione di offuscamento lato client

JavaScriptObfuscator.obfuscate(sourceCode, {
    vmObfuscation: true,
    vmAsyncExecutor: true,
    vmBytecodeArrayEncoding: true,
    vmBytecodeArrayEncodingKey: process.env.VM_KEY,            // e.g. 'mySecretKey123'
    vmBytecodeArrayEncodingKeyGetter:
        'fetch("/api/vm-key", { credentials: "include" }).then((res) => res.text())'
});

L'espressione del getter viene incorporata testualmente e valutata nel browser. È importante tenere la chiave di compilazione vmBytecodeArrayEncodingKey fuori dal repository del client: occorre iniettarla da una variabile d'ambiente o da un segreto in fase di build e restituire esattamente la stessa stringa dall'endpoint illustrato di seguito.

Come operano le due chiavi

La chiave dell'utente non viene mai utilizzata da sola: su entrambi i lati viene combinata con una chiave interna controllata dall'offuscatore:

  • In fase di compilazione. vmBytecodeArrayEncodingKey viene combinata con una chiave interna derivata dall'offuscatore e il bytecode array viene codificato con la chiave mista risultante.
  • A runtime. Il valore a cui si risolve il proprio vmBytecodeArrayEncodingKeyGetter — restituito dal server — viene combinato con la stessa chiave interna, ricostruita sul client a partire da vari fattori di esecuzione, per decodificare il bytecode.

Poiché entrambi i lati combinano la chiave dell'utente con quella interna, il getter deve risolversi esattamente nella stessa stringa passata come vmBytecodeArrayEncodingKey. Nessuno dei due elementi è sufficiente da solo: la chiave dell'utente senza quella interna non consente di decodificare il bytecode e la chiave interna è inutile senza la prima. È per questo motivo che fornire la propria chiave soltanto ai chiamanti autenticati rende inerte un bundle sottratto.

Lato server

L'endpoint decide quale chiave restituire in base a ciò che l'applicazione considera attendibile: una sessione valida, un Origin o un Referer previsto, una verifica della licenza e così via. L'accorgimento consiste nel non rifiutare i chiamanti non attendibili, restituendo invece una chiave errata. Il bytecode viene così decodificato in dati privi di senso e il codice protetto smette di funzionare da sé: un approccio più discreto rispetto a un evidente 401, che indicherebbe a un attaccante esattamente che cosa aggirare.

// Express example — the exact checks depend on your app
app.get('/api/vm-key', (req, res) => {
    const origin = req.get('origin');
    const trusted =
        req.session?.user &&                       // a valid session, and
        origin === 'https://app.example.com';      // the expected production origin

    res.type('text/plain').send(
        // Real key for valid users; a decoy for everyone else
        // (no session, or a localhost / unexpected origin).
        trusted ? process.env.VM_KEY : process.env.VM_DECOY_KEY
    );
});