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

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

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

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

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

यह वेब पुश की सुविधा नहीं है और मुझे एन्क्रिप्ट (सुरक्षित) करने का कोई तरीका नहीं पता. हालाँकि, आइए हर एक हिस्से को देखते हैं क्योंकि इससे आसानी से समझा जा सकता है कि ये लाइब्रेरी क्या काम कर रही हैं.

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

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

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

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

  1. ऐप्लिकेशन सर्वर, अपनी निजी ऐप्लिकेशन कुंजी से JSON की कुछ जानकारी पर साइन करता है.
  2. हस्ताक्षर की गई यह जानकारी, POST अनुरोध में हेडर के तौर पर पुश सेवा को भेजी जाती है.
  3. पुश सेवा, pushManager.subscribe() से मिली सेव की गई सार्वजनिक कुंजी का इस्तेमाल करती है. इससे यह पता चलता है कि मिली जानकारी पर, सार्वजनिक कुंजी से जुड़ी निजी कुंजी से हस्ताक्षर किया गया है या नहीं. याद रखें: सार्वजनिक पासकोड, 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 वैल्यू, "ऑडियंस" होती है. इसका मतलब है कि JWT किसके लिए है. वेब पुश के लिए, ऑडियंस पुश सेवा होती है. इसलिए, हम इसे पुश सेवा के ऑरिजिन पर सेट करते हैं.

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

Node.js में समयसीमा खत्म होने की तारीख, इन चीज़ों का इस्तेमाल करके सेट की जाती है:

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

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

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

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

तीसरी स्ट्रिंग, हस्ताक्षर, पहली दो स्ट्रिंग (JWT की जानकारी और JWT डेटा) को बिंदु वर्ण से जोड़ने पर बनती है. इसे हम "बिना हस्ताक्षर वाला टोकन" कहेंगे.

हस्ताक्षर करने की प्रोसेस के लिए, ES256 का इस्तेमाल करके "बिना हस्ताक्षर वाला टोकन" एन्क्रिप्ट करना ज़रूरी है. JWT के स्पेसिफ़िकेशन के मुताबिक, ES256 का मतलब है "P-256 कर्व और 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]

पेलोड एन्क्रिप्शन

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

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

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

मैसेज एन्क्रिप्शन के लिए स्पेसिफ़िकेशन में, पेलोड को एन्क्रिप्ट करने के बारे में बताया गया है.

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

ईसीडीएच और HKDF

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

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

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

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

मेरी जानकारी के मुताबिक, ईसीडीएच, कर्व की प्रॉपर्टी तय करता है. इन प्रॉपर्टी की मदद से, शेयर किया गया गुप्त पासवर्ड 'X' बनाने की "सुविधा" मिलती है.

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

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

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

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

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

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

Wikipedia पर 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);
}

इस उदाहरण के कोड के लिए, Mat Scale के लेख का इस्तेमाल किया गया है.

इसमें आम तौर पर ECDH और HKDF शामिल हैं.

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

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

इनपुट

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

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

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

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

हम इन कुंजियों को "स्थानीय कुंजियां" कहेंगे. इनका इस्तेमाल सिर्फ़ एन्क्रिप्शन के लिए किया जाता है. इनका ऐप्लिकेशन सर्वर कुंजियों से कोई संबंध नहीं होता.

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

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

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

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 के ज़रिए सिर्फ़ पुष्टि और शेयर की गई सीक्रेट जानकारी को चला रही है (यानी इसे क्रिप्टोग्राफ़िक तरीके से बेहतर बनाया जा रहा है).

संदर्भ

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

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

आखिरी कॉन्टेक्स्ट बफ़र एक लेबल होता है. इसमें सदस्यता के सार्वजनिक पासकोड में मौजूद बाइट की संख्या के बाद, पासकोड होता है. इसके बाद, स्थानीय सार्वजनिक पासकोड में मौजूद बाइट की संख्या के बाद, पासकोड होता है.

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

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

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

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

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

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 सिफर बनाते हैं. साथ ही, नॉन्स को इनिशलाइज़ेशन वेक्टर के तौर पर इस्तेमाल करते हैं.

Node में, यह इस तरह किया जाता है:

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-Type' और 'Content-Encoding' हेडर की वैल्यू में बदलाव नहीं किया जा सकता. यह नीचे दिखाया गया है.

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]

सभी चीज़ें एक साथ

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

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

तो इस POST अनुरोध के जवाब का क्या करें?

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

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

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

एचटीटीपी स्टेटस कोड के बारे में ज़्यादा जानने के लिए, वेब पुश स्टैंडर्ड (RFC8030) भी पढ़ें.

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

कोड लैब