אם אתם מתכננים לשלוח רק בקשות 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 | ✔️ | ✔️ |
זמן האחזור האופייני לבקשות פענוח משרת לשרת | עשרות אלפיות שנייה עם זמינות של 99.9% | עשרות אלפיות שנייה עם זמינות של 99.9% |
פענוח ואימות אסימון באופן מקומי בסביבת שרת מאובטחת | ❌ | ✔️ |
פענוח ואימות של אסימון בצד הלקוח | ❌ | ❌ |
עדכניות של תוצאת הבדיקה לגבי תקינות האפליקציה | אחסון במטמון וריענון אוטומטיים מסוימים על ידי Google Play | כל התוצאות מחושבות מחדש בכל בקשה |
מגבלות | ||
בקשות לכל אפליקציה ביום | 10,000 כברירת מחדל (אפשר לבקש הגדלה) | 10,000 כברירת מחדל (אפשר לבקש הגדלה) |
בקשות לכל מכונה של אפליקציה בדקה | התחלות חמות: 5 בכל דקה אסימוני תקינות: אין הגבלה לשימוש ציבורי* |
אסימוני תקינות: 5 לדקה |
הגנה | ||
מניעת פגיעה והתקפות דומות | שימוש בשדה requestHash |
שימוש בשדה nonce עם קישור תוכן על סמך נתוני הבקשה |
צמצום הסיכון להתקפות חוזרות והתקפות דומות | הפחתה אוטומטית על ידי Google Play | שימוש בשדה nonce עם לוגיקה בצד השרת |
* כל הבקשות, כולל בקשות ללא מגבלות ציבוריות, כפופות למגבלות הגנה לא ציבוריות בערכים גבוהים
שליחת בקשות דרך Classic API לעיתים רחוקות
יצירת אסימון תקינות צורכת זמן, נתונים וסוללה, וכל אפליקציה יכולה לשלוח מספר מוגבל של בקשות קלאסיות ביום. לכן, כדאי לשלוח בקשות רגילות כדי לבדוק שהפעולות בעלות הערך הגבוה ביותר או הרגישות ביותר הן אמיתיות רק אם אתם רוצים ערובה נוספת לבקשת אישור רגילה. לא מומלץ לשלוח בקשות קלאסיות לפעולות בתדירות גבוהה או לפעולות בעלות נמוכה. אל תשלחו בקשות קלאסיות בכל פעם שהאפליקציה עוברת לחזית או כל כמה דקות ברקע, ואל תבצעו קריאות ממספר גדול של מכשירים בו-זמנית. יכול להיות שנעצור את הבקשות של אפליקציה ששולחת יותר מדי בקשות קלאסיות, כדי להגן על המשתמשים מפני הטמעות שגויות.
הימנעות משמירת תוצאות בדיקה במטמון
שמירת הכרעה במטמון מגדילה את הסיכון להתקפות כמו זליגת מידע והפעלה חוזרת, שבהן נעשה שימוש חוזר בהחלטה תקינה מסביבה לא מהימנה. אם אתם שוקלים לשלוח בקשה קלאסית ולאחר מכן לשמור אותה במטמון לשימוש מאוחר יותר, מומלץ לבצע במקום זאת בקשה רגילה על פי דרישה. בקשות רגילות כוללות אחסון במטמון במכשיר, אבל Google Play משתמשת בשיטות הגנה נוספות כדי לצמצם את הסיכון להתקפות שחזור ולדליפה.
שימוש בשדה ה-nonce כדי להגן על בקשות קלאסיות
ב-Play Integrity API יש שדה שנקרא nonce
, שאפשר להשתמש בו כדי להגן על האפליקציה מפני תקיפות מסוימות, כמו תקיפות של שחזור ושינוי. Play Integrity API מחזיר את הערך שהגדרתם בשדה הזה, בתוך תגובת האימות החתומה. כדי להגן על האפליקציה מפני התקפות, חשוב לפעול לפי ההנחיות ליצירת ערכים חד-פעמיים.
ניסיון חוזר לשלוח בקשות קלאסיות עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff)
תנאי סביבה, כמו חיבור לאינטרנט לא יציב או מכשיר עמווס, עלולים לגרום לכישלון בבדיקות תקינות המכשיר. כתוצאה מכך, יכול להיות שלא ייוצרו תוויות למכשיר שאמין מבחינה אחרת. כדי לצמצם את התרחישים האלה, מומלץ לכלול אפשרות לניסיון חוזר עם זמן אחזור מעריכי.
סקירה כללית
כשהמשתמש מבצע באפליקציה פעולה בעלת ערך גבוה שאתם רוצים להגן עליה באמצעות בדיקת תקינות, מבצעים את השלבים הבאים:
- הקצה העורפי של האפליקציה בצד השרת יוצר ערך ייחודי ושולח אותו ללוגיקת הצד הלקוח. בשאר השלבים, המערכת תתייחס ללוגיקת הקוד הזו בתור 'האפליקציה'.
- האפליקציה יוצרת את
nonce
מהערך הייחודי ומהתוכן של הפעולה בעלת הערך הגבוה. לאחר מכן, הוא קורא ל-Play Integrity API ומעביר את הערך שלnonce
. - האפליקציה מקבלת מ-Play Integrity API קביעה חתומה ומוצפנת.
- האפליקציה מעבירה את התוצאה החתומה והמוצפנת לקצה העורפי של האפליקציה.
- הקצה העורפי של האפליקציה שולח את התוצאה לשרת של Google Play. השרת של Google Play מפענח ומאמת את התוצאה, ומחזיר את התוצאות לקצה העורפי של האפליקציה.
- הצד העורפי של האפליקציה מחליט איך להמשיך, על סמך האותות שמופיעים בתוכן של האסימון.
- הקצה העורפי של האפליקציה שולח את תוצאות ההחלטות לאפליקציה.
יצירת קוד חד-פעמי (nonce)
כשמגנים על פעולה באפליקציה באמצעות Play Integrity API, אפשר להשתמש בשדה nonce
כדי לצמצם סוגים מסוימים של התקפות, כמו התקפות מניפולציה על ידי גורם צד שלישי (PITM) והתקפות שחזור. Play Integrity API מחזיר את הערך שהגדרתם בשדה הזה בתוך תגובת האימות החתומה.
הפורמט של הערך שמוגדר בשדה nonce
חייב להיות תקין:
String
- כתובת URL בטוחה
- מקודדים כ-Base64 ולא עוברים גיבוב
- 16 תווים לפחות
- עד 500 תווים
ריכזנו כאן כמה דרכים נפוצות לשימוש בשדה nonce
ב-Play Integrity API. כדי לקבל את ההגנה החזקה ביותר מפני nonce
, אפשר לשלב בין השיטות הבאות.
הוספת גיבוב של בקשה להגנה מפני פגיעה
אפשר להשתמש בפרמטר nonce
בבקשת API קלאסית באופן דומה לפרמטר requestHash
בבקשת API רגילה, כדי להגן על תוכן הבקשה מפני פגיעה.
כשמבקשים קביעת תקינות:
- חישוב סיכום של כל הפרמטרים הקריטיים של הבקשה (למשל, SHA256 של שרשור בקשה יציב) מהפעולה של המשתמש או מהבקשה מהשרת שמתרחשת.
- משתמשים ב-
setNonce
כדי להגדיר את השדהnonce
לערך של הסיכום המחושב.
כשמקבלים קביעת תקינות:
- מפענחים ומאמתים את טוקן השלמות, ומקבלים את הסיכום (digest) מהשדה
nonce
. - מחשבים סיכום של הבקשה באותו אופן שבו הוא מחושב באפליקציה (למשל, SHA256 של שרשור בקשה יציב).
- השוואה בין הסיכומים בצד האפליקציה לבין הסיכומים בצד השרת. אם הם לא תואמים, הבקשה לא מהימנה.
הוספת ערכים ייחודיים להגנה מפני התקפות שליחה מחדש
כדי למנוע ממשתמשים זדוניים לעשות שימוש חוזר בתשובות קודמות מ-Play Integrity API, אפשר להשתמש בשדה nonce
כדי לזהות באופן ייחודי כל הודעה.
כשמבקשים קביעת תקינות:
- קבלת ערך ייחודי גלובלי באופן שמשתמשים זדוניים לא יכולים לחזות. לדוגמה, מספר אקראי מאובטח מבחינה קריפטוגרפית שנוצר בצד השרת יכול להיות ערך כזה, או מזהה קיים, כמו מזהה סשן או מזהה עסקה. גרסה פשוטה פחות ומאובטחת פחות היא יצירת מספר אקראי במכשיר. מומלץ ליצור ערכים של 128 ביט או יותר.
- קוראים לפונקציה
setNonce()
כדי להגדיר את השדהnonce
לערך הייחודי משלב 1.
כשמקבלים קביעת תקינות:
- מפענחים ומאמתים את אסימון השלמות, ומקבלים את הערך הייחודי מהשדה
nonce
. - אם הערך משלב 1 נוצר בשרת, צריך לוודא שהערך הייחודי שהתקבל היה אחד מהערכים שנוצרו, ושנעשה בו שימוש בפעם הראשונה (השרת יצטרך לשמור תיעוד של הערכים שנוצרו למשך פרק זמן מתאים). אם הערך הייחודי שהתקבל כבר נמצא בשימוש או לא מופיע ברשומה, דוחים את הבקשה
- אחרת, אם הערך הייחודי נוצר במכשיר, צריך לוודא שהערך שהתקבל משמש בפעם הראשונה (השרת צריך לשמור תיעוד של ערכים שכבר נראו למשך פרק זמן מתאים). אם כבר נעשה שימוש בערך הייחודי שהתקבל, צריך לדחות את הבקשה.
שילוב של הגנות מפני התקפות זיוף והתקפות שחזור (מומלץ)
אפשר להשתמש בשדה nonce
כדי להגן בו-זמנית מפני התקפות מניעת שירות (DoS) ומפני התקפות שחזור. כדי לעשות זאת, יוצרים את הערך הייחודי כפי שמתואר למעלה וכוללים אותו כחלק מהבקשה. לאחר מכן מחשבים את גיבוב הבקשה, תוך הקפדה על הכללת הערך הייחודי כחלק מהגיבוב. כך נראית הטמעה שמשלבת את שתי הגישות:
כשמבקשים קביעת תקינות:
- המשתמש מתחיל את הפעולה בעלת הערך הגבוה.
- מקבלים ערך ייחודי לפעולה הזו כפי שמתואר בקטע הכללת ערכים ייחודיים להגנה מפני התקפות שליחה מחדש.
- מכינים הודעה שרוצים להגן עליה. כוללים את הערך הייחודי משלב 2 בהודעה.
- האפליקציה מחשבת סיכום של ההודעה שהיא רוצה להגן עליה, כפי שמתואר בקטע הוספת גיבוב של בקשה להגנה מפני פגיעה. מכיוון שההודעה מכילה את הערך הייחודי, הערך הייחודי הוא חלק מהגיבוב.
- משתמשים ב-
setNonce()
כדי להגדיר את השדהnonce
ל-digest המחושב מהשלב הקודם.
כשמקבלים קביעת תקינות:
- אחזור הערך הייחודי מהבקשה
- מפענחים ומאמתים את טוקן השלמות, ומקבלים את הסיכום (digest) מהשדה
nonce
. - כפי שמתואר בקטע הוספת גיבוב של בקשה להגנה מפני פגיעה, מחשבים מחדש את הסיכום בצד השרת ובודקים שהוא תואם לסיכום שהתקבל מאסימון השלמות.
- כפי שמתואר בקטע הכללת ערכים ייחודיים להגנה מפני התקפות של השמעה חוזרת, בודקים את התקינות של הערך הייחודי.
בתרשים התהליך הבא מוצגים השלבים האלה עם nonce
בצד השרת:
שליחת בקשה לקבלת פסיקה לגבי תקינות
אחרי שיוצרים nonce
, אפשר לבקש מ-Google Play קביעה לגבי תקינות. כדי לעשות זאת, מבצעים את השלבים הבאים:
- יוצרים
IntegrityManager
, כפי שמתואר בדוגמאות הבאות. - יוצרים
IntegrityTokenRequest
ומספקים אתnonce
באמצעות השיטהsetNonce()
ב-builder המשויך. גם באפליקציות שמופצות אך ורק מחוץ ל-Google Play ובערכות SDK צריך לציין את מספר הפרויקט ב-Google Cloud באמצעות השיטהsetCloudProjectNumber()
. אפליקציות ב-Google Play מקושרות לפרויקט ב-Cloud ב-Play Console, ואין צורך להגדיר את מספר הפרויקט ב-Cloud בבקשה. משתמשים במנהל כדי לקרוא ל-
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());
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); }
מותאמת
/// 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 Web Token (JWT) בתצוגת עץ, כלומר JSON Web Encryption (JWE) של JSON Web Signature (JWS). רכיבי JWE ו-JWS מיוצגים באמצעות סריאליזציה קומפקטית.
יש תמיכה רחבה באלגוריתמים להצפנה ולחתימה בהטמעות שונות של JWT:
פענוח ואימות בשרתים של Google (מומלץ)
Play Integrity API מאפשר לכם לפענח ולאמת את קביעה לגבי תקינות האפליקציה בשרתים של Google, וכך לשפר את האבטחה של האפליקציה. כדי לעשות זאת, צריך לבצע את השלבים הבאים:
- יוצרים חשבון שירות בפרויקט ב-Google Cloud שמקושר לאפליקציה.
בשרת של האפליקציה, שולפים את אסימון הגישה מפרטי הכניסה של חשבון השירות באמצעות ההיקף
playintegrity
, ושולחים את הבקשה הבאה:playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
קוראים את תגובת ה-JSON.
פענוח ואימות באופן מקומי
אם בוחרים לנהל ולהוריד את מפתחות ההצפנה של התשובות, אפשר לפענח ולאמת את האסימון המוחזר בסביבת השרת המאובטחת שלכם.
אפשר לקבל את האסימון המוחזר באמצעות השיטה IntegrityTokenResponse#token()
.
בדוגמה הבאה מוסבר איך לפענח את מפתח ה-AES ואת מפתח ה-EC הציבורי המקודד ב-DER לאימות חתימות מ-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) שמתקבל הוא אסימון בטקסט פשוט שמכיל פסקי דין לגבי תקינות.