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

Mat Scales

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

अब Chrome 50 (और डेस्कटॉप पर Firefox के मौजूदा वर्शन में) में, आप पुश के साथ कुछ आर्बिट्रेरी डेटा भेज सकते हैं ताकि क्लाइंट अतिरिक्त अनुरोध करने से बच सके. हालांकि, बड़ी ताकत के साथ ज़िम्मेदारी भी बढ़ जाती है. इसलिए, सभी पेलोड डेटा को एन्क्रिप्ट करना ज़रूरी है.

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

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

क्लाइंट-साइड के बदलाव

अगर आपने पहले ही पेलोड के बिना पुश नोटिफ़िकेशन लागू कर लिए हैं, तो आपको क्लाइंट-साइड पर सिर्फ़ दो छोटे बदलाव करने होंगे.

पहला, सदस्यता की जानकारी को अपने बैकएंड सर्वर पर भेजते समय, आपको कुछ और जानकारी इकट्ठा करनी होगी. अगर आपने अपने सर्वर पर भेजने के लिए, PushSubscription ऑब्जेक्ट को सीरियलाइज़ करने के लिए, पहले से ही JSON.stringify() का इस्तेमाल किया है, तो आपको कुछ भी बदलने की ज़रूरत नहीं है. सदस्यता में अब कुंजी प्रॉपर्टी में कुछ अतिरिक्त डेटा होगा.

> JSON.stringify(subscription)
{"endpoint":"https://android.googleapis.com/gcm/send/f1LsxkKphfQ:APA91bFUx7ja4BK4JVrNgVjpg1cs9lGSGI6IMNL4mQ3Xe6mDGxvt_C_gItKYJI9CAx5i_Ss6cmDxdWZoLyhS2RJhkcv7LeE6hkiOsK6oBzbyifvKCdUYU7ADIRBiYNxIVpLIYeZ8kq_A",
"keys":{"p256dh":"BLc4xRzKlKORKWlbdgFaBrrPK3ydWAHo4M0gs0i1oEKgPpWC5cW8OCzVrOQRv-1npXRWk8udnW3oYhIO4475rds=",
"auth":"5I2Bu2oKdyy9CwL8QVF0NQ=="}}

p256dh और auth, दोनों वैल्यू को Base64 के एक वैरिएंट में एन्कोड किया गया है. इसे यूआरएल-Safe Base64 कहा जाएगा.

अगर आपको सीधे बाइट की जानकारी चाहिए, तो सदस्यता पर नए getKey() तरीके का इस्तेमाल करें. यह तरीका, पैरामीटर को ArrayBuffer के तौर पर दिखाता है. आपको auth और p256dh, दोनों पैरामीटर की ज़रूरत है.

> new Uint8Array(subscription.getKey('auth'));
[228, 141, 129, ...] (16 bytes)

> new Uint8Array(subscription.getKey('p256dh'));
[4, 183, 56, ...] (65 bytes)

दूसरा बदलाव, push इवेंट के ट्रिगर होने पर एक नई डेटा प्रॉपर्टी होती है. इसमें, मिले डेटा को पार्स करने के लिए, सिंक्रोनस तरीके इस्तेमाल किए जाते हैं. जैसे, .text(), .json(), .arrayBuffer(), और .blob().

self.addEventListener('push', function(event) {
  if (event.data) {
    console.log(event.data.json());
  }
});

सर्वर साइड में हुए बदलाव

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

इस बारे में जानकारी अपेक्षाकृत जटिल है. एन्क्रिप्शन से जुड़ी किसी भी चीज़ के लिए, खुद की लाइब्रेरी बनाने के बजाय, किसी ऐसी लाइब्रेरी का इस्तेमाल करना बेहतर होता है जिसे लगातार अपडेट किया जा रहा हो. Chrome टीम ने Node.js के लिए एक लाइब्रेरी पब्लिश की है. जल्द ही, यह लाइब्रेरी अन्य भाषाओं और प्लैटफ़ॉर्म के लिए भी उपलब्ध होगी. यह एन्क्रिप्शन और वेब पुश प्रोटोकॉल, दोनों को मैनेज करता है. इससे Node.js सर्वर से पुश मैसेज भेजना webpush.sendWebPush(message, subscription) जितना आसान हो जाता है.

हालांकि, हम लाइब्रेरी का इस्तेमाल करने का सुझाव ज़रूर देते हैं, लेकिन यह एक नई सुविधा है और ऐसी कई लोकप्रिय भाषाएं हैं जो अभी तक कोई लाइब्रेरी नहीं हैं. अगर आपको इसे खुद लागू करना है, तो यहां इसकी जानकारी दी गई है.

हम Node-flavored JavaScript का इस्तेमाल करके एल्गोरिदम के बारे में बताएंगे. हालांकि, किसी भी भाषा में बुनियादी सिद्धांत एक जैसे होने चाहिए.

इनपुट

किसी मैसेज को एन्क्रिप्ट करने के लिए, हमें सबसे पहले क्लाइंट से मिले सदस्यता ऑब्जेक्ट से दो चीज़ें हासिल करनी होंगी. अगर आपने क्लाइंट पर JSON.stringify() का इस्तेमाल किया है और उसे अपने सर्वर पर भेजा है, तो क्लाइंट की सार्वजनिक कुंजी keys.p256dh फ़ील्ड में सेव की जाती है. वहीं, पुष्टि करने के लिए शेयर किया गया सीक्रेट कोड keys.auth फ़ील्ड में सेव किया जाता है. जैसा कि ऊपर बताया गया है, ये दोनों यूआरएल-सेफ़ Base64 कोड में बदले जाएंगे. क्लाइंट की सार्वजनिक कुंजी का बाइनरी फ़ॉर्मैट, बिना कंप्रेस किए हुए P-256 एलिप्टिक कर्व पॉइंट होता है.

const clientPublicKey = new Buffer(subscription.keys.p256dh, 'base64');
const clientAuthSecret = new Buffer(subscription.keys.auth, 'base64');

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

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

हमें कुछ नया डेटा भी जनरेट करना होगा. हमें 16-बाइट का ऐसा सॉल्ट चाहिए जो क्रिप्टोग्राफ़िक तरीके से सुरक्षित हो और एलिप्टिक कर्व वाली सार्वजनिक/निजी कुंजियों का जोड़ा हो. पुश एन्क्रिप्शन स्पेसिफ़िकेशन में इस्तेमाल किए गए खास कर्व को P-256 या prime256v1 कहा जाता है. सबसे अच्छी सुरक्षा के लिए, हर बार मैसेज एन्क्रिप्ट (सुरक्षित) करते समय, कुंजी का जोड़ा फिर से जनरेट किया जाना चाहिए. साथ ही, आपको कभी भी नमक का फिर से इस्तेमाल नहीं करना चाहिए.

ECDH

आइए, थोड़ी देर के लिए एलिप्टिक कर्व क्रिप्टोग्राफ़ी की एक खास प्रॉपर्टी के बारे में बात करते हैं. वैल्यू पाने के लिए, आपकी निजी कुंजी को किसी दूसरे व्यक्ति की सार्वजनिक कुंजी के साथ जोड़ने की प्रक्रिया काफ़ी आसान है. तो क्या? अगर दूसरा पक्ष अपनी निजी कुंजी और आपकी सार्वजनिक कुंजी लेता है, तो उससे भी बिलकुल वही मान मिलेगा!

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

const crypto = require('crypto');

const salt = crypto.randomBytes(16);

// Node has ECDH built-in to the standard crypto library. For some languages
// you may need to use a third-party library.
const serverECDH = crypto.createECDH('prime256v1');
const serverPublicKey = serverECDH.generateKeys();
const sharedSecret = serverECDH.computeSecret(clientPublicKey);

HKDF

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

इसके काम करने के तरीके का एक नतीजा यह है कि इससे आपको किसी भी बिट की संख्या को सीक्रेट करके, 255 गुना तक किसी भी साइज़ का दूसरा सीक्रेट बनाने में मदद मिलती है. साथ ही, किसी भी हैशिंग एल्गोरिदम से बने हैश के साथ भी ऐसा किया जा सकता है. पुश के लिए, खास जानकारी के लिए हमें SHA-256 का इस्तेमाल करना होता है, जिसकी हैश लंबाई 32 बाइट (256 बिट) होती है.

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

मैंने यहां Node के एक वर्शन का कोड शामिल किया है. हालांकि, आरएफ़सी 5869 में जाकर, यह पता लगाया जा सकता है कि यह कोड कैसे काम करता है.

HKDF के इनपुट में, साल्ट, कुछ शुरुआती कुंजी बनाने वाला कॉन्टेंट (ikm), मौजूदा इस्तेमाल के उदाहरण (जानकारी) के लिए स्ट्रक्चर्ड डेटा का एक वैकल्पिक हिस्सा, और मनचाही आउटपुट कुंजी की लंबाई (बाइट में) शामिल होती है.

// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
  if (length > 32) {
    throw new Error('Cannot return keys of more than 32 bytes, ${length} requested');
  }

  // 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);
}

एन्क्रिप्शन पैरामीटर का पता लगाना

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

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

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

कॉन्टेंट एन्क्रिप्शन के लिए जानकारी का टाइप 'aesgcm' है. यह पुश एन्क्रिप्शन के लिए इस्तेमाल किए जाने वाले सिफर का नाम है.

const authInfo = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(clientAuthSecret, sharedSecret, authInfo, 32);

function createInfo(type, clientPublicKey, serverPublicKey) {
  const len = type.length;

  // The start index for each element within the buffer is:
  // value               | length | start    |
  // -----------------------------------------
  // 'Content-Encoding: '| 18     | 0        |
  // type                | len    | 18       |
  // nul byte            | 1      | 18 + len |
  // 'P-256'             | 5      | 19 + len |
  // nul byte            | 1      | 24 + len |
  // client key length   | 2      | 25 + len |
  // client key          | 65     | 27 + len |
  // server key length   | 2      | 92 + len |
  // server key          | 65     | 94 + len |
  // For the purposes of push encryption the length of the keys will
  // always be 65 bytes.
  const info = new Buffer(18 + len + 1 + 5 + 1 + 2 + 65 + 2 + 65);

  // The string 'Content-Encoding: ', as utf-8
  info.write('Content-Encoding: ');
  // The 'type' of the record, a utf-8 string
  info.write(type, 18);
  // A single null-byte
  info.write('\0', 18 + len);
  // The string 'P-256', declaring the elliptic curve being used
  info.write('P-256', 19 + len);
  // A single null-byte
  info.write('\0', 24 + len);
  // The length of the client's public key as a 16-bit integer
  info.writeUInt16BE(clientPublicKey.length, 25 + len);
  // Now the actual client public key
  clientPublicKey.copy(info, 27 + len);
  // Length of our public key
  info.writeUInt16BE(serverPublicKey.length, 92 + len);
  // The key itself
  serverPublicKey.copy(info, 94 + len);

  return info;
}

// Derive the Content Encryption Key
const contentEncryptionKeyInfo = createInfo('aesgcm', clientPublicKey, serverPublicKey);
const contentEncryptionKey = hkdf(salt, prk, contentEncryptionKeyInfo, 16);

// Derive the Nonce
const nonceInfo = createInfo('nonce', clientPublicKey, serverPublicKey);
const nonce = hkdf(salt, prk, nonceInfo, 12);

पैडिंग (जगह)

एक और बात, और मज़ाक़िया और विवादित उदाहरण का समय. मान लें कि आपके बॉस के पास एक सर्वर है, जो कंपनी के स्टॉक की कीमत के साथ हर कुछ मिनटों में उसे पुश मैसेज भेजता है. इसके लिए सादा मैसेज हमेशा 32-बिट वाला पूर्णांक होगा, जिसकी वैल्यू सेंट में होगी. वह कैटरिंग स्टाफ़ के साथ भी एक गुप्त डील करती है. इसका मतलब है कि वे "ब्रेक रूम में डोनट" को डिलीवर करने से पांच मिनट पहले उसे भेज सकते हैं, ताकि वह "इस्तेमाल के लिए उपलब्ध होने पर" वहां जाकर सबसे अच्छा डोनट ले सके.

Web Push के लिए इस्तेमाल किया जाने वाला साइफ़र, एन्क्रिप्ट (सुरक्षित) की गई वैल्यू बनाता है. ये वैल्यू, एन्क्रिप्ट (सुरक्षित) नहीं किए गए इनपुट के मुकाबले 16 बाइट ज़्यादा होती हैं. "ब्रेक रूम में डोनट", स्टॉक की 32-बिट कीमत से लंबा है. इसलिए, कोई भी कर्मचारी मैसेज को डिक्रिप्ट किए बिना, डेटा की लंबाई से यह पता लगा सकता है कि डोनट कब आ रहे हैं.

इस वजह से, वेब पुश प्रोटोकॉल की मदद से, डेटा की शुरुआत में पैडिंग जोड़ी जा सकती है. इसका इस्तेमाल कैसे किया जाए, यह आपके ऐप्लिकेशन पर निर्भर करता है. हालांकि, ऊपर दिए गए उदाहरण में, सभी मैसेज को 32 बाइट तक पैड किया जा सकता है. इससे, मैसेज की लंबाई के आधार पर उन्हें अलग करना मुश्किल हो जाता है.

पैडिंग वैल्यू, 16-बिट बिग-एंडियन इंटिजर होती है. इससे पैडिंग की लंबाई का पता चलता है. इसके बाद, पैडिंग के NUL बाइट होते हैं. इसलिए, कम से कम पैडिंग दो बाइट है - संख्या शून्य 16 बिट में एन्कोड की गई है.

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

जब आपका पुश मैसेज क्लाइंट पर पहुंचेगा, तो ब्राउज़र अपने-आप पैडिंग हटा देगा. इससे आपके क्लाइंट को सिर्फ़ बिना पैडिंग वाला मैसेज मिलेगा.

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

अब हमारे पास एन्क्रिप्शन करने के लिए सभी चीज़ें मौजूद हैं. वेब पुश के लिए ज़रूरी साइफ़र, GCM का इस्तेमाल करके AES128 है. हम कॉन्टेंट एन्क्रिप्ट करने वाली अपनी पासकोड को पासकोड के तौर पर और नॉन्स को इनिशलाइज़ेशन वेक्टर (IV) के तौर पर इस्तेमाल करते हैं.

इस उदाहरण में, हमारा डेटा एक स्ट्रिंग है, लेकिन यह कोई बाइनरी डेटा हो सकता है. हर पोस्ट के लिए, 4078 बाइट से 4096 बाइट तक के पेलोड भेजे जा सकते हैं. इसमें एन्क्रिप्शन की जानकारी के लिए 16 बाइट और पैडिंग के लिए कम से कम दो बाइट होते हैं.

// Create a buffer from our data, in this case a UTF-8 encoded string
const plaintext = new Buffer('Push notification payload!', 'utf8');
const cipher = crypto.createCipheriv('id-aes128-GCM', contentEncryptionKey,
nonce);

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

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

वेब पुश

वाह! अब आपके पास एन्क्रिप्ट (सुरक्षित) किया गया पेलोड है. आपको उपयोगकर्ता की सदस्यता के हिसाब से तय किए गए एंडपॉइंट पर, एचटीटीपी पोस्ट का एक आसान अनुरोध करना होगा.

आपको तीन हेडर सेट करने होंगे.

Encryption: salt=<SALT>
Crypto-Key: dh=<PUBLICKEY>
Content-Encoding: aesgcm

<SALT> और <PUBLICKEY>, एन्क्रिप्शन में इस्तेमाल होने वाली साल्ट और सर्वर की सार्वजनिक कुंजी हैं. इन्हें यूआरएल-सेफ़ Base64 के तौर पर एन्कोड किया गया है.

वेब पुश प्रोटोकॉल का इस्तेमाल करने पर, पीओएसटी का मुख्य हिस्सा सिर्फ़ एन्क्रिप्ट किए गए मैसेज के रॉ बाइट होते हैं. हालांकि, जब तक Chrome और Firebase Cloud Messaging इस प्रोटोकॉल के साथ काम नहीं करते, तब तक अपने मौजूदा JSON पेलोड में डेटा को आसानी से शामिल किया जा सकता है. इसके लिए, यह तरीका अपनाएं.

{
    "registration_ids": [ "…" ],
    "raw_data": "BIXzEKOFquzVlr/1tS1bhmobZ…"
}

rawData प्रॉपर्टी की वैल्यू, एन्क्रिप्ट किए गए मैसेज को base64 कोड में बदला गया होना चाहिए.

डीबग करने वाला टूल / पुष्टि करने वाला टूल

Chrome के इंजीनियर पीटर बेवरलू ने इस सुविधा को लागू किया है. साथ ही, उन्होंने इस सुविधा के स्पेसिफ़िकेशन पर भी काम किया है. उन्होंने पुष्टि करने वाला टूल बनाया है.

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

का इस्तेमाल करने के सबसे सही तरीकों के साथ-साथ, पूरा दस्तावेज़ ज़रूर देखें