پروتکل ارسال وب

دیده‌ایم که چگونه می‌توان از کتابخانه برای راه‌اندازی پیام‌های فشاری استفاده کرد، اما این کتابخانه‌ها دقیقاً چه کاری انجام می‌دهند؟

خب، آن‌ها درخواست‌های شبکه را انجام می‌دهند و درعین‌حال مطمئن می‌شوند که این درخواست‌ها در قالب درست باشند. مشخصه‌ای که این درخواست شبکه را تعریف می‌کند پروتکل Web Push است.

نمودار ارسال پیام لحظه‌ای از سرور شما به سرویس
لحظه‌ای.

این بخش توضیح می‌دهد که چگونه سرور می‌تواند خود را با کلیدهای سرور برنامه شناسایی کند و چگونه بار مفید رمزگذاری‌شده و داده‌های مرتبط ارسال می‌شود.

This isn't a pretty side of web push and I'm no expert at encryption, but let's look through each piece since it's handy to know what these libraries are doing under the hood.

کلیدهای سرور برنامه

وقتی کاربری را مشترک می‌کنیم، applicationServerKey را ارسال می‌کنیم. این کلید به سرویس ارسال داده منتقل می‌شود و برای بررسی اینکه برنامه‌ای که کاربر در آن مشترک شده است همان برنامه‌ای است که پیام‌های ارسالی را راه‌اندازی می‌کند استفاده می‌شود.

وقتی پیام لحظه‌ای را راه‌اندازی می‌کنیم، مجموعه‌ای از سرصفحه‌ها را ارسال می‌کنیم که به سرویس لحظه‌ای اجازه می‌دهد برنامه را اصالت‌سنجی کند. (این مورد توسط مشخصات VAPID تعریف شده است.)

همه این‌ها واقعاً به چه معنا است و دقیقاً چه اتفاقی می‌افتد؟ این‌ها مراحلی است که برای اصالت‌سنجی سرور برنامه انجام می‌شود:

  1. سرور برنامه برخی‌از اطلاعات JSON را با کلید برنامه خصوصی خود امضا می‌کند.
  2. این اطلاعات امضاشده به‌عنوان سرصفحه در درخواست POST به سرویس ارسال مستقیم فرستاده می‌شود.
  3. سرویس ارسال از کلید عمومی ذخیره‌شده‌ای که از pushManager.subscribe() دریافت کرده است استفاده می‌کند تا بررسی کند اطلاعات دریافتی با کلید خصوصی مربوط به کلید عمومی امضا شده است یا نه. به‌خاطر داشته باشید: کلید عمومی همان applicationServerKey است که به تماس اشتراک منتقل می‌شود.
  4. اگر اطلاعات امضاشده معتبر باشد، سرویس push پیام push را به کاربر ارسال می‌کند.

نمونه‌ای از این جریان اطلاعات در زیر آمده است. (به راهنمای پایین سمت راست برای نشان دادن کلیدهای عمومی و خصوصی توجه کنید.)

تصویری از نحوه استفاده از کلید سرور برنامه خصوصی هنگام ارسال پیام.

«اطلاعات امضاشده» اضافه‌شده به سرصفحه در درخواست، یک JSON Web Token است.

نشان وب JSON

توکن وب JSON (یا به‌اختصار JWT) روشی برای ارسال پیام به طرف سوم است به‌گونه‌ای که گیرنده بتواند فرستنده آن را اعتبارسنجی کند.

وقتی طرف سوم پیامی دریافت می‌کند، باید کلید عمومی فرستنده را دریافت کند و از آن برای اعتبارسنجی امضای JWT استفاده کند. اگر امضا معتبر باشد، JWT باید با کلید خصوصی منطبق امضا شده باشد، بنابراین باید از فرستنده موردانتظار باشد.

کتابخانه‌های زیادی در jwt.io/ وجود دارد که می‌توانند امضا را برای شما انجام دهند و توصیه می‌کنم درصورت امکان این کار را انجام دهید. برای تکمیل، بیایید ببینیم چگونه می‌توان یک JWT امضاشده را به‌صورت دستی ایجاد کرد.

ارسال وب و JWTهای امضاشده

JWT امضاشده فقط یک رشته است، اگرچه می‌توان آن را به‌عنوان سه رشته درنظر گرفت که با نقطه به هم متصل شده‌اند.

تصویری از رشته‌ها در JSON Web
Token.

رشته اول و دوم (اطلاعات JWT و داده‌های JWT) بخش‌هایی از JSON هستند که کدبندی ۶۴پایه‌ای شده‌اند، یعنی به‌صورت عمومی قابل‌خواندن هستند.

رشته اول اطلاعاتی درباره خود 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;

این زمان ۱۲ ساعت است نه ۲۴ ساعت تا از هرگونه مشکل مربوط به تفاوت ساعت بین برنامه ارسال‌کننده و سرویس push جلوگیری شود.

درنهایت، مقدار sub باید نشانی وب یا نشانی ایمیل mailto باشد. این کار برای این است که اگر سرویس push نیاز به تماس با فرستنده داشته باشد، بتواند اطلاعات تماس را از JWT پیدا کند. (به همین دلیل است که کتابخانه وب-پوش به نشانی ایمیل نیاز دارد).

درست مانند «اطلاعات JWT»، «داده‌های JWT» نیز به‌صورت رشته base64 امن برای نشانی وب کدبندی می‌شود.

رشته سوم، یعنی امضا، نتیجه گرفتن دو رشته اول (اطلاعات JWT و داده‌های JWT)، پیوستن آن‌ها با نویسه نقطه (که آن را «نشان بدون امضا» می‌نامیم)، و امضا کردن آن است.

فرایند امضا کردن مستلزم رمزگذاری «توکن امضانشده» بااستفاده از ES256 است. طبق مشخصات JWT،‏ ES256 مخفف «ECDSA بااستفاده از منحنی 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);
});

سرویس push می‌تواند بااستفاده از کلید عمومی سرور برنامه، JWT را اعتبارسنجی کند تا امضا را رمزگشایی کند و مطمئن شود رشته رمزگشایی‌شده با «توکن امضانشده» (یعنی دو رشته اول در JWT) یکسان است.

JWT امضاشده (یعنی هر سه رشته که با نقطه به هم متصل شده‌اند) به‌عنوان سرایند Authorization با پیشوند WebPush به سرویس ارسال لحظه‌ای وب فرستاده می‌شود، مانند:

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

«پروتکل ارسال وب» همچنین بیان می‌کند که کلید سرور برنامه عمومی باید در سرصفحه Crypto-Key به‌عنوان رشته کدبندی‌شده base64 امن URL با p256ecdsa= پیشوند ارسال شود.

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

رمزگذاری بارداده

در ادامه، بیایید ببینیم چگونه می‌توانیم یک بار داده را با پیام لحظه‌ای ارسال کنیم تا وقتی برنامه وب ما پیام لحظه‌ای دریافت می‌کند، بتواند به داده‌هایی که دریافت می‌کند دسترسی داشته باشد.

سؤال رایجی که برای هرکسی که از سرویس‌های ارسال دیگر استفاده کرده است پیش می‌آید این است که چرا بار داده ارسال وب باید رمزگذاری شود؟ با برنامه‌های به زبان اصلی دستگاه، پیام‌های لحظه‌ای می‌توانند داده‌ها را به‌صورت نوشتار ساده ارسال کنند.

یکی از زیبایی‌های ارسال لحظه‌ای وب این است که چون همه سرویس‌های ارسال لحظه‌ای از یک میانای برنامه‌سازی کاربردی (پروتکل ارسال لحظه‌ای وب) استفاده می‌کنند، توسعه‌دهندگان نیازی ندارند بدانند سرویس ارسال لحظه‌ای چیست. می‌توانیم درخواستی با قالب درست ایجاد کنیم و انتظار داشته باشیم پیام لحظه‌ای ارسال شود. نکته منفی این است که توسعه‌دهندگان می‌توانند پیام‌ها را به سرویس فشاری ارسال کنند که قابل‌اعتماد نیست. با رمزگذاری بار مفید، سرویس لحظه‌ای نمی‌تواند داده‌های ارسال‌شده را بخواند. فقط مرورگر می‌تواند اطلاعات را رمزگشایی کند. این کار از داده‌های کاربر محافظت می‌کند.

رمزگذاری بار مفید در مشخصات رمزگذاری پیام تعریف شده است.

قبل‌از اینکه مراحل خاص رمزگذاری بار داده پیام‌های لحظه‌ای را بررسی کنیم، باید برخی‌از تکنیک‌هایی را که درطول فرایند رمزگذاری استفاده می‌شود پوشش دهیم. (Massive hat tip to Mat Scales for his excellent article on push encryption.)

ECDH و HKDF

‫ECDH و HKDF هر دو در سراسر فرایند رمزگذاری استفاده می‌شوند و مزایایی برای هدف رمزگذاری اطلاعات ارائه می‌دهند.

ECDH: تبادل کلید رمزنگاری منحنی بیضوی دیفی-هلمن

فرض کنید دو نفر به نام‌های آلیس و باب می‌خواهند اطلاعاتی را هم‌رسانی کنند. مهسا و مجید هرکدام کلیدهای عمومی و خصوصی خودشان را دارند. مهسا و مجید کلیدهای عمومی‌شان را با یکدیگر هم‌رسانی می‌کنند.

ویژگی مفید کلیدهای تولیدشده با ECDH این است که آلیس می‌تواند از کلید خصوصی خود و کلید عمومی باب برای ایجاد مقدار مخفی «X» استفاده کند. باب هم می‌تواند همین کار را انجام دهد، کلید خصوصی خودش و کلید عمومی آلیس را بگیرد و به‌طور مستقل همان مقدار «X» را ایجاد کند. این کار «X» را به یک رمز مشترک تبدیل می‌کند و مهسا و مجید فقط باید کلید عمومی خود را هم‌رسانی کنند. اکنون باب و آلیس می‌توانند از «X» برای رمزگذاری و رمزگشایی پیام‌های بین خودشان استفاده کنند.

تا جایی که من می‌دانم، «رمزنگاری منحنی بیضوی دیفی-هلمن» ویژگی‌های منحنی‌هایی را تعریف می‌کند که این «قابلیت» را فراهم می‌کنند تا یک رمز مشترک «X» ایجاد شود.

این توضیح کلی درباره ECDH است؛ اگر می‌خواهید بیشتر بدانید، توصیه می‌کنم ویدیو نمای کلی ECDH با جزئیات بیشتر را ببینید.

ازنظر کد، اکثر زبان‌ها / پلاتفرم‌ها با کتابخانه‌هایی ارائه می‌شوند که تولید این کلیدها را آسان می‌کند.

در گره، موارد زیر را انجام می‌دهیم:

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

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

HKDF: تابع استخراج کلید مبتنی بر HMAC

ویکی‌پدیا شرح مختصری از HKDF دارد:

HKDF یک تابع استخراج کلید مبتنی بر HMAC است که هرگونه مواد کلید ضعیف را به مواد کلید قوی ازنظر رمزنگاری تبدیل می‌کند. برای مثال، می‌توان از آن برای تبدیل رمزهای مشترک مبادله‌شده دیفی-هلمن به مواد کلیدی مناسب برای استفاده در رمزگذاری، بررسی یکپارچگی، یا اصالت‌سنجی استفاده کرد.

اساساً، HKDF ورودی‌ای را که چندان امن نیست می‌گیرد و آن را امن‌تر می‌کند.

مشخصات تعریف‌کننده این رمزگذاری مستلزم استفاده از 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);
}

با تشکر از مقاله Mat Scale برای این کد نمونه.

این به‌طور کلی شامل ECDH و HKDF می‌شود.

ECDH روشی امن برای هم‌رسانی کلیدهای عمومی و تولید رمز مشترک است. HKDF روشی برای دریافت مواد ناامن و ایمن کردن آن است.

این کلید درطول رمزگذاری بار مفید ما استفاده خواهد شد. سپس بیایید ببینیم چه چیزی را به‌عنوان ورودی درنظر می‌گیریم و چگونه رمزگذاری می‌شود.

ورودی‌ها

وقتی می‌خواهیم پیام لحظه‌ای با بار داده به کاربر ارسال کنیم، به سه ورودی نیاز داریم:

  1. خود پایه‌بار.
  2. راز auth از PushSubscription.
  3. کلید p256dh از PushSubscription.

دیده‌ایم که مقادیر auth و p256dh از PushSubscription بازیابی می‌شوند، اما برای یادآوری سریع، با داشتن اشتراک به این مقادیر نیاز داریم:

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

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

با مقدار auth باید به‌عنوان یک راز رفتار شود و نباید خارج از برنامه شما هم‌رسانی شود.

کلید p256dh یک کلید عمومی است، گاهی‌اوقات به آن کلید عمومی کارخواه گفته می‌شود. در اینجا به p256dh به‌عنوان کلید عمومی اشتراک اشاره می‌کنیم. کلید عمومی اشتراک توسط مرورگر تولید می‌شود. مرورگر کلید خصوصی را مخفی نگه می‌دارد و از آن برای رمزگشایی بار مفید استفاده می‌کند.

این سه مقدار، auth، p256dh، و payload به‌عنوان ورودی لازم هستند و نتیجه فرایند رمزگذاری، پایه‌بار رمزگذاری‌شده، مقدار سالت، و کلید عمومی خواهد بود که فقط برای رمزگذاری داده‌ها استفاده می‌شود.

Salt

نمک باید ۱۶ بایت داده تصادفی باشد. در 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) ترکیبی از رمز اصالت‌سنجی اشتراک push و رمز مشترکی است که به‌تازگی ایجاد کردیم.

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

شاید برایتان سؤال باشد که رشته Content-Encoding: auth\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) استفاده کنیم.

کلید رمزگذاری محتوا و عدد یک‌بار مصرف

nonce مقداری است که از حملات بازپخش جلوگیری می‌کند زیرا فقط باید یک‌بار استفاده شود.

کلید رمزگذاری محتوا (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]);

این اطلاعات ازطریق HKDF با ترکیب کردن salt و 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,
);

قبل‌از اینکه بار داده را رمزگذاری کنیم، باید مشخص کنیم که چقدر لایه به جلوی بار داده اضافه شود. دلیل اینکه می‌خواهیم لایه‌گذاری اضافه کنیم این است که از خطر اینکه استراق‌سمع‌کنندگان بتوانند «انواع» پیام‌ها را براساس اندازه بار مفید تعیین کنند جلوگیری می‌کند.

باید دو بایت لایه‌گذاری اضافه کنید تا طول هر لایه‌گذاری اضافی را نشان دهد.

برای مثال، اگر هیچ لایه‌گذاری‌ای اضافه نکرده باشید، دو بایت با مقدار ۰ خواهید داشت، یعنی هیچ لایه‌گذاری‌ای وجود ندارد، پس‌از این دو بایت، بار مفید را خواهید خواند. اگر ۵ بایت لایه‌گذاری اضافه کرده باشید، دو بایت اول مقدار ۵ خواهند داشت، بنابراین مصرف‌کننده پنج بایت دیگر را می‌خواند و سپس شروع به خواندن بار مفید می‌کند.

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

اکنون محموله رمزگذاری‌شده را داریم. هورا!

تنها چیزی که باقی می‌ماند تعیین نحوه ارسال این بار به سرویس push است.

سرصفحه‌ها و بدنه بار مفید رمزگذاری‌شده

برای ارسال این پایه‌بار رمزگذاری‌شده به سرویس ارسال مستقیم، باید چند سرصفحه متفاوت را در درخواست POST خود تعریف کنیم.

سرصفحه رمزگذاری

سرصفحه «رمزگذاری» باید حاوی نمک استفاده‌شده برای رمزگذاری بار مفید باشد.

‫salt‏ ۱۶ بایتی باید با کدبندی امن نشانی وب base64 کدبندی شود و به سرصفحه «رمزگذاری» اضافه شود، مانند:

Encryption: salt=[URL Safe Base64 Encoded Salt]

سرایند Crypto-Key

دیدیم که از سرصفحه 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 / کلیدهای سرور برنامه (یعنی نحوه شناسایی برنامه با سرویس push) رفته‌ایم و سراغ سرصفحه‌های استفاده‌شده برای ارسال بار خالص رمزگذاری‌شده رفته‌ایم.

سرایندهای دیگری وجود دارد که سرویس‌های ارسال مستقیم برای تغییر رفتار پیام‌های ارسالی استفاده می‌کنند. برخی‌از این سرصفحه‌ها الزامی هستند، درحالی‌که برخی دیگر اختیاری‌اند.

سرایند TTL

الزامی

TTL (یا زمان زنده ماندن) عدد صحیحی است که تعداد ثانیه‌هایی را که می‌خواهید پیام لحظه‌ای شما قبل‌از تحویل در سرویس لحظه‌ای زنده بماند مشخص می‌کند. وقتی TTL منقضی شود، پیام از صف سرویس push برداشته می‌شود و ارسال نخواهد شد.

TTL: [Time to live in seconds]

اگر TTL را روی صفر تنظیم کنید، سرویس push تلاش می‌کند پیام را فوراً تحویل دهد، اما اگر دستگاه دردسترس نباشد، پیام شما فوراً از صف سرویس push حذف می‌شود.

ازنظر فنی، سرویس لحظه‌ای می‌تواند درصورت تمایل TTL پیام لحظه‌ای را کاهش دهد. با بررسی سرصفحه TTL در پاسخ سرویس ارسال مستقیم می‌توانید متوجه شوید که این اتفاق افتاده است یا نه.

موضوع

اختیاری

موضوعات رشته‌هایی هستند که می‌توانند برای جایگزینی پیام‌های معلق با یک پیام جدید استفاده شوند، اگر نام موضوعات مطابقت داشته باشد.

این کار در سناریوهایی مفید است که چندین پیام درحین آفلاین بودن دستگاه ارسال می‌شود و شما واقعاً می‌خواهید کاربر فقط آخرین پیام را هنگام روشن شدن دستگاه ببیند.

فوریت

اختیاری

فوریت به سرویس لحظه‌ای نشان می‌دهد که پیام چقدر برای کاربر مهم است. سرویس push می‌تواند از این برای کمک به حفظ عمر باتری دستگاه کاربر استفاده کند، به این صورت که وقتی باتری کم است، فقط برای پیام‌های مهم بیدار شود.

مقدار سرایند به‌صورت زیر تعریف می‌شود. مقدار پیش‌فرض normal است.

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

همه باهم

اگر سؤالات بیشتری درباره نحوه عملکرد همه این‌ها دارید، همیشه می‌توانید ببینید که کتابخانه‌ها چگونه پیام‌های فشاری را در سازمان web-push-libs راه‌اندازی می‌کنند.

وقتی محموله رمزگذاری‌شده و سرصفحه‌های بالا را داشتید، فقط باید درخواست POST به endpoint در PushSubscription ارسال کنید.

پس با پاسخ به این درخواست پست (POST) چه می‌کنیم؟

پاسخ از سرویس Push

وقتی درخواستی به سرویس push ارسال کردید، باید کد وضعیت پاسخ را بررسی کنید تا متوجه شوید درخواست موفقیت‌آمیز بوده است یا نه.

کد وضعیت شرح
201 ایجاد شد. درخواست ارسال پیام فشاری دریافت و پذیرفته شد.
429 بیش‌ازحد درخواست وجود دارد. یعنی سرور برنامه شما به حد مجاز بسامد در سرویس push رسیده است. سرویس push باید شامل سرصفحه «Retry-After» باشد تا نشان دهد چقدر باید بگذرد تا بتوان درخواست دیگری ارسال کرد.
۴۰۰ درخواست نامعتبر است. این معمولاً به این معنی است که یکی از سرصفحه‌های شما نامعتبر است یا قالب‌بندی نادرستی دارد.
404 پیدا نشد. این نشان می‌دهد که اشتراک منقضی شده است و نمی‌توان از آن استفاده کرد. در این حالت باید `PushSubscription` را حذف کنید و منتظر بمانید تا کارخواه کاربر را دوباره مشترک کند.
410 رفته. اشتراک دیگر معتبر نیست و باید از سرور برنامه برداشته شود. این را می‌توان با فراخوانی `unsubscribe()` در `PushSubscription` بازتولید کرد.
413 اندازه پایه‌بار خیلی بزرگ است. حداقل اندازه بار داده‌ای که سرویس push باید پشتیبانی کند ۴۰۹۶ بایت (یا ۴ کیلوبایت) است.

همچنین می‌توانید برای اطلاعات بیشتر درباره کدهای وضعیت HTTP، استاندارد Web Push (RFC8030) را بخوانید.

مقصد بعدی کجاست

Code labs