เราได้เห็นวิธีใช้ไลบรารีเพื่อทริกเกอร์ข้อความ Push แล้ว แต่ไลบรารีเหล่านี้ทำอะไรกันแน่
โดยจะส่งคำขอเครือข่ายพร้อมกับตรวจสอบว่าคำขอดังกล่าวอยู่ในรูปแบบที่ถูกต้อง ข้อกำหนดที่ระบุคำขอเครือข่ายนี้คือโปรโตคอล Web Push
ส่วนนี้จะอธิบายวิธีที่เซิร์ฟเวอร์สามารถระบุตัวตนด้วยคีย์เซิร์ฟเวอร์แอปพลิเคชัน รวมถึงวิธีส่งเพย์โหลดที่เข้ารหัสและข้อมูลที่เกี่ยวข้อง
นี่ไม่ใช่ด้านที่สวยงามของ Web Push และฉันไม่ใช่ผู้เชี่ยวชาญด้านการเข้ารหัส แต่เรามาดูแต่ละส่วนกัน เนื่องจากเป็นประโยชน์ที่จะทราบว่าไลบรารีเหล่านี้ทำอะไรอยู่เบื้องหลัง
คีย์เซิร์ฟเวอร์ของแอปพลิเคชัน
เมื่อเราให้ผู้ใช้สมัครใช้บริการ เราจะส่ง applicationServerKey เข้าไป คีย์นี้จะส่งไปยังบริการพุชและใช้เพื่อตรวจสอบว่าแอปพลิเคชันที่สมัครใช้บริการของผู้ใช้เป็นแอปพลิเคชันที่ทริกเกอร์ข้อความพุชด้วย
เมื่อเราทริกเกอร์ข้อความ Push จะมีชุดส่วนหัวที่เราส่งซึ่งช่วยให้บริการ Push ตรวจสอบสิทธิ์แอปพลิเคชันได้ (ซึ่งกำหนดโดยข้อกำหนด VAPID)
ทั้งหมดนี้หมายความว่าอย่างไรและจะเกิดอะไรขึ้นบ้าง ขั้นตอนที่ใช้ในการตรวจสอบสิทธิ์เซิร์ฟเวอร์แอปพลิเคชันมีดังนี้
- เซิร์ฟเวอร์แอปพลิเคชันจะลงนามข้อมูล JSON บางอย่างด้วยคีย์แอปพลิเคชันส่วนตัว
- ระบบจะส่งข้อมูลที่ลงนามนี้ไปยังบริการพุชเป็นส่วนหัวในคำขอ POST
- บริการพุชจะใช้คีย์สาธารณะที่จัดเก็บไว้ซึ่งได้รับจาก
pushManager.subscribe()เพื่อตรวจสอบว่าข้อมูลที่ได้รับมีการลงนามโดยคีย์ส่วนตัวที่เกี่ยวข้องกับคีย์สาธารณะ โปรดทราบ: คีย์สาธารณะคือapplicationServerKeyที่ส่งไปยังการเรียกใช้การสมัครใช้บริการ - หากข้อมูลที่ลงชื่อถูกต้อง บริการพุชจะส่งข้อความพุชไปยังผู้ใช้
ตัวอย่างขั้นตอนการส่งข้อมูลมีดังนี้ (โปรดสังเกตคำอธิบายที่ด้านซ้ายล่างซึ่งระบุคีย์สาธารณะและคีย์ส่วนตัว)
"ข้อมูลที่ลงชื่อ" ที่เพิ่มลงในส่วนหัวในคำขอคือ JSON Web Token
โทเค็นเว็บ JSON
โทเค็นเว็บ JSON (หรือเรียกสั้นๆ ว่า JWT) เป็นวิธีส่งข้อความไปยังบุคคลที่สามเพื่อให้ผู้รับสามารถตรวจสอบได้ว่าใครเป็นผู้ส่ง
เมื่อบุคคลที่สามได้รับข้อความ บุคคลที่สามจะต้องรับคีย์สาธารณะของผู้ส่งและใช้คีย์ดังกล่าวเพื่อตรวจสอบความถูกต้องของลายเซ็น JWT หากลายเซ็นถูกต้อง แสดงว่า JWT ต้องได้รับการลงนามด้วยคีย์ส่วนตัวที่ตรงกัน ดังนั้นจึงต้องมาจากผู้ส่งที่คาดไว้
มีไลบรารีจำนวนมากใน jwt.io/ ที่สามารถลงชื่อให้คุณได้ และเราขอแนะนำให้คุณทำเช่นนั้นหากทำได้ มาดูวิธีสร้าง JWT ที่ลงชื่อด้วยตนเองเพื่อให้สมบูรณ์กัน
ข้อความ Push บนเว็บและ JWT ที่ลงชื่อแล้ว
JWT ที่ลงนามแล้วเป็นเพียงสตริง แต่ก็อาจมองว่าเป็นสตริง 3 รายการที่เชื่อมต่อกันด้วยจุด
สตริงแรกและสตริงที่ 2 (ข้อมูล JWT และข้อมูล JWT) เป็นส่วนของ JSON ที่เข้ารหัส Base64 ซึ่งหมายความว่าทุกคนสามารถอ่านได้
สตริงแรกคือข้อมูลเกี่ยวกับ JWT เอง ซึ่งระบุอัลกอริทึมที่ใช้สร้างลายเซ็น
ข้อมูล JWT สำหรับข้อความ Push บนเว็บต้องมีข้อมูลต่อไปนี้
{
"typ": "JWT",
"alg": "ES256"
}
สตริงที่ 2 คือข้อมูล JWT ซึ่งจะให้ข้อมูลเกี่ยวกับผู้ส่ง JWT, ผู้รับ และระยะเวลาที่ JWT มีผล
สำหรับข้อความ Push บนเว็บ ข้อมูลจะมีรูปแบบดังนี้
{
"aud": "https://some-push-service.org",
"exp": "1469618703",
"sub": "mailto:example@web-push-book.org"
}
ค่า aud คือ "กลุ่มเป้าหมาย" ซึ่งก็คือผู้ที่ JWT มีไว้ให้ สำหรับข้อความ Push บนเว็บ กลุ่มเป้าหมายคือบริการข้อความ Push เราจึงตั้งค่าเป็นต้นทางของบริการข้อความ Push
ค่า exp คือวันหมดอายุของ JWT ซึ่งจะป้องกันไม่ให้ผู้สอดแนมนำ JWT ไปใช้ซ้ำได้หากดักจับได้ การหมดอายุคือการประทับเวลาเป็นวินาทีและต้องไม่เกิน 24 ชั่วโมง
ใน Node.js ระบบจะตั้งค่าการหมดอายุโดยใช้โค้ดต่อไปนี้
Math.floor(Date.now() / 1000) + 12 * 60 * 60;
โดยใช้เวลา 12 ชั่วโมงแทนที่จะเป็น 24 ชั่วโมงเพื่อหลีกเลี่ยงปัญหาเกี่ยวกับความแตกต่างของนาฬิการะหว่างแอปพลิเคชันที่ส่งและบริการพุช
สุดท้าย ค่า sub ต้องเป็น URL หรืออีเมล mailto
เพื่อให้สามารถค้นหาข้อมูลติดต่อจาก JWT ได้หากบริการพุชจำเป็นต้องติดต่อผู้ส่ง (นี่คือเหตุผลที่ไลบรารี Web Push ต้องใช้อีเมล)
ข้อมูล JWT จะเข้ารหัสเป็นสตริง Base64 ที่ปลอดภัยสำหรับ URL เช่นเดียวกับข้อมูล JWT
สตริงที่ 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);
});
บริการพุชสามารถตรวจสอบ 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]
การเข้ารหัสเพย์โหลด
ต่อไปเราจะมาดูวิธีส่งเพย์โหลดพร้อมข้อความ Push เพื่อให้เว็บแอปของเราเข้าถึงข้อมูลที่ได้รับเมื่อได้รับข้อความ Push
คำถามที่พบบ่อยจากผู้ที่เคยใช้บริการพุชอื่นๆ คือเหตุใดเพย์โหลดของข้อความ Push บนเว็บจึงต้องเข้ารหัส สำหรับแอปที่มาพร้อมเครื่อง ข้อความ Push จะส่งข้อมูลเป็นข้อความธรรมดาได้
ข้อดีอย่างหนึ่งของข้อความ Push บนเว็บคือ เนื่องจากบริการ Push ทั้งหมดใช้ API เดียวกัน (โปรโตคอลข้อความ Push บนเว็บ) นักพัฒนาแอปจึงไม่ต้องสนใจว่าใครเป็นผู้ให้บริการ Push เราสามารถส่งคำขอในรูปแบบที่ถูกต้องและคาดหวังให้ระบบส่งข้อความ Push ได้ ข้อเสียของวิธีนี้คือนักพัฒนาแอปอาจส่งข้อความไปยังบริการพุชที่ไม่น่าเชื่อถือได้ การเข้ารหัสเพย์โหลดทำให้บริการพุชไม่สามารถอ่านข้อมูลที่ส่งได้ มีเพียงเบราว์เซอร์เท่านั้นที่สามารถถอดรหัสข้อมูลได้ ซึ่งจะช่วยปกป้องข้อมูลของผู้ใช้
การเข้ารหัสเพย์โหลดจะกำหนดไว้ในข้อกำหนดการเข้ารหัสข้อความ
ก่อนที่จะดูขั้นตอนเฉพาะในการเข้ารหัสเพย์โหลดข้อความ Push เราควรพูดถึงเทคนิคบางอย่างที่จะใช้ในระหว่างกระบวนการเข้ารหัส (ขอขอบคุณ Mat Scales เป็นอย่างยิ่งสำหรับบทความที่ยอดเยี่ยมเกี่ยวกับการเข้ารหัสข้อความ Push)
ECDH และ HKDF
ทั้ง ECDH และ HKDF จะใช้ตลอดกระบวนการเข้ารหัสและให้ประโยชน์เพื่อวัตถุประสงค์ในการเข้ารหัสข้อมูล
ECDH: การแลกเปลี่ยนคีย์ Diffie-Hellman แบบ Elliptic Curve
สมมติว่ามีคน 2 คนที่ต้องการแชร์ข้อมูลกัน คือ ขวัญใจและบัญชา ทั้งขวัญใจและบัญชามีคีย์สาธารณะและคีย์ส่วนตัวของตนเอง Alice และ Bob แชร์คีย์สาธารณะของตนให้กันและกัน
คุณสมบัติที่เป็นประโยชน์ของคีย์ที่สร้างด้วย ECDH คืออลิซสามารถใช้คีย์ส่วนตัวของตนเองและคีย์สาธารณะของบ็อบเพื่อสร้างค่าลับ "X" ได้ บ็อบก็ทำแบบเดียวกันได้ โดยใช้คีย์ส่วนตัวของตนเองและคีย์สาธารณะของอลิซเพื่อสร้างค่า "X" เดียวกันอย่างอิสระ ซึ่งทำให้ "X" เป็นความลับที่ใช้ร่วมกัน และ Alice กับ Bob ต้องแชร์เฉพาะคีย์สาธารณะของตนเท่านั้น ตอนนี้บ็อบและอลิซสามารถใช้ "X" เพื่อเข้ารหัสและถอดรหัสข้อความระหว่างกันได้แล้ว
เท่าที่ฉันทราบ ECDH จะกำหนดคุณสมบัติของเส้นโค้งที่อนุญาตให้ใช้ "ฟีเจอร์" นี้ในการสร้างข้อมูลลับที่ใช้ร่วมกัน "X"
นี่เป็นคำอธิบายระดับสูงเกี่ยวกับ ECDH หากต้องการดูข้อมูลเพิ่มเติม เราขอแนะนำให้ดูวิดีโอภาพรวม ECDH ที่ละเอียดมากขึ้น
ในแง่ของโค้ด ภาษา/แพลตฟอร์มส่วนใหญ่มาพร้อมกับไลบรารีที่ช่วยให้สร้างคีย์เหล่านี้ได้ง่าย
ใน Node เราจะทำดังนี้
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 อย่างคร่าวๆ
ECDH เป็นวิธีที่ปลอดภัยในการแชร์คีย์สาธารณะและสร้างข้อมูลลับที่ใช้ร่วมกัน HKDF เป็นวิธีที่นำข้อมูลที่ไม่ปลอดภัยมาทำให้ปลอดภัย
ระบบจะใช้ข้อมูลนี้ในระหว่างการเข้ารหัสเพย์โหลด ต่อไปเรามาดูสิ่งที่เราใช้เป็นอินพุตและวิธีเข้ารหัสกัน
อินพุต
เมื่อต้องการส่งข้อความ Push ไปยังผู้ใช้ที่มีเพย์โหลด เราต้องมีอินพุต 3 รายการดังนี้
- เพย์โหลดเอง
- ความลับของ
authจากPushSubscription - คีย์
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();
เราจะเรียกคีย์เหล่านี้ว่า "คีย์ในเครื่อง" โดยจะใช้เพียงเพื่อการเข้ารหัสและไม่มีส่วนเกี่ยวข้องกับคีย์เซิร์ฟเวอร์ของแอปพลิเคชัน
เมื่อมีเพย์โหลด, ข้อมูลลับการตรวจสอบสิทธิ์ และคีย์สาธารณะของการสมัครใช้บริการเป็นอินพุต รวมถึงมีซอลต์และชุดคีย์ในเครื่องที่สร้างขึ้นใหม่ เราก็พร้อมที่จะทำการเข้ารหัสจริง
คีย์ลับที่แชร์
ขั้นตอนแรกคือการสร้างข้อมูลลับที่ใช้ร่วมกันโดยใช้คีย์สาธารณะของการสมัครใช้บริการและคีย์ส่วนตัวใหม่ของเรา (จำคำอธิบาย ECDH เกี่ยวกับ Alice และ Bob ได้ไหม ง่ายๆ เพียงเท่านี้)
const sharedSecret = localKeysCurve.computeSecret(
subscription.keys.p256dh,
'base64',
);
ซึ่งจะใช้ในขั้นตอนถัดไปเพื่อคำนวณคีย์สุ่มเทียม (PRK)
คีย์สุ่มเทียม
คีย์สุ่มเทียม (PRK) คือการรวมกันของข้อมูลลับการตรวจสอบสิทธิ์ของการสมัครใช้บริการพุชและข้อมูลลับที่ใช้ร่วมกันที่เราเพิ่งสร้างขึ้น
const authEncBuff = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(subscription.keys.auth, sharedSecret, authEncBuff, 32);
คุณอาจสงสัยว่าสตริง Content-Encoding: auth\0 มีไว้เพื่ออะไร
กล่าวโดยย่อคือไม่มีวัตถุประสงค์ที่ชัดเจน แม้ว่าเบราว์เซอร์จะถอดรหัสข้อความขาเข้าและค้นหาการเข้ารหัสเนื้อหาที่คาดไว้ได้ก็ตาม
\0 จะเพิ่มไบต์ที่มีค่าเป็น 0 ต่อท้ายบัฟเฟอร์ เบราว์เซอร์ที่ถอดรหัสข้อความจะคาดหวังว่าจะมีจำนวนไบต์สำหรับการเข้ารหัสเนื้อหา ตามด้วยไบต์ที่มีค่า 0 และตามด้วยข้อมูลที่เข้ารหัส
คีย์สุ่มเทียมของเราจะเรียกใช้การตรวจสอบสิทธิ์ ความลับที่ใช้ร่วมกัน และข้อมูลการเข้ารหัสผ่าน HKDF (กล่าวคือ ทำให้การเข้ารหัสมีความรัดกุมยิ่งขึ้น)
บริบท
"บริบท" คือชุดไบต์ที่ใช้ในการคำนวณค่า 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 คือค่าที่ป้องกันการโจมตีแบบ Replay เนื่องจากควรใช้เพียงครั้งเดียว
คีย์การเข้ารหัสเนื้อหา (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 เป็นเวกเตอร์การเริ่มต้น
ใน Node จะดำเนินการดังนี้
const cipher = crypto.createCipheriv(
'id-aes128-GCM',
contentEncryptionKey,
nonce,
);
ก่อนที่จะเข้ารหัสเพย์โหลด เราต้องกำหนดจำนวน Padding ที่ต้องการเพิ่มที่ด้านหน้าของเพย์โหลด เหตุผลที่เราต้องการเพิ่มระยะห่างจากขอบคือเพื่อป้องกันความเสี่ยงที่ผู้ดักฟังจะระบุ "ประเภท" ของข้อความตามขนาดเพย์โหลดได้
คุณต้องเพิ่มระยะห่างจากขอบ 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()]);
ตอนนี้เรามีเพย์โหลดที่เข้ารหัสแล้ว เย้!
สิ่งที่เหลืออยู่คือการกำหนดวิธีส่งเพย์โหลดนี้ไปยังบริการพุช
ส่วนหัวและเนื้อหาเพย์โหลดที่เข้ารหัส
หากต้องการส่งเพย์โหลดที่เข้ารหัสนี้ไปยังบริการพุช เราจำเป็นต้องกำหนดส่วนหัวที่แตกต่างกัน 2-3 รายการในคำขอ POST
ส่วนหัวของการเข้ารหัส
ส่วนหัว "Encryption" ต้องมี Salt ที่ใช้สำหรับการเข้ารหัสเพย์โหลด
Salt ขนาด 16 ไบต์ควรเข้ารหัสแบบ Base64 ที่ปลอดภัยสำหรับ URL และเพิ่มลงในส่วนหัวของการเข้ารหัส ดังนี้
Encryption: salt=[URL Safe Base64 Encoded Salt]
ส่วนหัว Crypto-Key
เราพบว่ามีการใช้ส่วนหัว Crypto-Key ในส่วน "คีย์เซิร์ฟเวอร์ของแอปพลิเคชัน" เพื่อเก็บคีย์สาธารณะของเซิร์ฟเวอร์แอปพลิเคชัน
นอกจากนี้ ส่วนหัวนี้ยังใช้เพื่อแชร์คีย์สาธารณะในเครื่องที่ใช้ในการเข้ารหัสเพย์โหลดด้วย
ส่วนหัวที่ได้จะมีลักษณะดังนี้
Crypto-Key: dh=[URL Safe Base64 Encoded Local Public Key String]; p256ecdsa=[URL Safe Base64 Encoded Public Application Server Key]
ประเภทเนื้อหา ความยาว และส่วนหัวของการเข้ารหัส
ส่วนหัว Content-Length คือจำนวนไบต์ในเพย์โหลดที่เข้ารหัส ส่วนหัว "Content-Type" และ "Content-Encoding" เป็นค่าคงที่
ซึ่งจะแสดงอยู่ด้านล่าง
Content-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'
เมื่อตั้งค่าส่วนหัวเหล่านี้แล้ว เราจำเป็นต้องส่งเพย์โหลดที่เข้ารหัสเป็นเนื้อหาของคำขอ โปรดทราบว่า Content-Type ตั้งค่าเป็น application/octet-stream เนื่องจากต้องส่งเพย์โหลดที่เข้ารหัสเป็นสตรีมไบต์
ใน NodeJS เราจะทำดังนี้
const pushRequest = https.request(httpsOptions, function(pushResponse) {
pushRequest.write(encryptedPayload);
pushRequest.end();
ต้องการส่วนหัวเพิ่มเติมไหม
เราได้พูดถึงส่วนหัวที่ใช้สำหรับ JWT/คีย์เซิร์ฟเวอร์แอปพลิเคชัน (เช่น วิธีระบุแอปพลิเคชันด้วยบริการพุช) และเราได้พูดถึงส่วนหัวที่ใช้ในการส่งเพย์โหลดที่เข้ารหัส
มีส่วนหัวเพิ่มเติมที่บริการพุชใช้เพื่อเปลี่ยนลักษณะการทำงานของข้อความที่ส่ง โดยจำเป็นต้องระบุส่วนหัวบางรายการ ในขณะที่ส่วนหัวอื่นๆ จะเป็นส่วนหัวที่ไม่บังคับ
ส่วนหัว TTL
จำเป็น
TTL (หรือระยะเวลาที่ใช้งานได้) คือจำนวนเต็มที่ระบุจำนวนวินาทีที่คุณต้องการให้ข้อความ Push อยู่ในบริการ Push ก่อนที่จะส่ง เมื่อ TTL หมดอายุ ระบบจะนำข้อความออกจากคิวบริการพุชและจะไม่ส่งข้อความดังกล่าว
TTL: [Time to live in seconds]
หากคุณตั้งค่า TTL เป็น 0 บริการพุชจะพยายามส่งข้อความทันที แต่หากเข้าถึงอุปกรณ์ไม่ได้ ระบบจะนำข้อความออกจากคิวบริการพุชทันที
ในทางเทคนิคแล้ว บริการพุชสามารถลด TTL ของข้อความพุชได้หากต้องการ คุณสามารถตรวจสอบว่าเกิดกรณีนี้ขึ้นหรือไม่โดยดูส่วนหัว TTL ในการตอบกลับจากบริการพุช
หัวข้อ
ไม่บังคับ
หัวข้อคือสตริงที่ใช้แทนที่ข้อความที่รอดำเนินการด้วยข้อความใหม่ได้หากมีชื่อหัวข้อที่ตรงกัน
ซึ่งจะมีประโยชน์ในสถานการณ์ที่มีการส่งข้อความหลายข้อความขณะที่อุปกรณ์ออฟไลน์ และคุณต้องการให้ผู้ใช้เห็นเฉพาะข้อความล่าสุดเมื่อเปิดอุปกรณ์เท่านั้น
กรณีเร่งด่วน
ไม่บังคับ
ความเร่งด่วนจะระบุให้ Push Service ทราบว่าข้อความมีความสำคัญต่อผู้ใช้มากน้อยเพียงใด บริการพุชสามารถใช้ข้อมูลนี้เพื่อช่วยประหยัดอายุการใช้งานแบตเตอรี่ของอุปกรณ์ของผู้ใช้โดยจะปลุกอุปกรณ์เฉพาะเมื่อมีข้อความสำคัญเมื่อแบตเตอรี่เหลือน้อย
ค่าส่วนหัวจะกำหนดไว้ดังที่แสดงด้านล่าง ค่าเริ่มต้นคือ normal
Urgency: [very-low | low | normal | high]
ทุกอย่างรวมกัน
หากมีคำถามเพิ่มเติมเกี่ยวกับวิธีการทำงานของทั้งหมดนี้ คุณสามารถดูวิธีที่ไลบรารีทริกเกอร์ข้อความ Push ได้ทุกเมื่อในองค์กร web-push-libs
เมื่อมีเพย์โหลดที่เข้ารหัสและส่วนหัวด้านบนแล้ว คุณเพียงแค่ต้องส่งคำขอ POST ไปยัง endpoint ใน PushSubscription
แล้วเราจะทำอย่างไรกับการตอบกลับคำขอ POST นี้
การตอบกลับจากบริการพุช
เมื่อส่งคำขอไปยังบริการพุชแล้ว คุณต้องตรวจสอบรหัสสถานะของการตอบกลับ เนื่องจากรหัสดังกล่าวจะบอกว่าคำขอสำเร็จหรือไม่
| รหัสสถานะ | คำอธิบาย |
|---|---|
| 201 | สร้างแล้ว ได้รับและยอมรับคำขอส่งข้อความ Push แล้ว |
| 429 | มีคำขอมากเกินไป ซึ่งหมายความว่าเซิร์ฟเวอร์แอปพลิเคชันของคุณถึงขีดจำกัดอัตราคำขอด้วยบริการพุชแล้ว บริการพุชควรมีส่วนหัว "Retry-After" เพื่อระบุระยะเวลาก่อนที่จะส่งคำขออื่นได้ |
| 400 | คำขอไม่ถูกต้อง ซึ่งโดยทั่วไปหมายความว่าส่วนหัวรายการใดรายการหนึ่งไม่ถูกต้องหรือจัดรูปแบบไม่ถูกต้อง |
| 404 | ไม่พบ ซึ่งหมายความว่าการสมัครใช้บริการหมดอายุแล้วและใช้ไม่ได้ ในกรณีนี้ คุณควรลบ `PushSubscription` และรอให้ไคลเอ็นต์สมัครใช้บริการผู้ใช้อีกครั้ง |
| 410 | หมดแล้ว การสมัครใช้บริการไม่ถูกต้องอีกต่อไปและควรนำออกจากเซิร์ฟเวอร์แอปพลิเคชัน ซึ่งทำซ้ำได้โดยการเรียกใช้ `unsubscribe()` ใน `PushSubscription` |
| 413 | เพย์โหลดมีขนาดใหญ่เกินไป เพย์โหลดขนาดเล็กที่สุดที่บริการพุชต้องรองรับคือ 4096 ไบต์ (หรือ 4 KB) |
นอกจากนี้ คุณยังอ่านข้อมูลเพิ่มเติมเกี่ยวกับรหัสสถานะ HTTP ได้ที่มาตรฐาน Web Push (RFC8030)
ขั้นตอนต่อไป
- ภาพรวมข้อความ Push จากเว็บ
- วิธีการทำงานของข้อความ Push
- การสมัครใช้บริการของผู้ใช้
- UX ของสิทธิ์
- การส่งข้อความด้วยไลบรารี Web Push
- โปรโตคอล Web Push
- การจัดการเหตุการณ์ Push
- การแสดงการแจ้งเตือน
- ลักษณะการทำงานของการแจ้งเตือน
- รูปแบบการแจ้งเตือนที่พบบ่อย
- คำถามที่พบบ่อยเกี่ยวกับข้อความ Push
- ปัญหาที่พบได้ทั่วไปและการรายงานข้อบกพร่อง