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

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

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

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

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

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

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

  1. מאחזרים את מפתחות החתימה של Google.
  2. מוודאים שהחתימה של מפתח החתימה הביניים תקפה באמצעות אחד ממפתחות החתימה הבסיסיים שלא פג תוקפם.
  3. מוודאים שתוקף מפתח החתימה הביניים של מטען הייעודי לא פג.
  4. מוודאים שהחתימה של מטען הייעודי תקפה באמצעות מפתח החתימה הביניים.
  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. מערכות משולבות דוחות כל מפתח שתוקפו פג.

הודעה חתומה

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

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

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

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

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

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

קלפים

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

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

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 מאומת באמצעות קריפטוגרמה של 3D Secure, CRYPTOGRAM_3DS authMethod. הוא כולל את השדות הנוספים הבאים:

שם סוג תיאור
cryptogram מחרוזת קריפטוגרם של 3D 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"
    
  },
  
  "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 עבור כרטיסי ויזה ומאסטרקארד שלא מופיעים בטבלה הזו.

אימות חתימה

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

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

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

‫Google משתמשת באלגוריתם לחתימה דיגיטלית של עקומות אליפטיות (ECDSA) כדי לחתום על ההודעות עם הפרמטרים הבאים: ECDSA over NIST P-256 עם SHA-256 כפונקציית הגיבוב, כפי שמוגדר ב-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 משתמשת בשיטת ההצפנה המשולבת של עקומות אליפטיות (ECIES) כדי לאבטח את טוקן אמצעי התשלום שמוחזר בתגובה של Google Pay API. בתוכנית ההצפנה נעשה שימוש בפרמטרים הבאים:

פרמטר הגדרה
שיטת הצפנה של מפתח

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

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

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

  • אסור לספק מלח.
  • המידע צריך להיות בקידוד ASCII של Google עבור גרסת הפרוטוקול 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, עם הפרמטר הבא:
      • אסור לספק מלח. לפי 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. אפשר לקבל אותו באמצעות ECPublicKey.getEncoded().

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

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

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

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

  1. משתמשים ב-OpenSSL כדי ליצור זוג מפתחות חדש.
  2. פותחים את מסוף Google Pay ו-Wallet כשנכנסים לחשבון Google‏ ששימש בעבר לניהול האפליקציה שלך באמצעות Google Play.
  3. בכרטיסייה Google Pay API, בחלונית Direct integration, לוחצים על Manage לצד המפתח הציבורי הקיים. לוחצים על הוספת עוד מפתח.
  4. בוחרים את שדה הזנת הטקסט מפתח ציבורי להצפנה ומוסיפים את המפתח הציבורי החדש שנוצר בקידוד 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, בחלונית Direct integration, לוחצים על Manage לצד המפתח הציבורי הקיים. לצד המפתח הציבורי הישן, לוחצים על מחיקה ואז על שמירת מפתחות ההצפנה.

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

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

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

כדי לבצע אימות חתימה ופענוח הודעה, משתמשים בספריית paymentmethodtoken של Tink. הספרייה הזו זמינה רק ב-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. מידע נוסף על יצירת מפתח פרטי בקידוד base64 של PKCS8 זמין במאמר הכנת המפתחות והרשמה ב-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 מאוחזרים ונשמרים במטמון בזיכרון
    • אימות חתימה
    • פענוח