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

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

סקירה כללית

תרשים רצף שמציג את העיצוב הכללי של שלמות האפליקציה ב-Play
API

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

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

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

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

בקשה לאסימון תקינות (על פי דרישה):

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

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

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

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

  • כשהאפליקציה מופעלת (למשל, בהפעלה במצב התחלתי (cold start). הכנת ספק האסימונים הוא אסינכרוני ולכן לא ישפיע על שעת ההפעלה. האפשרות הזו פועלות היטב אם אתם מתכננים להגיש בקשה לקביעת תקינות זמן קצר לאחר אפליקציה מופעלת, לדוגמה, כשמשתמש נכנס למשחק או שחקן מצטרף למשחק.
  • כשהאפליקציה נפתחת (כלומר, בהפעלה במצב ביניים [warm start]). עם זאת, שימו לב שכל אפליקציה המכונה יכולה להכין את אסימון התקינות רק עד 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));

אחדות

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

מותאמת

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

אחדות

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

מותאמת

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

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

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

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

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

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

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

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