پروتکل فشار وب

Matt Gaunt

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

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

نمودار ارسال یک پیام فشار از سرور شما به یک سرویس فشار.

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

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

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

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

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

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

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

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

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

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

توکن وب JSON

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

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

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

وب پوش و JWT های امضا شده

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

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

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

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

اطلاعات JWT برای web push باید شامل اطلاعات زیر باشد:

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

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

برای web push، داده‌ها این فرمت را خواهند داشت:

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

مقدار aud همان "مخاطب" است، یعنی اینکه JWT برای چه کسی است. برای web push مخاطب، سرویس push است، بنابراین آن را روی مبدا سرویس push تنظیم می‌کنیم.

مقدار exp ، تاریخ انقضای JWT است، این کار مانع از آن می‌شود که جاسوسان در صورت قطع JWT، بتوانند دوباره از آن استفاده کنند. تاریخ انقضا یک مهر زمانی بر حسب ثانیه است و نباید بیشتر از ۲۴ ساعت باشد.

در Node.js، انقضا با استفاده از دستور زیر تنظیم می‌شود:

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

برای جلوگیری از هرگونه مشکل ناشی از اختلاف ساعت بین برنامه ارسال کننده و سرویس پوش، به جای ۲۴ ساعت، ۱۲ ساعت در نظر گرفته شده است.

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

درست مانند JWT Info، JWT Data نیز به صورت یک رشته‌ی base64 ایمن URL کدگذاری می‌شود.

رشته سوم، یعنی امضا، حاصل گرفتن دو رشته اول (JWT Info و JWT Data)، اتصال آنها با یک کاراکتر نقطه است که ما آن را "unsigned token" می‌نامیم و امضای آن.

فرآیند امضا نیاز به رمزگذاری "توکن امضا نشده" با استفاده از 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 ، مانند زیر به سرویس web push ارسال می‌شود:

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

پروتکل Web Push همچنین بیان می‌کند که کلید سرور برنامه عمومی باید در هدر Crypto-Key به عنوان یک رشته رمزگذاری شده base64 ایمن URL با p256ecdsa= به عنوان ضمیمه به آن ارسال شود.

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

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

در ادامه، نگاهی خواهیم داشت به اینکه چگونه می‌توانیم یک payload را به همراه یک پیام push ارسال کنیم، به طوری که وقتی برنامه وب ما یک پیام push دریافت می‌کند، بتواند به داده‌های دریافتی دسترسی پیدا کند.

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

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

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

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

ECDH و HKDF

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

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

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

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

تا جایی که من می‌دانم، ECDH ویژگی‌های منحنی‌هایی را تعریف می‌کند که این «ویژگی» ایجاد یک راز مشترک «X» را ممکن می‌سازند.

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

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

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

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

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

HKDF: تابع مشتق‌گیری کلید مبتنی بر HMAC

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

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

اساساً، HKDF ورودی‌هایی را که امنیت خاصی ندارند، دریافت کرده و آن‌ها را امن‌تر می‌کند.

مشخصاتی که این رمزگذاری را تعریف می‌کند، نیاز به استفاده از SHA-256 به عنوان الگوریتم هش ما دارد و کلیدهای حاصل برای HKDF در web push نباید بیش از 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. رمز 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 به عنوان ورودی مورد نیاز هستند و نتیجه فرآیند رمزگذاری، payload رمزگذاری شده، یک مقدار salt و یک کلید عمومی خواهد بود که فقط برای رمزگذاری داده‌ها استفاده می‌شود.

نمک

Salt باید ۱۶ بایت داده تصادفی باشد. در NodeJS، برای ایجاد Salt به صورت زیر عمل می‌کنیم:

const salt = crypto.randomBytes(16);

کلیدهای عمومی/خصوصی

کلیدهای عمومی و خصوصی باید با استفاده از یک منحنی بیضوی P-256 تولید شوند، که ما در Node به این صورت انجام می‌دهیم:

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

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

ما به این کلیدها «کلیدهای محلی» می‌گوییم. آنها فقط برای رمزگذاری استفاده می‌شوند و هیچ ارتباطی با کلیدهای سرور برنامه ندارند.

با داشتن بار داده، رمز احراز هویت و کلید عمومی اشتراک به عنوان ورودی و با یک salt تازه تولید شده و مجموعه‌ای از کلیدهای محلی، آماده‌ایم تا عملاً مقداری رمزگذاری انجام دهیم.

راز مشترک

اولین قدم ایجاد یک راز مشترک با استفاده از کلید عمومی اشتراک و کلید خصوصی جدیدمان است (توضیح 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 یک بایت با مقدار ۰ به انتهای بافر اضافه می‌کند. این انتظار مرورگرهایی است که پیام را رمزگشایی می‌کنند و انتظار دارند تعداد زیادی بایت برای کدگذاری محتوا، به دنبال یک بایت با مقدار ۰ و به دنبال آن داده‌های رمزگذاری شده، اضافه شود.

کلید تصادفی کاذب ما به سادگی مجوز، راز مشترک و یک قطعه اطلاعات رمزگذاری را از طریق 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,
]);

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

با استفاده از این مقدار زمینه می‌توانیم از آن در ایجاد یک nonce و یک کلید رمزگذاری محتوا (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);

این به ما کلید رمزگذاری nonce و محتوا را می‌دهد.

انجام رمزگذاری

حالا که کلید رمزگذاری محتوا را داریم، می‌توانیم فایل مخرب را رمزگذاری کنیم.

ما یک رمز AES128 با استفاده از کلید رمزگذاری محتوا به عنوان کلید ایجاد می‌کنیم و نانس یک بردار مقداردهی اولیه است.

در Node این کار به این صورت انجام می‌شود:

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

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

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

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

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

سپس padding و payload خود را از طریق این رمز اجرا می‌کنیم.

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

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

تنها چیزی که باقی می‌ماند این است که مشخص کنیم این payload چگونه به سرویس push ارسال می‌شود.

هدرها و بدنه‌ی فایل‌های مخرب رمزگذاری شده

برای ارسال این payload رمزگذاری شده به سرویس push، باید چند هدر مختلف را در درخواست POST خود تعریف کنیم.

سربرگ رمزگذاری

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

نمک ۱۶ بایتی باید به صورت base64 URL safe کدگذاری شده و به هدر رمزگذاری اضافه شود، مانند این:

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

هدرهای اضافی وجود دارند که سرویس‌های ارسال از آنها برای تغییر رفتار پیام‌های ارسالی استفاده می‌کنند. برخی از این هدرها ضروری و برخی دیگر اختیاری هستند.

هدر TTL

مورد نیاز

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

TTL: [Time to live in seconds]

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

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

موضوع

اختیاری

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

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

فوریت

اختیاری

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

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

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

همه چیز با هم

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

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

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

پاسخ از سرویس فشار

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

کد وضعیت توضیحات
۲۰۱ ایجاد شد. درخواست ارسال پیام فشار دریافت و پذیرفته شد.
۴۲۹ تعداد درخواست‌ها خیلی زیاد است. به این معنی که سرور برنامه شما به محدودیت سرعت با سرویس ارسال (push service) رسیده است. سرویس ارسال باید شامل یک هدر «بعد از تلاش مجدد» باشد تا نشان دهد چه مدت طول می‌کشد تا درخواست دیگری ارسال شود.
۴۰۰ درخواست نامعتبر. این به طور کلی به این معنی است که یکی از هدرهای شما نامعتبر است یا به طور نادرست قالب بندی شده است.
۴۰۴ یافت نشد. این نشان می‌دهد که اشتراک منقضی شده و قابل استفاده نیست. در این حالت باید `PushSubscription` را حذف کنید و منتظر بمانید تا کلاینت، کاربر را دوباره مشترک کند.
۴۱۰ اشتراک دیگر معتبر نیست و باید از سرور برنامه حذف شود. این مورد را می‌توان با فراخوانی `unsubscribe()` در `PushSubscription` دوباره ایجاد کرد.
۴۱۳ اندازه بار داده (Payload) خیلی بزرگ است. حداقل اندازه بار داده‌ای که یک سرویس ارسال باید پشتیبانی کند، ۴۰۹۶ بایت (یا ۴ کیلوبایت) است.

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

کجا برویم؟

آزمایشگاه‌های کد