ওয়েব পুশ প্রোটোকল

Matt Gaunt

আমরা দেখেছি কীভাবে একটি লাইব্রেরি ব্যবহার করে পুশ মেসেজ পাঠানো যায়, কিন্তু এই লাইব্রেরিগুলো আসলে কী করছে?

আচ্ছা, তারা সঠিক ফরম্যাটের নেটওয়ার্ক রিকোয়েস্টগুলো নিশ্চিত করছে। যে স্পেকটি এই নেটওয়ার্ক রিকোয়েস্টকে সংজ্ঞায়িত করে, তা হলো ওয়েব পুশ প্রোটোকল

আপনার সার্ভার থেকে একটি পুশ সার্ভিসে পুশ মেসেজ পাঠানোর ডায়াগ্রাম।

এই অংশে বর্ণনা করা হয়েছে যে, সার্ভার কীভাবে অ্যাপ্লিকেশন সার্ভার কী ব্যবহার করে নিজেকে শনাক্ত করে এবং কীভাবে এনক্রিপ্টেড পেলোড ও সংশ্লিষ্ট ডেটা পাঠানো হয়।

ওয়েব পুশের এই দিকটা খুব একটা সুন্দর নয় এবং আমি এনক্রিপশনে কোনো বিশেষজ্ঞ নই, কিন্তু চলুন প্রতিটি অংশ খতিয়ে দেখা যাক, কারণ এই লাইব্রেরিগুলো আড়ালে কী করছে তা জানাটা সুবিধাজনক।

অ্যাপ্লিকেশন সার্ভার কী

যখন আমরা কোনো ব্যবহারকারীকে সাবস্ক্রাইব করি, তখন আমরা একটি applicationServerKey পাস করি। এই কী-টি পুশ সার্ভিসে পাঠানো হয় এবং এটি যাচাই করতে ব্যবহৃত হয় যে, যে অ্যাপ্লিকেশনটি ব্যবহারকারীকে সাবস্ক্রাইব করেছে, সেটিই পুশ মেসেজগুলো ট্রিগার করছে কি না।

যখন আমরা একটি পুশ মেসেজ পাঠাই, তখন আমরা কিছু হেডার পাঠাই যা পুশ সার্ভিসকে অ্যাপ্লিকেশনটি প্রমাণীকরণ করতে সাহায্য করে। (এটি VAPID স্পেক দ্বারা সংজ্ঞায়িত।)

এই সবকিছুর আসল অর্থ কী এবং ঠিক কী ঘটে? অ্যাপ্লিকেশন সার্ভার অথেনটিকেশনের জন্য এই পদক্ষেপগুলো নেওয়া হয়:

  1. অ্যাপ্লিকেশন সার্ভার তার ব্যক্তিগত অ্যাপ্লিকেশন কী ব্যবহার করে কিছু JSON তথ্য স্বাক্ষর করে।
  2. এই স্বাক্ষরিত তথ্যটি একটি POST অনুরোধের হেডার হিসেবে পুশ সার্ভিসে পাঠানো হয়।
  3. পুশ সার্ভিসটি pushManager.subscribe() থেকে প্রাপ্ত সংরক্ষিত পাবলিক কী ব্যবহার করে যাচাই করে যে, প্রাপ্ত তথ্যটি পাবলিক কী-এর সাথে সম্পর্কিত প্রাইভেট কী দ্বারা স্বাক্ষরিত কিনা। মনে রাখবেন : পাবলিক কী হলো সেই applicationServerKey যা `subscribe` কলে পাস করা হয়।
  4. স্বাক্ষরিত তথ্য বৈধ হলে পুশ সার্ভিসটি ব্যবহারকারীর কাছে পুশ মেসেজটি পাঠিয়ে দেয়।

এই তথ্য প্রবাহের একটি উদাহরণ নিচে দেওয়া হলো। (পাবলিক ও প্রাইভেট কী নির্দেশ করার জন্য নীচের বাম দিকের লিজেন্ডটি লক্ষ্য করুন।)

বার্তা পাঠানোর সময় প্রাইভেট অ্যাপ্লিকেশন সার্ভার কী কীভাবে ব্যবহৃত হয় তার চিত্র।

অনুরোধের হেডারে যোগ করা 'স্বাক্ষরিত তথ্য' হলো একটি JSON ওয়েব টোকেন।

JSON ওয়েব টোকেন

একটি JSON ওয়েব টোকেন (সংক্ষেপে JWT) হলো তৃতীয় পক্ষের কাছে বার্তা পাঠানোর একটি পদ্ধতি, যার মাধ্যমে প্রাপক যাচাই করতে পারে যে বার্তাটি কে পাঠিয়েছে।

যখন কোনো তৃতীয় পক্ষ একটি বার্তা গ্রহণ করে, তখন তাদের প্রেরকের পাবলিক কী সংগ্রহ করতে হয় এবং সেটি ব্যবহার করে JWT-এর স্বাক্ষর যাচাই করতে হয়। যদি স্বাক্ষরটি বৈধ হয়, তাহলে JWT-টি অবশ্যই সংশ্লিষ্ট প্রাইভেট কী দিয়ে স্বাক্ষরিত হয়েছে, অর্থাৎ এটি প্রত্যাশিত প্রেরকের কাছ থেকেই এসেছে।

jwt.io/- তে এমন অনেক লাইব্রেরি আছে যা আপনার হয়ে সাইনিংয়ের কাজটি করে দিতে পারে এবং আমি আপনাকে পরামর্শ দেবো, যেখানে সম্ভব আপনি সেটাই করুন। বিষয়টি পরিষ্কারভাবে বোঝার জন্য, চলুন দেখে নেওয়া যাক কীভাবে ম্যানুয়ালি একটি সাইন করা JWT তৈরি করতে হয়।

ওয়েব পুশ এবং স্বাক্ষরিত JWT

একটি স্বাক্ষরিত JWT হলো কেবল একটি স্ট্রিং, যদিও এটিকে ডট দিয়ে যুক্ত তিনটি স্ট্রিং হিসেবেও ভাবা যেতে পারে।

একটি JSON ওয়েব টোকেনের স্ট্রিংগুলির একটি উদাহরণ।

প্রথম এবং দ্বিতীয় স্ট্রিং (The JWT info এবং JWT data) হলো 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 হস্তগত হলে সেটিকে পুনরায় ব্যবহার করা থেকে বিরত রাখে। এই মেয়াদ শেষ হওয়ার তারিখটি সেকেন্ডে একটি টাইমস্ট্যাম্প হিসেবে থাকে এবং তা অবশ্যই ২৪ ঘণ্টার বেশি হবে না।

Node.js-এ মেয়াদ শেষ হওয়ার তারিখটি নিম্নোক্তভাবে নির্ধারণ করা হয়:

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

প্রেরক অ্যাপ্লিকেশন এবং পুশ সার্ভিসের মধ্যে ঘড়ির পার্থক্যজনিত সমস্যা এড়ানোর জন্য এটি ২৪ ঘণ্টার পরিবর্তে ১২ ঘণ্টা।

অবশেষে, sub ভ্যালুটি অবশ্যই একটি ইউআরএল (URL) অথবা একটি mailto ইমেল অ্যাড্রেস হতে হবে। এর কারণ হলো, যদি কোনো পুশ সার্ভিসকে প্রেরকের সাথে যোগাযোগ করার প্রয়োজন হয়, তবে এটি যেন জেডব্লিউটি (JWT) থেকে যোগাযোগের তথ্য খুঁজে নিতে পারে। (এই কারণেই ওয়েব-পুশ লাইব্রেরির একটি ইমেল অ্যাড্রেসের প্রয়োজন ছিল)।

JWT Info-এর মতোই, JWT Data-ও একটি URL-নিরাপদ base64 স্ট্রিং হিসেবে এনকোড করা হয়।

তৃতীয় স্ট্রিংটি, অর্থাৎ সিগনেচার, হলো প্রথম দুটি স্ট্রিং (JWT Info এবং JWT Data)-কে একটি ডট ক্যারেক্টার দিয়ে যুক্ত করে, যাকে আমরা "আনসাইনড টোকেন" বলব, এবং সেটিকে সাইন করার ফলাফল।

স্বাক্ষর করার প্রক্রিয়ার জন্য ES256 ব্যবহার করে 'স্বাক্ষরবিহীন টোকেন' এনক্রিপ্ট করতে হয়। JWT স্পেক অনুযায়ী, ES256 হলো 'P-256 কার্ভ এবং SHA-256 হ্যাশ অ্যালগরিদম ব্যবহার করে ECDSA'-এর সংক্ষিপ্ত রূপ। ওয়েব ক্রিপ্টো ব্যবহার করে আপনি নিম্নোক্তভাবে স্বাক্ষরটি তৈরি করতে পারেন:

// 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 (অর্থাৎ ডট দিয়ে যুক্ত তিনটি স্ট্রিং), WebPush শব্দটি সামনে যুক্ত করে Authorization হেডার হিসেবে ওয়েব পুশ সার্ভিসে পাঠানো হয়, যেমন:

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

ওয়েব পুশ প্রোটোকল আরও বলে যে, পাবলিক অ্যাপ্লিকেশন সার্ভার কী অবশ্যই Crypto-Key হেডারে একটি ইউআরএল-নিরাপদ বেস৬৪ এনকোডেড স্ট্রিং হিসেবে পাঠাতে হবে, যার শুরুতে p256ecdsa= যুক্ত থাকবে।

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

পেলোড এনক্রিপশন

এরপর চলুন দেখি কিভাবে একটি পুশ মেসেজের সাথে পেলোড পাঠানো যায়, যাতে আমাদের ওয়েব অ্যাপ পুশ মেসেজটি পেলে প্রাপ্ত ডেটা অ্যাক্সেস করতে পারে।

যারা অন্যান্য পুশ সার্ভিস ব্যবহার করেছেন, তাদের মনে একটি সাধারণ প্রশ্ন জাগে যে, ওয়েব পুশ পেলোড এনক্রিপ্ট করার প্রয়োজন কেন হয়? নেটিভ অ্যাপের ক্ষেত্রে, পুশ মেসেজ সাধারণ টেক্সট হিসেবেই ডেটা পাঠাতে পারে।

ওয়েব পুশের একটি সুবিধা হলো, যেহেতু সব পুশ সার্ভিস একই এপিআই (ওয়েব পুশ প্রোটোকল) ব্যবহার করে, তাই ডেভেলপারদের পুশ সার্ভিসটি কোনটি তা নিয়ে চিন্তা করতে হয় না। আমরা সঠিক ফরম্যাটে একটি অনুরোধ পাঠাতে পারি এবং একটি পুশ মেসেজ পাঠানো হবে বলে আশা করতে পারি। এর অসুবিধা হলো, ডেভেলপাররা এমন কোনো পুশ সার্ভিসে মেসেজ পাঠাতে পারেন যা বিশ্বাসযোগ্য নয়। পেলোড এনক্রিপ্ট করার মাধ্যমে, একটি পুশ সার্ভিস পাঠানো ডেটা পড়তে পারে না। শুধুমাত্র ব্রাউজারই সেই তথ্য ডিক্রিপ্ট করতে পারে। এটি ব্যবহারকারীর ডেটা সুরক্ষিত রাখে।

পেলোডের এনক্রিপশন মেসেজ এনক্রিপশন স্পেসিফিকেশনে সংজ্ঞায়িত করা হয়েছে।

একটি পুশ মেসেজের পেলোড এনক্রিপ্ট করার নির্দিষ্ট ধাপগুলো দেখার আগে, এনক্রিপশন প্রক্রিয়ার সময় ব্যবহৃত কিছু কৌশল নিয়ে আলোচনা করা যাক। (পুশ এনক্রিপশনের উপর তার চমৎকার নিবন্ধটির জন্য ম্যাট স্কেলসকে অশেষ ধন্যবাদ।)

ECDH এবং HKDF

এনক্রিপশন প্রক্রিয়া জুড়ে ECDH এবং HKDF উভয়ই ব্যবহৃত হয় এবং তথ্য এনক্রিপ্ট করার উদ্দেশ্যে সুবিধা প্রদান করে।

ECDH: উপবৃত্তাকার বক্ররেখা ডিফি-হেলম্যান কী বিনিময়

ধরুন, অ্যালিস এবং বব নামে দুজন ব্যক্তি আছেন যারা তথ্য আদান-প্রদান করতে চান। অ্যালিস এবং বব উভয়েরই নিজস্ব পাবলিক ও প্রাইভেট কী আছে। অ্যালিস এবং বব একে অপরের সাথে তাদের পাবলিক কী শেয়ার করেন।

ECDH দিয়ে তৈরি কী-গুলোর একটি সুবিধাজনক বৈশিষ্ট্য হলো, অ্যালিস তার প্রাইভেট কী এবং ববের পাবলিক কী ব্যবহার করে 'X' নামক একটি গোপন ভ্যালু তৈরি করতে পারে। ববও একইভাবে তার প্রাইভেট কী এবং অ্যালিসের পাবলিক কী ব্যবহার করে স্বাধীনভাবে একই ভ্যালু 'X' তৈরি করতে পারে। এর ফলে 'X' একটি শেয়ার্ড সিক্রেট হয়ে যায় এবং অ্যালিস ও ববকে শুধু তাদের পাবলিক কী শেয়ার করতে হয়। এখন বব এবং অ্যালিস তাদের মধ্যে বার্তা এনক্রিপ্ট ও ডিক্রিপ্ট করার জন্য 'X' ব্যবহার করতে পারে।

আমার জানামতে, ECDH কার্ভের সেই বৈশিষ্ট্যগুলোকে সংজ্ঞায়িত করে, যা একটি শেয়ার্ড সিক্রেট 'X' তৈরির এই "বৈশিষ্ট্য"টিকে অনুমোদন করে।

এটি ECDH-এর একটি প্রাথমিক ব্যাখ্যা; আপনি যদি আরও জানতে চান, তাহলে আমি একটি আরও বিশদ ECDH ওভারভিউ ভিডিও দেখার পরামর্শ দেব।

কোডের ক্ষেত্রে, বেশিরভাগ প্রোগ্রামিং ভাষা বা প্ল্যাটফর্মেই লাইব্রেরি থাকে, যা দিয়ে এই কী-গুলো সহজে তৈরি করা যায়।

নোডে আমরা নিম্নলিখিত কাজগুলো করব:

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

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

HKDF: HMAC ভিত্তিক কী ডিরাইভেশন ফাংশন

উইকিপিডিয়ায় এইচকেডিএফ- এর একটি সংক্ষিপ্ত বিবরণ রয়েছে:

HKDF হলো একটি HMAC ভিত্তিক কী ডিরাইভেশন ফাংশন যা যেকোনো দুর্বল কী মেটেরিয়ালকে ক্রিপ্টোগ্রাফিকভাবে শক্তিশালী কী মেটেরিয়ালে রূপান্তরিত করে। উদাহরণস্বরূপ, এটি ডিফি হেলম্যান এক্সচেঞ্জড শেয়ার্ড সিক্রেটকে এনক্রিপশন, ইন্টিগ্রিটি চেকিং বা অথেন্টিকেশনে ব্যবহারের জন্য উপযুক্ত কী মেটেরিয়ালে রূপান্তর করতে ব্যবহার করা যেতে পারে।

মূলত, এইচকেডিএফ কম সুরক্ষিত ইনপুটকে আরও সুরক্ষিত করে তুলবে।

এই এনক্রিপশনকে সংজ্ঞায়িতকারী স্পেসিফিকেশন অনুযায়ী আমাদের হ্যাশ অ্যালগরিদম হিসেবে SHA-256 ব্যবহার করতে হবে এবং ওয়েব পুশ-এ HKDF-এর জন্য প্রাপ্ত কী-গুলো ২৫৬ বিট (৩২ বাইট)-এর বেশি হওয়া উচিত নয়।

নোডে এটি এইভাবে বাস্তবায়ন করা যেতে পারে:

// 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 এবং HKDF অন্তর্ভুক্ত।

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 এই তিনটি ভ্যালুর প্রয়োজন হয় এবং এনক্রিপশন প্রক্রিয়ার ফলাফল হিসেবে পাওয়া যাবে এনক্রিপ্টেড পেলোড, একটি সল্ট ভ্যালু এবং শুধুমাত্র ডেটা এনক্রিপ্ট করার জন্য ব্যবহৃত একটি পাবলিক কী।

লবণ

সল্টটিকে ১৬ বাইটের র‍্যান্ডম ডেটা হতে হবে। NodeJS-এ একটি সল্ট তৈরি করতে আমরা নিম্নলিখিত পদক্ষেপগুলো অনুসরণ করি:

const salt = crypto.randomBytes(16);

পাবলিক / প্রাইভেট কী

পাবলিক এবং প্রাইভেট কীগুলো একটি P-256 এলিপটিক কার্ভ ব্যবহার করে তৈরি করা উচিত, যা আমরা Node-এ এইভাবে করে থাকি:

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

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

আমরা এই কীগুলোকে 'লোকাল কী' হিসেবে উল্লেখ করব। এগুলো শুধুমাত্র এনক্রিপশনের জন্য ব্যবহৃত হয় এবং অ্যাপ্লিকেশন সার্ভার কী-এর সাথে এদের কোনো সম্পর্ক নেই।

পেলোড, অথোরাইজেশন সিক্রেট ও সাবস্ক্রিপশন পাবলিক কী ইনপুট হিসেবে এবং নতুনভাবে তৈরি করা একটি সল্ট ও এক সেট লোকাল কী ব্যবহার করে, আমরা এখন এনক্রিপশন করার জন্য প্রস্তুত।

ভাগ করা গোপনীয়তা

প্রথম ধাপ হলো সাবস্ক্রিপশন পাবলিক কী এবং আমাদের নতুন প্রাইভেট কী ব্যবহার করে একটি শেয়ার্ড সিক্রেট তৈরি করা (অ্যালিস এবং ববের সাথে ECDH-এর ব্যাখ্যাটির কথা মনে আছে? ঠিক সেভাবেই)।

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

পরবর্তী ধাপে সিউডো র‍্যান্ডম কী (PRK) গণনা করতে এটি ব্যবহৃত হয়।

ছদ্ম এলোমেলো চাবি

সিউডো র‍্যান্ডম কী (PRK) হলো পুশ সাবস্ক্রিপশনের অথোরাইজেশন সিক্রেট এবং আমাদের এইমাত্র তৈরি করা শেয়ার্ড সিক্রেটের সমন্বয়।

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

চূড়ান্ত কনটেক্সট বাফারটি হলো একটি লেবেল, সাবস্ক্রিপশন পাবলিক কী-এর বাইট সংখ্যা, এরপরে মূল কী-টি, তারপর লোকাল পাবলিক কী-এর বাইট সংখ্যা এবং সবশেষে মূল কী-টি।

এই কনটেক্সট ভ্যালুটি ব্যবহার করে আমরা একটি ননস এবং একটি কনটেন্ট এনক্রিপশন কী (CEK) তৈরি করতে পারি।

বিষয়বস্তু এনক্রিপশন কী এবং ননস

ননস হলো এমন একটি মান যা রিপ্লে অ্যাটাক প্রতিরোধ করে, কারণ এটি শুধুমাত্র একবারই ব্যবহার করা উচিত।

কন্টেন্ট এনক্রিপশন কী (CEK) হলো সেই কী যা চূড়ান্তভাবে আমাদের পেলোড এনক্রিপ্ট করতে ব্যবহৃত হবে।

প্রথমে আমাদের ননস (nonce) এবং সিইকে (CEK)-এর জন্য ডেটা বাইট তৈরি করতে হবে, যা হলো মূলত একটি কন্টেন্ট এনকোডিং স্ট্রিং এবং তার পরে আমাদের এইমাত্র গণনা করা কনটেক্সট বাফারটি থাকবে:

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

এই তথ্যটি সল্ট এবং পিআরকে-এর সাথে ননসইনফো এবং চেকইনফো একত্রিত করে এইচকেডিএফ-এর মাধ্যমে প্রক্রিয়াজাত করা হয়:

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

আমাদের পেলোড এনক্রিপ্ট করার আগে, পেলোডের শুরুতে আমরা কী পরিমাণ প্যাডিং যোগ করতে চাই তা নির্ধারণ করতে হবে। প্যাডিং যোগ করার কারণ হলো, এটি পেলোডের আকারের উপর ভিত্তি করে আড়ি পাতা ব্যক্তিদের বার্তার "ধরন" নির্ধারণ করার ঝুঁকি প্রতিরোধ করে।

যেকোনো অতিরিক্ত প্যাডিংয়ের দৈর্ঘ্য বোঝাতে আপনাকে অবশ্যই দুই বাইট প্যাডিং যোগ করতে হবে।

উদাহরণস্বরূপ, যদি আপনি কোনো প্যাডিং যোগ না করেন, তাহলে আপনার কাছে ০ মানের দুটি বাইট থাকবে, অর্থাৎ কোনো প্যাডিং নেই; এই দুটি বাইটের পরে আপনি পেলোডটি পড়বেন। যদি আপনি ৫ বাইট প্যাডিং যোগ করেন, তাহলে প্রথম দুটি বাইটের মান হবে ৫, ফলে কনজিউমার তখন অতিরিক্ত আরও পাঁচ বাইট পড়বে এবং তারপর পেলোড পড়া শুরু করবে।

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

আমরা এখন আমাদের এনক্রিপ্টেড পেলোড পেয়ে গেছি। দারুণ!

এখন শুধু এটাই নির্ধারণ করতে হবে যে এই পেলোডটি পুশ সার্ভিসে কীভাবে পাঠানো হবে।

এনক্রিপ্টেড পেলোড হেডার এবং বডি

এই এনক্রিপ্টেড পেলোডটি পুশ সার্ভিসে পাঠানোর জন্য আমাদের POST রিকোয়েস্টে কয়েকটি ভিন্ন হেডার নির্ধারণ করতে হবে।

এনক্রিপশন হেডার

'Encryption' হেডারে পেলোড এনক্রিপ্ট করার জন্য ব্যবহৃত সল্ট অবশ্যই থাকতে হবে।

১৬ বাইটের সল্টটিকে বেস৬৪ ইউআরএল সেফ এনকোড করে এনক্রিপশন হেডারে যোগ করতে হবে, যেমনটা নিচে দেখানো হয়েছে:

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: [Time to live in seconds]

আপনি যদি TTL শূন্য সেট করেন, তাহলে পুশ সার্ভিসটি বার্তাটি অবিলম্বে পৌঁছে দেওয়ার চেষ্টা করবে, কিন্তু ডিভাইসটিতে পৌঁছানো না গেলে, আপনার বার্তাটি পুশ সার্ভিস কিউ থেকে সঙ্গে সঙ্গে বাদ হয়ে যাবে।

প্রযুক্তিগতভাবে একটি পুশ সার্ভিস চাইলে একটি পুশ মেসেজের TTL কমাতে পারে। পুশ সার্ভিস থেকে আসা রেসপন্সের TTL হেডার পরীক্ষা করে আপনি বুঝতে পারবেন যে এমনটা হয়েছে কিনা।

বিষয়

ঐচ্ছিক

টপিক হলো এমন স্ট্রিং যা ব্যবহার করে অপেক্ষাধীন বার্তাগুলোকে একটি নতুন বার্তা দিয়ে প্রতিস্থাপন করা যায়, যদি সেগুলোর টপিকের নাম মিলে যায়।

এটি এমন পরিস্থিতিতে উপযোগী যেখানে ডিভাইস অফলাইনে থাকা অবস্থায় একাধিক বার্তা পাঠানো হয়, এবং আপনি চান যে ডিভাইসটি চালু হলে ব্যবহারকারী যেন শুধুমাত্র সর্বশেষ বার্তাটিই দেখতে পায়।

জরুরি অবস্থা

ঐচ্ছিক

জরুরি অবস্থা পুশ সার্ভিসকে জানিয়ে দেয় যে একটি বার্তা ব্যবহারকারীর জন্য কতটা গুরুত্বপূর্ণ। ব্যাটারি কম থাকলে শুধুমাত্র গুরুত্বপূর্ণ বার্তার জন্য সক্রিয় হয়ে ব্যবহারকারীর ডিভাইসের ব্যাটারির আয়ু বাঁচাতে পুশ সার্ভিস এটি ব্যবহার করতে পারে।

হেডার ভ্যালুটি নিচে দেখানো অনুযায়ী সংজ্ঞায়িত করা হয়েছে। ডিফল্ট ভ্যালু হলো normal

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

সবকিছু একসাথে

এই পুরো প্রক্রিয়াটি কীভাবে কাজ করে সে সম্পর্কে আপনার যদি আরও প্রশ্ন থাকে, তাহলে আপনি web-push-libs org- এ গিয়ে দেখতে পারেন যে লাইব্রেরিগুলো কীভাবে পুশ মেসেজ ট্রিগার করে।

একবার আপনার কাছে একটি এনক্রিপ্টেড পেলোড এবং উপরের হেডারগুলো থাকলে, আপনাকে শুধু একটি PushSubscription endpoint একটি POST রিকোয়েস্ট পাঠাতে হবে।

তাহলে এই POST অনুরোধের প্রতিক্রিয়া নিয়ে আমরা কী করব?

পুশ পরিষেবা থেকে প্রতিক্রিয়া

কোনো পুশ সার্ভিসে অনুরোধ করার পর, আপনাকে রেসপন্সের স্ট্যাটাস কোডটি দেখতে হবে, কারণ সেটিই বলে দেবে অনুরোধটি সফল হয়েছে কি না।

স্ট্যাটাস কোড বর্ণনা
২০১ তৈরি করা হয়েছে। পুশ মেসেজ পাঠানোর অনুরোধটি গৃহীত ও অনুমোদিত হয়েছে।
৪২৯ অতিরিক্ত অনুরোধ। এর অর্থ হলো, আপনার অ্যাপ্লিকেশন সার্ভার একটি পুশ সার্ভিসের রেট লিমিটে পৌঁছে গেছে। পরবর্তী অনুরোধ করার আগে কতক্ষণ অপেক্ষা করতে হবে, তা বোঝানোর জন্য পুশ সার্ভিসে একটি 'Retry-After' হেডার থাকা উচিত।
৪০০ অবৈধ অনুরোধ। এর সাধারণ অর্থ হলো আপনার হেডারগুলোর মধ্যে কোনো একটি অবৈধ অথবা ভুলভাবে বিন্যস্ত।
৪০৪ খুঁজে পাওয়া যায়নি। এটি নির্দেশ করে যে সাবস্ক্রিপশনটির মেয়াদ শেষ হয়ে গেছে এবং এটি ব্যবহার করা যাবে না। এক্ষেত্রে আপনার `PushSubscription` মুছে ফেলা উচিত এবং ক্লায়েন্ট ব্যবহারকারীকে পুনরায় সাবস্ক্রাইব করার জন্য অপেক্ষা করা উচিত।
৪১০ চলে গেছে। সাবস্ক্রিপশনটি আর বৈধ নয় এবং অ্যাপ্লিকেশন সার্ভার থেকে এটি সরিয়ে ফেলা উচিত। একটি `PushSubscription`-এর উপর `unsubscribe()` কল করে এটি পুনরায় ঘটানো যেতে পারে।
৪১৩ পেলোডের আকার খুব বড়। একটি পুশ সার্ভিসকে অবশ্যই সর্বনিম্ন ৪০৯৬ বাইট (বা ৪ কিলোবাইট) আকারের পেলোড সমর্থন করতে হবে।

HTTP স্ট্যাটাস কোডগুলো সম্পর্কে আরও তথ্যের জন্য আপনি ওয়েব পুশ স্ট্যান্ডার্ড (RFC8030) পড়তে পারেন।

এরপর কোথায় যাবেন

কোড ল্যাব