قبل از Chrome 50، پیامهای فشار نمیتوانست حاوی دادههای باری باشد. هنگامی که رویداد "فشار" در سرویسکار شما فعال شد، تنها چیزی که میدانستید این بود که سرور میخواست چیزی را به شما بگوید، اما نه آنچه ممکن است باشد. سپس باید یک درخواست پیگیری به سرور ارائه میدادید و جزئیات اعلان را برای نمایش به دست میآورید، که ممکن است در شرایط بد شبکه با شکست مواجه شود.
اکنون در کروم 50 (و در نسخه فعلی فایرفاکس روی دسکتاپ) می توانید برخی از داده های دلخواه را همراه با فشار ارسال کنید تا مشتری بتواند از درخواست اضافی جلوگیری کند. با این حال، با قدرت زیاد، مسئولیت بزرگی به همراه دارد، بنابراین تمام دادههای محموله باید رمزگذاری شوند.
رمزگذاری بارها بخش مهمی از داستان امنیتی برای فشار وب است. HTTPS هنگام برقراری ارتباط بین مرورگر و سرور خود امنیت به شما می دهد، زیرا به سرور اعتماد دارید. با این حال، مرورگر انتخاب میکند که از کدام ارائهدهنده فشار برای تحویل بار واقعی استفاده شود، بنابراین شما، بهعنوان توسعهدهنده برنامه، هیچ کنترلی روی آن ندارید.
در اینجا، HTTPS فقط میتواند تضمین کند که هیچکس نمیتواند پیامی را که در حال انتقال به ارائهدهنده سرویس فشار است، جاسوسی کند. هنگامی که آنها آن را دریافت کردند، آزادند هر کاری را که دوست دارند انجام دهند، از جمله انتقال مجدد محموله به شخص ثالث یا تغییر بدخواهانه آن به چیز دیگری. برای محافظت در برابر این، از رمزگذاری استفاده میکنیم تا اطمینان حاصل کنیم که سرویسهای فشار نمیتوانند محمولههای حمل و نقل را بخوانند یا دستکاری کنند.
تغییرات سمت مشتری
اگر قبلاً اعلانهای فشاری را بدون بار پیادهسازی کردهاید ، تنها دو تغییر کوچک وجود دارد که باید در سمت مشتری انجام دهید.
این اول این است که وقتی اطلاعات اشتراک را به سرور باطن خود ارسال می کنید، باید اطلاعات اضافی را جمع آوری کنید. اگر قبلاً از JSON.stringify()
روی شی PushSubscription برای سریال سازی آن برای ارسال به سرور خود استفاده می کنید، نیازی به تغییر چیزی ندارید. اشتراک اکنون مقداری داده اضافی در ویژگی keys خواهد داشت.
> JSON.stringify(subscription)
{"endpoint":"https://android.googleapis.com/gcm/send/f1LsxkKphfQ:APA91bFUx7ja4BK4JVrNgVjpg1cs9lGSGI6IMNL4mQ3Xe6mDGxvt_C_gItKYJI9CAx5i_Ss6cmDxdWZoLyhS2RJhkcv7LeE6hkiOsK6oBzbyifvKCdUYU7ADIRBiYNxIVpLIYeZ8kq_A",
"keys":{"p256dh":"BLc4xRzKlKORKWlbdgFaBrrPK3ydWAHo4M0gs0i1oEKgPpWC5cW8OCzVrOQRv-1npXRWk8udnW3oYhIO4475rds=",
"auth":"5I2Bu2oKdyy9CwL8QVF0NQ=="}}
دو مقدار p256dh
و auth
در گونهای از Base64 که من URL-Safe Base64 مینامم کدگذاری شدهاند.
اگر میخواهید به جای بایتها درست به دست آورید، میتوانید از متد getKey()
جدید در اشتراکی استفاده کنید که پارامتری را به عنوان ArrayBuffer
برمیگرداند. دو پارامتر مورد نیاز auth
و p256dh
هستند.
> new Uint8Array(subscription.getKey('auth'));
[228, 141, 129, ...] (16 bytes)
> new Uint8Array(subscription.getKey('p256dh'));
[4, 183, 56, ...] (65 bytes)
تغییر دوم یک ویژگی داده جدید است که رویداد push
فعال می شود. دارای روشهای همزمان مختلف برای تجزیه دادههای دریافتی، مانند .text()
.json()
، .arrayBuffer()
و .blob()
.
self.addEventListener('push', function(event) {
if (event.data) {
console.log(event.data.json());
}
});
تغییرات سمت سرور
در سمت سرور، همه چیز کمی بیشتر تغییر می کند. فرآیند اصلی این است که شما از اطلاعات کلید رمزگذاری که از مشتری دریافت کرده اید برای رمزگذاری بار استفاده می کنید و سپس آن را به عنوان متن درخواست POST به نقطه پایانی در اشتراک ارسال می کنید و تعدادی هدر HTTP اضافی اضافه می کنید.
جزئیات نسبتاً پیچیده هستند، و مانند هر چیزی که به رمزگذاری مربوط می شود، بهتر است از یک کتابخانه توسعه یافته فعال استفاده کنید تا کتابخانه خودتان. تیم کروم کتابخانهای را برای Node.js منتشر کرده است که به زودی زبانها و پلتفرمهای بیشتری را در اختیار دارد. این کار هم رمزگذاری و هم پروتکل فشار وب را کنترل می کند، به طوری که ارسال پیام فشار از سرور Node.js به آسانی webpush.sendWebPush(message, subscription)
است.
در حالی که ما قطعا استفاده از کتابخانه را توصیه می کنیم، این یک ویژگی جدید است و بسیاری از زبان های محبوب وجود دارند که هنوز هیچ کتابخانه ای ندارند. اگر لازم است این را برای خودتان پیاده کنید، در اینجا جزئیات وجود دارد.
من الگوریتم ها را با استفاده از جاوا اسکریپت با طعم گره نشان خواهم داد، اما اصول اولیه باید در هر زبانی یکسان باشد.
ورودی ها
برای رمزگذاری یک پیام، ابتدا باید دو چیز را از شیء اشتراکی که از مشتری دریافت کردیم دریافت کنیم. اگر از JSON.stringify()
روی کلاینت استفاده کرده اید و آن را به سرور خود ارسال کرده اید، کلید عمومی مشتری در قسمت keys.p256dh
ذخیره می شود، در حالی که راز احراز هویت مشترک در قسمت keys.auth
است. همانطور که در بالا ذکر شد، هر دوی این ها با URL ایمن Base64 کدگذاری می شوند. فرمت باینری کلید عمومی مشتری یک نقطه منحنی بیضوی P-256 فشرده نشده است.
const clientPublicKey = new Buffer(subscription.keys.p256dh, 'base64');
const clientAuthSecret = new Buffer(subscription.keys.auth, 'base64');
کلید عمومی به ما امکان می دهد پیام را به گونه ای رمزگذاری کنیم که فقط با استفاده از کلید خصوصی مشتری رمزگشایی شود.
کلیدهای عمومی معمولاً عمومی در نظر گرفته می شوند، بنابراین برای اینکه به مشتری اجازه دهیم تأیید کند که پیام توسط یک سرور قابل اعتماد ارسال شده است، از رمز تأیید اعتبار نیز استفاده می کنیم. جای تعجب نیست که این باید مخفی بماند، فقط با سرور برنامه ای که می خواهید برای شما پیام ارسال کند به اشتراک گذاشته شود و مانند یک رمز عبور با آن رفتار شود.
همچنین باید داده های جدیدی تولید کنیم. ما به یک نمک تصادفی امن 16 بایتی و یک جفت کلید منحنی بیضوی عمومی/خصوصی نیاز داریم. منحنی خاصی که توسط مشخصات رمزگذاری فشاری استفاده می شود P-256 یا prime256v1
نامیده می شود. برای بهترین امنیت، هر بار که پیامی را رمزگذاری می کنید، جفت کلید باید از ابتدا ایجاد شود و هرگز نباید از نمک استفاده مجدد کنید.
ECDH
بیایید کمی کنار بگذاریم و در مورد ویژگی منظم رمزنگاری منحنی بیضوی صحبت کنیم. فرآیند نسبتاً ساده ای وجود دارد که کلید خصوصی شما را با کلید عمومی شخص دیگری ترکیب می کند تا یک مقدار را استخراج کند. پس چی؟ خوب، اگر طرف مقابل کلید خصوصی خود و کلید عمومی شما را بگیرد، دقیقاً همان مقدار را بدست خواهد آورد!
این اساس پروتکل توافقنامه کلید منحنی بیضوی Diffie-Hellman (ECDH) است که به هر دو طرف اجازه میدهد راز مشترک یکسانی داشته باشند حتی اگر آنها فقط کلیدهای عمومی را رد و بدل کنند. ما از این راز مشترک به عنوان مبنایی برای کلید رمزگذاری واقعی خود استفاده خواهیم کرد.
const crypto = require('crypto');
const salt = crypto.randomBytes(16);
// Node has ECDH built-in to the standard crypto library. For some languages
// you may need to use a third-party library.
const serverECDH = crypto.createECDH('prime256v1');
const serverPublicKey = serverECDH.generateKeys();
const sharedSecret = serverECDH.computeSecret(clientPublicKey);
HKDF
در حال حاضر زمان برای دیگری کنار. فرض کنید برخی از دادههای مخفی دارید که میخواهید از آنها به عنوان کلید رمزگذاری استفاده کنید، اما از نظر رمزنگاری به اندازه کافی امن نیستند. میتوانید از تابع استخراج کلید مبتنی بر HMAC (HKDF) برای تبدیل یک راز با امنیت پایین به راز با امنیت بالا استفاده کنید.
یکی از پیامدهای روش کار این است که به شما امکان میدهد یک راز از هر تعداد بیت را بردارید و یک راز دیگر با هر اندازه تا 255 برابر طولانیتر از هش تولید شده توسط هر الگوریتم هش که استفاده میکنید تولید کنید. برای فشار، مشخصات ما را ملزم به استفاده از SHA-256 می کند که طول هش آن 32 بایت (256 بیت) است.
همانطور که اتفاق می افتد، ما می دانیم که فقط باید کلیدهایی با اندازه حداکثر 32 بایت تولید کنیم. این بدان معنی است که ما می توانیم از یک نسخه ساده شده از الگوریتم استفاده کنیم که نمی تواند اندازه های خروجی بزرگتر را مدیریت کند.
من کد یک نسخه Node را در زیر قرار داده ام، اما می توانید نحوه عملکرد آن را در RFC 5869 دریابید.
ورودیهای HKDF عبارتند از نمک، مقداری مواد کلیدی اولیه (ikm)، یک قطعه اختیاری از دادههای ساختاریافته خاص برای استفاده فعلی (اطلاعات) و طول کلید خروجی مورد نظر بر حسب بایت.
// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
if (length > 32) {
throw new Error('Cannot return keys of more than 32 bytes, ${length} requested');
}
// Extract
const keyHmac = crypto.createHmac('sha256', salt);
keyHmac.update(ikm);
const key = keyHmac.digest();
// Expand
const infoHmac = crypto.createHmac('sha256', key);
infoHmac.update(info);
// A one byte long buffer containing only 0x01
const ONE_BUFFER = new Buffer(1).fill(1);
infoHmac.update(ONE_BUFFER);
return infoHmac.digest().slice(0, length);
}
استخراج پارامترهای رمزگذاری
ما اکنون از HKDF برای تبدیل دادههایی که داریم به پارامترهای رمزگذاری واقعی استفاده میکنیم.
اولین کاری که ما انجام می دهیم این است که از HKDF برای ترکیب راز تأیید اعتبار مشتری و راز مشترک به یک راز طولانی تر و امن تر از نظر رمزنگاری استفاده می کنیم. در مشخصات به این کلید شبه تصادفی (PRK) گفته میشود، بنابراین من آن را در اینجا مینامم، اگرچه متخصصان رمزنگاری ممکن است توجه داشته باشند که این یک PRK کاملاً نیست.
اکنون کلید رمزگذاری محتوای نهایی و یک نانس را ایجاد می کنیم که به رمز ارسال می شود. اینها با ساختن یک ساختار داده ساده برای هر یک، که در مشخصات به عنوان اطلاعات نامیده می شود ، ایجاد می شوند که حاوی اطلاعات خاص منحنی بیضی، فرستنده و گیرنده اطلاعات به منظور تأیید بیشتر منبع پیام است. سپس از HKDF با PRK، نمک و اطلاعات استفاده می کنیم تا کلید و بدون اندازه صحیح را بدست آوریم.
نوع اطلاعات برای رمزگذاری محتوا "aesgcm" است که نام رمز مورد استفاده برای رمزگذاری فشاری است.
const authInfo = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(clientAuthSecret, sharedSecret, authInfo, 32);
function createInfo(type, clientPublicKey, serverPublicKey) {
const len = type.length;
// The start index for each element within the buffer is:
// value | length | start |
// -----------------------------------------
// 'Content-Encoding: '| 18 | 0 |
// type | len | 18 |
// nul byte | 1 | 18 + len |
// 'P-256' | 5 | 19 + len |
// nul byte | 1 | 24 + len |
// client key length | 2 | 25 + len |
// client key | 65 | 27 + len |
// server key length | 2 | 92 + len |
// server key | 65 | 94 + len |
// For the purposes of push encryption the length of the keys will
// always be 65 bytes.
const info = new Buffer(18 + len + 1 + 5 + 1 + 2 + 65 + 2 + 65);
// The string 'Content-Encoding: ', as utf-8
info.write('Content-Encoding: ');
// The 'type' of the record, a utf-8 string
info.write(type, 18);
// A single null-byte
info.write('\0', 18 + len);
// The string 'P-256', declaring the elliptic curve being used
info.write('P-256', 19 + len);
// A single null-byte
info.write('\0', 24 + len);
// The length of the client's public key as a 16-bit integer
info.writeUInt16BE(clientPublicKey.length, 25 + len);
// Now the actual client public key
clientPublicKey.copy(info, 27 + len);
// Length of our public key
info.writeUInt16BE(serverPublicKey.length, 92 + len);
// The key itself
serverPublicKey.copy(info, 94 + len);
return info;
}
// Derive the Content Encryption Key
const contentEncryptionKeyInfo = createInfo('aesgcm', clientPublicKey, serverPublicKey);
const contentEncryptionKey = hkdf(salt, prk, contentEncryptionKeyInfo, 16);
// Derive the Nonce
const nonceInfo = createInfo('nonce', clientPublicKey, serverPublicKey);
const nonce = hkdf(salt, prk, nonceInfo, 12);
بالشتک
دیگر به کنار، و زمان برای یک مثال احمقانه و ساختگی. فرض کنید که رئیس شما سروری دارد که هر چند دقیقه یک بار با قیمت سهام شرکت به او پیام فشار میفرستد. پیام ساده برای این همیشه یک عدد صحیح 32 بیتی با ارزش بر حسب سنت خواهد بود. او همچنین با پرسنل پذیرایی معامله ی دزدکی دارد، به این معنی که آنها می توانند رشته "دونات ها در اتاق استراحت" را 5 دقیقه قبل از تحویل واقعی برای او بفرستند تا "به طور تصادفی" هنگام ورود آنها آنجا باشد و بهترین آن را بگیرد.
رمز استفاده شده توسط Web Push مقادیر رمزگذاری شده ای ایجاد می کند که دقیقا 16 بایت بیشتر از ورودی رمزگذاری نشده است. از آنجایی که «دونات در اتاق استراحت» طولانیتر از قیمت سهام 32 بیتی است، هر کارمند جاسوسی میتواند بدون رمزگشایی پیامها، فقط از روی طول داده، تشخیص دهد که دوناتها چه زمانی میرسند.
به همین دلیل، پروتکل فشار وب به شما امکان می دهد تا به ابتدای داده ها padding اضافه کنید. نحوه استفاده از این به برنامه شما بستگی دارد، اما در مثال بالا میتوانید تمام پیامها را دقیقاً 32 بایت داشته باشید، که تشخیص پیامها را فقط بر اساس طول غیرممکن میکند.
مقدار padding یک عدد صحیح بزرگ اندیان 16 بیتی است که طول padding و به دنبال آن تعداد NUL
بایت padding را مشخص می کند. بنابراین حداقل padding دو بایت است - عدد صفر به 16 بیت کدگذاری شده است.
const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeroes, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);
هنگامی که پیام فشار شما به مشتری می رسد، مرورگر می تواند به طور خودکار هرگونه بالشتک را حذف کند، بنابراین کد مشتری شما فقط پیام بدون پد را دریافت می کند.
رمزگذاری
اکنون ما در نهایت همه چیزهایی را برای انجام رمزگذاری داریم. رمز مورد نیاز برای Web Push AES128 با استفاده از GCM است. ما از کلید رمزگذاری محتوای خود به عنوان کلید و از nonce به عنوان بردار اولیه (IV) استفاده می کنیم.
در این مثال داده های ما یک رشته است، اما می تواند هر داده باینری باشد. میتوانید محمولههایی تا اندازه 4078 بایت - حداکثر 4096 بایت در هر پست، با 16 بایت برای اطلاعات رمزگذاری و حداقل 2 بایت برای padding ارسال کنید.
// Create a buffer from our data, in this case a UTF-8 encoded string
const plaintext = new Buffer('Push notification payload!', 'utf8');
const cipher = crypto.createCipheriv('id-aes128-GCM', contentEncryptionKey,
nonce);
const result = cipher.update(Buffer.concat(padding, plaintext));
cipher.final();
// Append the auth tag to the result - https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
return Buffer.concat([result, cipher.getAuthTag()]);
فشار وب
اوه! اکنون که یک بار رمزگذاری شده دارید، فقط باید یک درخواست HTTP POST نسبتاً ساده به نقطه پایانی مشخص شده توسط اشتراک کاربر ارسال کنید.
شما باید سه هدر تنظیم کنید.
Encryption: salt=<SALT>
Crypto-Key: dh=<PUBLICKEY>
Content-Encoding: aesgcm
<SALT>
و <PUBLICKEY>
کلید عمومی نمک و سرور هستند که در رمزگذاری استفاده میشوند و بهعنوان Base64 ایمن URL کدگذاری میشوند.
هنگام استفاده از پروتکل Web Push، بدنه POST فقط بایت های خام پیام رمزگذاری شده است. با این حال، تا زمانی که Chrome و Firebase Cloud Messaging از پروتکل پشتیبانی نکنند، میتوانید بهراحتی دادهها را به شرح زیر در payload JSON موجود خود قرار دهید.
{
"registration_ids": [ "…" ],
"raw_data": "BIXzEKOFquzVlr/1tS1bhmobZ…"
}
مقدار ویژگی rawData
باید نمایش کدگذاری شده base64 پیام رمزگذاری شده باشد.
اشکال زدایی / تأیید کننده
پیتر بورلو، یکی از مهندسان کروم که این ویژگی را پیادهسازی کرده است (و همچنین یکی از افرادی است که روی این مشخصات کار کرده است)، یک تأییدکننده ایجاد کرده است .
با دریافت کد خود برای خروجی هر یک از مقادیر میانی رمزگذاری، می توانید آنها را در تأیید کننده قرار دهید و بررسی کنید که در مسیر درستی هستید.
،قبل از Chrome 50، پیامهای فشار نمیتوانست حاوی دادههای باری باشد. هنگامی که رویداد "فشار" در سرویسکار شما فعال شد، تنها چیزی که میدانستید این بود که سرور میخواست چیزی را به شما بگوید، اما نه آنچه ممکن است باشد. سپس باید یک درخواست پیگیری به سرور ارائه میدادید و جزئیات اعلان را برای نمایش به دست میآورید، که ممکن است در شرایط بد شبکه با شکست مواجه شود.
اکنون در کروم 50 (و در نسخه فعلی فایرفاکس روی دسکتاپ) می توانید برخی از داده های دلخواه را همراه با فشار ارسال کنید تا مشتری بتواند از درخواست اضافی جلوگیری کند. با این حال، با قدرت زیاد، مسئولیت بزرگی به همراه دارد، بنابراین تمام دادههای محموله باید رمزگذاری شوند.
رمزگذاری بارها بخش مهمی از داستان امنیتی برای فشار وب است. HTTPS هنگام برقراری ارتباط بین مرورگر و سرور خود امنیت به شما می دهد، زیرا به سرور اعتماد دارید. با این حال، مرورگر انتخاب میکند که از کدام ارائهدهنده فشار برای تحویل بار واقعی استفاده شود، بنابراین شما، بهعنوان توسعهدهنده برنامه، هیچ کنترلی روی آن ندارید.
در اینجا، HTTPS فقط میتواند تضمین کند که هیچکس نمیتواند پیامی را که در حال انتقال به ارائهدهنده سرویس فشار است، جاسوسی کند. هنگامی که آنها آن را دریافت کردند، آزادند هر کاری را که دوست دارند انجام دهند، از جمله انتقال مجدد محموله به شخص ثالث یا تغییر بدخواهانه آن به چیز دیگری. برای محافظت در برابر این، از رمزگذاری استفاده میکنیم تا اطمینان حاصل کنیم که سرویسهای فشار نمیتوانند محمولههای حمل و نقل را بخوانند یا دستکاری کنند.
تغییرات سمت مشتری
اگر قبلاً اعلانهای فشاری را بدون بار پیادهسازی کردهاید ، تنها دو تغییر کوچک وجود دارد که باید در سمت مشتری انجام دهید.
این اول این است که وقتی اطلاعات اشتراک را به سرور باطن خود ارسال می کنید، باید اطلاعات اضافی را جمع آوری کنید. اگر قبلاً از JSON.stringify()
روی شی PushSubscription برای سریال سازی آن برای ارسال به سرور خود استفاده می کنید، نیازی به تغییر چیزی ندارید. اشتراک اکنون مقداری داده اضافی در ویژگی keys خواهد داشت.
> JSON.stringify(subscription)
{"endpoint":"https://android.googleapis.com/gcm/send/f1LsxkKphfQ:APA91bFUx7ja4BK4JVrNgVjpg1cs9lGSGI6IMNL4mQ3Xe6mDGxvt_C_gItKYJI9CAx5i_Ss6cmDxdWZoLyhS2RJhkcv7LeE6hkiOsK6oBzbyifvKCdUYU7ADIRBiYNxIVpLIYeZ8kq_A",
"keys":{"p256dh":"BLc4xRzKlKORKWlbdgFaBrrPK3ydWAHo4M0gs0i1oEKgPpWC5cW8OCzVrOQRv-1npXRWk8udnW3oYhIO4475rds=",
"auth":"5I2Bu2oKdyy9CwL8QVF0NQ=="}}
دو مقدار p256dh
و auth
در گونهای از Base64 که من URL-Safe Base64 مینامم کدگذاری شدهاند.
اگر میخواهید به جای بایتها درست به دست آورید، میتوانید از متد getKey()
جدید در اشتراکی استفاده کنید که پارامتری را به عنوان ArrayBuffer
برمیگرداند. دو پارامتر مورد نیاز auth
و p256dh
هستند.
> new Uint8Array(subscription.getKey('auth'));
[228, 141, 129, ...] (16 bytes)
> new Uint8Array(subscription.getKey('p256dh'));
[4, 183, 56, ...] (65 bytes)
تغییر دوم یک ویژگی داده جدید است که رویداد push
فعال می شود. دارای روشهای همزمان مختلف برای تجزیه دادههای دریافتی، مانند .text()
.json()
، .arrayBuffer()
و .blob()
.
self.addEventListener('push', function(event) {
if (event.data) {
console.log(event.data.json());
}
});
تغییرات سمت سرور
در سمت سرور، همه چیز کمی بیشتر تغییر می کند. فرآیند اصلی این است که شما از اطلاعات کلید رمزگذاری که از مشتری دریافت کرده اید برای رمزگذاری بار استفاده می کنید و سپس آن را به عنوان متن درخواست POST به نقطه پایانی در اشتراک ارسال می کنید و تعدادی هدر HTTP اضافی اضافه می کنید.
جزئیات نسبتاً پیچیده هستند، و مانند هر چیزی که به رمزگذاری مربوط می شود، بهتر است از یک کتابخانه توسعه یافته فعال استفاده کنید تا کتابخانه خودتان. تیم کروم کتابخانهای را برای Node.js منتشر کرده است که به زودی زبانها و پلتفرمهای بیشتری را در اختیار دارد. این کار هم رمزگذاری و هم پروتکل فشار وب را کنترل می کند، به طوری که ارسال پیام فشار از سرور Node.js به آسانی webpush.sendWebPush(message, subscription)
است.
در حالی که ما قطعا استفاده از کتابخانه را توصیه می کنیم، این یک ویژگی جدید است و بسیاری از زبان های محبوب وجود دارند که هنوز هیچ کتابخانه ای ندارند. اگر لازم است این را برای خودتان پیاده کنید، در اینجا جزئیات وجود دارد.
من الگوریتم ها را با استفاده از جاوا اسکریپت با طعم گره نشان خواهم داد، اما اصول اولیه باید در هر زبانی یکسان باشد.
ورودی ها
برای رمزگذاری یک پیام، ابتدا باید دو چیز را از شیء اشتراکی که از مشتری دریافت کردیم دریافت کنیم. اگر از JSON.stringify()
روی کلاینت استفاده کرده اید و آن را به سرور خود ارسال کرده اید، کلید عمومی مشتری در قسمت keys.p256dh
ذخیره می شود، در حالی که راز احراز هویت مشترک در قسمت keys.auth
است. همانطور که در بالا ذکر شد، هر دوی این ها با URL ایمن Base64 کدگذاری می شوند. فرمت باینری کلید عمومی مشتری یک نقطه منحنی بیضوی P-256 فشرده نشده است.
const clientPublicKey = new Buffer(subscription.keys.p256dh, 'base64');
const clientAuthSecret = new Buffer(subscription.keys.auth, 'base64');
کلید عمومی به ما امکان می دهد پیام را به گونه ای رمزگذاری کنیم که فقط با استفاده از کلید خصوصی مشتری رمزگشایی شود.
کلیدهای عمومی معمولاً عمومی در نظر گرفته می شوند، بنابراین برای اینکه به مشتری اجازه دهیم تأیید کند که پیام توسط یک سرور قابل اعتماد ارسال شده است، از رمز تأیید اعتبار نیز استفاده می کنیم. جای تعجب نیست که این باید مخفی بماند، فقط با سرور برنامه ای که می خواهید برای شما پیام ارسال کند به اشتراک گذاشته شود و مانند یک رمز عبور با آن رفتار شود.
همچنین باید داده های جدیدی تولید کنیم. ما به یک نمک تصادفی امن 16 بایتی و یک جفت کلید منحنی بیضوی عمومی/خصوصی نیاز داریم. منحنی خاصی که توسط مشخصات رمزگذاری فشاری استفاده می شود P-256 یا prime256v1
نامیده می شود. برای بهترین امنیت، هر بار که پیامی را رمزگذاری می کنید، جفت کلید باید از ابتدا ایجاد شود و هرگز نباید از نمک استفاده مجدد کنید.
ECDH
بیایید کمی کنار بگذاریم و در مورد ویژگی منظم رمزنگاری منحنی بیضوی صحبت کنیم. فرآیند نسبتاً ساده ای وجود دارد که کلید خصوصی شما را با کلید عمومی شخص دیگری ترکیب می کند تا یک مقدار را استخراج کند. پس چی؟ خوب، اگر طرف مقابل کلید خصوصی خود و کلید عمومی شما را بگیرد، دقیقاً همان مقدار را بدست خواهد آورد!
این اساس پروتکل توافقنامه کلید منحنی بیضوی Diffie-Hellman (ECDH) است که به هر دو طرف اجازه میدهد راز مشترک یکسانی داشته باشند حتی اگر آنها فقط کلیدهای عمومی را رد و بدل کنند. ما از این راز مشترک به عنوان مبنایی برای کلید رمزگذاری واقعی خود استفاده خواهیم کرد.
const crypto = require('crypto');
const salt = crypto.randomBytes(16);
// Node has ECDH built-in to the standard crypto library. For some languages
// you may need to use a third-party library.
const serverECDH = crypto.createECDH('prime256v1');
const serverPublicKey = serverECDH.generateKeys();
const sharedSecret = serverECDH.computeSecret(clientPublicKey);
HKDF
در حال حاضر زمان برای دیگری کنار. فرض کنید برخی از دادههای مخفی دارید که میخواهید از آنها به عنوان کلید رمزگذاری استفاده کنید، اما از نظر رمزنگاری به اندازه کافی امن نیستند. میتوانید از تابع استخراج کلید مبتنی بر HMAC (HKDF) برای تبدیل یک راز با امنیت پایین به راز با امنیت بالا استفاده کنید.
یکی از پیامدهای روش کار این است که به شما امکان میدهد یک راز از هر تعداد بیت را بردارید و یک راز دیگر با هر اندازه تا 255 برابر طولانیتر از هش تولید شده توسط هر الگوریتم هش که استفاده میکنید تولید کنید. برای فشار، مشخصات ما را ملزم به استفاده از SHA-256 می کند که طول هش آن 32 بایت (256 بیت) است.
همانطور که اتفاق می افتد، ما می دانیم که فقط باید کلیدهایی با اندازه حداکثر 32 بایت تولید کنیم. این بدان معنی است که ما می توانیم از یک نسخه ساده شده از الگوریتم استفاده کنیم که نمی تواند اندازه های خروجی بزرگتر را مدیریت کند.
من کد یک نسخه Node را در زیر قرار داده ام، اما می توانید نحوه عملکرد آن را در RFC 5869 دریابید.
ورودیهای HKDF عبارتند از نمک، مقداری مواد کلیدی اولیه (ikm)، یک قطعه اختیاری از دادههای ساختاریافته خاص برای استفاده فعلی (اطلاعات) و طول کلید خروجی مورد نظر بر حسب بایت.
// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
if (length > 32) {
throw new Error('Cannot return keys of more than 32 bytes, ${length} requested');
}
// Extract
const keyHmac = crypto.createHmac('sha256', salt);
keyHmac.update(ikm);
const key = keyHmac.digest();
// Expand
const infoHmac = crypto.createHmac('sha256', key);
infoHmac.update(info);
// A one byte long buffer containing only 0x01
const ONE_BUFFER = new Buffer(1).fill(1);
infoHmac.update(ONE_BUFFER);
return infoHmac.digest().slice(0, length);
}
استخراج پارامترهای رمزگذاری
ما اکنون از HKDF برای تبدیل دادههایی که داریم به پارامترهای رمزگذاری واقعی استفاده میکنیم.
اولین کاری که ما انجام می دهیم این است که از HKDF برای ترکیب راز تأیید اعتبار مشتری و راز مشترک به یک راز طولانی تر و امن تر از نظر رمزنگاری استفاده می کنیم. در مشخصات به این کلید شبه تصادفی (PRK) گفته میشود، بنابراین من آن را در اینجا مینامم، اگرچه متخصصان رمزنگاری ممکن است توجه داشته باشند که این یک PRK کاملاً نیست.
اکنون کلید رمزگذاری محتوای نهایی و یک نانس را ایجاد می کنیم که به رمز ارسال می شود. اینها با ساختن یک ساختار داده ساده برای هر یک، که در مشخصات به عنوان اطلاعات نامیده می شود ، ایجاد می شوند که حاوی اطلاعات خاص منحنی بیضی، فرستنده و گیرنده اطلاعات به منظور تأیید بیشتر منبع پیام است. سپس از HKDF با PRK، نمک و اطلاعات استفاده می کنیم تا کلید و بدون اندازه صحیح را بدست آوریم.
نوع اطلاعات برای رمزگذاری محتوا "aesgcm" است که نام رمز مورد استفاده برای رمزگذاری فشاری است.
const authInfo = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(clientAuthSecret, sharedSecret, authInfo, 32);
function createInfo(type, clientPublicKey, serverPublicKey) {
const len = type.length;
// The start index for each element within the buffer is:
// value | length | start |
// -----------------------------------------
// 'Content-Encoding: '| 18 | 0 |
// type | len | 18 |
// nul byte | 1 | 18 + len |
// 'P-256' | 5 | 19 + len |
// nul byte | 1 | 24 + len |
// client key length | 2 | 25 + len |
// client key | 65 | 27 + len |
// server key length | 2 | 92 + len |
// server key | 65 | 94 + len |
// For the purposes of push encryption the length of the keys will
// always be 65 bytes.
const info = new Buffer(18 + len + 1 + 5 + 1 + 2 + 65 + 2 + 65);
// The string 'Content-Encoding: ', as utf-8
info.write('Content-Encoding: ');
// The 'type' of the record, a utf-8 string
info.write(type, 18);
// A single null-byte
info.write('\0', 18 + len);
// The string 'P-256', declaring the elliptic curve being used
info.write('P-256', 19 + len);
// A single null-byte
info.write('\0', 24 + len);
// The length of the client's public key as a 16-bit integer
info.writeUInt16BE(clientPublicKey.length, 25 + len);
// Now the actual client public key
clientPublicKey.copy(info, 27 + len);
// Length of our public key
info.writeUInt16BE(serverPublicKey.length, 92 + len);
// The key itself
serverPublicKey.copy(info, 94 + len);
return info;
}
// Derive the Content Encryption Key
const contentEncryptionKeyInfo = createInfo('aesgcm', clientPublicKey, serverPublicKey);
const contentEncryptionKey = hkdf(salt, prk, contentEncryptionKeyInfo, 16);
// Derive the Nonce
const nonceInfo = createInfo('nonce', clientPublicKey, serverPublicKey);
const nonce = hkdf(salt, prk, nonceInfo, 12);
بالشتک
دیگر به کنار، و زمان برای یک مثال احمقانه و ساختگی. فرض کنید که رئیس شما سروری دارد که هر چند دقیقه یک بار با قیمت سهام شرکت به او پیام فشار میفرستد. پیام ساده برای این همیشه یک عدد صحیح 32 بیتی با ارزش بر حسب سنت خواهد بود. او همچنین با پرسنل پذیرایی معامله ی دزدکی دارد، به این معنی که آنها می توانند رشته "دونات ها در اتاق استراحت" را 5 دقیقه قبل از تحویل واقعی برای او بفرستند تا "به طور تصادفی" هنگام ورود آنها آنجا باشد و بهترین آن را بگیرد.
رمز استفاده شده توسط Web Push مقادیر رمزگذاری شده ای ایجاد می کند که دقیقا 16 بایت بیشتر از ورودی رمزگذاری نشده است. از آنجایی که «دونات در اتاق استراحت» طولانیتر از قیمت سهام 32 بیتی است، هر کارمند جاسوسی میتواند بدون رمزگشایی پیامها، فقط از روی طول داده، تشخیص دهد که دوناتها چه زمانی میرسند.
به همین دلیل، پروتکل فشار وب به شما امکان می دهد تا به ابتدای داده ها padding اضافه کنید. نحوه استفاده از این به برنامه شما بستگی دارد، اما در مثال بالا میتوانید تمام پیامها را دقیقاً 32 بایت داشته باشید، که تشخیص پیامها را فقط بر اساس طول غیرممکن میکند.
مقدار padding یک عدد صحیح بزرگ اندیان 16 بیتی است که طول padding و به دنبال آن تعداد NUL
بایت padding را مشخص می کند. بنابراین حداقل padding دو بایت است - عدد صفر به 16 بیت کدگذاری شده است.
const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeroes, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);
هنگامی که پیام فشار شما به مشتری می رسد، مرورگر می تواند به طور خودکار هرگونه بالشتک را حذف کند، بنابراین کد مشتری شما فقط پیام بدون پد را دریافت می کند.
رمزگذاری
اکنون ما در نهایت همه چیزهایی را برای انجام رمزگذاری داریم. رمز مورد نیاز برای Web Push AES128 با استفاده از GCM است. ما از کلید رمزگذاری محتوای خود به عنوان کلید و از nonce به عنوان بردار اولیه (IV) استفاده می کنیم.
در این مثال داده های ما یک رشته است، اما می تواند هر داده باینری باشد. میتوانید محمولههایی تا اندازه 4078 بایت - حداکثر 4096 بایت در هر پست، با 16 بایت برای اطلاعات رمزگذاری و حداقل 2 بایت برای padding ارسال کنید.
// Create a buffer from our data, in this case a UTF-8 encoded string
const plaintext = new Buffer('Push notification payload!', 'utf8');
const cipher = crypto.createCipheriv('id-aes128-GCM', contentEncryptionKey,
nonce);
const result = cipher.update(Buffer.concat(padding, plaintext));
cipher.final();
// Append the auth tag to the result - https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
return Buffer.concat([result, cipher.getAuthTag()]);
فشار وب
اوه! اکنون که یک بار رمزگذاری شده دارید، فقط باید یک درخواست HTTP POST نسبتاً ساده به نقطه پایانی مشخص شده توسط اشتراک کاربر ارسال کنید.
شما باید سه هدر تنظیم کنید.
Encryption: salt=<SALT>
Crypto-Key: dh=<PUBLICKEY>
Content-Encoding: aesgcm
<SALT>
و <PUBLICKEY>
کلید عمومی نمک و سرور هستند که در رمزگذاری استفاده میشوند و بهعنوان Base64 ایمن URL کدگذاری میشوند.
هنگام استفاده از پروتکل Web Push، بدنه POST فقط بایت های خام پیام رمزگذاری شده است. با این حال، تا زمانی که Chrome و Firebase Cloud Messaging از پروتکل پشتیبانی نکنند، میتوانید بهراحتی دادهها را به شرح زیر در payload JSON موجود خود قرار دهید.
{
"registration_ids": [ "…" ],
"raw_data": "BIXzEKOFquzVlr/1tS1bhmobZ…"
}
مقدار ویژگی rawData
باید نمایش کدگذاری شده base64 پیام رمزگذاری شده باشد.
اشکال زدایی / تأیید کننده
پیتر بورلو، یکی از مهندسان کروم که این ویژگی را پیادهسازی کرده است (و همچنین یکی از افرادی است که روی این مشخصات کار کرده است)، یک تأییدکننده ایجاد کرده است .
با دریافت کد خود برای خروجی هر یک از مقادیر میانی رمزگذاری، می توانید آنها را در تأیید کننده قرار دهید و بررسی کنید که در مسیر درستی هستید.
،قبل از Chrome 50، پیامهای فشار نمیتوانست حاوی دادههای باری باشد. هنگامی که رویداد "فشار" در سرویسکار شما فعال شد، تنها چیزی که میدانستید این بود که سرور میخواست چیزی را به شما بگوید، اما نه آنچه ممکن است باشد. سپس باید یک درخواست پیگیری به سرور ارائه میدادید و جزئیات اعلان را برای نمایش به دست میآورید، که ممکن است در شرایط بد شبکه با شکست مواجه شود.
اکنون در کروم 50 (و در نسخه فعلی فایرفاکس روی دسکتاپ) می توانید برخی از داده های دلخواه را همراه با فشار ارسال کنید تا مشتری بتواند از درخواست اضافی جلوگیری کند. با این حال، با قدرت زیاد، مسئولیت بزرگی به همراه دارد، بنابراین تمام دادههای محموله باید رمزگذاری شوند.
رمزگذاری بارها بخش مهمی از داستان امنیتی برای فشار وب است. HTTPS هنگام برقراری ارتباط بین مرورگر و سرور خود امنیت به شما می دهد، زیرا به سرور اعتماد دارید. با این حال، مرورگر انتخاب میکند که از کدام ارائهدهنده فشار برای تحویل بار واقعی استفاده شود، بنابراین شما، بهعنوان توسعهدهنده برنامه، هیچ کنترلی روی آن ندارید.
در اینجا، HTTPS فقط میتواند تضمین کند که هیچکس نمیتواند پیامی را که در حال انتقال به ارائهدهنده سرویس فشار است، جاسوسی کند. هنگامی که آنها آن را دریافت کردند، آزادند هر کاری را که دوست دارند انجام دهند، از جمله انتقال مجدد محموله به شخص ثالث یا تغییر بدخواهانه آن به چیز دیگری. برای محافظت در برابر این، از رمزگذاری استفاده میکنیم تا اطمینان حاصل کنیم که سرویسهای فشار نمیتوانند محمولههای حمل و نقل را بخوانند یا دستکاری کنند.
تغییرات سمت مشتری
اگر قبلاً اعلانهای فشاری را بدون بار پیادهسازی کردهاید ، تنها دو تغییر کوچک وجود دارد که باید در سمت مشتری انجام دهید.
این اول این است که وقتی اطلاعات اشتراک را به سرور باطن خود ارسال می کنید، باید اطلاعات اضافی را جمع آوری کنید. اگر قبلاً از JSON.stringify()
روی شی PushSubscription برای سریال سازی آن برای ارسال به سرور خود استفاده می کنید، نیازی به تغییر چیزی ندارید. اشتراک اکنون مقداری داده اضافی در ویژگی keys خواهد داشت.
> JSON.stringify(subscription)
{"endpoint":"https://android.googleapis.com/gcm/send/f1LsxkKphfQ:APA91bFUx7ja4BK4JVrNgVjpg1cs9lGSGI6IMNL4mQ3Xe6mDGxvt_C_gItKYJI9CAx5i_Ss6cmDxdWZoLyhS2RJhkcv7LeE6hkiOsK6oBzbyifvKCdUYU7ADIRBiYNxIVpLIYeZ8kq_A",
"keys":{"p256dh":"BLc4xRzKlKORKWlbdgFaBrrPK3ydWAHo4M0gs0i1oEKgPpWC5cW8OCzVrOQRv-1npXRWk8udnW3oYhIO4475rds=",
"auth":"5I2Bu2oKdyy9CwL8QVF0NQ=="}}
دو مقدار p256dh
و auth
در گونهای از Base64 که من URL-Safe Base64 مینامم کدگذاری شدهاند.
اگر میخواهید به جای بایتها درست به دست آورید، میتوانید از متد getKey()
جدید در اشتراکی استفاده کنید که پارامتری را به عنوان ArrayBuffer
برمیگرداند. دو پارامتر مورد نیاز auth
و p256dh
هستند.
> new Uint8Array(subscription.getKey('auth'));
[228, 141, 129, ...] (16 bytes)
> new Uint8Array(subscription.getKey('p256dh'));
[4, 183, 56, ...] (65 bytes)
تغییر دوم یک ویژگی داده جدید است که رویداد push
فعال می شود. دارای روشهای همزمان مختلف برای تجزیه دادههای دریافتی، مانند .text()
.json()
، .arrayBuffer()
و .blob()
.
self.addEventListener('push', function(event) {
if (event.data) {
console.log(event.data.json());
}
});
تغییرات سمت سرور
در سمت سرور، همه چیز کمی بیشتر تغییر می کند. فرآیند اصلی این است که شما از اطلاعات کلید رمزگذاری که از مشتری دریافت کرده اید برای رمزگذاری بار استفاده می کنید و سپس آن را به عنوان متن درخواست POST به نقطه پایانی در اشتراک ارسال می کنید و تعدادی هدر HTTP اضافی اضافه می کنید.
جزئیات نسبتاً پیچیده هستند، و مانند هر چیزی که به رمزگذاری مربوط می شود، بهتر است از یک کتابخانه توسعه یافته فعال استفاده کنید تا کتابخانه خودتان. تیم کروم کتابخانهای را برای Node.js منتشر کرده است که به زودی زبانها و پلتفرمهای بیشتری را در اختیار دارد. این کار هم رمزگذاری و هم پروتکل فشار وب را کنترل می کند، به طوری که ارسال پیام فشار از سرور Node.js به آسانی webpush.sendWebPush(message, subscription)
است.
در حالی که ما قطعا استفاده از کتابخانه را توصیه می کنیم، این یک ویژگی جدید است و بسیاری از زبان های محبوب وجود دارند که هنوز هیچ کتابخانه ای ندارند. اگر لازم است این را برای خودتان پیاده کنید، در اینجا جزئیات وجود دارد.
من الگوریتم ها را با استفاده از جاوا اسکریپت با طعم گره نشان خواهم داد، اما اصول اولیه باید در هر زبانی یکسان باشد.
ورودی ها
برای رمزگذاری یک پیام، ابتدا باید دو چیز را از شیء اشتراکی که از مشتری دریافت کردیم دریافت کنیم. اگر از JSON.stringify()
روی کلاینت استفاده کرده اید و آن را به سرور خود ارسال کرده اید، کلید عمومی مشتری در قسمت keys.p256dh
ذخیره می شود، در حالی که راز احراز هویت مشترک در قسمت keys.auth
است. همانطور که در بالا ذکر شد، هر دوی این ها با URL ایمن Base64 کدگذاری می شوند. فرمت باینری کلید عمومی مشتری یک نقطه منحنی بیضوی P-256 فشرده نشده است.
const clientPublicKey = new Buffer(subscription.keys.p256dh, 'base64');
const clientAuthSecret = new Buffer(subscription.keys.auth, 'base64');
کلید عمومی به ما امکان می دهد پیام را به گونه ای رمزگذاری کنیم که فقط با استفاده از کلید خصوصی مشتری رمزگشایی شود.
کلیدهای عمومی معمولاً عمومی در نظر گرفته می شوند، بنابراین برای اینکه به مشتری اجازه دهیم تأیید کند که پیام توسط یک سرور قابل اعتماد ارسال شده است، از رمز تأیید اعتبار نیز استفاده می کنیم. جای تعجب نیست که این باید مخفی بماند، فقط با سرور برنامه ای که می خواهید برای شما پیام ارسال کند به اشتراک گذاشته شود و مانند یک رمز عبور با آن رفتار شود.
همچنین باید داده های جدیدی تولید کنیم. ما به یک نمک تصادفی امن 16 بایتی و یک جفت کلید منحنی بیضوی عمومی/خصوصی نیاز داریم. منحنی خاصی که توسط مشخصات رمزگذاری فشاری استفاده می شود P-256 یا prime256v1
نامیده می شود. برای بهترین امنیت، هر بار که پیامی را رمزگذاری می کنید، جفت کلید باید از ابتدا ایجاد شود و هرگز نباید از نمک استفاده مجدد کنید.
ECDH
بیایید کمی کنار بگذاریم و در مورد ویژگی منظم رمزنگاری منحنی بیضوی صحبت کنیم. فرآیند نسبتاً ساده ای وجود دارد که کلید خصوصی شما را با کلید عمومی شخص دیگری ترکیب می کند تا یک مقدار را استخراج کند. پس چی؟ خوب، اگر طرف مقابل کلید خصوصی خود و کلید عمومی شما را بگیرد، دقیقاً همان مقدار را بدست خواهد آورد!
این اساس پروتکل توافقنامه کلید منحنی بیضوی Diffie-Hellman (ECDH) است که به هر دو طرف اجازه میدهد راز مشترک یکسانی داشته باشند حتی اگر آنها فقط کلیدهای عمومی را رد و بدل کنند. ما از این راز مشترک به عنوان مبنایی برای کلید رمزگذاری واقعی خود استفاده خواهیم کرد.
const crypto = require('crypto');
const salt = crypto.randomBytes(16);
// Node has ECDH built-in to the standard crypto library. For some languages
// you may need to use a third-party library.
const serverECDH = crypto.createECDH('prime256v1');
const serverPublicKey = serverECDH.generateKeys();
const sharedSecret = serverECDH.computeSecret(clientPublicKey);
HKDF
در حال حاضر زمان برای دیگری کنار. فرض کنید برخی از دادههای مخفی دارید که میخواهید از آنها به عنوان کلید رمزگذاری استفاده کنید، اما از نظر رمزنگاری به اندازه کافی امن نیستند. میتوانید از تابع استخراج کلید مبتنی بر HMAC (HKDF) برای تبدیل یک راز با امنیت پایین به راز با امنیت بالا استفاده کنید.
یکی از پیامدهای روش کار این است که به شما امکان میدهد یک راز از هر تعداد بیت را بردارید و یک راز دیگر با هر اندازه تا 255 برابر طولانیتر از هش تولید شده توسط هر الگوریتم هش که استفاده میکنید تولید کنید. برای فشار، مشخصات ما را ملزم به استفاده از SHA-256 می کند که طول هش آن 32 بایت (256 بیت) است.
همانطور که اتفاق می افتد، ما می دانیم که فقط باید کلیدهایی با اندازه حداکثر 32 بایت تولید کنیم. این بدان معنی است که ما می توانیم از یک نسخه ساده شده از الگوریتم استفاده کنیم که نمی تواند اندازه های خروجی بزرگتر را مدیریت کند.
من کد یک نسخه Node را در زیر قرار داده ام، اما می توانید نحوه عملکرد آن را در RFC 5869 دریابید.
ورودیهای HKDF عبارتند از نمک، مقداری مواد کلیدی اولیه (ikm)، یک قطعه اختیاری از دادههای ساختاریافته خاص برای استفاده فعلی (اطلاعات) و طول کلید خروجی مورد نظر بر حسب بایت.
// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
if (length > 32) {
throw new Error('Cannot return keys of more than 32 bytes, ${length} requested');
}
// Extract
const keyHmac = crypto.createHmac('sha256', salt);
keyHmac.update(ikm);
const key = keyHmac.digest();
// Expand
const infoHmac = crypto.createHmac('sha256', key);
infoHmac.update(info);
// A one byte long buffer containing only 0x01
const ONE_BUFFER = new Buffer(1).fill(1);
infoHmac.update(ONE_BUFFER);
return infoHmac.digest().slice(0, length);
}
استخراج پارامترهای رمزگذاری
ما اکنون از HKDF برای تبدیل دادههایی که داریم به پارامترهای رمزگذاری واقعی استفاده میکنیم.
اولین کاری که ما انجام می دهیم این است که از HKDF برای ترکیب راز تأیید اعتبار مشتری و راز مشترک به یک راز طولانی تر و امن تر از نظر رمزنگاری استفاده می کنیم. در مشخصات به این کلید شبه تصادفی (PRK) گفته میشود، بنابراین من آن را در اینجا مینامم، اگرچه متخصصان رمزنگاری ممکن است توجه داشته باشند که این یک PRK کاملاً نیست.
اکنون کلید رمزگذاری محتوای نهایی و یک نانس را ایجاد می کنیم که به رمز ارسال می شود. اینها با ساختن یک ساختار داده ساده برای هر یک، که در مشخصات به عنوان اطلاعات نامیده می شود ، ایجاد می شوند که حاوی اطلاعات خاص منحنی بیضی، فرستنده و گیرنده اطلاعات به منظور تأیید بیشتر منبع پیام است. سپس از HKDF با PRK، نمک و اطلاعات استفاده می کنیم تا کلید و بدون اندازه صحیح را بدست آوریم.
نوع اطلاعات برای رمزگذاری محتوا "aesgcm" است که نام رمز مورد استفاده برای رمزگذاری فشاری است.
const authInfo = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(clientAuthSecret, sharedSecret, authInfo, 32);
function createInfo(type, clientPublicKey, serverPublicKey) {
const len = type.length;
// The start index for each element within the buffer is:
// value | length | start |
// -----------------------------------------
// 'Content-Encoding: '| 18 | 0 |
// type | len | 18 |
// nul byte | 1 | 18 + len |
// 'P-256' | 5 | 19 + len |
// nul byte | 1 | 24 + len |
// client key length | 2 | 25 + len |
// client key | 65 | 27 + len |
// server key length | 2 | 92 + len |
// server key | 65 | 94 + len |
// For the purposes of push encryption the length of the keys will
// always be 65 bytes.
const info = new Buffer(18 + len + 1 + 5 + 1 + 2 + 65 + 2 + 65);
// The string 'Content-Encoding: ', as utf-8
info.write('Content-Encoding: ');
// The 'type' of the record, a utf-8 string
info.write(type, 18);
// A single null-byte
info.write('\0', 18 + len);
// The string 'P-256', declaring the elliptic curve being used
info.write('P-256', 19 + len);
// A single null-byte
info.write('\0', 24 + len);
// The length of the client's public key as a 16-bit integer
info.writeUInt16BE(clientPublicKey.length, 25 + len);
// Now the actual client public key
clientPublicKey.copy(info, 27 + len);
// Length of our public key
info.writeUInt16BE(serverPublicKey.length, 92 + len);
// The key itself
serverPublicKey.copy(info, 94 + len);
return info;
}
// Derive the Content Encryption Key
const contentEncryptionKeyInfo = createInfo('aesgcm', clientPublicKey, serverPublicKey);
const contentEncryptionKey = hkdf(salt, prk, contentEncryptionKeyInfo, 16);
// Derive the Nonce
const nonceInfo = createInfo('nonce', clientPublicKey, serverPublicKey);
const nonce = hkdf(salt, prk, nonceInfo, 12);
بالشتک
دیگر به کنار، و زمان برای یک مثال احمقانه و ساختگی. فرض کنید که رئیس شما سروری دارد که هر چند دقیقه یک بار با قیمت سهام شرکت به او پیام فشار میفرستد. پیام ساده برای این همیشه یک عدد صحیح 32 بیتی با ارزش بر حسب سنت خواهد بود. او همچنین با پرسنل پذیرایی معامله ی دزدکی دارد، به این معنی که آنها می توانند رشته "دونات ها در اتاق استراحت" را 5 دقیقه قبل از تحویل واقعی برای او بفرستند تا "به طور تصادفی" هنگام ورود آنها آنجا باشد و بهترین آن را بگیرد.
رمز استفاده شده توسط Web Push مقادیر رمزگذاری شده ای ایجاد می کند که دقیقا 16 بایت بیشتر از ورودی رمزگذاری نشده است. از آنجایی که «دونات در اتاق استراحت» طولانیتر از قیمت سهام 32 بیتی است، هر کارمند جاسوسی میتواند بدون رمزگشایی پیامها، فقط از روی طول داده، تشخیص دهد که دوناتها چه زمانی میرسند.
به همین دلیل، پروتکل فشار وب به شما امکان می دهد تا به ابتدای داده ها padding اضافه کنید. نحوه استفاده از این به برنامه شما بستگی دارد، اما در مثال بالا میتوانید تمام پیامها را دقیقاً 32 بایت داشته باشید، که تشخیص پیامها را فقط بر اساس طول غیرممکن میکند.
مقدار padding یک عدد صحیح بزرگ اندیان 16 بیتی است که طول padding و به دنبال آن تعداد NUL
بایت padding را مشخص می کند. بنابراین حداقل padding دو بایت است - عدد صفر به 16 بیت کدگذاری شده است.
const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeroes, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);
هنگامی که پیام فشار شما به مشتری می رسد، مرورگر می تواند به طور خودکار هرگونه بالشتک را حذف کند، بنابراین کد مشتری شما فقط پیام بدون پد را دریافت می کند.
رمزگذاری
اکنون ما در نهایت همه چیزهایی را برای انجام رمزگذاری داریم. رمز مورد نیاز برای Web Push AES128 با استفاده از GCM است. ما از کلید رمزگذاری محتوای خود به عنوان کلید و از nonce به عنوان بردار اولیه (IV) استفاده می کنیم.
در این مثال داده های ما یک رشته است، اما می تواند هر داده باینری باشد. میتوانید محمولههایی تا اندازه 4078 بایت - حداکثر 4096 بایت در هر پست، با 16 بایت برای اطلاعات رمزگذاری و حداقل 2 بایت برای padding ارسال کنید.
// Create a buffer from our data, in this case a UTF-8 encoded string
const plaintext = new Buffer('Push notification payload!', 'utf8');
const cipher = crypto.createCipheriv('id-aes128-GCM', contentEncryptionKey,
nonce);
const result = cipher.update(Buffer.concat(padding, plaintext));
cipher.final();
// Append the auth tag to the result - https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
return Buffer.concat([result, cipher.getAuthTag()]);
فشار وب
اوه! اکنون که یک بار رمزگذاری شده دارید، فقط باید یک درخواست HTTP POST نسبتاً ساده به نقطه پایانی مشخص شده توسط اشتراک کاربر ارسال کنید.
شما باید سه هدر تنظیم کنید.
Encryption: salt=<SALT>
Crypto-Key: dh=<PUBLICKEY>
Content-Encoding: aesgcm
<SALT>
و <PUBLICKEY>
کلید عمومی نمک و سرور هستند که در رمزگذاری استفاده میشوند و بهعنوان Base64 ایمن URL کدگذاری میشوند.
هنگام استفاده از پروتکل Web Push، بدنه POST فقط بایت های خام پیام رمزگذاری شده است. با این حال، تا زمانی که Chrome و Firebase Cloud Messaging از پروتکل پشتیبانی نکنند، میتوانید بهراحتی دادهها را به شرح زیر در payload JSON موجود خود قرار دهید.
{
"registration_ids": [ "…" ],
"raw_data": "BIXzEKOFquzVlr/1tS1bhmobZ…"
}
مقدار ویژگی rawData
باید نمایش کدگذاری شده base64 پیام رمزگذاری شده باشد.
اشکال زدایی / تأیید کننده
پیتر بورلو، یکی از مهندسان کروم که این ویژگی را پیادهسازی کرده است (و همچنین یکی از افرادی است که روی این مشخصات کار کرده است)، یک تأییدکننده ایجاد کرده است .
با دریافت کد خود برای خروجی هر یک از مقادیر میانی رمزگذاری ، می توانید آنها را در تأیید کننده چسبانده و بررسی کنید که در مسیر صحیح قرار دارید.
،قبل از Chrome 50 ، پیام های فشار نمی توانند حاوی داده های بارگذاری باشند. هنگامی که رویداد "فشار" در کارگر سرویس شما شلیک شد ، تمام آنچه شما می دانستید این بود که سرور در تلاش بود تا چیزی را به شما بگوید ، اما نه آنچه ممکن است باشد. سپس شما مجبور بودید درخواست پیگیری را به سرور ارائه دهید و جزئیات اطلاع رسانی را برای نشان دادن بدست آورید ، که ممکن است در شرایط نامناسب شبکه شکست بخورد.
اکنون در Chrome 50 (و در نسخه فعلی Firefox در دسک تاپ) می توانید برخی از داده های دلخواه را به همراه فشار ارسال کنید تا مشتری بتواند از درخواست اضافی خودداری کند. با این حال ، با قدرت عالی مسئولیت بزرگی به وجود می آید ، بنابراین تمام داده های بارگذاری باید رمزگذاری شوند.
رمزگذاری Payloads بخش مهمی از داستان امنیتی برای فشار وب است. HTTPS هنگام برقراری ارتباط بین مرورگر و سرور شخصی خود به شما امنیت می دهد ، زیرا به سرور اعتماد دارید. با این حال ، مرورگر انتخاب می کند که از ارائه دهنده فشار برای تحویل بار در واقع استفاده می شود ، بنابراین شما ، به عنوان توسعه دهنده برنامه ، هیچ کنترلی بر آن ندارید.
در اینجا ، HTTPS فقط می تواند تضمین کند که هیچ کس نمی تواند پیام را در انتقال به ارائه دهنده خدمات فشار وارد کند. پس از دریافت آن ، آنها می توانند آنچه را که دوست دارند انجام دهند ، از جمله انتقال مجدد بار به شخص ثالث یا تغییر بدخواهانه آن را به چیز دیگری تغییر دهید. برای محافظت از این امر از رمزگذاری استفاده می کنیم تا اطمینان حاصل کنیم که خدمات فشار نمی توانند با بارگذاری بار در حمل و نقل ، بخوانند یا دستکاری کنند.
تغییرات سمت مشتری
اگر قبلاً اعلان های فشار را بدون بار بارهای اجرا کرده اید ، فقط دو تغییر کوچک وجود دارد که باید در سمت مشتری ایجاد کنید.
این اول این است که وقتی اطلاعات اشتراک را به سرور باطن خود ارسال می کنید ، باید اطلاعات اضافی را جمع آوری کنید. اگر قبلاً از JSON.stringify()
در شیء pushsubscription استفاده کرده اید تا آن را برای ارسال به سرور خود سریال کنید ، دیگر نیازی به تغییر چیزی ندارید. این اشتراک اکنون داده های اضافی در ویژگی کلیدها خواهد داشت.
> JSON.stringify(subscription)
{"endpoint":"https://android.googleapis.com/gcm/send/f1LsxkKphfQ:APA91bFUx7ja4BK4JVrNgVjpg1cs9lGSGI6IMNL4mQ3Xe6mDGxvt_C_gItKYJI9CAx5i_Ss6cmDxdWZoLyhS2RJhkcv7LeE6hkiOsK6oBzbyifvKCdUYU7ADIRBiYNxIVpLIYeZ8kq_A",
"keys":{"p256dh":"BLc4xRzKlKORKWlbdgFaBrrPK3ydWAHo4M0gs0i1oEKgPpWC5cW8OCzVrOQRv-1npXRWk8udnW3oYhIO4475rds=",
"auth":"5I2Bu2oKdyy9CwL8QVF0NQ=="}}
دو مقدار p256dh
و auth
در نوع Base64 رمزگذاری شده اند که من آن را URL-Safe Base64 می نامم.
اگر می خواهید به جای آن در بایت ها درست شوید ، می توانید از روش جدید getKey()
در اشتراک استفاده کنید که یک پارامتر را به عنوان یک ArrayBuffer
بازگرداند. دو پارامتر مورد نیاز شما auth
و p256dh
هستند.
> new Uint8Array(subscription.getKey('auth'));
[228, 141, 129, ...] (16 bytes)
> new Uint8Array(subscription.getKey('p256dh'));
[4, 183, 56, ...] (65 bytes)
تغییر دوم یک ویژگی جدید داده هنگام آتش سوزی رویداد push
است. این روشهای همزمان مختلف برای تجزیه داده های دریافت شده ، مانند .text()
، .json()
، .arrayBuffer()
و .blob()
دارد.
self.addEventListener('push', function(event) {
if (event.data) {
console.log(event.data.json());
}
});
تغییرات سمت سرور
از طرف سرور ، همه چیز کمی بیشتر تغییر می کند. فرآیند اصلی این است که شما از اطلاعات کلید رمزگذاری که از مشتری دریافت کرده اید برای رمزگذاری بار استفاده می کنید و سپس آن را به عنوان بدنه درخواست پست به نقطه انتهایی در اشتراک ارسال می کنید و برخی از هدرهای HTTP اضافی را اضافه می کنید.
جزئیات نسبتاً پیچیده هستند و مانند هر چیزی که مربوط به رمزگذاری باشد ، بهتر است از یک کتابخانه با توسعه فعال استفاده کنید تا اینکه خود را بچرخانید. تیم Chrome کتابخانه ای را برای Node.js منتشر کرده است که به زودی زبان ها و سیستم عامل های بیشتری به وجود می آید. این هر دو رمزگذاری و پروتکل فشار وب را کنترل می کند ، به طوری که ارسال پیام فشار از سرور Node.js به آسانی webpush.sendWebPush(message, subscription)
آسان است.
در حالی که ما قطعاً استفاده از یک کتابخانه را توصیه می کنیم ، این یک ویژگی جدید است و بسیاری از زبان های محبوب وجود دارند که هنوز هیچ کتابخانه ای ندارند. اگر نیاز به اجرای این کار برای خودتان دارید ، در اینجا جزئیات آورده شده است.
من الگوریتم ها را با استفاده از JavaScript با طعم گره نشان می دهم ، اما اصول اساسی باید به هر زبانی یکسان باشد.
ورودی ها
برای رمزگذاری یک پیام ، ابتدا باید دو مورد را از شی اشتراک که از مشتری دریافت کردیم دریافت کنیم. اگر از JSON.stringify()
در مشتری استفاده کرده اید و آن را به سرور خود منتقل کرده اید ، کلید عمومی مشتری در قسمت keys.p256dh
ذخیره می شود ، در حالی که راز تأیید اعتبار مشترک در قسمت keys.auth
است. همانطور که در بالا ذکر شد ، هر دوی این موارد URL-SAFE BASE64 رمزگذاری می شوند. قالب باینری کلید عمومی مشتری یک نقطه منحنی بیضوی P256 فشرده نشده است.
const clientPublicKey = new Buffer(subscription.keys.p256dh, 'base64');
const clientAuthSecret = new Buffer(subscription.keys.auth, 'base64');
کلید عمومی به ما امکان می دهد پیام را به گونه ای رمزگذاری کنیم که فقط با استفاده از کلید خصوصی مشتری قابل رمزگشایی باشد.
کلیدهای عمومی معمولاً به نظر می رسد که خوب ، عمومی است ، بنابراین به مشتری اجازه می دهد تا تأیید کند که این پیام توسط یک سرور قابل اعتماد ارسال شده است ، ما نیز از راز احراز هویت استفاده می کنیم. با کمال تعجب ، این باید مخفی نگه داشته شود ، فقط با سرور برنامه که می خواهید پیام برای شما ارسال کنید به اشتراک گذاشته شود و مانند رمز عبور رفتار کنید.
ما همچنین باید داده های جدیدی تولید کنیم. ما به یک نمک تصادفی با امنیت 16 بایت و یک جفت عمومی/خصوصی از کلیدهای منحنی بیضوی نیاز داریم. منحنی خاص که توسط مشخصات رمزگذاری فشار استفاده می شود P-256 یا prime256v1
نامیده می شود. برای بهترین امنیت ، هر بار که یک پیام را رمزگذاری می کنید ، جفت کلید باید از ابتدا تولید شود و هرگز نباید از نمک استفاده کنید.
ECDH
بیایید کمی کنار بگذاریم تا در مورد خاصیت شسته و رفته رمزنگاری منحنی بیضوی صحبت کنیم. فرآیند نسبتاً ساده ای وجود دارد که کلید خصوصی شما را با کلید عمومی شخص دیگری برای به دست آوردن یک ارزش ترکیب می کند. پس چی؟ خوب ، اگر طرف مقابل کلید خصوصی و کلید عمومی شما را بگیرد ، دقیقاً همان ارزش را بدست می آورد!
این اساس پروتکل توافق نامه کلیدی منحنی بیضوی Diffie-Hellman (ECDH) است ، که به هر دو طرف اجازه می دهد تا یک راز مشترک یکسان داشته باشند ، حتی اگر آنها فقط کلیدهای عمومی را رد و بدل می کردند. ما از این راز مشترک به عنوان پایه ای برای کلید رمزگذاری واقعی خود استفاده خواهیم کرد.
const crypto = require('crypto');
const salt = crypto.randomBytes(16);
// Node has ECDH built-in to the standard crypto library. For some languages
// you may need to use a third-party library.
const serverECDH = crypto.createECDH('prime256v1');
const serverPublicKey = serverECDH.generateKeys();
const sharedSecret = serverECDH.computeSecret(clientPublicKey);
HKDF
در حال حاضر زمان دیگری برای کنار گذاشتن. بیایید بگوییم که شما برخی از داده های مخفی را دارید که می خواهید به عنوان یک کلید رمزگذاری از آن استفاده کنید ، اما به اندازه کافی از نظر رمزنگاری امن نیست. شما می توانید از عملکرد مشتق کلیدی مبتنی بر HMAC (HKDF) استفاده کنید تا یک راز را با امنیت کم به یکی از امنیت بالا تبدیل کنید.
یکی از پیامد نحوه عملکرد آن این است که به شما امکان می دهد تا از هر تعداد بیت راز بگیرید و راز دیگری با هر اندازه تا 255 برابر بیشتر از هش تولید شده توسط هر الگوریتم هشدار دهنده استفاده کنید. برای فشار ، مشخصات ما را ملزم به استفاده از SHA-256 می کند ، که دارای طول هش 32 بایت (256 بیت) است.
همانطور که اتفاق می افتد ، ما می دانیم که ما فقط باید کلیدهایی را تا 32 بایت تولید کنیم. این بدان معنی است که ما می توانیم از یک نسخه ساده از الگوریتم استفاده کنیم که نمی تواند اندازه خروجی بزرگتر را کنترل کند.
من کد نسخه گره را در زیر گنجانده ام ، اما می توانید دریابید که چگونه در RFC 5869 کار می کند.
ورودی های HKDF یک نمک ، برخی از مواد کلید اولیه (IKM) ، یک قطعه اختیاری از داده های ساختاری خاص برای استفاده فعلی (اطلاعات) و طول در بایت های کلید خروجی مورد نظر است.
// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
if (length > 32) {
throw new Error('Cannot return keys of more than 32 bytes, ${length} requested');
}
// Extract
const keyHmac = crypto.createHmac('sha256', salt);
keyHmac.update(ikm);
const key = keyHmac.digest();
// Expand
const infoHmac = crypto.createHmac('sha256', key);
infoHmac.update(info);
// A one byte long buffer containing only 0x01
const ONE_BUFFER = new Buffer(1).fill(1);
infoHmac.update(ONE_BUFFER);
return infoHmac.digest().slice(0, length);
}
استخراج پارامترهای رمزگذاری
اکنون ما از HKDF برای تبدیل داده های مورد نظر خود به پارامترهای رمزگذاری واقعی استفاده می کنیم.
اولین کاری که ما انجام می دهیم استفاده از HKDF برای مخلوط کردن مشتری راز و راز مشترک در یک راز طولانی تر و رمزنگاری امن تر است. در مشخصات این نکته به عنوان یک کلید شبه تصادفی (PRK) گفته می شود ، بنابراین این همان چیزی است که من آن را در اینجا می نامم ، اگرچه ممکن است پاکسازی های رمزنگاری شوند که این یک PRK نیست.
اکنون کلید رمزگذاری محتوای نهایی و یک غیرقانونی را ایجاد می کنیم که به رمز منتقل می شود. اینها با ساختن یک ساختار داده ساده برای هر یک از آنها ، که در مشخصات آن به عنوان اطلاعاتی ذکر شده است ، ایجاد می شود که حاوی اطلاعاتی خاص برای منحنی بیضوی ، فرستنده و گیرنده اطلاعات به منظور تأیید بیشتر منبع پیام است. سپس ما از HKDF با PRK ، نمک و اطلاعات خود استفاده می کنیم تا کلید و عدم اندازه صحیح را بدست آوریم.
نوع اطلاعات برای رمزگذاری محتوا "AESGCM" است که نام رمزنگاری است که برای رمزگذاری فشار استفاده می شود.
const authInfo = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(clientAuthSecret, sharedSecret, authInfo, 32);
function createInfo(type, clientPublicKey, serverPublicKey) {
const len = type.length;
// The start index for each element within the buffer is:
// value | length | start |
// -----------------------------------------
// 'Content-Encoding: '| 18 | 0 |
// type | len | 18 |
// nul byte | 1 | 18 + len |
// 'P-256' | 5 | 19 + len |
// nul byte | 1 | 24 + len |
// client key length | 2 | 25 + len |
// client key | 65 | 27 + len |
// server key length | 2 | 92 + len |
// server key | 65 | 94 + len |
// For the purposes of push encryption the length of the keys will
// always be 65 bytes.
const info = new Buffer(18 + len + 1 + 5 + 1 + 2 + 65 + 2 + 65);
// The string 'Content-Encoding: ', as utf-8
info.write('Content-Encoding: ');
// The 'type' of the record, a utf-8 string
info.write(type, 18);
// A single null-byte
info.write('\0', 18 + len);
// The string 'P-256', declaring the elliptic curve being used
info.write('P-256', 19 + len);
// A single null-byte
info.write('\0', 24 + len);
// The length of the client's public key as a 16-bit integer
info.writeUInt16BE(clientPublicKey.length, 25 + len);
// Now the actual client public key
clientPublicKey.copy(info, 27 + len);
// Length of our public key
info.writeUInt16BE(serverPublicKey.length, 92 + len);
// The key itself
serverPublicKey.copy(info, 94 + len);
return info;
}
// Derive the Content Encryption Key
const contentEncryptionKeyInfo = createInfo('aesgcm', clientPublicKey, serverPublicKey);
const contentEncryptionKey = hkdf(salt, prk, contentEncryptionKeyInfo, 16);
// Derive the Nonce
const nonceInfo = createInfo('nonce', clientPublicKey, serverPublicKey);
const nonce = hkdf(salt, prk, nonceInfo, 12);
بالشتک
کنار دیگر ، و زمان برای نمونه ای احمقانه و متناقض. بیایید بگوییم که رئیس شما سرور دارد که هر چند دقیقه یک بار با قیمت سهام شرکت برای او پیام فشار ارسال می کند. پیام ساده برای این کار همیشه یک عدد صحیح 32 بیتی با ارزش در سنت خواهد بود. او همچنین با کارکنان پذیرایی معامله ای دلهره آور دارد و این بدان معنی است که آنها می توانند رشته "دونات در اتاق استراحت" را برای او ارسال کنند 5 دقیقه قبل از تحویل آنها ، به طوری که او می تواند "به طور تصادفی" در هنگام ورود و گرفتن بهترین آنها را بدست آورد.
رمزگذاری شده توسط Web Push مقادیر رمزگذاری شده ای را ایجاد می کند که دقیقاً 16 بایت طولانی تر از ورودی بدون رمزگذاری است. از آنجا که "دونات در اتاق استراحت" طولانی تر است که قیمت سهام 32 بیتی ، هر کارمند snooping قادر خواهد بود بگوید چه زمانی پیراشکی ها بدون رمزگشایی پیام ها ، فقط از طول داده ها وارد می شوند.
به همین دلیل ، پروتکل فشار وب به شما امکان می دهد تا به ابتدای داده ها بالشتک اضافه کنید. نحوه استفاده از این مورد به برنامه شما است ، اما در مثال بالا می توانید همه پیام ها را دقیقاً 32 بایت قرار دهید ، و تشخیص پیام های مبتنی بر طول غیرممکن است.
مقدار بالشتک یک عدد صحیح 16 بیتی است که طول بالشتک را مشخص می کند و به دنبال آن تعداد بایت های NUL
از بالشتک است. بنابراین حداقل بالشتک دو بایت است - عدد صفر رمزگذاری شده در 16 بیت.
const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeroes, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);
هنگامی که پیام فشار شما به مشتری می رسد ، مرورگر قادر خواهد بود به طور خودکار هر نوع بالشتک را از بین ببرد ، بنابراین کد مشتری شما فقط پیام غیرقانونی را دریافت می کند.
رمزگذاری
اکنون ما در نهایت همه کارها را برای انجام رمزگذاری داریم. رمزنگاری مورد نیاز برای فشار وب AES128 با استفاده از GCM است. ما از کلید رمزگذاری محتوای خود به عنوان کلید و غیرقانونی به عنوان بردار اولیه سازی (IV) استفاده می کنیم.
در این مثال داده های ما یک رشته است ، اما می تواند داده های باینری باشد. شما می توانید بارهای تا اندازه 4078 بایت را ارسال کنید - حداکثر بایت 4096 بایت در هر پست ، با 16 بایت برای اطلاعات رمزگذاری و حداقل 2 بایت برای بالشتک.
// Create a buffer from our data, in this case a UTF-8 encoded string
const plaintext = new Buffer('Push notification payload!', 'utf8');
const cipher = crypto.createCipheriv('id-aes128-GCM', contentEncryptionKey,
nonce);
const result = cipher.update(Buffer.concat(padding, plaintext));
cipher.final();
// Append the auth tag to the result - https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
return Buffer.concat([result, cipher.getAuthTag()]);
فشار وب
اوه! اکنون که یک بار رمزگذاری شده دارید ، فقط باید یک درخواست نسبتاً ساده HTTP ارسال کنید تا نقطه پایانی مشخص شده توسط اشتراک کاربر.
شما باید سه هدر تنظیم کنید.
Encryption: salt=<SALT>
Crypto-Key: dh=<PUBLICKEY>
Content-Encoding: aesgcm
<SALT>
و <PUBLICKEY>
کلید عمومی نمک و سرور است که در رمزگذاری استفاده می شود ، به عنوان URL-Safe Base64 رمزگذاری شده است.
هنگام استفاده از پروتکل فشار وب ، بدنه پست فقط بایت های خام پیام رمزگذاری شده است. با این حال ، تا زمانی که پیام رسانی ابری Chrome و Firebase از پروتکل پشتیبانی کند ، می توانید به راحتی داده ها را در Payload موجود در JSON موجود به شرح زیر بگنجانید.
{
"registration_ids": [ "…" ],
"raw_data": "BIXzEKOFquzVlr/1tS1bhmobZ…"
}
مقدار خاصیت rawData
باید بازنمایی رمزگذاری شده Base64 از پیام رمزگذاری شده باشد.
اشکال زدایی / تأیید کننده
پیتر بورلو ، یکی از مهندسان Chrome که این ویژگی را پیاده سازی کرده است (و همچنین یکی از افرادی که روی مشخصات کار کرده است) ، یک تأیید کننده ایجاد کرده است.
با دریافت کد خود برای خروجی هر یک از مقادیر میانی رمزگذاری ، می توانید آنها را در تأیید کننده چسبانده و بررسی کنید که در مسیر صحیح قرار دارید.