โปรโตคอล Web Push

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

นั่นคือ อุปกรณ์กำลังส่งคำขอเครือข่ายไปพร้อมกับตรวจสอบว่าคำขอดังกล่าวอยู่ในรูปแบบที่ถูกต้อง ข้อกำหนดที่กําหนดคําขอเครือข่ายนี้คือ Web Push Protocol

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

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

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

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

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

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

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

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

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

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

"ข้อมูลที่ลงนาม" ที่เพิ่มลงในส่วนหัวในคําขอคือ JSON Web Token

โทเค็นเว็บ JSON

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

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

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

พุชบนเว็บและ JWT ที่ลงนามแล้ว

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

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

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

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

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

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

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

สําหรับ Web Push ข้อมูลจะมีรูปแบบดังนี้

{
  "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 ชั่วโมงเพื่อหลีกเลี่ยงปัญหาเกี่ยวกับความแตกต่างของนาฬิการะหว่างแอปพลิเคชันส่งและบริการ Push

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ECDH และ HKDF

ระบบใช้ทั้ง ECDH และ HKDF ตลอดทั้งกระบวนการเข้ารหัสและมอบสิทธิประโยชน์สำหรับการเข้ารหัสข้อมูล

ECDH: การแลกเปลี่ยนกุญแจ Elliptic Curve Diffie-Hellman

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

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

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

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

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

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

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

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

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

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

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

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

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

ใน Node การติดตั้งใช้งานอาจมีลักษณะดังนี้

// 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

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

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

อินพุต

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

  1. ตัวเพย์โหลดเอง
  2. auth secret from the 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 ต้องเป็นข้อมูลแบบสุ่ม 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();

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

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

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

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

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

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

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

คีย์แบบสุ่มจำลอง (PRK) คือคีย์ที่รวมข้อมูลลับของการตรวจสอบสิทธิ์การสมัครใช้บริการ Push เข้ากับข้อมูลลับที่ใช้ร่วมกันที่เราเพิ่งสร้าง

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

คุณอาจสงสัยว่าสตริง Content-Encoding: auth\0 มีไว้เพื่ออะไร กล่าวโดยย่อคือ ไม่ได้ระบุวัตถุประสงค์อย่างชัดเจน แม้ว่าเบราว์เซอร์จะถอดรหัสข้อความขาเข้าและมองหาการเข้ารหัสเนื้อหาที่คาดไว้ได้ \0 บวกไบต์ที่มีค่าเป็น 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 ซึ่งรวม Salt และ PRK เข้ากับ nonceInfo และ cekInfo

// The nonce should be 12 bytes long
const nonce = hkdf(salt, prk, nonceInfo, 12);

// The CEK should be 16 bytes long
const contentEncryptionKey = hkdf(salt, prk, cekInfo, 16);

ซึ่งจะให้ Nonce และคีย์การเข้ารหัสเนื้อหาแก่เรา

ดำเนินการเข้ารหัส

เมื่อเรามีคีย์การเข้ารหัสเนื้อหาแล้ว เราก็จะเข้ารหัสเพย์โหลดได้

เราสร้างการเข้ารหัส AES128 โดยใช้คีย์การเข้ารหัสเนื้อหาเป็นคีย์ และ Nonce เป็นเวกเตอร์การเริ่มต้น

วิธีการในโหนดมีดังนี้

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

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

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

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

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

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

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

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

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

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

เกลือ 16 ไบต์ควรเข้ารหัส Base64 URL-safe และเพิ่มลงในส่วนหัวการเข้ารหัส ดังนี้

Encryption: salt=[URL Safe Base64 Encoded Salt]

ส่วนหัวคีย์การเข้ารหัส

เราพบว่าส่วนหัว Crypto-Key มีการใช้ภายใต้ส่วน "คีย์เซิร์ฟเวอร์แอปพลิเคชัน" เพื่อเก็บคีย์แอปพลิเคชันเซิร์ฟเวอร์สาธารณะ

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

ส่วนหัวที่ได้จะมีลักษณะดังนี้

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

ส่วนหัวประเภท ความยาว และการเข้ารหัสเนื้อหา

ส่วนหัว Content-Length คือจำนวนไบต์ในเพย์โหลดที่เข้ารหัส ส่วนหัว "Content-Type" และ "Content-Encoding" เป็นค่าคงที่ ซึ่งจะแสดงอยู่ด้านล่าง

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

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

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

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

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

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

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

ส่วนหัว TTL

จำเป็น

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

TTL: [Time to live in seconds]

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

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

หัวข้อ

ไม่บังคับ

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

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

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

ไม่บังคับ

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

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

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

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

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

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

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

การตอบสนองจากบริการพุช

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

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

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

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

Code Lab