Tworzenie klasycznego żądania do interfejsu API

Jeśli planujesz tylko wysyłać standardowe żądania interfejsu API, które są odpowiednie dla większości deweloperów, możesz przejść do sekcji oceny integralności. Ta strona zawiera informacje o wysyłaniu klasycznych żądań interfejsu API w celu uzyskania werdyktu integralności. Są one obsługiwane na Androidzie 4.4 (poziom interfejsu API 19) i nowszych.

co należy wziąć pod uwagę

Porównanie żądań standardowych i klasycznych

W zależności od potrzeb związanych z bezpieczeństwem i zapobieganiem nadużyciom aplikacja może wysyłać żądania standardowe lub klasyczne albo ich kombinację. Żądania standardowe są odpowiednie dla wszystkich aplikacji i gier. Można ich używać do sprawdzania, czy jakiekolwiek działanie lub wywołanie serwera jest autentyczne, a także do delegowania części ochrony przed ponownym odtwarzaniem i wyciekiem danych do Google Play. Żądania klasyczne są droższe w wykonywaniu, a Ty odpowiadasz za ich prawidłowe wdrożenie, aby chronić się przed wypływem danych i pewnymi typami ataków. Prośby klasyczne powinny być wysyłane rzadziej niż prośby standardowe, na przykład sporadycznie, aby sprawdzić, czy działanie o wysokiej wartości lub o charakterze poufnym jest autentyczne.

W tabeli poniżej znajdziesz główne różnice między tymi 2 typami próśb:

Standardowe żądanie interfejsu API Klasyczne żądanie do interfejsu API
Wymagania wstępne
Minimalna wymagana wersja pakietu Android SDK Android 5.0 (poziom interfejsu API 21) lub nowszy. Android 4.4 (poziom API 19) lub nowszy
Wymagania Google Play Sklep Google Play i Usługi Google Play Sklep Google Play i Usługi Google Play
Szczegóły integracji
Wymagany czas rozgrzewania interfejsu API ✔️ (kilka sekund)
Typowy czas oczekiwania na żądanie Kilkaset milisekund kilka sekund
Potencjalna częstotliwość żądań Często (sprawdzanie na żądanie, czy nie trzeba wykonać żadnego działania lub odpowiedzieć na prośbę) rzadko (jednorazowe sprawdzanie działań o najwyższej wartości lub najbardziej poufnych żądań);
tymczasowe zawieszenia użytkowników Większość rozgrzewania trwa krócej niż 10 s, ale obejmuje wywołanie serwera, dlatego zalecamy długi czas oczekiwania (np. 1 minutę). Prośby o wyrok są wysyłane po stronie klienta. Większość żądań trwa krócej niż 10 s, ale obejmuje wywołanie serwera, dlatego zalecamy długi limit czasu (np.1 minuta).
Token oceny integralności
Zawiera informacje o urządzeniu, aplikacji i koncie ✔️ ✔️
Pamięć podręczna tokenów Chronione buforowanie na urządzeniu przez Google Play Niezalecane
Odszyfrowanie i weryfikacja tokena za pomocą serwera Google Play ✔️ ✔️
Typowe opóźnienie żądania odszyfrowania między serwerami 10 milisekund z dostępnością na poziomie 99,9999% 10 milisekund z dostępnością na poziomie 99,9999%
Odszyfrowanie i weryfikacja tokena lokalnie w bezpiecznym środowisku serwera ✔️
Odszyfrowywanie i weryfikowanie tokena po stronie klienta
Aktualność oceny integralności automatyczne buforowanie i odświeżanie przez Google Play. Wszystkie werdykty są ponownie obliczane w przypadku każdego żądania.
Limity
Liczba żądań na aplikację dziennie Domyślnie 10 tys. (można poprosić o zwiększenie) Domyślnie 10 tys. (można poprosić o zwiększenie)
Liczba żądań na instancję aplikacji na minutę Rozgrzewka: 5 na minutę
Tokeny integralności: brak limitu publicznego*
Tokeny integralności: 5 na minutę
Ochrona
zapobieganie manipulowaniu i podobnym atakom; Użyj pola requestHash Korzystanie z pola nonce z wiązaniem treści na podstawie danych żądania
Zapobieganie atakom polegającym na odtwarzaniu i podobnym atakom Automatyczne zapobieganie przez Google Play Używanie pola nonce z logiką po stronie serwera

* Wszystkie żądania, w tym te bez publicznych limitów, podlegają niepublicznym limitom obronnym przy wysokich wartościach

rzadko wysyłać żądania klasyczne;

Generowanie tokena integralności wymaga czasu, danych i energii z baterii, a każda aplikacja ma maksymalną liczbę żądań klasycznych, które może wysyłać dziennie. Dlatego żądania klasyczne należy wysyłać tylko wtedy, gdy chcesz sprawdzić, czy działania o najwyższej wartości lub działania związane z poufnymi danymi są autentyczne, a standardowe żądanie nie wystarczy. Nie wysyłaj żądań klasycznych w przypadku działań o wysokiej częstotliwości lub niskiej wartości. Nie wysyłaj klasycznych żądań za każdym razem, gdy aplikacja przechodzi na pierwszy plan, ani co kilka minut w tle. Unikaj też wywoływania z dużej liczby urządzeń jednocześnie. Aplikacja wysyłająca zbyt dużo klasycznych żądań może zostać ograniczona, aby chronić użytkowników przed nieprawidłową implementacją.

Unikanie buforowania werdyktów

Buforowanie werdyktu zwiększa ryzyko ataków takich jak wydostanie się i odtwarzanie, w których dobry werdykt jest ponownie używany z niezaufanego środowiska. Jeśli rozważasz wysłanie klasycznego żądania, a potem zapisanie go w pamięci podręcznej na później, zalecamy zamiast tego wysłanie standardowego żądania na żądanie. Standardowe żądania obejmują buforowanie na urządzeniu, ale Google Play stosuje dodatkowe metody ochrony, aby zmniejszyć ryzyko ataków polegających na odtwarzaniu i wycieku danych.

Używanie pola nonce do ochrony klasycznych żądań

Interfejs Play Integrity API udostępnia pole nonce, które może służyć do dodatkowej ochrony aplikacji przed określonymi atakami, takimi jak ataki polegające na odtwarzaniu i podrobieniu. Interfejs Play Integrity API zwraca wartość ustawioną w tym polu w ramach podpisanej odpowiedzi dotyczącej integralności. Aby chronić aplikację przed atakami, postępuj zgodnie z instrukcjami dotyczącymi generowania losowych ciągów znaków.

Ponownie wysyłać klasyczne żądania ze wzrastającym czasem do ponowienia

Testy integralności mogą się nie udać z powodu warunków środowiskowych, takich jak niestabilne połączenie z internetem lub przeciążone urządzenie. Może to spowodować wygenerowanie etykiet z wartością „nie” na urządzeniu, które jest zaufane. Aby uniknąć takich sytuacji, dodaj opcję ponownego próbowania z wzrastającym czasem do ponowienia.

Omówienie

Diagram sekwencji przedstawiający ogólny projekt interfejsu Play Integrity API

Gdy użytkownik wykona w aplikacji działanie o wysokiej wartości, które chcesz chronić za pomocą kontroli integralności, wykonaj te czynności:

  1. Backend serwera aplikacji generuje i wysyła unikalną wartość do logiki po stronie klienta. W pozostałych krokach będziemy używać określenia „aplikacja”.
  2. Aplikacja tworzy nonce na podstawie wartości unikalnej i treści działania o wysokiej wartości. Następnie wywołuje interfejs Play Integrity API, przekazując parametrnonce.
  3. Aplikacja otrzymuje podpisaną i zaszyfrowaną ocenę od interfejsu Play Integrity API.
  4. Aplikacja przekazuje podpisany i zaszyfrowany werdyk backendowi aplikacji.
  5. Backend aplikacji wysyła werdykt do serwera Google Play. Serwer Google Play odszyfrowuje i weryfikuje werdyk, a potem zwraca wyniki do backendu aplikacji.
  6. Serwer backendu aplikacji podejmuje decyzję na podstawie sygnałów zawartych w ładunku tokenu.
  7. Backend aplikacji wysyła wyniki decyzji do aplikacji.

Generowanie wartości losowej

Gdy chronisz działanie w aplikacji za pomocą interfejsu Play Integrity API, możesz wykorzystać pole nonce, aby ograniczyć skuteczność niektórych typów ataków, takich jak ataki typu „osoba w środku” (PITM) i ataki polegające na odtwarzaniu. Interfejs Play Integrity API zwraca wartość ustawioną w tym polu w ramach podpisanej odpowiedzi integralności.

Wartość ustawiona w polu nonce musi być prawidłowo sformatowana:

  • String
  • Bezpieczny URL
  • Kodowanie w formacie Base64 bez zawijania.
  • Minimalna liczba znaków to 16.
  • Maksymalnie 500 znaków

Oto kilka najczęstszych sposobów używania pola nonce w interfejsie Play Integrity API. Aby zapewnić sobie najlepszą ochronę przed nonce, możesz połączyć te metody.

Dołącz hasz żądania, aby chronić się przed nieuprawnionymi modyfikacjami

Parametru nonce możesz używać w klasycznym żądaniu interfejsu API podobnie jak parametru requestHash w standardowym żądaniu interfejsu API, aby chronić zawartość żądania przed modyfikacją.

Gdy żądasz oceny integralności:

  1. Oblicz digest wszystkich kluczowych parametrów żądania (np. SHA256 stabilnej serializacji żądania) na podstawie działania użytkownika lub żądania serwera, które jest realizowane.
  2. Użyj funkcji setNonce, aby ustawić pole nonce na wartość wygenerowanego digestu.

Gdy otrzymasz ocenę integralności:

  1. Odkoduj i sprawdź token integralności oraz pobierz skrót z pola nonce.
  2. Oblicz digest żądania w taki sam sposób jak w aplikacji (np. SHA256 z użyciem stabilnej serializacji żądania).
  3. Porównaj skróty po stronie aplikacji i po stronie serwera. Jeśli się nie zgadzają, prośba nie jest wiarygodna.

Uwzględniaj unikalne wartości, aby chronić się przed atakami typu replay

Aby uniemożliwić złośliwym użytkownikom ponowne używanie poprzednich odpowiedzi z interfejsu Play Integrity API, możesz użyć pola nonce, aby jednoznacznie zidentyfikować każdą wiadomość.

Gdy żądasz oceny integralności:

  1. Uzyskaj globalnie unikalną wartość w sposób, który nie da się przewidzieć złośliwym użytkownikom. Może to być na przykład zabezpieczone za pomocą kryptografii losowe liczby wygenerowane po stronie serwera lub wcześniejszy identyfikator, np. identyfikator sesji lub transakcji. Prostszym i mniej bezpiecznym rozwiązaniem jest wygenerowanie losowego numeru na urządzeniu. Zalecamy tworzenie wartości o długości co najmniej 128 bitów.
  2. Zadzwoń do funkcji setNonce(), aby ustawić pole nonce na niepowtarzalną wartość z kroku 1.

Gdy otrzymasz ocenę integralności:

  1. Odkoduj i sprawdź integralność tokena, a potem uzyskaj unikalną wartość z pola nonce.
  2. Jeśli wartość z etapu 1 została wygenerowana na serwerze, sprawdź, czy otrzymana unikalna wartość była jedną z wygenerowanych wartości i czy jest używana po raz pierwszy (serwer musi przechowywać zapis wygenerowanych wartości przez odpowiedni czas). Jeśli otrzymana wartość unikalna została już użyta lub nie występuje w rekordzie, odrzuć żądanie.
  3. Jeśli jednak wartość unikalna została wygenerowana na urządzeniu, sprawdź, czy otrzymana wartość jest używana po raz pierwszy (serwer musi przechowywać przez odpowiedni czas rekordy wartości, które zostały już użyte). Jeśli otrzymana unikalna wartość została już użyta, odrzuć żądanie.

Połącz oba zabezpieczenia przed nieuprawnionymi modyfikacjami i atakami polegającymi na odtwarzaniu (zalecane)

Pola nonce można używać do ochrony przed manipulacją i atakami typu replay jednocześnie. Aby to zrobić, wygeneruj unikalną wartość w sposób opisany powyżej i uwzględnij ją w swojej prośbie. Następnie oblicz żądanie haszując je, pamiętając o uwzględnieniu unikalnej wartości jako części haszu. Implementacja, która łączy obie metody:

Gdy żądasz oceny integralności:

  1. Użytkownik inicjuje działanie o wysokiej wartości.
  2. Uzyskaj unikalną wartość dla tego działania zgodnie z opisem w sekcji Uwzględniaj unikalne wartości, aby chronić się przed atakami typu replay.
  3. Przygotuj wiadomość, którą chcesz chronić. Uwzględnij w wiadomości unikalną wartość z kroku 2.
  4. Aplikacja oblicza skrót wiadomości, którą chce chronić, zgodnie z opisem w sekcji Uwzględnij hasz żądania, aby chronić przed modyfikacją. Ponieważ wiadomość zawiera unikalną wartość, jest ona częścią hasza.
  5. Użyj polecenia setNonce(), aby ustawić pole nonce na obliczone podsumowanie z poprzedniego kroku.

Gdy otrzymasz ocenę integralności:

  1. Pobieranie unikalnej wartości z żądania
  2. Odkoduj i sprawdź token integralności oraz pobierz skrót z pola nonce.
  3. Zgodnie z opisem w sekcji Dołącz hasz żądania, aby chronić przed modyfikacją ponownie oblicz ciąg znaków po stronie serwera i sprawdź, czy pasuje on do ciągu znaków uzyskanego z tokena integralności.
  4. Zgodnie z opisem w sekcji Uwzględniaj unikalne wartości, aby chronić się przed atakami polegającymi na odtwarzaniu sprawdź poprawność unikalnej wartości.

Na tym diagramie sekwencji pokazano te kroki po stronie serwera:nonce

Sekwencyjny diagram pokazujący, jak chronić się przed atakami polegającymi na modyfikacji i odtwarzaniu danych

Prośba o ocenę integralności

Po wygenerowaniu nonce możesz poprosić o ocenę integralności w Google Play. Aby to zrobić:

  1. Utwórz IntegrityManager, jak pokazano w tych przykładach.
  2. Utwórz obiekt IntegrityTokenRequest, podając parametr nonce za pomocą metody setNonce() w powiązanym kreatorze. Aplikacje rozpowszechniane wyłącznie poza Google Play i pakietami SDK muszą też określać numer projektu Google Cloud za pomocą metody setCloudProjectNumber(). Aplikacje w Google Play są połączone z projektem Cloud w Konsoli Play, więc nie trzeba podawać numeru projektu Cloud w prośbie.
  3. Zadzwoń do requestIntegrityToken() za pomocą menedżera, podając IntegrityTokenRequest.

Kotlin

// Receive the nonce from the secure server.
val nonce: String = ...

// Create an instance of a manager.
val integrityManager =
    IntegrityManagerFactory.create(applicationContext)

// Request the integrity token by providing a nonce.
val integrityTokenResponse: Task<IntegrityTokenResponse> =
    integrityManager.requestIntegrityToken(
        IntegrityTokenRequest.builder()
             .setNonce(nonce)
             .build())

Java

import com.google.android.gms.tasks.Task; ...

// Receive the nonce from the secure server.
String nonce = ...

// Create an instance of a manager.
IntegrityManager integrityManager =
    IntegrityManagerFactory.create(getApplicationContext());

// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse =
    integrityManager
        .requestIntegrityToken(
            IntegrityTokenRequest.builder().setNonce(nonce).build());

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    // Receive the nonce from the secure server.
    var nonce = ...

    // Create an instance of a manager.
    var integrityManager = new IntegrityManager();

    // Request the integrity token by providing a nonce.
    var tokenRequest = new IntegrityTokenRequest(nonce);
    var requestIntegrityTokenOperation =
        integrityManager.RequestIntegrityToken(tokenRequest);

    // Wait for PlayAsyncOperation to complete.
    yield return requestIntegrityTokenOperation;

    // Check the resulting error code.
    if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
    {
        AppendStatusLog("IntegrityAsyncOperation failed with error: " +
                requestIntegrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var tokenResponse = requestIntegrityTokenOperation.GetResult();
}

Unreal Engine

// .h
void MyClass::OnRequestIntegrityTokenCompleted(
  EIntegrityErrorCode ErrorCode,
  UIntegrityTokenResponse* Response)
{
  // Check the resulting error code.
  if (ErrorCode == EIntegrityErrorCode::Integrity_NO_ERROR)
  {
    // Get the token.
    FString Token = Response->Token;
  }
}

// .cpp
void MyClass::RequestIntegrityToken()
{
  // Receive the nonce from the secure server.
  FString Nonce = ...

  // Create the Integrity Token Request.
  FIntegrityTokenRequest Request = { Nonce };

  // Create a delegate to bind the callback function.
  FIntegrityOperationCompletedDelegate Delegate;

  // Bind the completion handler (OnRequestIntegrityTokenCompleted) to the delegate.
  Delegate.BindDynamic(this, &MyClass::OnRequestIntegrityTokenCompleted);

  // Initiate the integrity token request, passing the delegate to handle the result.
  GetGameInstance()
    ->GetSubsystem<UIntegrityManager>()
    ->RequestIntegrityToken(Request, Delegate);
}

Rodzimy użytkownik

/// Create an IntegrityTokenRequest opaque object.
const char* nonce = RequestNonceFromServer();
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce);

/// Prepare an IntegrityTokenResponse opaque type pointer and call
/// IntegerityManager_requestIntegrityToken().
IntegrityTokenResponse* response;
IntegrityErrorCode error_code =
        IntegrityManager_requestIntegrityToken(request, &response);

/// ...
/// Proceed to polling iff error_code == INTEGRITY_NO_ERROR
if (error_code != INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.
/// Note, the polling shouldn't block the thread where the IntegrityManager
/// is running.

IntegrityResponseStatus response_status;

/// Check for error codes.
IntegrityErrorCode error_code =
        IntegrityTokenResponse_getStatus(response, &response_status);
if (error_code == INTEGRITY_NO_ERROR
    && response_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrity_token = IntegrityTokenResponse_getToken(response);
    SendTokenToServer(integrity_token);
}
/// ...
/// Remember to free up resources.
IntegrityTokenRequest_destroy(request);
IntegrityTokenResponse_destroy(response);
IntegrityManager_destroy();

Odszyfruj i sprawdź ocenę integralności

Gdy żądasz oceny integralności, interfejs Play Integrity API udostępnia podpisany token odpowiedzi. Wartość nonce, którą podasz w żądaniu, stanie się częścią tokenu odpowiedzi.

Format tokena

Token jest zagnieżżonym tokenem internetowym JSON (JWT), który jest szyfrowaniem internetowym JSON (JWE) podpisu internetowego JSON (JWS). Komponenty JWE i JWS są reprezentowane za pomocą kompaktowej serializacji.

Algorytmy szyfrowania i podpisywania są dobrze obsługiwane w różnych implementacjach JWT:

  • JWE używa A256KW dla alg i A256GCM dla enc.

  • JWS używa algorytmu ES256.

Odszyfrowanie i weryfikacja na serwerach Google (zalecane)

Interfejs Play Integrity API umożliwia odszyfrowywanie i weryfikowanie oceny integralności na serwerach Google, co zwiększa bezpieczeństwo aplikacji. Aby to zrobić:

  1. Utwórz konto usługi w projekcie Google Cloud połączonym z Twoją aplikacją.
  2. Na serwerze aplikacji pobierz token dostępu z uprawnień konta usługi za pomocą zakresu playintegrity i wyślij to żądanie:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. Przeczytaj odpowiedź w formacie JSON.

Odszyfrowanie i weryfikacja lokalnie

Jeśli zdecydujesz się zarządzać kluczami szyfrowania odpowiedzi i pobierać je, możesz odszyfrować i zweryfikować zwrócony token w swoim bezpiecznym środowisku serwera. Zwrócony token możesz uzyskać, korzystając z metody IntegrityTokenResponse#token().

Z tego przykładu dowiesz się, jak odkodować klucz AES i klucz publiczny EC zakodowany w formacie DER do weryfikacji podpisu z Konsoli Play na klucze językowe (w naszym przypadku języka programowania Java) w backendzie aplikacji. Pamiętaj, że klucze są kodowane w formacie base64 za pomocą domyślnych flag.

Kotlin

// base64OfEncodedDecryptionKey is provided through Play Console.
var decryptionKeyBytes: ByteArray =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT)

// Deserialized encryption (symmetric) key.
var decryptionKey: SecretKey = SecretKeySpec(
    decryptionKeyBytes,
    /* offset= */ 0,
    AES_KEY_SIZE_BYTES,
    AES_KEY_TYPE
)

// base64OfEncodedVerificationKey is provided through Play Console.
var encodedVerificationKey: ByteArray =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT)

// Deserialized verification (public) key.
var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE)
    .generatePublic(X509EncodedKeySpec(encodedVerificationKey))

Java

// base64OfEncodedDecryptionKey is provided through Play Console.
byte[] decryptionKeyBytes =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT);

// Deserialized encryption (symmetric) key.
SecretKey decryptionKey =
    new SecretKeySpec(
        decryptionKeyBytes,
        /* offset= */ 0,
        AES_KEY_SIZE_BYTES,
        AES_KEY_TYPE);

// base64OfEncodedVerificationKey is provided through Play Console.
byte[] encodedVerificationKey =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT);
// Deserialized verification (public) key.
PublicKey verificationKey =
    KeyFactory.getInstance(EC_KEY_TYPE)
        .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));

Następnie użyj tych kluczy, aby najpierw odszyfrować token integralności (część JWE), a następnie zweryfikować i wyodrębnić zagnieżdżoną część JWS.

Kotlin

val jwe: JsonWebEncryption =
    JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption
jwe.setKey(decryptionKey)

// This also decrypts the JWE token.
val compactJws: String = jwe.getPayload()

val jws: JsonWebSignature =
    JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature
jws.setKey(verificationKey)

// This also verifies the signature.
val payload: String = jws.getPayload()

Java

JsonWebEncryption jwe =
    (JsonWebEncryption)JsonWebStructure
        .fromCompactSerialization(integrityToken);
jwe.setKey(decryptionKey);

// This also decrypts the JWE token.
String compactJws = jwe.getPayload();

JsonWebSignature jws =
    (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws);
jws.setKey(verificationKey);

// This also verifies the signature.
String payload = jws.getPayload();

Wynikiem jest token w postaci zwykłego tekstu, który zawiera werdykty dotyczące integralności.