שליחת בקשה קלאסית ל-API

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

שיקולים

השוואה בין בקשות רגילות לבקשות קלאסיות

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

בטבלה הבאה מפורטים ההבדלים העיקריים בין שני הסוגים של בקשות:

בקשת API רגילה בקשת API קלאסית
דרישות מוקדמות
נדרשת גרסת Android SDK מינימלית Android 5.0 (רמת API 21) ואילך Android 4.4 (רמת API 19) ואילך
הדרישות של Google Play חנות Google Play ו-Google Play Services חנות Google Play ו-Google Play Services
פרטי שילוב
נדרש הכנה של API ✔️ (כמה שניות)
זמן אחזור אופייני של בקשה כמה מאות אלפיות שנייה כמה שניות
תדירות הבקשה הפוטנציאלית לעיתים קרובות (בדיקה על פי דרישה של כל פעולה או בקשה) לעיתים רחוקות (בדיקה חד פעמית של הפעולות עם הערך הגבוה ביותר או רוב הבקשות הרגישות)
חסימות זמניות רוב תגובות החימום נמשכות פחות מ-10 שניות, אבל הן כוללות קריאה לשרת, לכן מומלץ להגדיר זמן קצוב לתפוגה (למשל דקה). בקשות לקבלת החלטות מתבצעות בצד הלקוח רוב הבקשות אורכות פחות מ-10 שניות, אבל הן כוללות קריאה לשרת, לכן מומלץ להגדיר זמן קצוב לתפוגה (למשל דקה)
אסימון קביעת תקינות
מכיל פרטי מכשיר, אפליקציה וחשבון ✔️ ✔️
שמירת אסימון במטמון שמירה במטמון במכשיר על ידי Google Play לא מומלץ
פענוח ואימות של האסימון באמצעות שרת Google Play ✔️ ✔️
זמן אחזור אופייני לפענוח בקשה משרת לשרת 10 אלפיות השנייה עם זמינות של 3 תשיעיות 10 אלפיות השנייה עם זמינות של 3 תשיעיות
פענוח ואימות של האסימון באופן מקומי בסביבת שרת מאובטחת ✔️
פענוח ואימות של האסימון בצד הלקוח
עדכניות של קביעת תקינות תכונות מסוימות של שמירה במטמון ורענון אוטומטיות על ידי Google Play כל הקביעות מחושבים מחדש עבור כל בקשה
מגבלות
בקשות ביום לכל אפליקציה 10,000 כברירת מחדל (אפשר לבקש להגדיל) 10,000 כברירת מחדל (אפשר לבקש להגדיל)
בקשות לכל מופע של אפליקציה בדקה מכשירי חימום: 5 לדקה
אסימוני תקינות: אין מגבלה ציבורית*
אסימוני תקינות: 5 לדקה
הגנה
צמצום הבעיה מפני פריצה ומתקפות דומות שימוש בשדה requestHash שימוש בשדה nonce עם קישור תוכן על סמך נתוני הבקשה
צמצום של ניסיון חוזר והתקפות דומות צמצום אוטומטי של Google Play שימוש בשדה nonce עם לוגיקה בצד השרת

* כל הבקשות, כולל בקשות ללא הגבלות ציבוריות, כפופים למגבלות הגנה לא ציבוריות בערכים גבוהים

שליחת בקשות קלאסיות לעיתים רחוקות

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

הימנעות משמירה במטמון של תוצאות הבדיקה

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

שימוש בשדה צופן חד-פעמי כדי להגן על בקשות קלאסיות

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

ניסיון חוזר של בקשות קלאסיות עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff)

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

סקירה כללית

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

כשהמשתמש מבצע באפליקציה פעולה בעלת ערך גבוה שעליה אתם רוצים להגן כולל בדיקת תקינות, צריך להשלים את השלבים הבאים:

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

יצירת צופן חד-פעמי (nonce)

כשמגינים על פעולה באפליקציה באמצעות Play Integrity API, אפשר להשתמש בשדה nonce כדי לצמצם סוגים מסוימים של מתקפות, כמו התקפות מסוג 'אדם בתווך' (PITM) והתקפות חוזרות ונשנות. ההצגה Integrity API מחזיר את הערך שהגדרתם בשדה הזה בתוך תגובת תקינות.

הערך שמוגדר בשדה nonce צריך להיות בפורמט תקין:

  • String
  • מתאים לכתובות URL
  • מתבצע קידוד כ-Base64 וללא אריזה
  • 16 תווים לפחות
  • עד 500 תווים

בהמשך מפורטות כמה דרכים נפוצות לשימוש בשדה nonce ב-Play Integrity API. כדי לקבל את ההגנה החזקה ביותר מ-nonce, אפשר לשלב יש כמה שיטות.

הוספת גיבוב של הבקשה כדי להגן מפני פגיעה

אפשר להשתמש בפרמטר nonce בבקשת API קלאסית באופן דומה הפרמטר requestHash בבקשת API רגילה כדי להגן על התוכן בקשה נגד פגיעה.

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

  1. מחשבים תקציר של כל הפרמטרים הקריטיים של הבקשה (למשל, SHA256 של בקשה לתצוגה סריאלית) מפעולת המשתמש או בקשת השרת מה קורה.
  2. משתמשים בפונקציה setNonce כדי להגדיר את השדה nonce לערך של התקציר המחושב.

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

  1. מפענחים ומאמתים את אסימון התקינות ומשיגים את התקציר שדה nonce.
  2. מחשבים תקציר של הבקשה באותו אופן כמו באפליקציה (למשל SHA256 לביצוע סריאליזציה של בקשה יציבה).
  3. השוו בין התקצירים בצד האפליקציה ובצד השרת. אם הם לא תואמים, הבקשה לא מהימנה.

צריך לכלול ערכים ייחודיים כדי להגן מפני התקפות שליחה מחדש

כדי למנוע ממשתמשים זדוניים לעשות שימוש חוזר בתגובות קודמות ב-Play Integrity API אפשר להשתמש בשדה nonce כדי לזהות כל אחד מהם באופן ייחודי הודעה.

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

  1. קבלת ערך ייחודי גלובלי באופן שמשתמשים זדוניים לא יכולים לחזות. לדוגמה, מספר אקראי מאובטח מבחינה קריפטוגרפית שנוצר בצד השרת יכול להיות ערך כזה או מזהה קיים, כמו סשן או מזהה עסקה. בגרסה פשוטה יותר ומאובטחת פחות היא ליצור גרסה אקראית המספר במכשיר. מומלץ ליצור ערכים באורך של 128 ביט לפחות.
  2. קוראים לפונקציה setNonce() כדי להגדיר את השדה nonce לערך הייחודי משלב 1.

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

  1. מפענחים ומאמתים את אסימון התקינות ומשיגים את הערך הייחודי שדה nonce.
  2. אם הערך משלב 1 נוצר בשרת, צריך לוודא היה אחד מהערכים שנוצרו, נעשה בו שימוש בפעם הראשונה (השרת שלך יצטרך לשמור תיעוד של למשך זמן מתאים). אם נעשה שימוש בערך הייחודי שהתקבל כבר או לא מופיע ברשומה, דחה את הבקשה
  3. לחלופין, אם הערך הייחודי נוצר במכשיר, עליך לבדוק נעשה שימוש בפעם הראשונה בערך שהתקבל (השרת שלך צריך לשמור רשומה של ערכים שכבר נראו למשך זמן מתאים). אם התקבלו כבר נעשה שימוש בערך ייחודי, יש לדחות את הבקשה.

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

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

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

  1. המשתמש יוזם את הפעולה בעלת הערך הגבוה.
  2. מקבלים ערך ייחודי לפעולה הזו, כפי שמתואר במאמר הכללת ערכים ייחודיים להגנה מפני התקפות שליחה מחדש.
  3. מכינים את ההודעה שרוצים להגן עליה. צריך לכלול את הערך הייחודי משלב 2 בהודעה.
  4. האפליקציה מחשבת תקציר של ההודעה שעליה היא רוצה להגן, למשל שמתואר במאמר הוספת גיבוב של בקשה כדי להגן מפני התעסקות במכשיר. מאחר שההודעה מכילה את הערכים , הערך הייחודי הוא חלק מהגיבוב.
  5. משתמשים בפונקציה setNonce() כדי להגדיר את השדה nonce לתקציר המחושב לשלב הקודם.

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

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

תרשים הרצף הבא מדגים שלבים אלה באמצעות צד השרת nonce:

תרשים רצף שמראה איך להגן מפני פגיעה והפעלה מחדש
מתקפות

בקשה לקביעת תקינות

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

  1. יוצרים IntegrityManager כמו בדוגמאות הבאות.
  2. צריך לבנות IntegrityTokenRequest, שמספק את ה-nonce דרך השיטה setNonce() ב-builder המשויך. אפליקציות שמופצות באופן בלעדי מחוץ ל-Google Play, וערכות SDK צריכות לציין גם את Google Cloud את מספר הפרויקט באמצעות ה-method setCloudProjectNumber(). אפליקציות ב-Google אפליקציות Play מקושרות לפרויקט בענן ב-Play Console ולא צריך אותן מגדירים את מספר הפרויקט ב-Cloud בבקשה.
  3. עליך להשתמש במנהל כדי להתקשר אל requestIntegrityToken() ולספק את 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());

אחדות

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

מותאמת

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

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

כשמבקשים קביעת תקינות, Play Integrity API מספק אסימון התגובה. הnonce שכללת בבקשה הופך לחלק אסימון התגובה.

פורמט האסימון

האסימון הוא אסימון אינטרנט מסוג JSON (JWT) מקונן, היא JSON Web Encryption (JWE) של JSON Web Signature (JWS). רכיבי JWE ו-JWS מיוצגים באמצעות קומפקטי סריאליזציה הקצר הזה. התשובות שלך יעזרו לנו להשתפר.

יש תמיכה טובה באלגוריתמים של ההצפנה או החתימה במגוון שיטות JWT יישומים:

  • JWE משתמש ב-A256KW בשביל alg ו- A256GCM ל-enc

  • JWS משתמש ב-ES256.

פענוח ואימות בשרתים של Google (מומלץ)

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

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

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

פענוח ואימות באופן מקומי

אם בחרתם לנהל ולהוריד את המפתחות שלכם להצפנת תשובות: לפענח ולאמת את האסימון שהוחזר בסביבת השרת המאובטחת שלכם. אפשר לקבל את האסימון שהוחזר באמצעות הפקודה IntegrityTokenResponse#token() .

בדוגמה הבאה אפשר לראות איך לפענח את המפתח של AES והציבור בקידוד DER מפתח EC לאימות חתימה מ-Play Console למפתח ספציפי לשפה (במקרה שלנו, שפת התכנות Java) בקצה העורפי של האפליקציה. הערה שהמקשים בקידוד base64 באמצעות דגלי ברירת מחדל.

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

אחר כך משתמשים במפתחות האלה כדי לפענח קודם את אסימון התקינות (החלק JWE) ואז לאמת ולחלץ את החלק ה-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();

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