โปรโตคอล Web Push

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

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

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

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

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

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

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

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

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

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

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

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

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

โทเค็นเว็บ JSON

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

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

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

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

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

ภาพประกอบของสตริงในเว็บ JSON
โทเค็น

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

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

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

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

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

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

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

ค่า aud คือ "กลุ่มเป้าหมาย" กล่าวคือ JWT มีไว้สำหรับใคร สำหรับเว็บพุช กลุ่มเป้าหมายคือบริการพุช เราจึงตั้งค่านั้นเป็นแหล่งที่มาของการพุช service [บริการ]

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

ใน Node.js การหมดอายุจะตั้งค่าโดยใช้:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

การเข้ารหัสของเพย์โหลดมีการกำหนดไว้ใน Message Encryption spec

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

ECDH และ HKDF

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

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

ลองนึกภาพว่าคุณมีคน 2 คนที่ต้องการแชร์ข้อมูลกัน คือชื่อ Alice และ Bob ทั้ง Alice และ Bob ต่างก็มีคีย์สาธารณะและคีย์ส่วนตัวเป็นของตัวเอง Alice และ Bob แชร์คีย์สาธารณะให้กันและกัน

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

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

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

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

ในโหนด เราจะดำเนินการต่อไปนี้

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

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

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

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

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

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

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

ในโหนด สิ่งนี้สามารถนำไปใช้ได้ ดังนี้

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

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

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

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

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

ส่วนนี้จะครอบคลุม ECDH และ HKDF

ซึ่งเป็นวิธีที่ปลอดภัยในการแชร์คีย์สาธารณะและสร้างข้อมูลลับที่ใช้ร่วมกัน 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 ต้องเป็นข้อมูลแบบสุ่มขนาด 16 ไบต์ ใน NodeJS เราจะดำเนินการดังนี้เพื่อสร้าง Salt

const salt = crypto.randomBytes(16);

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

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

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

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

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

Pseudo Random Key (PRK) คือชุดค่าผสมของการตรวจสอบสิทธิ์ของการสมัครใช้บริการพุช และข้อมูลลับที่ใช้ร่วมกันที่เราเพิ่งสร้างขึ้น

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

คุณอาจสงสัยว่าสตริง Content-Encoding: auth\0 มีไว้เพื่ออะไร กล่าวโดยสรุปคือ เบราว์เซอร์ไม่มีจุดประสงค์ที่ชัดเจน แม้ว่าเบราว์เซอร์จะ ถอดรหัสข้อความขาเข้าและมองหาการเข้ารหัสเนื้อหาที่คาดไว้ \0 บวกไบต์ที่มีค่าเป็น 0 ต่อท้ายบัฟเฟอร์ นี่คือ โดยการถอดรหัสข้อความซึ่งคาดหวังไบต์จำนวนมาก สำหรับการเข้ารหัสเนื้อหา ตามด้วยไบต์ที่มีค่า 0 ตามด้วยพารามิเตอร์ ข้อมูลที่เข้ารหัส

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

บริบท

"บริบท" เป็นชุดไบต์ที่ใช้ในการคำนวณค่า 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()]);

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

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

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

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

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

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

Salt 16 ไบต์ควรเข้ารหัส URL base64 อย่างปลอดภัยและเพิ่มลงในส่วนหัวการเข้ารหัส ดังนี้

Encryption: salt=[URL Safe Base64 Encoded Salt]

ส่วนหัว Crypto-Key

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

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

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

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-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'

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

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

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

ส่วนหัวเพิ่มเติมหรือไม่

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

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

ส่วนหัว TTL

จำเป็น

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

TTL: [Time to live in seconds]

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

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

หัวข้อ

ไม่บังคับ

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

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

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

ไม่บังคับ

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

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

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

รวมทุกอย่างเข้าด้วยกัน

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

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

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

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

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

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

สถานที่ที่จะไปต่อ

Code Lab