โปรโตคอล Web Push

เราได้เห็นวิธีใช้ไลบรารีเพื่อทริกเกอร์ข้อความพุช แต่ไลบรารีเหล่านี้ทำอะไรกันแน่

โดยจะส่งคำขอเครือข่ายพร้อมทั้งตรวจสอบว่าคำขอดังกล่าวมี รูปแบบที่ถูกต้อง ข้อกำหนดที่กำหนดคำขอเครือข่ายนี้คือโปรโตคอล Web Push

แผนภาพการส่งข้อความพุชจากเซิร์ฟเวอร์ไปยังบริการพุช

ส่วนนี้จะอธิบายวิธีที่เซิร์ฟเวอร์สามารถระบุตัวเองด้วยคีย์ของแอปพลิเคชัน เซิร์ฟเวอร์ และวิธีส่งเพย์โหลดที่เข้ารหัสและข้อมูลที่เกี่ยวข้อง

นี่ไม่ใช่ด้านที่สวยงามของเว็บพุช และฉันก็ไม่ใช่ผู้เชี่ยวชาญด้านการเข้ารหัส แต่มาดูแต่ละส่วนกันดีกว่า เพราะการรู้ว่าไลบรารีเหล่านี้ทำอะไรอยู่เบื้องหลังก็เป็นประโยชน์

คีย์เซิร์ฟเวอร์แอปพลิเคชัน

เมื่อเราสมัครใช้บริการให้ผู้ใช้ เราจะส่ง applicationServerKey คีย์นี้จะ ส่งไปยังบริการพุชและใช้เพื่อตรวจสอบว่าแอปพลิเคชันที่สมัครรับข้อมูล ของผู้ใช้เป็นแอปพลิเคชันที่ทริกเกอร์ข้อความพุชด้วย

เมื่อเราทริกเกอร์ข้อความพุช จะมีชุดส่วนหัวที่เราส่งซึ่ง อนุญาตให้บริการพุชตรวจสอบสิทธิ์แอปพลิเคชัน (ข้อกำหนด VAPID เป็นตัวกำหนด)

ทั้งหมดนี้หมายความว่าอย่างไรและเกิดอะไรขึ้น ขั้นตอนที่ใช้สำหรับการตรวจสอบสิทธิ์เซิร์ฟเวอร์แอปพลิเคชันมีดังนี้

  1. เซิร์ฟเวอร์แอปพลิเคชันจะลงนามข้อมูล JSON บางอย่างด้วยคีย์แอปพลิเคชันส่วนตัว
  2. ระบบจะส่งข้อมูลที่ลงนามนี้ไปยังบริการพุชเป็นส่วนหัวในคำขอ POST
  3. บริการพุชใช้คีย์สาธารณะที่จัดเก็บไว้ซึ่งได้รับจาก pushManager.subscribe() เพื่อตรวจสอบว่าข้อมูลที่ได้รับนั้นลงนามด้วย คีย์ส่วนตัวที่เกี่ยวข้องกับคีย์สาธารณะ โปรดทราบ: คีย์สาธารณะคือ applicationServerKey ที่ส่งไปยังการเรียกใช้ฟังก์ชันการสมัครรับข้อมูล
  4. หากข้อมูลที่ลงนามถูกต้อง บริการพุชจะส่งข้อความพุช ไปยังผู้ใช้

ตัวอย่างการไหลเวียนของข้อมูลนี้แสดงอยู่ด้านล่าง (โปรดสังเกตคำอธิบายที่ด้านซ้ายล่างเพื่อระบุ คีย์สาธารณะและคีย์ส่วนตัว)

ภาพแสดงวิธีใช้คีย์เซิร์ฟเวอร์แอปพลิเคชันส่วนตัวเมื่อส่งข้อความ

"ข้อมูลที่ลงนาม" ที่เพิ่มลงในส่วนหัวในคำขอคือโทเค็นเว็บ JSON

โทเค็นเว็บ JSON

โทเค็นเว็บ JSON (หรือ JWT แบบย่อ) เป็นวิธี ส่งข้อความไปยังบุคคลที่สามเพื่อให้ผู้รับตรวจสอบได้ว่า ใครเป็นผู้ส่ง

เมื่อบุคคลที่สามได้รับข้อความ บุคคลที่สามจะต้องรับคีย์สาธารณะของผู้ส่ง และใช้คีย์ดังกล่าวเพื่อตรวจสอบลายเซ็นของ JWT หาก ลายเซ็นถูกต้อง แสดงว่า JWT ต้องลงนามด้วย คีย์ส่วนตัวที่ตรงกัน จึงต้องมาจากผู้ส่งที่คาดไว้

มีไลบรารีมากมายใน jwt.io/ ที่ สามารถลงนามให้คุณได้ และเราขอแนะนำให้คุณทำเช่นนั้นในที่ที่คุณ ทำได้ เพื่อความสมบูรณ์ มาดูวิธีสร้าง JWT ที่ลงชื่อด้วยตนเองกัน

พุชจากเว็บและ JWT ที่ลงชื่อ

JWT ที่ลงชื่อเป็นเพียงสตริง แต่คุณอาจคิดว่าเป็นสตริง 3 รายการที่เชื่อมต่อกันด้วยจุด

ภาพสตริงในโทเค็นเว็บ JSON

สตริงแรกและสตริงที่ 2 (ข้อมูล JWT และข้อมูล JWT) เป็นส่วนหนึ่งของ JSON ที่เข้ารหัส Base64 ซึ่งหมายความว่าสามารถอ่านได้แบบสาธารณะ

สตริงแรกคือข้อมูลเกี่ยวกับ JWT เอง ซึ่งระบุว่าใช้อัลกอริทึมใด ในการสร้างลายเซ็น

ข้อมูล JWT สำหรับการแจ้งเตือนแบบพุชบนเว็บต้องมีข้อมูลต่อไปนี้

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

สตริงที่ 2 คือข้อมูล JWT ซึ่งจะให้ข้อมูลเกี่ยวกับผู้ส่ง JWT, ผู้รับ JWT และระยะเวลาที่ JWT ใช้ได้

สําหรับการแจ้งเตือนแบบพุชบนเว็บ ข้อมูลจะมีรูปแบบดังนี้

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

ค่า aud คือ "audience" ซึ่งหมายถึงผู้ที่ JWT มีไว้สำหรับ สำหรับเว็บพุช กลุ่มเป้าหมายคือบริการพุช ดังนั้นเราจึงตั้งค่าเป็นต้นทางของบริการพุช

ค่า exp คือการหมดอายุของ JWT ซึ่งจะป้องกันไม่ให้ผู้สอดแนมนำ JWT กลับมาใช้ซ้ำได้หากดักรับ JWT การหมดอายุคือการประทับเวลาใน วินาที และต้องไม่เกิน 24 ชั่วโมง

ใน Node.js คุณตั้งค่าการหมดอายุได้โดยใช้คำสั่งต่อไปนี้

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

โดยจะใช้เวลา 12 ชั่วโมงแทน 24 ชั่วโมงเพื่อหลีกเลี่ยง ปัญหาเกี่ยวกับความแตกต่างของเวลาในแอปพลิเคชันที่ส่งและบริการพุช

สุดท้าย ค่า sub ต้องเป็น URL หรืออีเมล mailto เพื่อว่าหากบริการพุชจำเป็นต้องติดต่อผู้ส่ง ก็จะค้นหาข้อมูลติดต่อจาก JWT ได้ (ด้วยเหตุนี้ไลบรารีการแจ้งเตือนแบบพุชบนเว็บจึงต้องใช้อีเมล)

เช่นเดียวกับข้อมูล JWT ข้อมูล JWT จะได้รับการเข้ารหัสเป็นสตริง base64 ที่ปลอดภัยสำหรับ URL

สตริงที่ 3 ซึ่งเป็นลายเซ็นคือผลลัพธ์ของการนำสตริง 2 รายการแรก (ข้อมูล JWT และข้อมูล JWT) มาต่อกันด้วยอักขระจุด ซึ่งเราจะเรียกว่า "โทเค็นที่ไม่ได้ลงนาม" และลงนาม

กระบวนการลงนามต้องเข้ารหัส "โทเค็นที่ไม่ได้ลงนาม" โดยใช้ ES256 ตามข้อกำหนด JWT แล้ว ES256 เป็นคำย่อของ "ECDSA ที่ใช้เส้นโค้ง P-256 และ อัลกอริทึมแฮช SHA-256" คุณสร้างลายเซ็นได้ดังนี้โดยใช้ Web Crypto

// Utility function for UTF-8 encoding a string to an ArrayBuffer.
const utf8Encoder = new TextEncoder('utf-8');

// The unsigned token is the concatenation of the URL-safe base64 encoded
// header and body.
const unsignedToken = .....;

// Sign the |unsignedToken| using ES256 (SHA-256 over ECDSA).
const key = {
  kty: 'EC',
  crv: 'P-256',
  x: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(1, 33)),
  y: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(33, 65)),
  d: window.uint8ArrayToBase64Url(applicationServerKeys.privateKey),
};

// Sign the |unsignedToken| with the server's private key to generate
// the signature.
return crypto.subtle.importKey('jwk', key, {
  name: 'ECDSA', namedCurve: 'P-256',
}, true, ['sign'])
.then((key) => {
  return crypto.subtle.sign({
    name: 'ECDSA',
    hash: {
      name: 'SHA-256',
    },
  }, key, utf8Encoder.encode(unsignedToken));
})
.then((signature) => {
  console.log('Signature: ', signature);
});

บริการ Push สามารถตรวจสอบ JWT ได้โดยใช้คีย์เซิร์ฟเวอร์แอปพลิเคชันสาธารณะ เพื่อถอดรหัสลายเซ็นและตรวจสอบว่าสตริงที่ถอดรหัสแล้วเหมือนกับ "โทเค็นที่ไม่ได้ลงชื่อ" (เช่น สตริง 2 รายการแรกใน JWT)

ระบบจะส่ง JWT ที่ลงชื่อแล้ว (เช่น สตริงทั้ง 3 รายการที่เชื่อมต่อกันด้วยจุด) ไปยังบริการเว็บ พุชเป็นส่วนหัว Authorization โดยมี WebPush นำหน้า ดังนี้

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

โปรโตคอลการแจ้งเตือนแบบพุชบนเว็บยังระบุว่าต้องส่งคีย์เซิร์ฟเวอร์แอปพลิเคชันสาธารณะในส่วนหัว Crypto-Key เป็นสตริงที่เข้ารหัส Base64 ที่ปลอดภัยสำหรับ URL โดยมี p256ecdsa= นำหน้า

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

การเข้ารหัสเพย์โหลด

ต่อไปมาดูวิธีส่งเพย์โหลดพร้อมข้อความพุชกัน เพื่อให้เมื่อเว็บแอป ได้รับข้อความพุช ก็จะเข้าถึงข้อมูลที่ได้รับได้

คำถามที่พบบ่อยสำหรับผู้ที่เคยใช้บริการพุชอื่นๆ คือเหตุใดจึงต้องเข้ารหัสเพย์โหลดของเว็บพุช แอปเนทีฟจะส่งข้อมูลเป็นข้อความธรรมดาได้

ข้อดีอย่างหนึ่งของเว็บพุชคือเนื่องจากบริการพุชทั้งหมดใช้ API เดียวกัน (โปรโตคอลเว็บพุช) นักพัฒนาแอปจึงไม่ต้องกังวลว่าบริการพุชนั้นเป็นของใคร เราสามารถส่งคำขอในรูปแบบที่ถูกต้องและคาดหวังว่าจะได้รับข้อความพุช ข้อเสียของวิธีนี้คือ นักพัฒนาแอปอาจส่งข้อความไปยังบริการพุชที่ไม่น่าเชื่อถือ การเข้ารหัสเพย์โหลดจะทำให้บริการพุชอ่านข้อมูลที่ส่งไม่ได้ มีเพียงเบราว์เซอร์เท่านั้นที่ถอดรหัสข้อมูลได้ ซึ่งจะช่วยปกป้องข้อมูลของผู้ใช้

การเข้ารหัสเพย์โหลดจะกำหนดไว้ในข้อกำหนดการเข้ารหัสข้อความ

ก่อนที่จะไปดูขั้นตอนเฉพาะในการเข้ารหัสเพย์โหลดของข้อความพุช เราควรพูดถึงเทคนิคบางอย่างที่จะใช้ในระหว่างกระบวนการเข้ารหัส (ขอขอบคุณ Mat Scales สำหรับบทความที่ยอดเยี่ยมเกี่ยวกับการเข้ารหัสแบบพุช เป็นอย่างยิ่ง)

ECDH และ HKDF

ทั้ง ECDH และ HKDF ใช้ตลอดกระบวนการเข้ารหัสและมีประโยชน์ต่อ วัตถุประสงค์ในการเข้ารหัสข้อมูล

ECDH: การแลกเปลี่ยนคีย์ Diffie-Hellman แบบ Elliptic Curve

สมมติว่ามีคน 2 คนที่ต้องการแชร์ข้อมูลกันคือ ขวัญใจและบัญชา ทั้งขวัญใจและบัญชามีคีย์สาธารณะและคีย์ส่วนตัวของตนเอง Alice และ Bob แชร์คีย์สาธารณะของตนกับอีกฝ่าย

คุณสมบัติที่มีประโยชน์ของคีย์ที่สร้างด้วย ECDH คือ Alice สามารถใช้คีย์ส่วนตัวและคีย์สาธารณะของ Bob เพื่อสร้างค่าลับ "X" ได้ บ็อบก็ทำแบบเดียวกันได้ โดยใช้คีย์ส่วนตัวของตนเองและคีย์สาธารณะของอลิซเพื่อสร้างค่า "X" เดียวกันได้โดยอิสระ ซึ่งทำให้ "X" เป็นข้อมูลลับที่ใช้ร่วมกัน และอลิซกับบ็อบก็เพียงแค่ต้องแชร์คีย์สาธารณะของตน ตอนนี้บัญชาและขวัญใจ สามารถใช้ "X" เพื่อเข้ารหัสและถอดรหัสข้อความระหว่างกันได้แล้ว

เท่าที่ฉันทราบ ECDH จะกำหนดคุณสมบัติของเส้นโค้งที่อนุญาตให้ "ฟีเจอร์" นี้ ในการสร้างข้อมูลลับที่ใช้ร่วมกัน "X"

นี่คือคำอธิบาย ECDH ในระดับสูง หากต้องการทราบข้อมูลเพิ่มเติม เราขอแนะนำให้ดูวิดีโอภาพรวม ECDH ที่ละเอียดยิ่งขึ้น

ในส่วนของโค้ด ภาษา / แพลตฟอร์มส่วนใหญ่จะมีไลบรารีที่ช่วยให้สร้างคีย์เหล่านี้ได้ง่าย

ในโหนด เราจะทำดังนี้

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

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

HKDF: ฟังก์ชันการสร้างคีย์ที่อิงตาม HMAC

Wikipedia มีคำอธิบายสั้นๆ เกี่ยวกับ HKDF ดังนี้

HKDF เป็นฟังก์ชันการได้คีย์ที่อิงตาม HMAC ซึ่งแปลงเนื้อหาคีย์ที่อ่อนแอ ให้เป็นเนื้อหาคีย์ที่แข็งแกร่งทางคริปโตกราฟี ตัวอย่างเช่น ใช้เพื่อแปลง Shared Secret ที่แลกเปลี่ยน Diffie Hellman เป็นเนื้อหาคีย์ ที่เหมาะสำหรับการใช้ในการเข้ารหัส การตรวจสอบความสมบูรณ์ หรือการตรวจสอบสิทธิ์

โดยพื้นฐานแล้ว HKDF จะรับอินพุตที่ไม่ปลอดภัยเป็นพิเศษและทำให้ปลอดภัยยิ่งขึ้น

ข้อกำหนดที่กำหนดการเข้ารหัสนี้กำหนดให้ใช้อัลกอริทึมแฮช SHA-256 และคีย์ที่ได้สำหรับ HKDF ใน Web Push ไม่ควรยาวเกิน 256 บิต (32 ไบต์)

ในโหนด คุณสามารถติดตั้งใช้งานได้ดังนี้

// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
  // Extract
  const keyHmac = crypto.createHmac('sha256', salt);
  keyHmac.update(ikm);
  const key = keyHmac.digest();

  // Expand
  const infoHmac = crypto.createHmac('sha256', key);
  infoHmac.update(info);

  // A one byte long buffer containing only 0x01
  const ONE_BUFFER = new Buffer(1).fill(1);
  infoHmac.update(ONE_BUFFER);

  return infoHmac.digest().slice(0, length);
}

ขอขอบคุณบทความของ Mat Scale สำหรับโค้ดตัวอย่างนี้

ซึ่งครอบคลุมถึง ECDH และ HKDF

ECDH เป็นวิธีที่ปลอดภัยในการแชร์คีย์สาธารณะและสร้างข้อมูลลับที่ใช้ร่วมกัน HKDF เป็นวิธีในการนำ เนื้อหาที่ไม่ปลอดภัยมาทำให้ปลอดภัย

ระบบจะใช้ข้อมูลนี้ในระหว่างการเข้ารหัสเพย์โหลด ต่อไปเรามาดูกันว่าเราใช้ข้อมูลใดเป็น อินพุตและวิธีเข้ารหัสข้อมูลนั้น

อินพุต

เมื่อต้องการส่งข้อความพุชไปยังผู้ใช้ที่มีเพย์โหลด เราต้องมีข้อมูล 3 อย่าง ได้แก่

  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 ว่าคีย์สาธารณะของการสมัครใช้บริการ เบราว์เซอร์จะสร้างคีย์สาธารณะของการสมัครใช้บริการ เบราว์เซอร์จะเก็บคีย์ส่วนตัวไว้เป็นความลับและใช้เพื่อถอดรหัสเพย์โหลด

ค่าทั้ง 3 ค่า ได้แก่ auth, p256dh และ payload จำเป็นต้องใช้เป็นอินพุต และผลลัพธ์ของ กระบวนการเข้ารหัสจะเป็นเพย์โหลดที่เข้ารหัส ค่า Salt และคีย์สาธารณะที่ใช้สำหรับการ เข้ารหัสข้อมูลเท่านั้น

Salt

Salt ต้องเป็นข้อมูลแบบสุ่ม 16 ไบต์ ใน NodeJS เราจะทำดังนี้เพื่อสร้าง Salt

const salt = crypto.randomBytes(16);

คีย์สาธารณะ / คีย์ส่วนตัว

ควรสร้างคีย์สาธารณะและคีย์ส่วนตัวโดยใช้เส้นโค้งวงรี P-256 ซึ่งเราจะทำใน Node ดังนี้

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

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

เราจะเรียกคีย์เหล่านี้ว่า "คีย์ในพื้นที่" โดยจะใช้เฉพาะสำหรับการเข้ารหัสและไม่มีส่วนเกี่ยวข้องกับคีย์เซิร์ฟเวอร์ของแอปพลิเคชัน

เมื่อมีเพย์โหลด, รหัสลับการตรวจสอบสิทธิ์ และคีย์สาธารณะของการสมัครใช้บริการเป็นอินพุต รวมถึงมี Salt และชุดคีย์ในเครื่องที่สร้างขึ้นใหม่ เราก็พร้อมที่จะทำการเข้ารหัสแล้ว

คีย์ลับที่แชร์

ขั้นตอนแรกคือการสร้างข้อมูลลับที่ใช้ร่วมกันโดยใช้คีย์สาธารณะของการสมัครใช้บริการและคีย์ส่วนตัวใหม่ของเรา (จำคำอธิบาย ECDH กับ Alice และ Bob ได้ไหม) ต้องแบบนี้สิ)

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

ซึ่งจะใช้ในขั้นตอนถัดไปเพื่อคำนวณคีย์แบบสุ่มเทียม (PRK)

คีย์สุ่มเทียม

คีย์แบบสุ่มเทียม (PRK) คือการรวมกันของ auth secret ของการสมัครใช้บริการแบบพุช และข้อมูลลับที่ใช้ร่วมกันที่เราเพิ่งสร้างขึ้น

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 (กล่าวคือ ทำให้มีการเข้ารหัสที่รัดกุมกว่า)

บริบท

"บริบท" คือชุดไบต์ที่ใช้ในการคำนวณค่า 2 ค่าในภายหลังในการเข้ารหัส เบราว์เซอร์ ซึ่งโดยพื้นฐานแล้วคืออาร์เรย์ของไบต์ที่มีคีย์สาธารณะของการสมัครใช้บริการและคีย์สาธารณะ ในเครื่อง

const keyLabel = new Buffer('P-256\0', 'utf8');

// Convert subscription public key into a buffer.
const subscriptionPubKey = new Buffer(subscription.keys.p256dh, 'base64');

const subscriptionPubKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = subscriptionPubKey.length;

const localPublicKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = localPublicKey.length;

const contextBuffer = Buffer.concat([
  keyLabel,
  subscriptionPubKeyLength.buffer,
  subscriptionPubKey,
  localPublicKeyLength.buffer,
  localPublicKey,
]);

บัฟเฟอร์บริบทสุดท้ายคือป้ายกำกับ จำนวนไบต์ในคีย์สาธารณะของการสมัครรับข้อมูล ตามด้วยคีย์เอง จากนั้นคือจำนวนไบต์ของคีย์สาธารณะในเครื่อง ตามด้วยคีย์ เอง

ค่าบริบทนี้ช่วยให้เราใช้ค่าดังกล่าวในการสร้าง Nonce และคีย์การเข้ารหัสเนื้อหา (CEK) ได้

คีย์การเข้ารหัสเนื้อหาและ Nonce

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 ก็คือเพื่อป้องกันความเสี่ยงที่ผู้ดักฟังจะสามารถระบุ "ประเภท" ของข้อความตามขนาด Payload ได้

คุณต้องเพิ่มการเพิ่มพื้นที่ 2 ไบต์เพื่อระบุความยาวของการเพิ่มพื้นที่เพิ่มเติม

ตัวอย่างเช่น หากคุณไม่ได้เพิ่มการเพิ่มพื้นที่ คุณจะมี 2 ไบต์ที่มีค่า 0 นั่นคือไม่มีการเพิ่มพื้นที่ หลังจาก 2 ไบต์นี้ คุณจะอ่านเพย์โหลด หากคุณเพิ่ม Padding 5 ไบต์ 2 ไบต์แรกจะมีค่าเป็น 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()]);

ตอนนี้เรามีเพย์โหลดที่เข้ารหัสแล้ว เย้!

สิ่งที่คุณต้องทำต่อไปคือการกำหนดวิธีส่งเพย์โหลดนี้ไปยังบริการพุช

ส่วนหัวและเนื้อหาของเพย์โหลดที่เข้ารหัส

หากต้องการส่งเพย์โหลดที่เข้ารหัสนี้ไปยังบริการพุช เราต้องกำหนดส่วนหัวที่แตกต่างกัน 2-3 รายการในคำขอ POST

ส่วนหัวการเข้ารหัส

ส่วนหัว "การเข้ารหัส" ต้องมีค่า Salt ที่ใช้สำหรับการเข้ารหัสเพย์โหลด

ควรเข้ารหัส Salt ขนาด 16 ไบต์แบบ 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-Type" และ "Content-Encoding" เป็นค่าคงที่ ซึ่งแสดงอยู่ด้านล่าง

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

เมื่อตั้งค่าส่วนหัวเหล่านี้แล้ว เราจะต้องส่งเพย์โหลดที่เข้ารหัสเป็นเนื้อหา ของคำขอ โปรดทราบว่าได้ตั้งค่า Content-Type เป็น application/octet-stream เนื่องจากต้องส่งเพย์โหลดที่เข้ารหัสเป็นสตรีมของไบต์

ใน NodeJS เราจะทำดังนี้

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

หากต้องการส่วนหัวเพิ่มเติม

เราได้กล่าวถึงส่วนหัวที่ใช้สำหรับ JWT / คีย์เซิร์ฟเวอร์แอปพลิเคชัน (เช่น วิธีระบุแอปพลิเคชันด้วยบริการพุช) และส่วนหัวที่ใช้เพื่อส่งเพย์โหลดที่เข้ารหัส

นอกจากนี้ยังมีส่วนหัวเพิ่มเติมที่บริการพุชใช้เพื่อเปลี่ยนลักษณะการทำงานของ ข้อความที่ส่ง โดยคุณจะต้องระบุส่วนหัวบางรายการ ส่วนบางรายการจะระบุหรือไม่ก็ได้

ส่วนหัว TTL

จำเป็น

TTL (หรือ Time to Live) คือจำนวนเต็มที่ระบุจำนวนวินาที ที่คุณต้องการให้ข้อความพุชอยู่ในบริการพุชก่อนที่จะ นำส่ง เมื่อ TTL หมดอายุ ระบบจะนำข้อความออกจากคิวบริการพุชและจะไม่นำส่งข้อความ

TTL: [Time to live in seconds]

หากตั้งค่า TTL เป็น 0 บริการพุชจะพยายามส่งข้อความทันที แต่หากเข้าถึงอุปกรณ์ไม่ได้ ระบบจะนำข้อความออกจากคิวบริการพุชทันที

ในทางเทคนิคแล้ว บริการพุชสามารถลด TTL ของข้อความพุชได้หากต้องการ คุณจะทราบได้ว่าเกิดเหตุการณ์นี้ขึ้นหรือไม่โดยตรวจสอบส่วนหัว TTL ใน การตอบกลับจากบริการพุช

หัวข้อ

ไม่บังคับ

หัวข้อคือสตริงที่ใช้แทนที่ข้อความที่รอดำเนินการด้วยข้อความใหม่ได้หากมีชื่อหัวข้อที่ตรงกัน

ซึ่งจะมีประโยชน์ในกรณีที่ส่งข้อความหลายรายการขณะที่ อุปกรณ์ออฟไลน์ และคุณต้องการให้ผู้ใช้เห็นเฉพาะข้อความล่าสุด เมื่อเปิดอุปกรณ์

กรณีเร่งด่วน

ไม่บังคับ

ความเร่งด่วนจะระบุให้ทราบว่าข้อความมีความสำคัญต่อผู้ใช้มากน้อยเพียงใด ซึ่งบริการพุชสามารถใช้เพื่อช่วยประหยัดอายุการใช้งานแบตเตอรี่ของอุปกรณ์ผู้ใช้ได้โดยการ ปลุกเฉพาะข้อความสำคัญเมื่อแบตเตอรี่เหลือน้อย

ค่าส่วนหัวจะกำหนดตามที่แสดงด้านล่าง ค่าเริ่มต้นคือ normal

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

ทุกอย่างรวมกัน

หากมีคำถามเพิ่มเติมเกี่ยวกับวิธีการทำงานของฟีเจอร์นี้ คุณสามารถดูวิธีที่ไลบรารีทริกเกอร์ ข้อความพุชได้ทุกเมื่อใน the web-push-libs org

เมื่อมีเพย์โหลดที่เข้ารหัสและส่วนหัวข้างต้นแล้ว คุณเพียงต้องส่งคำขอ POST ไปยัง endpoint ใน PushSubscription

แล้วเราจะทำอย่างไรกับคำตอบของคำขอ POST นี้

การตอบกลับจากบริการพุช

เมื่อส่งคำขอไปยังบริการพุชแล้ว คุณต้องตรวจสอบรหัสสถานะ ของการตอบกลับ เนื่องจากรหัสนี้จะบอกว่าคำขอสำเร็จหรือไม่

รหัสสถานะ คำอธิบาย
201 สร้างแล้ว ได้รับและยอมรับคำขอส่งข้อความพุชแล้ว
429 มีคำขอมากเกินไป ซึ่งหมายความว่าเซิร์ฟเวอร์แอปพลิเคชันของคุณมีอัตราคำขอถึงขีดจำกัด สำหรับบริการพุช บริการพุชควรมีส่วนหัว "Retry-After" เพื่อระบุระยะเวลาก่อนที่จะส่งคำขออื่นได้
400 คำขอไม่ถูกต้อง โดยทั่วไปแล้ว ข้อผิดพลาดนี้หมายความว่าส่วนหัวอย่างใดอย่างหนึ่งไม่ถูกต้อง หรือจัดรูปแบบไม่ถูกต้อง
404 ไม่พบ ซึ่งบ่งชี้ว่าการสมัครใช้บริการหมดอายุแล้ว และใช้ไม่ได้ ในกรณีนี้ คุณควรลบ `PushSubscription` และรอให้ไคลเอ็นต์สมัครรับข้อมูลผู้ใช้ใหม่
410 หมดแล้ว การสมัครใช้บริการไม่มีผลอีกต่อไปและควรนำออกจากเซิร์ฟเวอร์แอปพลิเคชัน ซึ่งสามารถทำซ้ำได้โดยการเรียกใช้ `unsubscribe()` ใน `PushSubscription`
413 เพย์โหลดมีขนาดใหญ่เกินไป เพย์โหลดขนาดขั้นต่ำที่บริการพุชต้อง รองรับคือ 4096 ไบต์ (หรือ 4 KB)

นอกจากนี้ คุณยังอ่านข้อมูลเพิ่มเติมเกี่ยวกับรหัสสถานะ HTTP ได้ในมาตรฐานการแจ้งเตือนแบบพุชบนเว็บ (RFC8030)

ขั้นตอนถัดไป

ห้องทดลองโค้ด