Jeśli planujesz wysyłać tylko standardowe żądania API, które są odpowiednie dla większości deweloperów, możesz przejść do sekcji wyniki weryfikacji. Na tej stronie opisujemy wysyłanie klasycznych żądań interfejsu API dotyczących wyników weryfikacji integralności, które są obsługiwane na Androidzie 4.4 (poziom interfejsu API 19) i nowszych wersjach.
co należy wziąć pod uwagę
Porównanie żądań standardowych i klasycznych
W zależności od potrzeb aplikacji w zakresie bezpieczeństwa i ochrony przed nadużyciami możesz wysyłać żądania standardowe, klasyczne lub ich kombinację. Żądania standardowe są odpowiednie dla wszystkich aplikacji i gier. Można ich używać do sprawdzania, czy każde działanie lub wywołanie serwera jest autentyczne, a jednocześnie przekazywać Google Play część ochrony przed możliwością ponownego odtworzenia i ekstracją. Wysyłanie klasycznych żądań jest droższe, a Ty ponosisz odpowiedzialność za ich prawidłowe wdrożenie, aby chronić się przed eksfiltracją i niektórymi rodzajami ataków. Żądania klasyczne powinny być wysyłane rzadziej niż żądania standardowe, np. jednorazowo w celu sprawdzenia, czy bardzo wartościowe lub poufne działanie jest autentyczne.
W tej tabeli opisano najważniejsze różnice między tymi 2 rodzajami żądań:
Standardowe żądanie do interfejsu API | Żądanie do klasycznego interfejsu API | |
---|---|---|
Wymagania wstępne | ||
Wymagana minimalna wersja pakietu Android SDK | Android 5.0 (poziom 21 interfejsu API) lub nowszy | Android 4.4 (poziom 19 interfejsu API) lub nowszy |
Wymagania Google Play | Sklep Google Play i Usługi Google Play | Sklep Google Play i Usługi Google Play |
Szczegóły integracji | ||
Wymagane rozgrzanie interfejsu API | ✔️ (kilka sekund) | ❌ |
Typowy czas oczekiwania na żądanie | kilkuset milisekund, | kilka sekund, |
Potencjalna częstotliwość żądań | Częste (sprawdzanie na żądanie w przypadku każdej czynności lub prośby) | Rzadko (jednorazowe sprawdzanie w przypadku działań o najwyższej wartości lub najbardziej wrażliwych żądań) |
tymczasowe zawieszenia użytkowników | Większość rozgrzewek trwa mniej niż 10 sekund, ale wymaga wywołania serwera, dlatego zalecany jest długi limit czasu (np. 1 minuta). Żądania dotyczące decyzji są wysyłane po stronie klienta | Większość żądań trwa mniej niż 10 sekund, ale obejmuje wywołanie serwera, więc zalecany jest długi limit czasu (np.1 minuta). |
Token oceny integralności | ||
Zawiera szczegóły dotyczące urządzenia, aplikacji i konta | ✔️ | ✔️ |
Buforowanie tokenów | Chronione buforowanie na urządzeniu przez Google Play | Niezalecane |
Odszyfrowywanie i weryfikowanie tokena za pomocą serwera Google Play | ✔️ | ✔️ |
Typowe opóźnienie żądania odszyfrowania między serwerami | dziesiątki milisekund z dostępnością na poziomie 99,9%, | dziesiątki milisekund z dostępnością na poziomie 99,9%, |
odszyfrowywać i weryfikować token lokalnie w bezpiecznym środowisku serwera, | ❌ | ✔️ |
Odszyfrowywanie i weryfikowanie tokena po stronie klienta | ❌ | ❌ |
Częstotliwość aktualizacji oceny integralności | Automatyczne buforowanie i odświeżanie przez Google Play | Wszystkie wyniki są ponownie obliczane przy każdym żądaniu |
Limity | ||
Żądania z aplikacji dziennie | Domyślnie 10 tys. (można poprosić o zwiększenie) | Domyślnie 10 tys. (można poprosić o zwiększenie) |
Żądania na instancję aplikacji na minutę | Rozgrzewki: 5 na minutę Tokeny integralności: brak limitu publicznego* |
Tokeny integralności: 5 na minutę |
Ochrona | ||
Ochrona przed manipulacjami i podobnymi atakami | Użyj pola requestHash . |
Używanie pola nonce z powiązaniem treści na podstawie danych z żądania |
Ochrona przed atakami typu replay i podobnymi | Automatyczne działania łagodzące w Google Play | Używanie pola nonce z logiką po stronie serwera |
* Wszystkie żądania, w tym te bez publicznych limitów, podlegają niepublicznym limitom ochronnym przy wysokich wartościach.
Rzadkie wysyłanie żądań klasycznych
Generowanie tokena integralności wymaga czasu, danych i baterii, a każda aplikacja ma maksymalną liczbę żądań klasycznych, które może wysyłać dziennie. Dlatego klasyczne żądania należy wysyłać tylko wtedy, gdy chcesz mieć dodatkową gwarancję w porównaniu ze standardowym żądaniem, aby sprawdzić, czy działania o najwyższej wartości lub najbardziej wrażliwe są autentyczne. Nie należy wysyłać klasycznych żądań 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ń w tym samym czasie. Aplikacja, która wysyła zbyt wiele wywołań klasycznych żądań, może zostać ograniczona, aby chronić użytkowników przed nieprawidłową implementacją.
Unikanie buforowania wyników
Buforowanie oceny zwiększa ryzyko ataków, takich jak eksfiltracja i powtórzenie, w których dobra ocena jest ponownie wykorzystywana w niezaufanym środowisku. Jeśli rozważasz wysłanie klasycznego żądania, a następnie zapisanie go w pamięci podręcznej do późniejszego wykorzystania, zalecamy zamiast tego wysłanie standardowego żądania na żądanie. Standardowe żądania obejmują buforowanie na urządzeniu, ale Google Play stosuje dodatkowe techniki ochrony w celu ograniczenia ryzyka ataków typu replay i ekstracji.
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 niektórymi atakami, np. atakami typu replay i atakami polegającymi na manipulowaniu danymi. Interfejs Play Integrity API zwraca wartość ustawioną w tym polu w podpisanej odpowiedzi dotyczącej integralności. Aby chronić aplikację przed atakami, postępuj zgodnie z instrukcjami generowania wartości nonce.
Ponawianie klasycznych żądań ze wzrastającym czasem do ponowienia
Warunki środowiskowe, takie jak niestabilne połączenie z internetem lub przeciążone urządzenie, mogą spowodować niepowodzenie testów integralności urządzenia. Może to spowodować, że urządzenie, które w innych okolicznościach byłoby uznane za godne zaufania, nie będzie miało przypisanych etykiet. Aby uniknąć takich sytuacji, uwzględnij opcję ponowienia próby ze wzrastającym czasem do ponowienia.
Omówienie
Gdy użytkownik wykona w aplikacji działanie o wysokiej wartości, które chcesz chronić za pomocą sprawdzania integralności, wykonaj te czynności:
- Backend po stronie serwera aplikacji generuje i wysyła unikalną wartość do logiki po stronie klienta. W dalszej części tego artykułu będziemy odnosić się do tej logiki jako do „aplikacji”.
- Aplikacja tworzy
nonce
na podstawie unikalnej wartości i treści działania o wysokiej wartości. Następnie wywołuje interfejs Play Integrity API, przekazując do niego wartośćnonce
. - Aplikacja otrzymuje podpisaną i zaszyfrowaną ocenę z interfejsu Play Integrity API.
- Aplikacja przekazuje podpisany i zaszyfrowany wynik do backendu.
- Backend aplikacji wysyła wynik do serwera Google Play. Serwer Google Play odszyfrowuje i weryfikuje wynik, a następnie zwraca go do backendu aplikacji.
- Backend aplikacji określa sposób postępowania na podstawie sygnałów zawartych w ładunku tokena.
- Backend aplikacji przesyła wyniki decyzji do aplikacji.
Generowanie wartości nonce
Gdy chronisz działanie w aplikacji za pomocą interfejsu Play Integrity API, możesz wykorzystać pole nonce
, aby ograniczyć pewne rodzaje ataków, np. ataki typu „man-in-the-middle” (MITM) i ataki powtórzeniowe. Interfejs Play Integrity API zwraca wartość ustawioną w tym polu w podpisanej odpowiedzi dotyczącej integralności.
Wartość ustawiona w polu nonce
musi być poprawnie sformatowana:
String
- Bezpieczny w adresie URL
- Zakodowane w formacie Base64 i bez zawijania
- Minimum 16 znaków
- Może mieć maksymalnie 500 znaków.
Oto kilka typowych sposobów używania pola nonce
w interfejsie Play Integrity API. Aby uzyskać najsilniejszą ochronę przed nonce
, możesz połączyć ze sobą poniższe metody.
Dołączanie skrótu żądania w celu ochrony przed manipulacją
W żądaniu do klasycznego interfejsu API możesz użyć parametru nonce
podobnie jak parametru requestHash
w żądaniu do standardowego interfejsu API, aby chronić zawartość żądania przed manipulacją.
Gdy poprosisz o ocenę integralności:
- Obliczanie skrótu wszystkich krytycznych parametrów żądania (np. SHA256 stabilnej serializacji żądania) na podstawie działania użytkownika lub żądania serwera.
- Użyj
setNonce
, aby ustawić polenonce
na wartość obliczonego skrótu.
Gdy otrzymasz ocenę integralności:
- Zdekoduj i zweryfikuj token integralności, a następnie pobierz skrót z pola
nonce
. - Oblicz skrót żądania w taki sam sposób jak w aplikacji (np. SHA256 stabilnej serializacji żądania).
- Porównaj skróty po stronie aplikacji i po stronie serwera. Jeśli nie pasują, żądanie nie jest wiarygodne.
Uwzględniaj unikalne wartości, aby chronić się przed atakami typu replay
Aby uniemożliwić złośliwym użytkownikom ponowne wykorzystanie poprzednich odpowiedzi z interfejsu Play Integrity API, możesz użyć pola nonce
, aby jednoznacznie identyfikować każdą wiadomość.
Gdy poprosisz o ocenę integralności:
- Uzyskaj unikalną wartość globalną w sposób, którego złośliwi użytkownicy nie mogą przewidzieć. Może to być np. wygenerowana po stronie serwera bezpieczna kryptograficznie liczba losowa lub istniejący już identyfikator, np. identyfikator sesji lub transakcji. Prostszym i mniej bezpiecznym wariantem jest wygenerowanie na urządzeniu losowej liczby. Zalecamy tworzenie wartości o długości co najmniej 128 bitów.
- Wywołaj
setNonce()
, aby ustawić w polunonce
unikalną wartość z kroku 1.
Gdy otrzymasz ocenę integralności:
- Zdekoduj i zweryfikuj token integralności oraz uzyskaj unikalną wartość z pola
nonce
. - Jeśli wartość z kroku 1 została wygenerowana na serwerze, sprawdź, czy otrzymana unikalna wartość jest jedną z wygenerowanych wartości i czy jest używana po raz pierwszy (serwer musi przechowywać rekord wygenerowanych wartości przez odpowiedni czas). Jeśli otrzymana unikalna wartość została już użyta lub nie występuje w rekordzie, odrzuć żądanie.
- W przeciwnym razie, jeśli unikalna wartość została wygenerowana na urządzeniu, sprawdź, czy otrzymana wartość jest używana po raz pierwszy (serwer musi przechowywać zapis już widzianych wartości przez odpowiedni czas). Jeśli otrzymana unikalna wartość została już użyta, odrzuć żądanie.
Łączenie ochrony przed manipulacją i atakami typu replay (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 żądaniu. Następnie oblicz hash żądania, pamiętając, aby uwzględnić w nim unikalną wartość. Implementacja łącząca oba podejścia wygląda tak:
Gdy poprosisz o ocenę integralności:
- Użytkownik inicjuje działanie o wysokiej wartości.
- Uzyskaj unikalną wartość tego działania zgodnie z opisem w sekcji Dołączanie unikalnych wartości w celu ochrony przed atakami typu replay.
- Przygotuj wiadomość, którą chcesz chronić. Wiadomość musi zawierać unikalną wartość z kroku 2.
- Aplikacja oblicza skrót wiadomości, którą chce chronić, zgodnie z opisem w sekcji Dołączanie skrótu żądania w celu ochrony przed nieuprawnionymi modyfikacjami. Ponieważ wiadomość zawiera unikalną wartość, jest ona częścią haszu.
- Użyj
setNonce()
, aby ustawić polenonce
na obliczony skrót z poprzedniego kroku.
Gdy otrzymasz ocenę integralności:
- Pobierz unikalną wartość z żądania
- Zdekoduj i zweryfikuj token integralności, a następnie pobierz skrót z pola
nonce
. - Zgodnie z opisem w sekcji Dołączanie skrótu żądania w celu ochrony przed nieuprawnionymi modyfikacjami ponownie oblicz skrót po stronie serwera i sprawdź, czy jest zgodny ze skrótem uzyskanym z tokena integralności.
- Zgodnie z opisem w sekcji Uwzględnij unikalne wartości, aby chronić się przed atakami typu replay sprawdź ważność unikalnej wartości.
Poniższy diagram sekwencji ilustruje te kroki w przypadku tagowania po stronie serweranonce
:
Wysyłanie prośby o ocenę integralności
Po wygenerowaniu nonce
możesz poprosić Google Play o ocenę integralności. Aby to zrobić, wykonaj te czynności:
- Utwórz
IntegrityManager
, jak pokazano w przykładach poniżej. - Utwórz
IntegrityTokenRequest
, podającnonce
za pomocą metodysetNonce()
w powiązanym kreatorze. Aplikacje rozpowszechniane wyłącznie poza Google Play i pakiety SDK również muszą podać numer projektu Google Cloud za pomocą metodysetCloudProjectNumber()
. Aplikacje w Google Play są połączone z projektem w Google Cloud w Konsoli Play i nie muszą ustawiać numeru projektu w Google Cloud w żądaniu. Użyj menedżera, aby wywołać
requestIntegrityToken()
, podającIntegrityTokenRequest
.nonce
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();
Odszyfrowywanie i weryfikowanie oceny integralności
Gdy wysyłasz żądanie oceny integralności, interfejs Play Integrity API zwraca podpisany token odpowiedzi. nonce
, które umieścisz w żądaniu, staną się częścią tokena odpowiedzi.
Format tokena
Token jest zagnieżdż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 algorytmu A256KW w przypadku pola alg i algorytmu A256GCM w przypadku pola enc.
JWS używa ES256.
Odszyfrowywanie i weryfikowanie 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ć, wykonaj te czynności:
- Utwórz konto usługi w projekcie Google Cloud połączonym z Twoją aplikacją.
Na serwerze aplikacji pobierz token dostępu z danych logowania konta usługi za pomocą zakresu
playintegrity
i wyślij to żądanie:playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
Odczytaj odpowiedź JSON.
Odszyfrowywanie i weryfikowanie lokalnie
Jeśli zdecydujesz się zarządzać kluczami szyfrowania odpowiedzi i je pobierać, możesz odszyfrować i zweryfikować zwrócony token we własnym bezpiecznym środowisku serwera.
Zwrócony token możesz uzyskać za pomocą metody IntegrityTokenResponse#token()
.
Poniższy przykład pokazuje, jak zdekodować klucz AES i publiczny klucz EC zakodowany w formacie DER na potrzeby weryfikacji podpisu z Konsoli Play na klucze w określonym języku (w tym przypadku w języku programowania Java) w backendzie aplikacji. Pamiętaj, że klucze są kodowane w formacie Base64 przy użyciu flag domyślnych.
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 potem 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();
Wynikowy ładunek to token w formacie zwykłego tekstu, który zawiera wyniki weryfikacji integralności.
Rozwiązywanie problemów z werdyktem za pomocą komunikatu w Google Play (opcjonalnie)
Gdy serwer otrzyma ocenę integralności, może określić, jak postępować dalej. Jeśli wynik wskazuje na problem, np. brak licencji aplikacji, jej zmodyfikowanie lub naruszenie bezpieczeństwa urządzenia, możesz dać użytkownikom szansę na samodzielne rozwiązanie problemu.
Interfejs Play Integrity API umożliwia wyświetlanie okna Google Play, które zachęca użytkownika do podjęcia działania, np. pobrania oficjalnej wersji aplikacji z Google Play.
Aby dowiedzieć się, jak wywoływać te okna w aplikacji na podstawie odpowiedzi serwera, przeczytaj artykuł Okna naprawcze.