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

Matt Gaunt

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

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

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

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

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

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

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

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

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

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

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

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

"اطلاعات امضا شده" اضافه شده به هدر در درخواست، یک توکن وب JSON است.

توکن وب JSON

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

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

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

فشار وب و JWT های امضا شده

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

تصویری از رشته ها در وب JSON رمز

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

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

اطلاعات JWT برای فشار وب باید حاوی اطلاعات زیر باشد:

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

رشته دوم JWT Data است. این اطلاعات درباره فرستنده 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;

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

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

درست مانند اطلاعات JWT، داده های JWT به عنوان یک رشته URL ایمن base64 کدگذاری می شود.

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

فرآیند امضا نیاز به رمزگذاری "توکن بدون امضا" با استفاده از 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);
});

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

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

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

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

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

رمزگذاری بار

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

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

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

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

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

ECDH و HKDF

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

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

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

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

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

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

نمک

نمک باید 16 بایت داده تصادفی باشد. در 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 از طریق 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 با ترکیب نمک و 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 با استفاده از کلید رمزگذاری محتوا به عنوان کلید ایجاد می کنیم و nonce یک بردار اولیه است.

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

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

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

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

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

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

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

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

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

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

هدر رمزگذاری

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

نمک 16 بایتی باید با URL پایه 64 کدگذاری شده باشد و به هدر Encryption اضافه شود، مانند:

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

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

هدر TTL

مورد نیاز

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

TTL: [Time to live in seconds]

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

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

موضوع

اختیاری

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

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

فوریت

اختیاری

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

مقدار هدر مطابق شکل زیر تعریف می شود. مقدار پیش فرض normal است.

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

همه چیز با هم

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

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

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

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

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

کد وضعیت توضیحات
201 ایجاد شد. درخواست ارسال پیام فشار دریافت و پذیرفته شد.
429 درخواست های خیلی زیاد به این معنی که سرور برنامه شما با یک سرویس فشار به محدودیت نرخ رسیده است. سرویس فشار باید شامل یک سرصفحه "Retry-After" باشد تا نشان دهد چه مدت قبل از درخواست دیگری می توان انجام داد.
400 درخواست نامعتبر این به طور کلی به این معنی است که یکی از سرصفحه‌های شما نامعتبر است یا قالب بندی نامناسبی دارد.
404 یافت نشد. این نشان می دهد که اشتراک منقضی شده است و نمی توان از آن استفاده کرد. در این صورت باید «PushSubscription» را حذف کنید و منتظر بمانید تا مشتری مجدداً کاربر را اشتراک کند.
410 رفته اشتراک دیگر معتبر نیست و باید از سرور برنامه حذف شود. این را می توان با فراخوانی 'unsubscribe()' در 'PushSubscription' بازتولید کرد.
413 اندازه بار بسیار بزرگ است. حداقل اندازه باری که یک سرویس فشار باید پشتیبانی کند 4096 بایت (یا 4 کیلوبایت) است.

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

بعد کجا بریم

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

،

Matt Gaunt

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

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

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

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

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

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

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

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

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

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

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

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

"اطلاعات امضا شده" اضافه شده به هدر در درخواست، یک توکن وب JSON است.

توکن وب JSON

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

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

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

فشار وب و JWT های امضا شده

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

تصویری از رشته ها در وب JSON رمز

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

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

اطلاعات JWT برای فشار وب باید حاوی اطلاعات زیر باشد:

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

رشته دوم JWT Data است. این اطلاعات درباره فرستنده 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;

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

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

درست مانند اطلاعات JWT، داده های JWT به عنوان یک رشته URL ایمن base64 کدگذاری می شود.

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

فرآیند امضا نیاز به رمزگذاری "توکن بدون امضا" با استفاده از 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);
});

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

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

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

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

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

رمزگذاری بار

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

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

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

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

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

ECDH و HKDF

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

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

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

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

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

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

نمک

نمک باید 16 بایت داده تصادفی باشد. در 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 از طریق 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 با ترکیب نمک و 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 با استفاده از کلید رمزگذاری محتوا به عنوان کلید ایجاد می کنیم و nonce یک بردار اولیه است.

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

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

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

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

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

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

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

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

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

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

هدر رمزگذاری

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

نمک 16 بایتی باید با URL پایه 64 کدگذاری شده باشد و به هدر Encryption اضافه شود، مانند:

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

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

عنوان TTL

مورد نیاز

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

TTL: [Time to live in seconds]

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

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

موضوع

اختیاری

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

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

فوریت

اختیاری

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

مقدار هدر مطابق شکل زیر تعریف شده است. مقدار پیش فرض normal است.

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

همه چیز با هم

اگر سؤال دیگری در مورد چگونگی عملکرد این کار دارید ، همیشه می توانید ببینید که چگونه کتابخانه ها پیام های فشار را در Org Web-Push-Libs ایجاد می کنند.

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

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

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

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

کد وضعیت توضیحات
201 ایجاد شد. درخواست ارسال پیام فشار دریافت و پذیرفته شد.
429 درخواست های خیلی زیاد به این معنی که سرور برنامه شما با یک سرویس فشار به حد نرخ رسیده است. سرویس فشار باید شامل یک هدر "آزمایش مجدد" باشد تا نشان دهد چه مدت قبل از درخواست دیگر می توان انجام داد.
400 درخواست نامعتبر این به طور کلی به این معنی است که یکی از هدرهای شما نامعتبر یا نادرست است.
404 یافت نشد. این نشانگر این است که اشتراک منقضی شده و از آن استفاده نمی شود. در این حالت باید "PushSubscription" را حذف کرده و منتظر بمانید تا مشتری دوباره کاربر را دوباره ثبت کند.
410 رفته اشتراک دیگر معتبر نیست و باید از سرور برنامه حذف شود. این می تواند با تماس با "اشتراک ()" در "PushSubscription" بازتولید شود.
413 اندازه بار بسیار بزرگ. حداقل بار بارگذاری یک سرویس فشار که باید از آن پشتیبانی کند 4096 بایت (یا 4KB) است.

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

بعد کجا بریم

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

،

Matt Gaunt

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

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

نمودار ارسال پیام فشار از سرور خود به فشار خدمات

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

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

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

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

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

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

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

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

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

"اطلاعات امضا شده" که در درخواست به یک عنوان اضافه شده است ، یک توکن وب JSON است.

JSON WEB TOKEN

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

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

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

فشار وب و JWTS امضا شده

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;

برای جلوگیری از هرگونه مشکل در مورد اختلاف ساعت بین برنامه ارسال و سرویس فشار ، 12 ساعت به جای 24 ساعت است.

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

درست مانند اطلاعات JWT ، داده های JWT به عنوان یک رشته URL Safe Base64 رمزگذاری می شوند.

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

فرایند امضای نیاز به رمزگذاری "نشانه بدون امضا" با استفاده از 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);
});

یک سرویس فشار می تواند JWT را با استفاده از کلید سرور برنامه های عمومی برای رمزگشایی امضای تأیید کند و مطمئن شوید که رشته رمزگشایی شده همان "نشانه بدون امضا" است (یعنی دو رشته اول در JWT).

JWT امضا شده (یعنی هر سه رشته که به نقاط پیوستند) ، به عنوان عنوان Authorization با WebPush پیش بینی شده به سرویس Push Web ارسال می شود ، مانند این:

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

پروتکل فشار وب همچنین بیان می کند که کلید سرور برنامه های عمومی باید به عنوان یک رشته رمزگذاری شده URL Safe Base64 با p256ecdsa= IDEN Crypto-Key ارسال شود.

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

رمزگذاری بار

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

یک سؤال متداول که از هر کسی که از خدمات فشار دیگری استفاده کرده است ناشی می شود ، این است که چرا نیاز به بارگذاری بار وب نیاز به رمزگذاری دارد؟ با برنامه های بومی ، پیام های فشار می توانند داده ها را به عنوان متن ساده ارسال کنند.

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

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

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

ECDH و HKDF

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

ECDH: منحنی بیضوی Diffie-Hellman Exchange

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

خاصیت مفید کلیدهای تولید شده با ECDH این است که آلیس می تواند از کلید خصوصی خود و کلید عمومی باب برای ایجاد ارزش مخفی "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 است که هر ماده کلید ضعیف را به مواد کلیدی رمزنگاری قوی تبدیل می کند. به عنوان مثال می توان از آن برای تبدیل اسرار مشترک Diffie Helman به مواد کلیدی مناسب برای استفاده در رمزگذاری ، بررسی یکپارچگی یا احراز هویت استفاده کرد.

در اصل ، 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);
}

برای این کد مثال ، مقاله Hat to 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 به عنوان ورودی مورد نیاز است و نتیجه فرآیند رمزگذاری بار رمزگذاری شده ، یک مقدار نمک و یک کلید عمومی است که فقط برای رمزگذاری داده ها استفاده می شود.

نمک

نمک باید 16 بایت داده های تصادفی باشد. در Nodejs ، ما موارد زیر را برای ایجاد نمک انجام می دهیم:

const salt = crypto.randomBytes(16);

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

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

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

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

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

با استفاده از بار ، Auth Secret و اشتراک عمومی به عنوان ورودی و با نمک تازه تولید شده و مجموعه ای از کلیدهای محلی ، ما آماده هستیم تا در واقع رمزگذاری را انجام دهیم.

راز مشترک

اولین قدم ایجاد یک راز مشترک با استفاده از کلید عمومی اشتراک و کلید خصوصی جدید ما است (توضیحات ECDH را با آلیس و باب به خاطر بسپارید؟ دقیقاً مثل این).

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

این در مرحله بعدی برای محاسبه کلید شبه تصادفی (PRK) استفاده می شود.

کلید شبه تصادفی

کلید شبه تصادفی (PRK) ترکیبی از راز AUTH اشتراک فشار و راز مشترکی است که ما فقط ایجاد کردیم.

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 و به دنبال آن داده های رمزگذاری شده است.

کلید شبه تصادفی ما به سادگی در حال اجرای Auth ، Serven Secret و یک قطعه از اطلاعات رمزگذاری از طریق 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) استفاده کنیم.

کلید رمزگذاری محتوا و غیرقانونی

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

کلید رمزگذاری محتوا (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 با ترکیب نمک و 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 ایجاد می کنیم و Nonce یک بردار اولیه سازی است.

در گره این کار مانند این انجام می شود:

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

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

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

به عنوان مثال ، اگر به هیچ بالشی اضافه نکردید ، دو بایت با ارزش 0 دارید ، یعنی هیچ بالشتگی وجود ندارد ، پس از این دو بایت ، بار بار را می خوانید. اگر 5 بایت بالشتک اضافه کنید ، دو بایت اول ارزش 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 بایت باید URL Base64 URL رمزگذاری شده باشد و به هدر رمزگذاری اضافه شود ، مانند این:

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

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

عنوان TTL

مورد نیاز

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

TTL: [Time to live in seconds]

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

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

موضوع

اختیاری

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

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

فوریت

اختیاری

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

مقدار هدر مطابق شکل زیر تعریف شده است. مقدار پیش فرض normal است.

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

همه چیز با هم

اگر سؤال دیگری در مورد چگونگی عملکرد این کار دارید ، همیشه می توانید ببینید که چگونه کتابخانه ها پیام های فشار را در Org Web-Push-Libs ایجاد می کنند.

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

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

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

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

کد وضعیت توضیحات
201 ایجاد شد. درخواست ارسال پیام فشار دریافت و پذیرفته شد.
429 درخواست های خیلی زیاد به این معنی که سرور برنامه شما با یک سرویس فشار به حد نرخ رسیده است. سرویس فشار باید شامل یک هدر "آزمایش مجدد" باشد تا نشان دهد چه مدت قبل از درخواست دیگر می توان انجام داد.
400 درخواست نامعتبر این به طور کلی به این معنی است که یکی از هدرهای شما نامعتبر یا نادرست است.
404 یافت نشد. این نشانگر این است که اشتراک منقضی شده و از آن استفاده نمی شود. در این حالت باید "PushSubscription" را حذف کرده و منتظر بمانید تا مشتری دوباره کاربر را دوباره ثبت کند.
410 رفته اشتراک دیگر معتبر نیست و باید از سرور برنامه حذف شود. این می تواند با تماس با "اشتراک ()" در "PushSubscription" بازتولید شود.
413 اندازه بار بسیار بزرگ. حداقل بار بارگذاری یک سرویس فشار که باید از آن پشتیبانی کند 4096 بایت (یا 4KB) است.

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

بعد کجا بریم

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

،

Matt Gaunt

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

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

نمودار ارسال پیام فشار از سرور خود به فشار خدمات

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

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

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

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

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

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

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

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

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

"اطلاعات امضا شده" که در درخواست به یک عنوان اضافه شده است ، یک توکن وب JSON است.

JSON WEB TOKEN

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

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

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

فشار وب و JWTS امضا شده

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;

برای جلوگیری از هرگونه مشکل در مورد اختلاف ساعت بین برنامه ارسال و سرویس فشار ، 12 ساعت به جای 24 ساعت است.

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

درست مانند اطلاعات JWT ، داده های JWT به عنوان یک رشته URL Safe Base64 رمزگذاری می شوند.

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

فرایند امضای نیاز به رمزگذاری "نشانه بدون امضا" با استفاده از 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);
});

یک سرویس فشار می تواند JWT را با استفاده از کلید سرور برنامه های عمومی برای رمزگشایی امضای تأیید کند و مطمئن شوید که رشته رمزگشایی شده همان "نشانه بدون امضا" است (یعنی دو رشته اول در JWT).

JWT امضا شده (یعنی هر سه رشته که به نقاط پیوستند) ، به عنوان عنوان Authorization با WebPush پیش بینی شده به سرویس Push Web ارسال می شود ، مانند این:

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

پروتکل فشار وب همچنین بیان می کند که کلید سرور برنامه های عمومی باید به عنوان یک رشته رمزگذاری شده URL Safe Base64 با p256ecdsa= IDEN Crypto-Key ارسال شود.

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

رمزگذاری بار

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

یک سؤال متداول که از هر کسی که از خدمات فشار دیگری استفاده کرده است ناشی می شود ، این است که چرا نیاز به بارگذاری بار وب نیاز به رمزگذاری دارد؟ با برنامه های بومی ، پیام های فشار می توانند داده ها را به عنوان متن ساده ارسال کنند.

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

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

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

ECDH و HKDF

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

ECDH: منحنی بیضوی Diffie-Hellman Exchange

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

خاصیت مفید کلیدهای تولید شده با ECDH این است که آلیس می تواند از کلید خصوصی خود و کلید عمومی باب برای ایجاد ارزش مخفی "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 است که هر ماده کلید ضعیف را به مواد کلیدی رمزنگاری قوی تبدیل می کند. به عنوان مثال می توان از آن برای تبدیل اسرار مشترک Diffie Helman به مواد کلیدی مناسب برای استفاده در رمزگذاری ، بررسی یکپارچگی یا احراز هویت استفاده کرد.

در اصل ، 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);
}

برای این کد مثال ، مقاله Hat to 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 به عنوان ورودی مورد نیاز است و نتیجه فرآیند رمزگذاری بار رمزگذاری شده ، یک مقدار نمک و یک کلید عمومی است که فقط برای رمزگذاری داده ها استفاده می شود.

نمک

نمک باید 16 بایت داده های تصادفی باشد. در Nodejs ، ما موارد زیر را برای ایجاد نمک انجام می دهیم:

const salt = crypto.randomBytes(16);

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

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

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

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

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

با استفاده از بار ، Auth Secret و اشتراک عمومی به عنوان ورودی و با نمک تازه تولید شده و مجموعه ای از کلیدهای محلی ، ما آماده هستیم تا در واقع رمزگذاری را انجام دهیم.

راز مشترک

اولین قدم ایجاد یک راز مشترک با استفاده از کلید عمومی اشتراک و کلید خصوصی جدید ما است (توضیحات ECDH را با آلیس و باب به خاطر بسپارید؟ دقیقاً مثل این).

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

این در مرحله بعدی برای محاسبه کلید شبه تصادفی (PRK) استفاده می شود.

کلید شبه تصادفی

The Pseudo Random Key (PRK) is the combination of the push subscription's auth secret, and the shared secret we just created.

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

You might be wondering what the Content-Encoding: auth\0 string is for. In short, it doesn't have a clear purpose, although browsers could decrypt an incoming message and look for the expected content-encoding. The \0 adds a byte with a value of 0 to end of the Buffer. This is expected by browsers decrypting the message who will expect so many bytes for the content encoding, followed a byte with value 0, followed by the encrypted data.

Our Pseudo Random Key is simply running the auth, shared secret and a piece of encoding info through HKDF (ie making it cryptographically stronger).

زمینه

The "context" is a set of bytes that is used to calculate two values later on in the encryption browser. It's essentially an array of bytes containing the subscription public key and the local public key.

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

The final context buffer is a label, the number of bytes in the subscription public key, followed by the key itself, then the number of bytes local public key, followed by the key itself.

With this context value we can use it in the creation of a nonce and a content encryption key (CEK).

Content encryption key and nonce

A nonce is a value that prevents replay attacks as it should only be used once.

The content encryption key (CEK) is the key that will ultimately be used to encrypt our payload.

First we need to create the bytes of data for the nonce and CEK, which is simply a content encoding string followed by the context buffer we just calculated:

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

This information is run through HKDF combining the salt and PRK with the nonceInfo and 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);

This gives us our nonce and content encryption key.

Perform the encryption

Now that we have our content encryption key, we can encrypt the payload.

We create an AES128 cipher using the content encryption key as the key and the nonce is an initialization vector.

In Node this is done like so:

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

Before we encrypt our payload, we need to define how much padding we wish to add to the front of the payload. The reason we'd want to add padding is that it prevents the risk of eavesdroppers being able to determine "types" of messages based on the payload size.

You must add two bytes of padding to indicate the length of any additional padding.

For example, if you added no padding, you'd have two bytes with value 0, ie no padding exists, after these two bytes you'll be reading the payload. If you added 5 bytes of padding, the first two bytes will have a value of 5, so the consumer will then read an additional five bytes and then start reading the payload.

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

We then run our padding and payload through this cipher.

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

We now have our encrypted payload. آری

All that remains is to determine how this payload is sent to the push service.

Encrypted payload headers & body

To send this encrypted payload to the push service we need to define a few different headers in our POST request.

هدر رمزگذاری

The 'Encryption' header must contain the salt used for encrypting the payload.

The 16 byte salt should be base64 URL safe encoded and added to the Encryption header, like so:

Encryption: salt=[URL Safe Base64 Encoded Salt]

Crypto-Key header

We saw that the Crypto-Key header is used under the 'Application Server Keys' section to contain the public application server key.

This header is also used to share the local public key used to encrypt the payload.

The resulting header looks like this:

Crypto-Key: dh=[URL Safe Base64 Encoded Local Public Key String]; p256ecdsa=[URL Safe Base64 Encoded Public Application Server Key]

Content type, length & encoding headers

The Content-Length header is the number of bytes in the encrypted payload. 'Content-Type' and 'Content-Encoding' headers are fixed values. این در زیر نشان داده شده است.

Content-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'

With these headers set, we need to send the encrypted payload as the body of our request. Notice that the Content-Type is set to application/octet-stream . This is because the encrypted payload must be sent as a stream of bytes.

In NodeJS we would do this like so:

const pushRequest = https.request(httpsOptions, function(pushResponse) {
pushRequest.write(encryptedPayload);
pushRequest.end();

More headers?

We've covered the headers used for JWT / Application Server Keys (ie how to identify the application with the push service) and we've covered the headers used to send an encrypted payload.

There are additional headers that push services use to alter the behavior of sent messages. Some of these headers are required, while others are optional.

TTL header

مورد نیاز

TTL (or time to live) is an integer specifying the number of seconds you want your push message to live on the push service before it's delivered. When the TTL expires, the message will be removed from the push service queue and it won't be delivered.

TTL: [Time to live in seconds]

If you set a TTL of zero, the push service will attempt to deliver the message immediately, but if the device can't be reached, your message will be immediately dropped from the push service queue.

Technically a push service can reduce the TTL of a push message if it wants. You can tell if this has happened by examining the TTL header in the response from a push service.

موضوع

اختیاری

Topics are strings that can be used to replace a pending messages with a new message if they have matching topic names.

This is useful in scenarios where multiple messages are sent while a device is offline, and you really only want a user to see the latest message when the device is turned on.

فوریت

اختیاری

Urgency indicates to the push service how important a message is to the user. This can be used by the push service to help conserve the battery life of a user's device by only waking up for important messages when battery is low.

The header value is defined as shown below. The default value is normal .

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

همه چیز با هم

If you have further questions about how this all works you can always see how libraries trigger push messages on the web-push-libs org .

Once you have an encrypted payload, and the headers above, you just need to make a POST request to the endpoint in a PushSubscription .

So what do we do with the response to this POST request?

Response from push service

Once you've made a request to a push service, you need to check the status code of the response as that'll tell you whether the request was successful or not.

کد وضعیت توضیحات
201 ایجاد شد. The request to send a push message was received and accepted.
429 درخواست های خیلی زیاد Meaning your application server has reached a rate limit with a push service. The push service should include a 'Retry-After' header to indicate how long before another request can be made.
400 درخواست نامعتبر This generally means one of your headers is invalid or improperly formatted.
404 یافت نشد. This is an indication that the subscription is expired and can't be used. In this case you should delete the `PushSubscription` and wait for the client to resubscribe the user.
410 رفته The subscription is no longer valid and should be removed from application server. This can be reproduced by calling `unsubscribe()` on a `PushSubscription`.
413 Payload size too large. The minimum size payload a push service must support is 4096 bytes (or 4kb).

You can also read the Web Push standard (RFC8030) for more information about the HTTP status codes.

بعد کجا بریم

Code labs