Dokumentacja
/
Przepisy
/

Pobieranie klucza tablicy kodu bajtowego z backendu

Pobieranie klucza tablicy kodu bajtowego z backendu

Pro
v7.3.0+

Trzymaj klucz deszyfrujący kod bajtowy VM poza klientem - pobieraj go z backendu podczas ładowania za pomocą asynchronicznego gettera, zabezpieczonego uwierzytelnianiem, dzięki czemu skradziona paczka pozostaje bezużyteczna.

Problem

Opcja vmBytecodeArrayEncoding szyfruje kod bajtowy VM przy użyciu klucza. Jeśli klucz ten trafi do paczki, każdy, kto ma plik, dysponuje wszystkim, co potrzebne do jego odszyfrowania. Opcja vmBytecodeArrayEncodingKeyGetter pozwala przechowywać klucz gdzie indziej i wytwarzać go w czasie wykonania — ale getter synchroniczny może odczytać wyłącznie to, co już znajduje się po stronie klienta (zmienną globalną, ciasteczko, localStorage). Aby pobrać klucz z serwera — i dzięki temu zabezpieczyć go uwierzytelnianiem oraz móc unieważnić — getter musi być asynchroniczny.

Rozwiązanie

Należy włączyć vmAsyncExecutor (v7.3.0+). Przy asynchronicznym executorze getter klucza może zwracać obiekt Promise, dzięki czemu może pobrać (fetch) klucz z backendu przed uruchomieniem VM. Współdziałają tu trzy opcje:

  • vmBytecodeArrayEncoding: true — szyfruje tablicę kodu bajtowego.
  • vmBytecodeArrayEncodingKey — klucz używany na etapie kompilacji (przechowywany na serwerze, nie w paczce).
  • vmBytecodeArrayEncodingKeyGetter — wyrażenie JS zwracające ten sam klucz w czasie wykonania. Przy włączonym vmAsyncExecutor może zwracać obiekt Promise; bez niego getter musi zwrócić klucz synchronicznie.

Konfiguracja obfuskacji po stronie klienta

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())'
});

Wyrażenie gettera jest osadzane dosłownie i wykonywane w przeglądarce. Klucz vmBytecodeArrayEncodingKey używany na etapie kompilacji nie powinien trafić do repozytorium kodu klienta — należy wstrzykiwać go ze zmiennej środowiskowej lub z sekretu podczas budowania i serwować dokładnie ten sam ciąg z opisanego niżej endpointu.

Jak działają oba klucze

Klucz użytkownika nigdy nie jest używany samodzielnie — po obu stronach zostaje zmieszany z wewnętrznym kluczem kontrolowanym przez obfuskator:

  • Etap kompilacji. vmBytecodeArrayEncodingKey jest łączony z wewnętrznym kluczem wyprowadzanym przez obfuskator, a tablica kodu bajtowego zostaje zakodowana powstałym w ten sposób kluczem mieszanym.
  • Czas wykonania. Wartość, do jakiej rozwiązuje się vmBytecodeArrayEncodingKeyGetter — zwrócona przez serwer — jest łączona z tym samym kluczem wewnętrznym, odtwarzanym po stronie klienta na podstawie różnych czynników środowiska uruchomieniowego, w celu zdekodowania kodu bajtowego.

Ponieważ obie strony mieszają klucz użytkownika z kluczem wewnętrznym, getter musi rozwiązywać się do dokładnie tego samego ciągu, który przekazano jako vmBytecodeArrayEncodingKey. Żaden z elementów nie wystarcza samodzielnie: klucz użytkownika bez klucza wewnętrznego nie pozwala zdekodować kodu bajtowego, a klucz wewnętrzny bez klucza użytkownika jest bezużyteczny — dlatego właśnie serwowanie klucza wyłącznie uwierzytelnionym wywołaniom sprawia, że skradziona paczka pozostaje bezużyteczna.

Strona serwera

Endpoint decyduje, który klucz zwrócić, na podstawie tego, czemu ufa dana aplikacja — poprawnej sesji, oczekiwanego nagłówka Origin lub Referer, weryfikacji licencji i tak dalej. Sztuczka polega na tym, aby zamiast odrzucać niezaufane wywołania, zwracać błędny klucz. Kod bajtowy dekoduje się wtedy do bezsensownych danych, a chroniony kod psuje się sam z siebie, co jest mniej rzucające się w oczy niż oczywisty 401, który podpowiada atakującemu dokładnie to, co należy obejść.

// 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
    );
});