वेब पुश प्रोटोकॉल

हमने देखा है कि पुश मैसेज को ट्रिगर करने के लिए लाइब्रेरी का इस्तेमाल कैसे किया जा सकता है, लेकिन ये लाइब्रेरी असल में क्या कर रही हैं?

वे नेटवर्क अनुरोध कर रहे हैं और यह पक्का कर रहे हैं कि इस तरह के अनुरोध सही फ़ॉर्मैट में हों. इस नेटवर्क अनुरोध के बारे में बताने वाली खास जानकारी में वेब पुश प्रोटोकॉल को शामिल किया जाता है.

आपके सर्वर से पुश सेवा को पुश मैसेज भेजने का डायग्राम

इस सेक्शन में बताया गया है कि सर्वर, ऐप्लिकेशन सर्वर कुंजियों से अपनी पहचान कैसे करता है और एन्क्रिप्ट (सुरक्षित) किया गया पेलोड और उससे जुड़ा डेटा कैसे भेजा जाता है.

यह वेब पुश या एन्क्रिप्शन का कोई पहलू नहीं है. मैं एन्क्रिप्शन में भी कोई विशेषज्ञ नहीं हूं, लेकिन आइए हर एक चीज़ के बारे में जानते हैं, क्योंकि यह जानना आसान है कि ये लाइब्रेरी हुड में क्या कर रही हैं.

ऐप्लिकेशन के सर्वर की कुंजियां

जब हम किसी उपयोगकर्ता को सदस्यता लेते हैं, तो हमें applicationServerKey मिलता है. इस कुंजी को पुश सेवा को पास कर दिया जाता है. इसका इस्तेमाल यह जांचने के लिए किया जाता है कि जिस ऐप्लिकेशन ने उपयोगकर्ता की सदस्यता ली है वह वही ऐप्लिकेशन है जो पुश मैसेज ट्रिगर कर रहा है.

जब हम किसी पुश मैसेज को ट्रिगर करते हैं, तो हम भेजे जाने वाले हेडर का एक सेट होते हैं, जो पुश सेवा को ऐप्लिकेशन की पुष्टि करने की अनुमति देते हैं. (इसे VAPID स्पेसिफ़िकेशन में बताया गया है.)

इन सबका क्या मतलब है और असल में क्या होता है? ऐप्लिकेशन सर्वर की पुष्टि करने के लिए, ये कदम उठाए जा सकते हैं:

  1. ऐप्लिकेशन सर्वर, अपनी निजी ऐप्लिकेशन कुंजी की मदद से JSON की कुछ जानकारी पर हस्ताक्षर करता है.
  2. हस्ताक्षर की गई यह जानकारी पुश सेवा को, पोस्ट अनुरोध में हेडर के तौर पर भेजी जाती है.
  3. पुश सेवा, सेव किए गए उस सार्वजनिक पासकोड का इस्तेमाल करती है जो उसे pushManager.subscribe() से मिला है. इसके तहत, यह जांच की जाती है कि मिली जानकारी पर सार्वजनिक पासकोड से जुड़े निजी पासकोड से हस्ताक्षर किया गया है या नहीं. याद रखें: सार्वजनिक कुंजी, वह applicationServerKey होती है जिसे सदस्यता कॉल में पास किया जाता है.
  4. अगर हस्ताक्षर की गई जानकारी सही है, तो पुश सेवा उपयोगकर्ता को पुश मैसेज भेजती है.

जानकारी के इस फ़्लो का एक उदाहरण यहां दिया गया है. (सार्वजनिक और निजी कुंजियों को दिखाने के लिए नीचे बाईं ओर लेजेंड पर ध्यान दें.)

इलस्ट्रेशन में दिखाया गया है कि मैसेज भेजते समय, निजी ऐप्लिकेशन सर्वर कुंजी
का इस्तेमाल कैसे किया जाता है

अनुरोध में, हेडर में जोड़ी गई "साइन की गई जानकारी" एक JSON वेब टोकन है.

JSON वेब टोकन

JSON वेब टोकन (या कम शब्दों में JWT), किसी तीसरे पक्ष को मैसेज भेजने का एक तरीका है. इससे, मैसेज पाने वाला यह पुष्टि कर सकता है कि मैसेज किसने भेजा है.

जब तीसरे पक्ष को कोई मैसेज मिलता है, तो उसे भेजने वाले की सार्वजनिक कुंजी लेनी होगी और उसका इस्तेमाल JWT के हस्ताक्षर की पुष्टि करने के लिए करना होगा. अगर हस्ताक्षर मान्य है, तो JWT को मेल खाने वाली निजी कुंजी से साइन किया जाना चाहिए. इसलिए, यह सही ईमेल भेजने वाले व्यक्ति की ओर से होना चाहिए.

https://jwt.io/ पर ऐसी कई लाइब्रेरी हैं जो आपके लिए हस्ताक्षर कर सकती हैं और हमारा सुझाव है कि आप जहां हो सके वहां साइन करें. आइए, देखते हैं कि साइन किया हुआ JWT मैन्युअल तरीके से कैसे बनाया जाता है.

वेब पुश और साइन किए गए JWT

साइन किया गया JWT एक स्ट्रिंग है. हालांकि, इसे बिंदुओं से जुड़ी तीन स्ट्रिंग के तौर पर समझा जा सकता है.

JSON वेब टोकन में स्ट्रिंग का इलस्ट्रेशन

पहली और दूसरी स्ट्रिंग (JWT की जानकारी और JWT डेटा), JSON के वे हिस्से हैं जिन्हें base64 कोड में बदला गया है. इसका मतलब है कि इसे सार्वजनिक तौर पर पढ़ा जा सकता है.

पहली स्ट्रिंग, JWT के बारे में जानकारी देती है, जिससे यह पता चलता है कि हस्ताक्षर बनाने के लिए किस एल्गोरिदम का इस्तेमाल किया गया था.

वेब पुश के लिए JWT की जानकारी में यह जानकारी होनी चाहिए:

{
  "typ": "JWT",
  "alg": "ES256"
}

दूसरी स्ट्रिंग है JWT डेटा. इससे JWT भेजने वाले के बारे में जानकारी मिलती है, ताकि यह पता चल सके कि यह किस व्यक्ति के लिए है और यह कितने समय तक मान्य है.

वेब पुश के लिए, डेटा इस फ़ॉर्मैट में होगा:

{
  "aud": "https://some-push-service.org",
  "exp": "1469618703",
  "sub": "mailto:example@web-push-book.org"
}

aud की वैल्यू "audience" है. इसका मतलब है कि JWT किसके लिए है. वेब पुश के लिए, ऑडियंस पुश सेवा है, इसलिए हम इसे पुश सेवा के मूल पर सेट करते हैं.

exp वैल्यू, JWT की समयसीमा खत्म होने वाली है. इससे स्नूपर को इंटरसेप्ट करने पर, उन्हें JWT का दोबारा इस्तेमाल करने से रोका जाता है. समयसीमा, सेकंड में एक टाइमस्टैंप होती है. अब यह 24 घंटे की नहीं होनी चाहिए.

Node.js में समयसीमा खत्म होने की तारीख इनका इस्तेमाल करके सेट की गई है:

Math.floor(Date.now() / 1000) + 12 * 60 * 60;

ईमेल भेजने वाले ऐप्लिकेशन और पुश सेवा के बीच घड़ी के समय में अंतर की किसी भी समस्या से बचने के लिए, 24 घंटे के बजाय 12 घंटे लगते हैं.

आखिर में, sub वैल्यू या तो यूआरएल या mailto ईमेल पता होना चाहिए. ऐसा इसलिए है, ताकि अगर भेजने वाले तक पहुंचने के लिए किसी पुश सेवा को ज़रूरत पड़े, तो वह JWT से संपर्क जानकारी ढूंढ सके. (इस वजह से वेब-पुश लाइब्रेरी को ईमेल पते की ज़रूरत होती है).

JWT की जानकारी की तरह, JWT का डेटा भी यूआरएल के हिसाब से सुरक्षित base64 स्ट्रिंग के तौर पर एन्कोड किया जाता है.

तीसरी स्ट्रिंग, सिग्नेचर, पहली दो स्ट्रिंग (JWT Info and JWT डेटा) का नतीजा है. इन्हें एक डॉट कैरेक्टर के साथ जोड़कर बनाया जाता है. इस स्ट्रिंग को हम "अनसाइन्ड टोकन" कहते हैं और इस पर हस्ताक्षर करते हैं.

साइनिंग प्रोसेस में, ES256 का इस्तेमाल करके "बिना हस्ताक्षर वाले टोकन" को एन्क्रिप्ट (सुरक्षित) करना ज़रूरी होता है. JWT खास जानकारी के मुताबिक, ES256, "P-256 कर्व का इस्तेमाल करने वाले ECDSA और SHA-256 हैश एल्गोरिदम" का छोटा रूप है. वेब क्रिप्टो का इस्तेमाल करके, इस तरह से सिग्नेचर बनाया जा सकता है:

// Utility function for UTF-8 encoding a string to an ArrayBuffer.
const utf8Encoder = new TextEncoder('utf-8');

// The unsigned token is the concatenation of the URL-safe base64 encoded
// header and body.
const unsignedToken = .....;

// Sign the |unsignedToken| using ES256 (SHA-256 over ECDSA).
const key = {
  kty: 'EC',
  crv: 'P-256',
  x: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(1, 33)),
  y: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(33, 65)),
  d: window.uint8ArrayToBase64Url(applicationServerKeys.privateKey),
};

// Sign the |unsignedToken| with the server's private key to generate
// the signature.
return crypto.subtle.importKey('jwk', key, {
  name: 'ECDSA', namedCurve: 'P-256',
}, true, ['sign'])
.then((key) => {
  return crypto.subtle.sign({
    name: 'ECDSA',
    hash: {
      name: 'SHA-256',
    },
  }, key, utf8Encoder.encode(unsignedToken));
})
.then((signature) => {
  console.log('Signature: ', signature);
});

पुश सेवा, सिग्नेचर को डिक्रिप्ट करने के लिए सार्वजनिक ऐप्लिकेशन सर्वर कुंजी का इस्तेमाल करके JWT की पुष्टि कर सकती है. साथ ही, यह पक्का कर सकती है कि डिक्रिप्ट की गई स्ट्रिंग, "बिना साइन वाले टोकन" (जैसे कि JWT में पहली दो स्ट्रिंग) की तरह है.

साइन किया गया JWT (यानी कि बिंदुओं से जोड़ी गई सभी तीन स्ट्रिंग), वेब पुश सेवा को Authorization हेडर के तौर पर भेजा जाता है. ऐसा WebPush के साथ पहले किया जाता है, जैसे:

Authorization: 'WebPush [JWT Info].[JWT Data].[Signature]';

वेब पुश प्रोटोकॉल में यह भी बताया गया है कि सार्वजनिक ऐप्लिकेशन सर्वर कुंजी Crypto-Key हेडर में यूआरएल के तौर पर भेजी जानी चाहिए. यह सुरक्षित, base64 कोड में बदली गई स्ट्रिंग के तौर पर p256ecdsa= से पहले जोड़ा जाना चाहिए.

Crypto-Key: p256ecdsa=[URL Safe Base64 Public Application Server Key]

पेलोड को एन्क्रिप्ट (सुरक्षित) करने का तरीका

आइए, अब जानते हैं कि हम पुश मैसेज के साथ पेलोड कैसे भेज सकते हैं, ताकि जब हमारे वेब ऐप्लिकेशन को कोई पुश मैसेज मिले, तो वह उसे मिलने वाले डेटा को ऐक्सेस कर सके.

अन्य पुश सेवाओं का इस्तेमाल करने वाले हर व्यक्ति से अक्सर यह सवाल उठता है कि वेब पुश पेलोड को एन्क्रिप्ट (सुरक्षित) क्यों किया जाना चाहिए? नेटिव ऐप्लिकेशन में, पुश मैसेज की मदद से डेटा को सामान्य टेक्स्ट के तौर पर भेजा जा सकता है.

वेब पुश की खासियत यह है कि सभी पुश सेवाएं एक ही एपीआई (वेब पुश प्रोटोकॉल) का इस्तेमाल करती हैं, इसलिए डेवलपर को इस बात की परवाह नहीं होती कि पुश सेवा कौन है. हम सही फ़ॉर्मैट में अनुरोध कर सकते हैं और पुश मैसेज भेजे जाने की उम्मीद कर सकते हैं. इसका नुकसान यह है कि डेवलपर किसी ऐसी पुश सेवा को ईमेल भेज सकते हैं जो भरोसेमंद नहीं है. पेलोड को एन्क्रिप्ट करने से, पुश सेवा भेजे गए डेटा को नहीं पढ़ सकती. सिर्फ़ ब्राउज़र इस जानकारी को डिक्रिप्ट कर सकता है. इससे उपयोगकर्ता का डेटा सुरक्षित रहता है.

पेलोड को एन्क्रिप्ट (सुरक्षित) करने के तरीके के बारे में, मैसेज के लिए एन्क्रिप्ट (सुरक्षित) करने का तरीका लेख में बताया गया है.

किसी पुश मैसेज पेलोड को एन्क्रिप्ट (सुरक्षित) करने के खास तरीकों के बारे में जानने से पहले, हमें कुछ तकनीकों के बारे में बताना होगा. इनका इस्तेमाल एन्क्रिप्ट (सुरक्षित) करने की प्रक्रिया के दौरान किया जाएगा. (पुश एन्क्रिप्शन पर अपने बेहतरीन लेख के लिए, Mat Scales को हैट के बारे में शानदार टिप.)

ECDH और HKDF

ECDH और HKDF, दोनों का इस्तेमाल एन्क्रिप्ट (सुरक्षित) करने की पूरी प्रक्रिया में किया जाता है. साथ ही, जानकारी को एन्क्रिप्ट (सुरक्षित) करने के मकसद से इसके फ़ायदे भी मिलते हैं.

ECDH: एलिप्टिक कर्व डिफ़ी-हेलमैन की एक्सचेंज

मान लें कि आपके पास दो लोग हैं, जो जानकारी शेयर करना चाहते हैं, ऐलिस और बॉब. ऐलिस और बॉब, दोनों की अपनी सार्वजनिक और निजी कुंजियां हैं. ऐलिस और बॉब एक-दूसरे के साथ अपनी सार्वजनिक कुंजियां शेयर करते हैं.

ईसीडीएच की मदद से जनरेट की गई कुंजियों की खासियत यह है कि ऐलिस अपनी निजी कुंजी और बॉब की सार्वजनिक कुंजी का इस्तेमाल करके, सीक्रेट वैल्यू 'X' बना सकती हैं. बॉब भी ऐसा ही कर सकता है. इसके लिए, वह अपनी निजी कुंजी और ऐलिस की सार्वजनिक कुंजी का इस्तेमाल करके, स्वतंत्र रूप से 'X' वैल्यू बना सकता है. इससे 'X' एक राज़ हो जाता है और ऐलिस और बॉब को सिर्फ़ अपनी सार्वजनिक कुंजी शेयर करनी पड़ती है. अब बॉब और ऐलिस अपने बीच के मैसेज को एन्क्रिप्ट और डिक्रिप्ट करने के लिए 'X' का इस्तेमाल कर सकते हैं.

मेरी जानकारी के हिसाब से, ECDH में कर्व के उन एट्रिब्यूट के बारे में बताया गया है जिनसे शेयर किए गए सीक्रेट 'X' को बनाने की इस "सुविधा" को अनुमति मिलती है.

यह ECDH के बारे में काफ़ी जानकारी है. अगर आपको इस बारे में ज़्यादा जानकारी चाहिए, तो मेरा सुझाव है कि आप यह वीडियो देखें.

कोड की बात करें, तो ज़्यादातर भाषाओं / प्लैटफ़ॉर्म में लाइब्रेरी मौजूद होती हैं, ताकि इन कुंजियों को आसानी से जनरेट किया जा सके.

नोड में हम ये काम करेंगे:

const keyCurve = crypto.createECDH('prime256v1');
keyCurve.generateKeys();

const publicKey = keyCurve.getPublicKey();
const privateKey = keyCurve.getPrivateKey();

HKDF: एचएमएसी आधारित की डेरिवेशन फ़ंक्शन

Wikipedia में HKDF के बारे में कम शब्दों में जानकारी दी गई है:

HKDF, कुंजी पाने का एक एचएमएसी फ़ंक्शन है. यह किसी भी कमज़ोर कुंजी वाले कॉन्टेंट को क्रिप्टोग्राफ़िक तौर पर मज़बूत कुंजी सामग्री में बदल देता है. उदाहरण के लिए, इसका इस्तेमाल डिफ़ी हेलमैन के शेयर किए गए सीक्रेट को मुख्य कॉन्टेंट में बदलने के लिए किया जा सकता है. यह कॉन्टेंट एन्क्रिप्शन, इंटेग्रिटी जांच या पुष्टि करने के लिए सही होता है.

असल में, HKDF वह इनपुट लेगा जो खास तरह से सुरक्षित नहीं है और उसे ज़्यादा सुरक्षित बनाएगा.

इस एन्क्रिप्शन को परिभाषित करने के लिए ज़रूरी है कि हमारे हैश एल्गोरिदम के तौर पर SHA-256 का इस्तेमाल किया जाए और वेब पुश में HKDF के लिए मिलने वाली कुंजी, 256 बिट (32 बाइट) से ज़्यादा लंबी नहीं होनी चाहिए.

नोड में इसे इस तरह लागू किया जा सकता है:

// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
  // Extract
  const keyHmac = crypto.createHmac('sha256', salt);
  keyHmac.update(ikm);
  const key = keyHmac.digest();

  // Expand
  const infoHmac = crypto.createHmac('sha256', key);
  infoHmac.update(info);

  // A one byte long buffer containing only 0x01
  const ONE_BUFFER = new Buffer(1).fill(1);
  infoHmac.update(ONE_BUFFER);

  return infoHmac.digest().slice(0, length);
}

इस उदाहरण कोड के लिए, मैट स्केल के लेख पर हैट टिप.

इसमें ECDH और एचकेडीएफ़ के बारे में कम शब्दों में जानकारी दी गई है.

ECDH, सार्वजनिक कुंजियों को शेयर करने और एक शेयर किए गए सीक्रेट जनरेट करने का सुरक्षित तरीका है. HKDF असुरक्षित सामग्री को लेकर उसे सुरक्षित बनाने का एक तरीका है.

इसका इस्तेमाल, हमारे पेलोड को एन्क्रिप्ट (सुरक्षित) करने के दौरान किया जाएगा. आगे आइए देखते हैं कि हम इनपुट के रूप में क्या लेते हैं और इसे कैसे एन्क्रिप्ट किया जाता है.

इनपुट

जब हम पेलोड वाले किसी उपयोगकर्ता को पुश मैसेज भेजना चाहते हैं, तो हमें तीन इनपुट की ज़रूरत होती है:

  1. खुद पेलोड.
  2. PushSubscription का auth सीक्रेट.
  3. PushSubscription से मिली p256dh कुंजी.

हमने देखा है कि PushSubscription से auth और p256dh की वैल्यू फिर से पाई जा रही हैं. हालांकि, आपको याद दिला दें कि सदस्यता के लिए हमें इन वैल्यू की ज़रूरत होगी:

subscription.toJSON().keys.auth;
subscription.toJSON().keys.p256dh;

subscription.getKey('auth');
subscription.getKey('p256dh');

auth वैल्यू को सीक्रेट के तौर पर माना जाना चाहिए. साथ ही, इसे आपके ऐप्लिकेशन के बाहर शेयर नहीं किया जाना चाहिए.

p256dh कुंजी एक सार्वजनिक कुंजी होती है. इसे क्लाइंट की सार्वजनिक कुंजी भी कहा जाता है. यहां हम p256dh को सदस्यता सार्वजनिक कुंजी के तौर पर देखेंगे. सदस्यता के लिए सार्वजनिक कुंजी, ब्राउज़र से जनरेट होती है. ब्राउज़र, निजी कुंजी को सीक्रेट रखेगा और इसका इस्तेमाल पेलोड को डिक्रिप्ट करने के लिए किया जाएगा.

इनपुट के तौर पर auth, p256dh, और payload इन तीन वैल्यू की ज़रूरत होती है. इन्हें एन्क्रिप्ट (सुरक्षित) करने की प्रोसेस का नतीजा होता है. इसमें एन्क्रिप्ट (सुरक्षित) किया गया पेलोड, सॉल्ट वैल्यू, और एक सार्वजनिक कुंजी होती है जिसका इस्तेमाल सिर्फ़ डेटा को एन्क्रिप्ट करने के लिए किया जाता है.

नमक

सॉल्ट, 16 बाइट का रैंडम डेटा होना चाहिए. NodeJS में हम सॉल्ट बनाने के लिए ये काम करेंगे:

const salt = crypto.randomBytes(16);

सार्वजनिक / निजी कुंजियां

सार्वजनिक और निजी कुंजियां, P-256 एलिप्टिक कर्व का इस्तेमाल करके जनरेट की जानी चाहिए. हम नोड में इस तरह से काम करेंगे:

const localKeysCurve = crypto.createECDH('prime256v1');
localKeysCurve.generateKeys();

const localPublicKey = localKeysCurve.getPublicKey();
const localPrivateKey = localKeysCurve.getPrivateKey();

हम इन कुंजियों को "लोकल बटन" के तौर पर कहेंगे. इनका इस्तेमाल सिर्फ़ एन्क्रिप्ट (सुरक्षित) करने के लिए किया जाता है और ऐप्लिकेशन सर्वर कुंजियों के लिए कुछ भी नहीं किया जाता.

इनपुट के तौर पर पेलोड, ऑथराइज़ेशन सीक्रेट और सदस्यता वाली सार्वजनिक कुंजी की मदद से, जनरेट किए गए नए सॉफ़्टवेयर और लोकल कुंजियों के सेट की मदद से, हम एन्क्रिप्ट (सुरक्षित) करने के कुछ तरीके तैयार करने के लिए तैयार हैं.

शेयर किया गया सीक्रेट

सबसे पहले, सदस्यता वाली सार्वजनिक कुंजी और हमारी नई निजी कुंजी का इस्तेमाल करके, एक शेयर किया गया सीक्रेट तैयार करें. क्या आपको ऐलिस और बॉब के साथ हुई ECDH की जानकारी याद है? ठीक ऐसे ही).

const sharedSecret = localKeysCurve.computeSecret(
  subscription.keys.p256dh,
  'base64',
);

इसका इस्तेमाल, स्यूडो रैंडम की (पीआरके) का हिसाब लगाने के लिए अगले चरण में किया जाता है.

बदली हुई कोई भी कुंजी

स्यूडो रैंडम की (पीआरके), पुश सदस्यता की पुष्टि करने वाले सीक्रेट और अभी-अभी बनाए गए शेयर किए गए सीक्रेट को मिला-जुलाकर है.

const authEncBuff = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(subscription.keys.auth, sharedSecret, authEncBuff, 32);

आपको लग रहा होगा कि Content-Encoding: auth\0 स्ट्रिंग किस लिए काम करती है. कम शब्दों में कहें, तो इसका मकसद साफ़ तौर पर नहीं बताया जाता है. हालांकि, ब्राउज़र किसी आने वाले मैसेज को डिक्रिप्ट कर सकते हैं और कॉन्टेंट को कोड में बदलने की उम्मीद कर सकते हैं. \0, बफ़र के आखिर में 0 वैल्यू के साथ एक बाइट जोड़ता है. ऐसा तब होता है, जब ब्राउज़र आपके मैसेज को डिक्रिप्ट कर सकते हैं और कॉन्टेंट को कोड में बदलने के लिए बहुत ज़्यादा बाइट की ज़रूरत होगी. इसके बाद, 0 वैल्यू वाला बाइट होगा और उसके बाद एन्क्रिप्ट किया गया डेटा.

हमारी बदली हुई रैंडम कुंजी HKDF के ज़रिए, सिर्फ़ पुष्टि करने, शेयर किए गए सीक्रेट, और कोड में बदलने की जानकारी का एक हिस्सा चला रही है (यानी इसे क्रिप्टोग्राफ़िक तरीके से मज़बूत बनाना).

संदर्भ

"context" बाइट का एक सेट है, जिसका इस्तेमाल बाद में एन्क्रिप्ट (सुरक्षित) करने के ब्राउज़र में दो वैल्यू का हिसाब लगाने के लिए किया जाता है. यह खास तौर पर बाइट का एक कलेक्शन है, जिसमें सदस्यता के लिए सार्वजनिक पासकोड और लोकल पब्लिक पासकोड शामिल हैं.

const keyLabel = new Buffer('P-256\0', 'utf8');

// Convert subscription public key into a buffer.
const subscriptionPubKey = new Buffer(subscription.keys.p256dh, 'base64');

const subscriptionPubKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = subscriptionPubKey.length;

const localPublicKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = localPublicKey.length;

const contextBuffer = Buffer.concat([
  keyLabel,
  subscriptionPubKeyLength.buffer,
  subscriptionPubKey,
  localPublicKeyLength.buffer,
  localPublicKey,
]);

फ़ाइनल कॉन्टेक्स्ट बफ़र, एक लेबल होता है. यह 'सदस्यता सार्वजनिक कुंजी' में बाइट की संख्या होती है. इसके बाद कुंजी, फिर बाइट की संख्या, और उसके बाद की कुंजी का इस्तेमाल किया जाता है.

इस कॉन्टेक्स्ट वैल्यू की मदद से, हम इसका इस्तेमाल नॉन्स और कॉन्टेंट को एन्क्रिप्ट करने वाली कुंजी (सीईके) बनाने के लिए कर सकते हैं.

कॉन्टेंट को एन्क्रिप्ट (सुरक्षित) करने की कुंजी और नॉन्स

nonce वह वैल्यू है जो रीप्ले हमले को रोकती है, क्योंकि इसका इस्तेमाल सिर्फ़ एक बार किया जाना चाहिए.

कॉन्टेंट को एन्क्रिप्ट करने वाली कुंजी (सीईके), वह कुंजी है जिसका इस्तेमाल हमारे पेलोड को एन्क्रिप्ट (सुरक्षित) करने के लिए किया जाएगा.

सबसे पहले हमें नॉन्स और सीईके के लिए डेटा की बाइट बनानी होगी, जो सिर्फ़ कॉन्टेंट को कोड में बदलने वाली स्ट्रिंग है. इसके बाद, कॉन्टेक्स्ट बफ़र होता है, जिसका हमने अभी हिसाब लगाया है:

const nonceEncBuffer = new Buffer('Content-Encoding: nonce\0', 'utf8');
const nonceInfo = Buffer.concat([nonceEncBuffer, contextBuffer]);

const cekEncBuffer = new Buffer('Content-Encoding: aesgcm\0');
const cekInfo = Buffer.concat([cekEncBuffer, contextBuffer]);

यह जानकारी HKDF के ज़रिए भेजी जाती है, जिसमें सॉल्ट और PRK को nonceInfo और cekInfo के साथ मिलाया जाता है:

// The nonce should be 12 bytes long
const nonce = hkdf(salt, prk, nonceInfo, 12);

// The CEK should be 16 bytes long
const contentEncryptionKey = hkdf(salt, prk, cekInfo, 16);

इससे हमें नॉन्स और कॉन्टेंट को एन्क्रिप्ट (सुरक्षित) करने वाली कुंजी मिलती है.

एन्क्रिप्ट (सुरक्षित) करने की प्रोसेस शुरू करें

अब हमारे पास कॉन्टेंट को एन्क्रिप्ट (सुरक्षित) करने की कुंजी है, इसलिए हम पेलोड को एन्क्रिप्ट (सुरक्षित) कर सकते हैं.

हम कॉन्टेंट एन्क्रिप्शन कुंजी का इस्तेमाल करके एक AES128 साइफ़र बनाते हैं. साथ ही, नॉन्स इनिशलाइज़ेशन वेक्टर होता है.

नोड में यह इस तरह किया जाता है:

const cipher = crypto.createCipheriv(
  'id-aes128-GCM',
  contentEncryptionKey,
  nonce,
);

अपने पेलोड को एन्क्रिप्ट करने से पहले, हमें यह तय करना होगा कि पेलोड के आगे कितनी पैडिंग (जगह) जोड़नी है. हम पैडिंग (जगह) जोड़ना चाहते हैं. ऐसा इसलिए है, क्योंकि यह पेलोड के साइज़ के हिसाब से, ताक-झांक करने वालों को मैसेज के "टाइप" तय करने के जोखिम से बचाता है.

किसी भी अतिरिक्त पैडिंग की लंबाई को दिखाने के लिए, आपको दो बाइट पैडिंग (जगह) जोड़नी होगी.

उदाहरण के लिए, अगर आपने कोई पैडिंग (जगह) नहीं जोड़ी है, तो आपके पास वैल्यू 0 वाले दो बाइट होंगे.इसका मतलब है कि कोई भी पैडिंग (जगह) मौजूद नहीं है. इन दो बाइट के बाद, आपको पेलोड पढ़ना होगा. अगर आपने पांच बाइट पैडिंग (जगह) जोड़ी है, तो पहले दो बाइट की वैल्यू 5 होगी. इससे उपभोक्ता पांच बाइट और डेटा पढ़ेगा और फिर पेलोड को पढ़ना शुरू करेगा.

const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeros, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);

इसके बाद, हम इस साइफ़र के ज़रिए पैडिंग और पेलोड चलाते हैं.

const result = cipher.update(Buffer.concat(padding, payload));
cipher.final();

// Append the auth tag to the result -
// https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
const encryptedPayload = Buffer.concat([result, cipher.getAuthTag()]);

अब हमारे पास एन्क्रिप्ट (सुरक्षित) किया गया पेलोड है. हां!

बाकी सब यह तय करना है कि इस पेलोड को पुश सेवा तक कैसे भेजा जाए.

एन्क्रिप्ट (सुरक्षित) किए गए पेलोड हेडर और बॉडी

एन्क्रिप्ट (सुरक्षित) किए गए इस पेलोड को पुश सेवा पर भेजने के लिए, हमें अपने पोस्ट अनुरोध में कुछ अलग-अलग हेडर तय करने होंगे.

एन्क्रिप्ट (सुरक्षित) करने का तरीका बताने वाला हेडर

'एन्क्रिप्ट (सुरक्षित) करने के तरीके' हेडर में, पेलोड को एन्क्रिप्ट (सुरक्षित) करने के लिए इस्तेमाल किया जाने वाला साल्ट होना चाहिए.

16 बाइट वाला सॉल्ट को base64 यूआरएल सुरक्षित तरीके से कोड में बदला जाना चाहिए और उसे एन्क्रिप्शन हेडर में जोड़ा जाना चाहिए, जैसे:

Encryption: salt=[URL Safe Base64 Encoded Salt]

क्रिप्टो-की हेडर

हमने देखा कि Crypto-Key हेडर का इस्तेमाल 'ऐप्लिकेशन सर्वर कुंजियां' सेक्शन में, सार्वजनिक ऐप्लिकेशन सर्वर कुंजी को शामिल करने के लिए किया जाता है.

इस हेडर का इस्तेमाल उस लोकल सार्वजनिक कुंजी को शेयर करने के लिए भी किया जाता है जिसका इस्तेमाल पेलोड को एन्क्रिप्ट (सुरक्षित) करने के लिए किया जाता है.

मिलने वाला हेडर कुछ ऐसा दिखेगा:

Crypto-Key: dh=[URL Safe Base64 Encoded Local Public Key String]; p256ecdsa=[URL Safe Base64 Encoded Public Application Server Key]

कॉन्टेंट का टाइप, लंबाई, और कोड में बदलने का तरीका बताने वाले हेडर

Content-Length हेडर, एन्क्रिप्ट (सुरक्षित) किए गए पेलोड में बाइट की संख्या होती है. 'कॉन्टेंट-टाइप' और 'कॉन्टेंट-कोड में बदलने का तरीका' हेडर, तय वैल्यू होती हैं. इस बारे में नीचे बताया गया है.

Content-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'

इन हेडर के सेट होने पर, हमें एन्क्रिप्ट (सुरक्षित) किए गए पेलोड को अनुरोध के मुख्य हिस्से के तौर पर भेजना होगा. ध्यान दें कि Content-Type को application/octet-stream पर सेट किया गया है. इसकी वजह यह है कि एन्क्रिप्ट (सुरक्षित) किया गया पेलोड, बाइट स्ट्रीम के तौर पर भेजा जाना चाहिए.

NodeJS में हम ऐसा करेंगे:

const pushRequest = https.request(httpsOptions, function(pushResponse) {
pushRequest.write(encryptedPayload);
pushRequest.end();

और हेडर?

हमने JWT / ऐप्लिकेशन सर्वर कुंजियों के लिए इस्तेमाल किए जाने वाले हेडर (यानी पुश सेवा की मदद से ऐप्लिकेशन को पहचानने का तरीका) के बारे में जानकारी दी है. साथ ही, हमने एन्क्रिप्ट किए गए पेलोड भेजने के लिए इस्तेमाल किए जाने वाले हेडर के बारे में भी जानकारी दी है.

कुछ ऐसे अतिरिक्त हेडर भी होते हैं जिनका इस्तेमाल पुश सेवाओं को, भेजे गए ईमेल के व्यवहार में बदलाव करने के लिए किया जाता है. इनमें से कुछ हेडर ज़रूरी होते हैं, जबकि कुछ ज़रूरी नहीं होते.

TTL (टीटीएल) हेडर

ज़रूरी

TTL (या लाइव होने का समय) एक पूर्णांक है. इससे यह तय होता है कि आपका पुश मैसेज, डिलीवर होने से पहले पुश सेवा पर बना रहे. TTL की समयसीमा खत्म होने पर, मैसेज को पुश सेवा सूची से हटा दिया जाएगा और वह डिलीवर नहीं होगा.

TTL: [Time to live in seconds]

अगर TTL को शून्य पर सेट किया जाता है, तो पुश सेवा मैसेज को तुरंत डिलीवर करने की कोशिश करेगी लेकिन अगर डिवाइस तक पहुंचा न जा सके, तो आपका मैसेज तुरंत पुश सर्विस सूची से हटा दिया जाएगा.

तकनीकी तौर पर, पुश सेवा किसी पुश मैसेज के TTL को कम कर सकती है. ऐसा होने पर, पुश सेवा से मिले रिस्पॉन्स में TTL हेडर की जांच करके पता लगाया जा सकता है कि ऐसा हुआ है या नहीं.

विषय

ज़रूरी नहीं

विषय ऐसी स्ट्रिंग होती हैं जिनका इस्तेमाल, बाद में न दिखने वाले मैसेज की जगह नया मैसेज दिखाने के लिए किया जा सकता है. हालांकि, इसके लिए ज़रूरी है कि उन मैसेज के विषय के नाम मेल खाते हों.

यह उन स्थितियों में मददगार है जहां डिवाइस के ऑफ़लाइन होने पर कई मैसेज भेजे जाते हैं और आप यह चाहते हैं कि डिवाइस चालू होने पर ही उपयोगकर्ता को सबसे नया मैसेज दिखे.

अत्यावश्यकता

ज़रूरी नहीं

बहुत ज़रूरी होने का मतलब है कि पुश सेवा को कोई मैसेज उपयोगकर्ता के लिए कितना ज़रूरी है. इसका इस्तेमाल पुश सेवा, उपयोगकर्ता के डिवाइस की बैटरी लाइफ़ बचाने के लिए कर सकती है. ऐसा, बैटरी कम होने पर ही ज़रूरी मैसेज के लिए किया जा सकता है.

हेडर की वैल्यू इस तरह तय की गई है. डिफ़ॉल्ट वैल्यू normal है.

Urgency: [very-low | low | normal | high]

सब कुछ एक साथ

अगर इन सभी चीज़ों के काम करने के तरीके के बारे में आपका कोई और सवाल है, तो आप हमेशा यह देख सकते हैं कि लाइब्रेरी the web-push-libs संगठन पर पुश मैसेज को कैसे ट्रिगर करती हैं.

एन्क्रिप्ट (सुरक्षित) किया गया पेलोड और ऊपर दिए गए हेडर मिलने के बाद, आपको PushSubscription में endpoint को बस एक पोस्ट अनुरोध करना होगा.

तो हम इस पोस्ट अनुरोध के जवाब का क्या करते हैं?

पुश सेवा से मिला जवाब

पुश सेवा के लिए अनुरोध करने के बाद, आपको रिस्पॉन्स के स्टेटस कोड की जांच करनी होगी. इससे आपको पता चलेगा कि अनुरोध पूरा हुआ या नहीं.

स्थिति कोड ब्यौरा
201 बनाया गया. पुश मैसेज भेजने का अनुरोध मिल गया है और उसे स्वीकार कर लिया गया है.
429 बहुत सारे अनुरोध. इसका मतलब है कि पुश सेवा के साथ आपके ऐप्लिकेशन सर्वर की दर तय सीमा तक पहुंच गई है. पुश सेवा में 'फिर से कोशिश करें' हेडर शामिल होना चाहिए, ताकि यह पता चल सके कि दूसरा अनुरोध कितने समय तक किया जा सकता है.
400 अनुरोध अमान्य है. आम तौर पर, इसका मतलब है कि आपका कोई हेडर अमान्य है या उसे गलत तरीके से फ़ॉर्मैट किया गया है.
404 नहीं मिला. इससे पता चलता है कि सदस्यता की समयसीमा खत्म हो गई है और इसे इस्तेमाल नहीं किया जा सकता. इस मामले में, आपको `PushSubscription` को मिटाना चाहिए और तब तक इंतज़ार करना चाहिए, जब तक क्लाइंट उपयोगकर्ता की फिर से सदस्यता नहीं लेता.
410 हो गया. सदस्यता अब मान्य नहीं है और इसे ऐप्लिकेशन सर्वर से हटा दिया जाना चाहिए. `PushSubscription` पर मौजूद `unsubscribe()` को कॉल करके, इसे फिर से जनरेट किया जा सकता है.
413 पेलोड का साइज़ बहुत बड़ा है. किसी पुश सेवा के लिए काम करने वाला पेलोड कम से कम 4096 बाइट (या 4 केबी) होना चाहिए.

आगे कहां जाना है

कोड लैब