הצפנה של נתוני תשלומים למוֹכרים

‫Google Pay API מחזיר אמצעי תשלום ב-payload חתום ומוצפן של PaymentMethodToken. אמצעי התשלום שמוחזרים הם כרטיסים שמכילים מספר חשבון ראשי (PAN), או כרטיסים שעברו טוקניזציה ומכילים מספר חשבון ראשי (PAN) של המכשיר וקריפטוגרמות.

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

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

המדריך הזה רלוונטי רק ל-protocolVersion = ECv2.

מכיוון שאתם מקבלים את פרטי כרטיס התשלום ישירות, לפני שאתם ממשיכים, ודאו שהאפליקציה שלכם תואמת לתקן PCI DSS ושלשרתים שלכם יש את התשתית הנדרשת כדי לטפל בצורה מאובטחת בפרטי התשלום של המשתמש.

השלבים הבאים מתארים מה צריך לעשות כדי להשתמש במטען הייעודי (payload) של Google Pay API ECv2 PaymentMethodToken:

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

הקוד לדוגמה בספריית Tink מבצע את שלבים 1 עד 6.

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

ההודעה שמוחזרת על ידי Google בתגובה של PaymentData היא אובייקט JSON שעבר סריאליזציה וקודד ב-UTF-8, עם המפתחות שמפורטים בטבלה הבאה:

שם סוג תיאור
protocolVersion מחרוזת מזהה את סכימת ההצפנה או החתימה שבהן ההודעה נוצרה. כך הפרוטוקול יכול להתפתח עם הזמן, אם יש צורך.
signature מחרוזת מאמת שההודעה הגיעה מ-Google. הוא מקודד ב-Base64 ונוצר באמצעות ECDSA על ידי מפתח החתימה המתווך.
intermediateSigningKey אובייקט אובייקט JSON שמכיל את מפתח החתימה הזמני מ-Google. הוא מכיל את signedKey עם keyValue, keyExpiration ו-signatures. הוא עובר סריאליזציה כדי לפשט את תהליך האימות של החתימה על מפתח החתימה הזמני.
signedMessage מחרוזת אובייקט JSON שעבר סריאליזציה כמחרוזת בטוחה ל-HTML, שמכילה את encryptedMessage, ephemeralPublicKey ו-tag. הוא עובר סריאליזציה כדי לפשט את תהליך אימות החתימה.

דוגמה

התגובה הבאה היא אסימון של אמצעי תשלום ב-JSON:

{
  "protocolVersion":"ECv2",
  "signature":"MEQCIH6Q4OwQ0jAceFEkGF0JID6sJNXxOEi4r+mA7biRxqBQAiAondqoUpU/bdsrAOpZIsrHQS9nwiiNwOrr24RyPeHA0Q\u003d\u003d",
  "intermediateSigningKey":{
    "signedKey": "{\"keyExpiration\":\"1542323393147\",\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\"}",
    "signatures": ["MEYCIQCO2EIi48s8VTH+ilMEpoXLFfkxAwHjfPSCVED/QDSHmQIhALLJmrUlNAY8hDQRV/y1iKZGsWpeNmIP+z+tCQHQxP0v"]
  },
  "signedMessage":"{\"tag\":\"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\\u003d\",\"ephemeralPublicKey\":\"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\\u003d\",\"encryptedMessage\":\"mKOoXwi8OavZ\"}"
}

מפתח חתימה ביניים

הערך intermediateSigningKey הוא אובייקט JSON שעבר סריאליזציה בקידוד UTF-8, שמכיל את הערכים הבאים:

שם סוג תיאור
signedKey מחרוזת הודעה בקידוד Base64 שמכילה את תיאור התשלום של המפתח.
signatures מחרוזת מוודא שמפתח החתימה המתווך הגיע מ-Google. הוא מקודד ב-Base64 ונוצר באמצעות ECDSA.

מפתח חתום

השדה signedKey הוא אובייקט JSON שעבר סריאליזציה בקידוד UTF-8, ומכיל את הערכים הבאים:

שם סוג תיאור
keyValue מחרוזת גרסת Base64 של מפתח בקידוד מסוג ASN.1. ‫SubjectPublicKeyInfo מוגדר בתקן X.509.
keyExpiration מחרוזת התאריך והשעה שבהם פג התוקף של מפתח הביניים, באלפיות השנייה לפי שעון UTC מאז ראשית זמן יוניקס (Unix epoch). האינטגרטורים דוחים כל מפתח שפג תוקפו.

הודעה חתומה

השדה signedMessage הוא אובייקט JSON שעבר סריאליזציה בקידוד UTF-8, ומכיל את הערכים הבאים:

שם סוג תיאור
encryptedMessage מחרוזת הודעה מוצפנת בקידוד base64 שמכילה את פרטי התשלום וכמה שדות אבטחה נוספים.
ephemeralPublicKey מחרוזת מפתח ציבורי זמני בקידוד base64 שמשויך למפתח הפרטי כדי להצפין את ההודעה בפורמט נקודה לא דחוס. מידע נוסף זמין במאמר פורמט של מפתח ציבורי להצפנה.
tag מחרוזת ‫MAC בקידוד Base64 של encryptedMessage.

ההודעה מוצפנת

ה-encryptedMessage שפוענח הוא אובייקט JSON שעבר סריאליזציה וקודד ב-UTF-8. ה-JSON מכיל שתי רמות. הרמה החיצונית מכילה מטא-נתונים ושדות שנכללים מטעמי אבטחה, והרמה הפנימית היא אובייקט JSON נוסף שמייצג את פרטי הכניסה בפועל לתשלום.

לפרטים נוספים על encryptedMessage, עיינו בטבלאות הבאות ובדוגמאות לאובייקטים בפורמט JSON:

שם סוג תיאור
messageExpiration מחרוזת התאריך והשעה שבהם פג התוקף של ההודעה, באלפיות השנייה לפי זמן אוניברסלי מתואם (UTC) מאז ראשית זמן יוניקס (Unix epoch). האינטגרטורים צריכים לדחות כל הודעה שתוקפה פג.
messageId מחרוזת מזהה ייחודי של ההודעה, למקרה שצריך לבטל אותה או לאתר אותה בשלב מאוחר יותר.
paymentMethod מחרוזת סוג פרטי הכניסה לתשלום. בשלב הזה, רק CARD נתמך.
paymentMethodDetails אובייקט פרטי התשלום עצמם. הפורמט של האובייקט הזה נקבע לפי paymentMethod ומתואר בטבלאות הבאות.

קלפים

המאפיינים הבאים מרכיבים את פרטי הכניסה לאמצעי התשלום CARD:

שם סוג תיאור
pan מחרוזת מספר החשבון האישי שחויב. המחרוזת הזו מכילה רק ספרות.
expirationMonth מספר חודש התפוגה של הכרטיס, כאשר 1 מייצג את ינואר, 2 מייצג את פברואר וכן הלאה.
expirationYear מספר שנת התפוגה של הכרטיס, בת ארבע ספרות, למשל 2020.
authMethod מחרוזת שיטת האימות של העסקה בכרטיס.
merchantTokenId מחרוזת

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

  • המוֹכר מציין אובייקט MIT *TransactionInfo בבקשה.
  • המוכר כולל tokenUpdateUrl בבקשה.
  • המשתמש בוחר אמצעי תשלום שעבר טוקניזציה (למשל, DPAN).

PAN_ONLY

קטע הקוד הבא ב-JSON הוא דוגמה ל-encryptedMessage המלא של CARD paymentMethod עם PAN_ONLY authMethod.

{
  "paymentMethod": "CARD",
  "paymentMethodDetails": {
    "authMethod": "PAN_ONLY",
    "pan": "1111222233334444",
    "expirationMonth": 10,
    "expirationYear": 2025
  },
  "gatewayMerchantId": "some-merchant-id",
  "messageId": "some-message-id",
  "messageExpiration": "1759309000000"
}

CRYPTOGRAM_3DS

‫CARD אומת באמצעות קריפטוגרמה של 3-D Secure, CRYPTOGRAM_3DS authMethod. הוא כולל את השדות הנוספים הבאים:

שם סוג תיאור
cryptogram מחרוזת קריפטוגרמה של 3-D Secure.
eciIndicator מחרוזת המחרוזת הזו לא תמיד קיימת. הוא מוחזר רק לעסקאות עם טוקנים של מכשירים מאומתים ב-Android‏ (CRYPTOGRAM_3DS). צריך להעביר את הערך הזה בתהליך עיבוד התשלומים.

קטע הקוד הבא ב-JSON הוא דוגמה ל-encryptedMessage המלא של CARD paymentMethod עם CRYPTOGRAM_3DS authMethod:

{
  "paymentMethod": "CARD",
  "paymentMethodDetails": {
    "authMethod": "CRYPTOGRAM_3DS",
    "pan": "1111222233334444",
    "expirationMonth": 10,
    "expirationYear": 2025,
    "cryptogram": "AAAAAA...",
    "eciIndicator": "eci indicator",
    "merchantTokenId": "merchant-token-id",
    
  },
  
  "messageId": "some-message-id",
  "messageExpiration": "1759309000000"
}

eciIndicator

רשת הכרטיסים עשויה לספק את eciIndicator לעסקאות עם אסימוני מכשיר מאומתים (CRYPTOGRAM_3DS).

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

ערך eciIndicator רשת כרטיסי האשראי הגורם האחראי authMethod
""(empty) מאסטרקארד מוכר/סולק CRYPTOGRAM_3DS
02 מאסטרקארד מנפיק הכרטיס CRYPTOGRAM_3DS
06 מאסטרקארד מוכר/סולק CRYPTOGRAM_3DS
05 ויזה מנפיק הכרטיס CRYPTOGRAM_3DS
07 ויזה מוכר/סולק CRYPTOGRAM_3DS
""(empty) רשתות אחרות מוכר/סולק CRYPTOGRAM_3DS

ערכי ECI אחרים של VISA ו-Mastercard שלא מופיעים בטבלה הזו לא יוחזרו.

אימות חתימה

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

  • האלגוריתם ששימש ליצירת החתימה
  • מחרוזת הבייטים ששימשה ליצירת החתימה
  • המפתח הציבורי שתואם למפתח הפרטי ששימש ליצירת החתימה
  • החתימה עצמה

אלגוריתם החתימה

‫Google משתמשת באלגוריתם חתימה דיגיטלית של עקומה אליפטית (ECDSA) כדי לחתום על ההודעות עם הפרמטרים הבאים: ECDSA מעל NIST P-256 עם SHA-256 כפונקציית הגיבוב (hash), כפי שמוגדר ב-FIPS 186-4.

החתימה

החתימה נכללת ברמה החיצונית ביותר של ההודעה. הוא מקודד ב-base64 בפורמט בייטים של ASN.1. מידע נוסף על ASN.1 זמין בנספח א' של כלי IETF. החתימה מורכבת מהמספרים השלמים r ו-s של ECDSA. מידע נוסף זמין במאמר אלגוריתם ליצירת חתימה.

הדוגמה הבאה היא של פורמט הבייטים שצוין ב-ASN.1, שהוא הפורמט הסטנדרטי שנוצר על ידי הטמעות ECDSA של Java Cryptography Extension‏ (JCE).

ECDSA-Sig-Value :: = SEQUENCE {
 r INTEGER,
 s INTEGER
}

איך לבנות את מחרוזת הבייטים לחתימה של מפתח החתימה הזמני

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

signedStringForIntermediateSigningKeySignature =
length_of_sender_id || sender_id || length_of_protocol_version || protocol_version || length_of_signed_key || signed_key

הסימון "||" מציין שרשור. כל רכיב – sender_id, protocolVersion, signedKey – צריך להיות מקודד ב-UTF-8. הערך של signedKey חייב להיות המחרוזת של intermediateSigningKey.signedKey. אורך הבייטים של כל רכיב הוא 4 בייטים בפורמט Little-endian.

דוגמה

בדוגמה הזו נשתמש בטוקן הבא של אמצעי התשלום:

{
  "protocolVersion":"ECv2",
  "signature":"MEQCIH6Q4OwQ0jAceFEkGF0JID6sJNXxOEi4r+mA7biRxqBQAiAondqoUpU/bdsrAOpZIsrHQS9nwiiNwOrr24RyPeHA0Q\u003d\u003d",
  "intermediateSigningKey":{
    "signedKey": "{\"keyExpiration\":\"1542323393147\",\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\"}",
    "signatures": ["MEYCIQCO2EIi48s8VTH+ilMEpoXLFfkxAwHjfPSCVED/QDSHmQIhALLJmrUlNAY8hDQRV/y1iKZGsWpeNmIP+z+tCQHQxP0v"]
  },
  "signedMessage":"{\"tag\":\"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\\u003d\",\"ephemeralPublicKey\":\"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\\u003d\",\"encryptedMessage\":\"mKOoXwi8OavZ\"}"
}

הערך של sender_id הוא תמיד Google והערך של protocol_version הוא ECv2.

אם הערך של sender_id הוא Google, הערך של signedString מופיע כמו בדוגמה הבאה:

signedStringForIntermediateSigningKeySignature =
\x06\x00\x00\x00 || Google || | \x04\x00\x00\x00 || ECv2 || \xb5\x00\x00\x00 || {"keyExpiration":"1542323393147","keyValue":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\u003d\u003d"}

איך מאמתים את החתימה ב-signedStringForIntermediateSigningKeySignature

אלגוריתם האימות הרגיל של ECDSA משמש כשמרכיבים את המחרוזת החתומה של החתימה של מפתח החתימה המתווך. בפרוטוקול ECv2, צריך לעבור על כל החתימות ב-intermediateSigningKey.signatures ולנסות לאמת כל אחת מהן באמצעות מפתחות החתימה של Google שעדיין בתוקף ב-keys.json. אם לפחות אימות חתימה אחד פועל, האימות הושלם. אפשר להשתמש ב-intermediateSigningKey.signedKey.keyValue מאוחר יותר כדי לאמת את ה-signedStringForMessageSignature. ‫Google ממליצה מאוד להשתמש בספרייה קריפטוגרפית קיימת במקום בקוד אימות משלכם.

איך ליצור את מחרוזת הבייטים לחתימת ההודעה

כדי לאמת את החתימה בטוקן לדוגמה של אמצעי התשלום, יוצרים את signedStringForMessageSignature באמצעות הנוסחה הבאה:

signedStringForMessageSignature =
length_of_sender_id || sender_id || length_of_recipient_id || recipient_id || length_of_protocolVersion || protocolVersion || length_of_signedMessage || signedMessage

הסימון "||" מציין שרשור. כל רכיב – sender_id,‏ recipient_id, ‏ protocolVersion, ‏ signedMessage – צריך להיות מקודד ב-UTF-8. אורך הבייטים של כל רכיב הוא 4 בייטים בפורמט Little-endian. כשיוצרים את מחרוזת הבייטים, אין לנתח או לשנות את signedMessage. לדוגמה, אל תחליפו את \u003d בתו =.

דוגמה

הדוגמה הבאה היא אסימון לדוגמה של אמצעי תשלום:

{
  "protocolVersion":"ECv2",
  "signature":"MEQCIH6Q4OwQ0jAceFEkGF0JID6sJNXxOEi4r+mA7biRxqBQAiAondqoUpU/bdsrAOpZIsrHQS9nwiiNwOrr24RyPeHA0Q\u003d\u003d",
  "intermediateSigningKey":{
    "signedKey": "{\"keyExpiration\":\"1542323393147\",\"keyValue\":\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/1+3HBVSbdv+j7NaArdgMyoSAM43yRydzqdg1TxodSzA96Dj4Mc1EiKroxxunavVIvdxGnJeFViTzFvzFRxyCw\\u003d\\u003d\"}",
    "signatures": ["MEYCIQCO2EIi48s8VTH+ilMEpoXLFfkxAwHjfPSCVED/QDSHmQIhALLJmrUlNAY8hDQRV/y1iKZGsWpeNmIP+z+tCQHQxP0v"]
  },
  "signedMessage":"{\"tag\":\"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\\u003d\",\"ephemeralPublicKey\":\"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\\u003d\",\"encryptedMessage\":\"mKOoXwi8OavZ\"}"
}

הערך של sender_id הוא תמיד Google והערך של recipient_id הוא merchant:merchantId. הערך של merchantId תואם לערך שמופיע במסוף Google Pay ו-Wallet למוכרים עם גישה לסביבת הייצור.

אם הערך של sender_id הוא Google והערך של recipient_id הוא merchant:12345, השדה signedString מופיע כמו בדוגמה הבאה:

signedStringForMessageSignature =
\x06\x00\x00\x00 || Google || \x0e\x00\x00\x00 || merchant:12345 || | \x04\x00\x00\x00 || ECv2 || \xd2\x00\x00\x00 || {"tag":"jpGz1F1Bcoi/fCNxI9n7Qrsw7i7KHrGtTf3NrRclt+U\u003d","ephemeralPublicKey":"BJatyFvFPPD21l8/uLP46Ta1hsKHndf8Z+tAgk+DEPQgYTkhHy19cF3h/bXs0tWTmZtnNm+vlVrKbRU9K8+7cZs\u003d","encryptedMessage":"mKOoXwi8OavZ"}

איך מאמתים את החתימה ב-signedStringForMessageSignature

אלגוריתם הבדיקה הסטנדרטי של ECDSA משמש להרכבת המחרוזת החתומה. ה-intermediateSigningKey.signedKey.keyValue שאומת בשלב הקודם משמש לאימות ה-signedMessage. ‫Google ממליצה מאוד להשתמש בספרייה קריפטוגרפית קיימת במקום בקוד אימות משלכם.

מפרט של סכמת ההצפנה

Google משתמשת ב-Elliptic Curve Integrated Encryption Scheme‏ (ECIES) כדי לאבטח את האסימון של אמצעי התשלום שמוחזר בתגובה מה-API של Google Pay. אלה הפרמטרים של סכימת ההצפנה:

פרמטר הגדרה
שיטת אנקפסולציה של מפתחות (KEM)

‫ECIES-KEM, כמו שמוגדר ב-ISO 18033-2.

  • עקומה אליפטית: NIST P-256 (ידועה גם ב-OpenSSL בתור prime256v1).
  • ‫CheckMode, ‏OldCofactorMode, ‏SingleHashMode ו-CofactorMode הם 0.
  • פורמט הנקודה לא דחוס.
פונקציית גזירת מפתחות (KDF)

מבוסס-HMAC עם SHA-256‏ (HKDFwithSHA256).

  • אסור לספק ערך גיבוב (salt).
  • המידע חייב להיות "Google", מקודד ב-ASCII לגרסת הפרוטוקול ECv2.
  • צריך להפיק 256 ביטים למפתח AES256 ו-256 ביטים נוספים למפתח HMAC_SHA256.
אלגוריתם הצפנה סימטרי

‫DEM2, כפי שמוגדר ב-ISO 18033-2

אלגוריתם הצפנה: AES-256-CTR עם וקטור אתחול (IV) אפס וללא ריפוד.

אלגוריתם MAC ‫HMAC_SHA256 עם מפתח של 256 ביט, שנוצר באמצעות פונקציית יצירת המפתחות.

פורמט המפתח הציבורי להצפנה

המפתח הציבורי להצפנה וה-ephemeralPublicKey שמוחזרים ב-payloads של Google מעוצבים עם ייצוג base64 של המפתח בפורמט נקודה לא דחוס. הוא מורכב משני הרכיבים הבאים:

  • מספר קסם אחד שמציין את הפורמט (0x04).
  • שני מספרים שלמים גדולים של 32 בייטים שמייצגים את הקואורדינטות X ו-Y בעקומה האליפטית.

הפורמט הזה מתואר בפירוט רב יותר במאמר "Public Key Cryptography For The Financial Services Industry: The Elliptic Curve Digital Signature Algorithm (ECDSA)",‏ ANSI X9.62, ‏ 1998.

שימוש ב-OpenSSL ליצירת מפתח ציבורי

שלב 1: יוצרים מפתח פרטי

בדוגמה הבאה נוצר מפתח פרטי של עקומה אליפטית שמתאים לשימוש עם NIST P-256, והוא נכתב ב-key.pem:

openssl ecparam -name prime256v1 -genkey -noout -out key.pem

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

כדי לראות גם את המפתח הפרטי וגם את המפתח הציבורי, משתמשים בפקודה הבאה:

openssl ec -in key.pem -pubout -text -noout

הפקודה יוצרת פלט דומה לזה:

read EC key
Private-Key: (256 bit)
priv:
    08:f4:ae:16:be:22:48:86:90:a6:b8:e3:72:11:cf:
    c8:3b:b6:35:71:5e:d2:f0:c1:a1:3a:4f:91:86:8a:
    f5:d7
pub:
    04:e7:68:5c:ff:bd:02:ae:3b:dd:29:c6:c2:0d:c9:
    53:56:a2:36:9b:1d:f6:f1:f6:a2:09:ea:e0:fb:43:
    b6:52:c6:6b:72:a3:f1:33:df:fa:36:90:34:fc:83:
    4a:48:77:25:48:62:4b:42:b2:ae:b9:56:84:08:0d:
    64:a1:d8:17:66
ASN1 OID: prime256v1

שלב 2: יוצרים מפתח ציבורי בקידוד Base64

המפתח הפרטי והמפתח הציבורי שנוצרו בדוגמה של השלב האופציונלי הקודם מקודדים בבסיס הקסדצימלי. כדי לקבל מפתח ציבורי בקידוד Base64 בפורמט נקודה לא דחוס, משתמשים בפקודה הבאה:

openssl ec -in key.pem -pubout -text -noout 2> /dev/null | grep "pub:" -A5 | sed 1d | xxd -r -p | base64 | paste -sd "\0" - | tr -d '\n\r ' > publicKey.txt

הפקודה יוצרת קובץ publicKey.txt שהתוכן שלו, גרסת ה-base64 של המפתח בפורמט נקודה לא דחוס, דומה לזה:

BOdoXP+9Aq473SnGwg3JU1aiNpsd9vH2ognq4PtDtlLGa3Kj8TPf+jaQNPyDSkh3JUhiS0KyrrlWhAgNZKHYF2Y=

אסור שיהיו בקובץ רווחים מיותרים או חזרה לתחילת השורה. כדי לבדוק את זה, מריצים את הפקודה הבאה ב-Linux או ב-MacOS:

od -bc publicKey.txt

שלב 3: יצירת מפתח פרטי בקידוד base64 בפורמט PKCS #8

ספריית Tink מצפה שהמפתח הפרטי יהיה בקידוד base64 בפורמט PKCS #8. כדי ליצור את המפתח הפרטי בפורמט הזה מהמפתח הפרטי שנוצר בשלב הראשון, משתמשים בפקודה הבאה:

openssl pkcs8 -topk8 -inform PEM -outform DER -in key.pem -nocrypt | base64 | paste -sd "\0" -

הפקודה יוצרת פלט דומה לזה:

MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWV4oK8c/MZkCLk4qSCNjW0Zm6H0CBCtSYxkXkC9FBHehRANCAAQPldOnhO2/oXjdJD1dwlFPiNs6fcdoRgFu3/Z0iKj24SjTGyLRGAtYWLGXBZcDdPj3T2bJRHRVhE8Bc2AjkT7n

איך לפענח את האסימון של אמצעי התשלום

כדי לפענח את האסימון:

  1. משתמשים במפתח הפרטי וב-ephemeralPublicKey שקיבלתם כדי להפיק מפתח משותף באורך 512 ביט שמשתמש ב-ECIES-KEM. משתמשים בפרמטרים הבאים:
    • עקומה אליפטית: NIST P-256, שנקראת גם prime256v1 ב-OpenSSL.
    • ‫CheckMode, ‏OldCofactorMode, ‏SingleHashMode וגם CofactorMode הם 0.
    • פונקציית קידוד: פורמט נקודה לא דחוס.
    • פונקציית גזירת מפתחות: HKDFwithSHA256, כפי שמתואר ב-RFC 5869, עם הפרמטר הבא:
      • אסור לספק ערך גיבוב (salt). לפי ה-RFC, הערך הזה חייב להיות שווה ערך ל-salt של 32 בייטים מאופסים.
  2. מפצלים את המפתח שנוצר לשני מפתחות באורך 256 ביט: symmetricEncryptionKey ו-macKey.
  3. צריך לוודא שהשדה tag הוא MAC תקין עבור encryptedMessage.

    כדי ליצור את ה-MAC הצפוי, משתמשים ב-HMAC‏ (RFC 5869) עם פונקציית הגיבוב SHA256 ועם macKey שהתקבל בשלב 2.

  4. מפענחים את encryptedMessage באמצעות מצב AES-256-CTR, ועם הפרטים הבאים:

    • וקטור אתחול (IV) אפס.
    • ללא ריפוד.
    • הערך symmetricEncryptionKey שהתקבל בשלב 2.

ניהול מפתחות

מפתחות הצפנה של המוכר

המוכרים יוצרים מפתח ציבורי בהתאם למפרטים שמפורטים במפרט של סכימת הצפנה.

מפתחות חתימה בסיסיים של Google

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

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

המפתחות שסופקו דרך כתובת ה-URL הציבורית ממופים בפורמט הבא:

{
  "keys": [
    {
      "keyValue": "encoded public key",
      "protocolVersion": "ECv2"
      "keyExpiration":"2000000000000"
    },
    {
      "keyValue": "encoded public key",
      "protocolVersion": "ECv2"
      "keyExpiration":"3000000000000"
    }
  ]
}

הערך keyValue הוא גרסה של המפתח בקידוד base64, ללא גלישת טקסט או ריפוד, ב-ASN.1 מסוג SubjectPublicKeyInfo שמוגדר בתקן X.509. ב-Java, קידוד ASN.1 שאליו מתייחסים מיוצג על ידי X509EncodedKeySpec class. אפשר לקבל אותו באמצעות ECPublicKey.getEncoded().

כתובות ה-URL של סביבות הבדיקה והייצור מופיעות בקישורים הבאים:

רוטציית מפתחות

אם אתם מפענחים אסימון של אמצעי תשלום ישירות בשרתים שלכם באמצעות שילוב ישיר, אתם צריכים להחליף את המפתחות מדי שנה.

כדי להחליף מפתחות הצפנה:

  1. משתמשים ב-OpenSSL כדי ליצור זוג מפתחות חדש.
  2. פותחים את מסוף Google Pay ו-Wallet כשמחוברים לחשבון Google‏ ( ששימש בעבר לניהול האפליקציה ב-Google Play.
  3. בכרטיסייה Google Pay API, בחלונית שילוב ישיר, לוחצים על ניהול לצד המפתח הציבורי הקיים. לוחצים על הוספת עוד מפתח.
  4. בוחרים את השדה להזנת טקסט Public encryption key ומוסיפים את המפתח הציבורי החדש שנוצר, בקידוד base64 בפורמט נקודה לא דחוס.
  5. לוחצים על שמירת מפתחות ההצפנה.
  6. כדי להבטיח רוטציית מפתחות חלקה, צריך לתמוך בפענוח של המפתחות הפרטיים החדשים והישנים בזמן העברת המפתחות.

    אם אתם משתמשים בספריית Tink כדי לפענח את האסימון, השתמשו בקוד Java הבא כדי לתמוך בכמה מפתחות פרטיים:

    String decryptedMessage =
        new PaymentMethodTokenRecipient.Builder()
            .addRecipientPrivateKey(newPrivateKey)
            .addRecipientPrivateKey(oldPrivateKey);

    חשוב לוודא שהקוד לפענוח נפרס בסביבת הייצור, ולעקוב אחרי פענוחים מוצלחים.

  7. משנים את המפתח הציבורי שמוגדר בקוד.

    מחליפים את הערך של המאפיין publicKey במאפיין PaymentMethodTokenizationSpecification parameters:

    /**
     * @param publicKey public key retrieved from your server
     */
    private static JSONObject getTokenizationSpecification(String publicKey) {
      JSONObject tokenizationSpecification = new JSONObject();
      tokenizationSpecification.put("type", "DIRECT");
      tokenizationSpecification.put(
        "parameters",
        new JSONObject()
            .put("protocolVersion", "ECv2")
            .put("publicKey", publicKey));
      return tokenizationSpecification;
    }
  8. פורסים את הקוד משלב 4 בסביבת הייצור. אחרי פריסת הקוד, טרנזקציות של הצפנה ופענוח משתמשות בזוגות המפתחות החדשים.
  9. מוודאים שהמפתח הציבורי הישן לא משמש יותר להצפנת עסקאות.

  10. מסירים את המפתח הפרטי הישן.
  11. פותחים את מסוף Google Pay ו-Wallet כשמחוברים לחשבון Google שבו השתמשתם בעבר כדי להירשם כמפתחים ב-Google Pay.
  12. בכרטיסייה Google Pay API, בחלונית שילוב ישיר, לוחצים על ניהול לצד המפתח הציבורי הקיים. לוחצים על מחיקה לצד המפתח הציבורי הישן ולוחצים על שמירת מפתחות ההצפנה.

‫Google משתמשת במפתח שצוין במאפיין publicKey בתוך האובייקט PaymentMethodTokenizationSpecification parameters, כמו שמוצג בדוגמה הבאה:

{
  "protocolVersion": "ECv2",
  "publicKey": "BOdoXP+9Aq473SnGwg3JU1..."
}

משתמשים בספריית Tink כדי לנהל את התגובה המוצפנת.

כדי לבצע אימות חתימה ופענוח הודעות, צריך להשתמש בספריית Tink paymentmethodtoken. הספרייה הזו זמינה רק ב-Java. כדי להשתמש בו, מבצעים את השלבים הבאים:

  1. בקובץ pom.xml, מוסיפים את אפליקציית Tink paymentmethodtoken כיחסי תלות:

    <dependencies>
      <!-- other dependencies ... -->
      <dependency>
        <groupId>com.google.crypto.tink</groupId>
        <artifactId>apps-paymentmethodtoken</artifactId>
        <version>1.9.1</version>  <!-- or latest version -->
      </dependency>
    </dependencies>
  2. כשהשרת מופעל, מאחזרים מראש את מפתחות החתימה של Google כדי שהמפתח יהיה זמין בזיכרון. כך המשתמש לא יחווה עיכוב ברשת בזמן שתהליך הפענוח מאחזר את המפתחות.

    GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION.refreshInBackground();
  3. מפענחים את ההודעה באמצעות הקוד הבא, בהנחה ש-paymentMethodToken מאוחסן במשתנה encryptedMessage, ומחליפים את הקטעים המודגשים בהתאם לתרחיש שלכם.

    בבדיקות שלא בסביבת הייצור, מחליפים את INSTANCE_PRODUCTION ב-INSTANCE_TEST. אם השילוב לא פעיל או שלא הוגדר לו מפתח הצפנה, מחליפים את [YOUR MERCHANT ID] ב-.

    • פעיל
    • השילוב הישיר מופעל
    • מוגדר מפתח הצפנה

    אל תחליפו את [YOUR MERCHANT ID].

    String decryptedMessage =
        new PaymentMethodTokenRecipient.Builder()
        .fetchSenderVerifyingKeysWith(
            GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION)
        .recipientId("merchant:[YOUR MERCHANT ID]")
        // This guide applies only to protocolVersion = ECv2
        .protocolVersion("ECv2")
        // Multiple private keys can be added to support graceful
        // key rotations.
        .addRecipientPrivateKey(PrivateKey1)
        .addRecipientPrivateKey(PrivateKey2)
        .build()
        .unseal(encryptedMessage);
  4. מחליפים את PrivateKey1 בערך המתאים של המפתח הפרטי שמשויך לערך של המפתח הציבורי שרשום ב-Google, מהשלב הכנת המפתחות ורישום ב-Google. אפשר להוסיף עוד ערכים של מפתחות פרטיים בהמשך, כשצריך להחליף מפתחות עם Google. המשתנים יכולים להיות מחרוזת PKCS8 בקידוד Base64 או אובייקט ECPrivateKey. מידע נוסף על יצירת מפתח פרטי מסוג PKCS8 בקידוד Base64 זמין במאמר הכנת המפתחות והרשמה ל-Google.

  5. אם אתם לא יכולים להתקשר לשרת של Google בכל פעם שאתם מפענחים מפתחות, אתם יכולים לפענח באמצעות הקוד הבא ולהחליף את הקטעים המודגשים בהתאם לתרחיש שלכם.

    String decryptedMessage =
        new PaymentMethodTokenRecipient.Builder()
        .addSenderVerifyingKey("ECv2 key fetched from test or production url")
        .recipientId("merchant:[YOUR MERCHANT ID]")
        // This guide applies only to protocolVersion = ECv2
        .protocolVersion("ECv2")
        // Multiple private keys can be added to support graceful
        // key rotations.
        .addRecipientPrivateKey(PrivateKey1)
        .addRecipientPrivateKey(PrivateKey2)
        .build()
        .unseal(encryptedMessage);

    המפתח הנוכחי בסביבת הייצור תקף עד 14 באפריל 2038 בנסיבות רגילות, למעט במקרים של פריצה למפתח. במקרה של פריצה למפתחות, Google תודיע לכל המוכרים באמצעות פרטי הקשר שסופקו בפורטל לשירות עצמי, כדי לבקש טעינה מהירה יותר של keys.json.

    קטע הקוד מטפל בפרטי האבטחה הבאים, כדי שתוכלו להתמקד בצריכת המטען הייעודי (payload):

    • מפתחות החתימה של Google אוחזרו ונשמרו במטמון בזיכרון
    • אימות חתימה
    • פענוח