שליחת בקשה רגילה ל-API

בדף הזה מוסבר איך שולחים בקשות API רגילות לקבלת קביעות תקינות. הבקשות האלה נתמכות ב-Android 5.0 (רמת API‏ 21) ואילך. אתם יכולים לשלוח בקשת API רגילה לקבלת תוצאת תקינות בכל פעם שהאפליקציה מבצעת קריאה לשרת כדי לבדוק אם האינטראקציה אמיתית.

סקירה כללית

תרשים רצף שבו מוצג העיצוב הכללי של Play Integrity API

בקשה רגילה מורכבת משני חלקים:

  • הכנת ספק האסימון לאימות תקינות (פעולה חד-פעמית): צריך לבצע קריאה ל-Integrity API כדי להכין את ספק האסימון לאימות תקינות הרבה לפני שצריך לקבל את תוצאת הבדיקה. לדוגמה, אפשר לעשות זאת כשהאפליקציה מופעלת או ברקע לפני שצריך את ההחלטה לגבי תקינות האפליקציה.
  • בקשה לאסימון תקינות (על פי דרישה): בכל פעם שהאפליקציה שולחת בקשה לשרת שאתם רוצים לוודא שהיא אמיתית, אתם מבקשים אסימון תקינות ושולחים אותו לשרת הקצה העורפי של האפליקציה לצורך פענוח ואימות. לאחר מכן, השרת העורפי יכול להחליט איך לפעול.

הכנת ספק אסימון השלמות (פעולה חד-פעמית):

  1. האפליקציה קוראת לספק אסימון השלמות עם מספר הפרויקט ב-Google Cloud.
  2. האפליקציה שומרת את ספק אסימון השלמות בזיכרון לצורך קריאות נוספות לבדיקת אימות.

שליחת בקשה לטוקן תקינות (על פי דרישה):

  1. כדי להגן על פעולת המשתמש, האפליקציה מחשבת את הגיבוב (hash) של הבקשה שרוצים לשלוח (באמצעות אלגוריתם גיבוב מתאים, כמו SHA256).
  2. האפליקציה מבקשת אסימון תקינות ומעבירה את גיבוב הבקשה.
  3. האפליקציה מקבלת את אסימון השלמות החתום והמוצפן מ-Play Integrity API.
  4. האפליקציה מעבירה את אסימון השלמות לקצה העורפי של האפליקציה.
  5. הקצה העורפי של האפליקציה שולח את האסימון לשרת של Google Play. שרת Google Play מפענח ומאמת את התוצאה, ומחזיר את התוצאות לקצה העורפי של האפליקציה.
  6. הצד העורפי של האפליקציה מחליט איך להמשיך, על סמך האותות שמופיעים במטען הייעודי של האסימון.
  7. הקצה העורפי של האפליקציה שולח את תוצאות ההחלטות לאפליקציה.

הכנת ספק אסימון השלמות (פעולה חד-פעמית)

לפני ששולחים בקשה רגילה לקבלת קביעה לגבי תקינות מ-Google Play, צריך להכין (או "להפעיל") את ספק אסימון התקינות. כך Google Play יכולה לשמור במטמון באופן חכם מידע חלקי על האימות במכשיר, כדי לקצר את זמן האחזור בנתיב הקריטי כששולחים בקשה להצהרת תקינות. הכנת ספק הטוקנים מחדש היא דרך לחזור על בדיקות תקינות פחות כבדות במשאבים, וכך לקבל את ההחלטה הבאה לגבי התקינות עדכנית יותר.

אפשר להכין את ספק אסימון השלמות:

  • כשהאפליקציה מופעלת (כלומר, בהפעלה במצב התחלתי). ההכנה של ספק האסימונים היא אסינכרונית, ולכן לא תשפיע על זמן ההפעלה. האפשרות הזו מתאימה אם אתם מתכננים לשלוח בקשה לקביעת התקינות זמן קצר אחרי השקת האפליקציה, למשל כשמשתמש נכנס לחשבון או כששחקן מצטרף למשחק.
  • כשהאפליקציה נפתחת (כלומר בהפעלה מוכנה). עם זאת, חשוב לזכור שכל מכונה של האפליקציה יכולה להכין את אסימון השלמות רק עד 5 פעמים בדקה.
  • בכל שלב ברקע, כשרוצים להכין את האסימון מראש לפני שליחת בקשה לקבלת תוצאת תקינות.

כדי להכין את הספק של אסימון השלמות:

  1. יוצרים StandardIntegrityManager, כפי שמוצג בדוגמאות הבאות.
  2. יוצרים PrepareIntegrityTokenRequest ומספקים את מספר הפרויקט ב-Google Cloud באמצעות השיטה setCloudProjectNumber().
  3. משתמשים במנהל כדי לקרוא ל-prepareIntegrityToken(), ומספקים את הערך של PrepareIntegrityTokenRequest.

Java

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

// Create an instance of a manager.
StandardIntegrityManager standardIntegrityManager =
    IntegrityManagerFactory.createStandard(applicationContext);

StandardIntegrityTokenProvider integrityTokenProvider;
long cloudProjectNumber = ...;

// Prepare integrity token. Can be called once in a while to keep internal
// state fresh.
standardIntegrityManager.prepareIntegrityToken(
    PrepareIntegrityTokenRequest.builder()
        .setCloudProjectNumber(cloudProjectNumber)
        .build())
    .addOnSuccessListener(tokenProvider -> {
        integrityTokenProvider = tokenProvider;
    })
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator PrepareIntegrityTokenCoroutine() {
    long cloudProjectNumber = ...;

    // Create an instance of a standard integrity manager.
    var standardIntegrityManager = new StandardIntegrityManager();

    // Request the token provider.
    var integrityTokenProviderOperation =
      standardIntegrityManager.PrepareIntegrityToken(
        new PrepareIntegrityTokenRequest(cloudProjectNumber));

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

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

    // Get the response.
    var integrityTokenProvider = integrityTokenProviderOperation.GetResult();
}

Unreal Engine

// .h
void MyClass::OnPrepareIntegrityTokenCompleted(
  EStandardIntegrityErrorCode ErrorCode,
  UStandardIntegrityTokenProvider* Provider)
{
  // Check the resulting error code.
  if (ErrorCode == EStandardIntegrityErrorCode::StandardIntegrity_NO_ERROR)
  {
    // ...
  }
}

// .cpp
void MyClass::PrepareIntegrityToken()
{
  int64 CloudProjectNumber = ...

  // Create the Integrity Token Request.
  FPrepareIntegrityTokenRequest Request = { CloudProjectNumber };

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

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

  // Initiate the prepare integrity token operation, passing the delegate to handle the result.
  GetGameInstance()
    ->GetSubsystem<UStandardIntegrityManager>()
    ->PrepareIntegrityToken(Request, Delegate);
}

מותאמת

/// Initialize StandardIntegrityManager
StandardIntegrityManager_init(/* app's java vm */, /* an android context */);
/// Create a PrepareIntegrityTokenRequest opaque object.
int64_t cloudProjectNumber = ...;
PrepareIntegrityTokenRequest* tokenProviderRequest;
PrepareIntegrityTokenRequest_create(&tokenProviderRequest);
PrepareIntegrityTokenRequest_setCloudProjectNumber(tokenProviderRequest, cloudProjectNumber);

/// Prepare a StandardIntegrityTokenProvider opaque type pointer and call
/// StandardIntegrityManager_prepareIntegrityToken().
StandardIntegrityTokenProvider* tokenProvider;
StandardIntegrityErrorCode error_code =
        StandardIntegrityManager_prepareIntegrityToken(tokenProviderRequest, &tokenProvider);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_provider_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_getStatus(tokenProvider, &token_provider_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_provider_status == INTEGRITY_RESPONSE_COMPLETED)
{
    /// continue to request token from the token provider
}
/// ...
/// Remember to free up resources.
PrepareIntegrityTokenRequest_destroy(tokenProviderRequest);

הגנה על הבקשות מפני פגיעה (מומלץ)

כשבודקים פעולת משתמש באפליקציה באמצעות Play Integrity API, אפשר להשתמש בשדה requestHash כדי למנוע התקפות מניפולציה. לדוגמה, יכול להיות שמשחק ירצה לדווח על הציון של השחקן לשרת הקצה העורפי של המשחק, והשרת שלכם רוצה לוודא ששרת proxy לא שינה את הציון הזה. ‏Play Integrity API מחזיר את הערך שהגדרתם בשדה requestHash, בתוך התגובה החתומה על תקינות. בלי requestHash, אסימון השלמות יהיה קשור רק למכשיר, אבל לא לבקשה הספציפית, מה שעלול לאפשר התקפה. בהוראות הבאות מוסבר איך להשתמש בשדה requestHash בצורה יעילה:

כשמבקשים קביעת תקינות:

  • חישוב סיכום של כל הפרמטרים הרלוונטיים של הבקשה (למשל, SHA256 של שרשור בקשה יציב) מהפעולה של המשתמש או מהבקשה מהשרת שמתרחשת. האורך המקסימלי של הערך שמוגדר בשדה requestHash הוא 500 בייטים. צריך לכלול ב-requestHash את כל נתוני הבקשות מהאפליקציה שחשובים או רלוונטיים לפעולה שאתם בודקים או מגינים עליה. השדה requestHash נכלל באסימון תקינות כפי שהוא, כך שערכי שדה ארוכים עלולים להגדיל את גודל הבקשה.
  • מספקים את הסיכום כשדה requestHash ל-Play Integrity API ומקבלים את אסימון ההתקינות.

כשמקבלים קביעת תקינות:

  • מפענחים את אסימון השלמות ומחליצים את השדה requestHash.
  • מחשבים סיכום של הבקשה באותו אופן שבו הוא מחושב באפליקציה (למשל, SHA256 של שרשור בקשה יציב).
  • השוואה בין הסיכומים בצד האפליקציה לבין הסיכומים בצד השרת. אם הם לא תואמים, הבקשה לא מהימנה.

שליחת בקשה לקביעת תקינות (על פי דרישה)

אחרי שתסיימו להכין את ספק האסימונים לאימות תקינות, תוכלו להתחיל לבקש קבעות של תקינות מ-Google Play. כדי לעשות זאת, מבצעים את השלבים הבאים:

  1. קבלת StandardIntegrityTokenProvider
  2. יוצרים StandardIntegrityTokenRequest ומספקים את גיבוב הבקשה של פעולת המשתמש שרוצים להגן עליה באמצעות השיטה setRequestHash.
  3. משתמשים בספק של אסימון השלמות כדי לקרוא ל-request(), ומספקים את הערך של StandardIntegrityTokenRequest.

Java

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

StandardIntegrityTokenProvider integrityTokenProvider;

// See above how to prepare integrityTokenProvider.

// Request integrity token by providing a user action request hash. Can be called
// several times for different user actions.
String requestHash = "2cp24z...";
Task<StandardIntegrityToken> integrityTokenResponse =
    integrityTokenProvider.request(
        StandardIntegrityTokenRequest.builder()
            .setRequestHash(requestHash)
            .build());
integrityTokenResponse
    .addOnSuccessListener(response -> sendToServer(response.token()))
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    StandardIntegrityTokenProvider integrityTokenProvider;

    // See above how to prepare integrityTokenProvider.

    // Request integrity token by providing a user action request hash. Can be called
    // several times for different user actions.
    String requestHash = "2cp24z...";
    var integrityTokenOperation = integrityTokenProvider.Request(
      new StandardIntegrityTokenRequest(requestHash)
    );

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

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

    // Get the response.
    var integrityToken = integrityTokenOperation.GetResult();
}

Unreal Engine

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

// .cpp
void MyClass::RequestIntegrityToken()
{
  UStandardIntegrityTokenProvider* Provider = ...

  // Prepare the UStandardIntegrityTokenProvider.

  // Request integrity token by providing a user action request hash. Can be called
  // several times for different user actions.
  FString RequestHash = ...;
  FStandardIntegrityTokenRequest Request = { RequestHash };

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

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

  // Initiate the standard integrity token request, passing the delegate to handle the result.
  Provider->Request(Request, Delegate);
}

מותאמת

/// Create a StandardIntegrityTokenRequest opaque object.
const char* requestHash = ...;
StandardIntegrityTokenRequest* tokenRequest;
StandardIntegrityTokenRequest_create(&tokenRequest);
StandardIntegrityTokenRequest_setRequestHash(tokenRequest, requestHash);

/// Prepare a StandardIntegrityToken opaque type pointer and call
/// StandardIntegrityTokenProvider_request(). Can be called several times for
/// different user actions. See above how to prepare token provider.
StandardIntegrityToken* token;
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_request(tokenProvider, tokenRequest, &token);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityToken_getStatus(token, &token_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrityToken = StandardIntegrityToken_getToken(token);
}
/// ...
/// Remember to free up resources.
StandardIntegrityTokenRequest_destroy(tokenRequest);
StandardIntegrityToken_destroy(token);
StandardIntegrityTokenProvider_destroy(tokenProvider);
StandardIntegrityManager_destroy();

אם האפליקציה משתמשת באותו ספק אסימונים במשך זמן רב מדי, יכול להיות שתוקף הספק יפוג, וכתוצאה מכך תופיע השגיאה INTEGRITY_TOKEN_PROVIDER_INVALID בבקשת האסימון הבאה. כדי לטפל בשגיאה הזו, צריך לבקש ספק חדש.

פענוח ואימות של קביעת התקינות

אחרי ששולחים בקשה לקביעת תקינות, Play Integrity API מספק טוקן תגובה מוצפן. כדי לקבל את קביעות התקינות של המכשיר, צריך לפענח את אסימון התקינות בשרתים של Google. כדי לעשות זאת, מבצעים את השלבים הבאים:

  1. יוצרים חשבון שירות בפרויקט ב-Google Cloud שמקושר לאפליקציה.
  2. בשרת של האפליקציה, מאחזרים את אסימון הגישה מפרטי הכניסה של חשבון השירות באמצעות ההיקף playintegrity, ושולחים את הבקשה הבאה:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. קוראים את תגובת ה-JSON.

המטען הייעודי (Payload) שמתקבל הוא אסימון בטקסט פשוט שמכיל פסקי דין לגבי תקינות.

הגנה אוטומטית מפני הפעלה חוזרת

כדי למנוע התקפות של שחזור (replay), Google Play מונעת באופן אוטומטי שימוש חוזר בכרטיסי תקינות פעמים רבות. ניסיון לפענח שוב ושוב את אותו אסימון יוביל לתוצאות נקיות באופן הבא:

  • התוצאה של זיהוי המכשיר תהיה ריקה.
  • התוצאה של זיהוי האפליקציה והתוצאה של רישוי האפליקציה יוגדרו לערך UNEVALUATED.
  • כל אחד מהפסקאות האפשריים שמופעלים באמצעות Play Console יוגדר כ-UNEVALUATED (או כפסק דין ריק אם מדובר בפסק דין עם כמה ערכים).