เราได้เห็นวิธีใช้ไลบรารีเพื่อทริกเกอร์ข้อความพุชแล้ว แต่ไลบรารีเหล่านี้ทำอะไรกันอยู่บ้าง
นั่นเป็นเพราะพวกเขาส่งคำขอเครือข่าย ในขณะเดียวกันก็ตรวจสอบให้แน่ใจว่าคำขอเหล่านั้น อยู่ในรูปแบบที่ถูกต้อง ข้อกำหนดที่กำหนดคำขอเครือข่ายนี้คือโปรโตคอลพุชจากเว็บ
ส่วนนี้จะอธิบายวิธีที่เซิร์ฟเวอร์ระบุตัวตนด้วยคีย์เซิร์ฟเวอร์ของแอปพลิเคชัน รวมถึงวิธีส่งเพย์โหลดที่เข้ารหัสและข้อมูลที่เกี่ยวข้อง
นี่ไม่ใช่ด้านของการพุชจากเว็บและผมไม่ถนัดเรื่องการเข้ารหัส แต่มาลองดูรายละเอียดกันทีละส่วนเพราะไลบรารีเหล่านี้มีประโยชน์มาก
คีย์เซิร์ฟเวอร์แอปพลิเคชัน
เมื่อเราสมัครใช้บริการให้ผู้ใช้ เราจะส่งผ่าน applicationServerKey
ระบบจะส่งคีย์นี้ไปยังบริการพุชและใช้เพื่อตรวจสอบว่าแอปพลิเคชันที่สมัครใช้บริการแก่ผู้ใช้นั้นเป็นแอปพลิเคชันที่ทริกเกอร์ข้อความพุชด้วย
เมื่อเราทริกเกอร์ข้อความ Push จะมีส่วนหัวชุดหนึ่งที่เราจะส่งไปเพื่อให้บริการพุชสามารถตรวจสอบสิทธิ์แอปพลิเคชันได้ (ซึ่งกำหนดตามข้อกำหนด VAPID)
ทั้งหมดนี้หมายถึงอะไรและเกิดอะไรขึ้นกันแน่ ขั้นตอนดำเนินการ สำหรับการตรวจสอบสิทธิ์เซิร์ฟเวอร์แอปพลิเคชัน
- แอปพลิเคชันเซิร์ฟเวอร์จะรับรองข้อมูล JSON บางส่วนด้วยคีย์แอปพลิเคชันส่วนตัว
- ข้อมูลที่ลงนามแล้วนี้จะส่งไปยังบริการพุชเป็นส่วนหัวในคำขอ POST
- บริการพุชจะใช้คีย์สาธารณะที่เก็บไว้ซึ่งได้รับจาก
pushManager.subscribe()
เพื่อตรวจสอบว่าข้อมูลที่ได้รับลงนามโดยคีย์ส่วนตัวที่เกี่ยวข้องกับคีย์สาธารณะ ข้อควรจำ: คีย์สาธารณะคือapplicationServerKey
ที่ส่งผ่านไปยังการโทรสมัครรับข้อมูล - หากข้อมูลที่ลงชื่อถูกต้อง บริการพุชจะส่งข้อความพุชไปยังผู้ใช้
ตัวอย่างโฟลว์ของข้อมูลนี้แสดงอยู่ด้านล่าง (สังเกตคำอธิบายที่ด้านล่างซ้ายเพื่อระบุ คีย์สาธารณะและคีย์ส่วนตัว)
"ข้อมูลลงนาม" ที่เพิ่มในส่วนหัวในคำขอคือโทเค็นเว็บ JSON
เว็บโทเค็น JSON
โทเค็นเว็บ JSON (หรือเรียกสั้นๆ ว่า JWT) เป็นวิธีส่งข้อความถึงบุคคลที่สามเพื่อให้ผู้รับตรวจสอบได้ว่าใครเป็นผู้ส่งข้อความ
เมื่อบุคคลที่สามได้รับข้อความ บุคคลที่สามจะต้องขอคีย์สาธารณะของผู้ส่งและใช้คีย์ดังกล่าวเพื่อตรวจสอบลายเซ็นของ JWT หากลายเซ็นถูกต้อง JWT ต้องเซ็นชื่อด้วยคีย์ส่วนตัวที่ตรงกัน และต้องมาจากผู้ส่งที่คาดไว้
มีไลบรารีมากมายใน https://jwt.io/ ที่ให้คุณลงนามได้ และเราขอแนะนำให้ทำในที่ที่มีสิทธิ์ มาดูวิธีสร้าง JWT แบบลงชื่อด้วยตนเองเพื่อความครบถ้วนสมบูรณ์
พุชจากเว็บและ JWT ที่ลงชื่อ
JWT ที่มีสัญลักษณ์เป็นเพียงแค่สตริง แม้ว่าอาจมี 3 สตริงที่เชื่อมต่อกันด้วยจุด
สตริงแรกและสตริงที่ 2 (ข้อมูล 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 สำหรับใคร สำหรับเว็บพุช กลุ่มเป้าหมายคือบริการพุช เราจึงตั้งค่าเป็นต้นทางของบริการพุช
ค่า exp
คือการหมดอายุของ JWT ซึ่งจะป้องกันไม่ให้ผู้สอดแนมใช้ JWT ซ้ำได้หากสกัดกั้น โดยเวลาหมดอายุจะเป็นการประทับเวลาในหน่วยวินาทีและต้องไม่ยาวเกิน 24 ชั่วโมงอีกต่อไป
ใน Node.js มีการตั้งค่าการหมดอายุโดยใช้
Math.floor(Date.now() / 1000) + 12 * 60 * 60;
การหลีกเลี่ยงปัญหาเกี่ยวกับความแตกต่างของนาฬิการะหว่างแอปพลิเคชันการส่งและบริการพุชจะใช้เวลา 12 ชั่วโมงแทนที่จะเป็น 24 ชั่วโมง
สุดท้าย ค่า sub
ต้องเป็น URL หรืออีเมล mailto
เพื่อที่ว่าหากบริการพุชจำเป็นต้องติดต่อผู้ส่ง บริการจะสามารถค้นหาข้อมูลติดต่อจาก JWT ได้ (นี่คือเหตุผลที่ไลบรารีการพุชจากเว็บต้องการที่อยู่อีเมล)
ข้อมูล JWT จะได้รับการเข้ารหัสเป็นสตริง Base64 ที่ปลอดภัยของ URL เช่นเดียวกับข้อมูล JWT
สตริงที่ 3 ซึ่งก็คือลายเซ็น เป็นผลจากการนำ 2 สตริงแรก (ข้อมูล JWT และข้อมูล JWT) มารวมกันด้วยอักขระจุด ซึ่งเรา จะเรียกว่า "โทเค็นที่ไม่มีการรับรอง" และลงชื่อ
กระบวนการลงนามต้องมีการเข้ารหัส "โทเค็นที่ไม่มีการรับรอง" โดยใช้ ES256 ตาม JWT spec 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
เป็นสตริงที่เข้ารหัส URL อย่างปลอดภัย base64 ที่มี p256ecdsa=
ไว้ข้างหน้า
Crypto-Key: p256ecdsa=[URL Safe Base64 Public Application Server Key]
การเข้ารหัสเพย์โหลด
ต่อไปเราจะมาดูวิธีส่งเพย์โหลดพร้อมข้อความพุชเพื่อที่เมื่อเว็บแอปของเราได้รับข้อความพุช แอปนั้นจะเข้าถึงข้อมูลที่ได้รับได้
คำถามหนึ่งที่พบบ่อยจากผู้ใช้บริการพุชอื่นๆ คือเหตุใดเพย์โหลดเว็บจึงต้องมีการเข้ารหัส เมื่อใช้แอปที่มาพร้อมเครื่อง ข้อความพุชจะส่งข้อมูลในรูปแบบข้อความธรรมดาได้
ข้อดีอย่างหนึ่งของการพุชจากเว็บคือ เนื่องจากบริการพุชทั้งหมดใช้ API เดียวกัน (โปรโตคอลพุชจากเว็บ) นักพัฒนาซอฟต์แวร์จึงไม่ต้องสนใจว่าบริการพุชจะเป็นใคร เราสามารถส่งคำขอในรูปแบบที่ถูกต้องและคาดว่าระบบจะส่งข้อความพุช ข้อเสียของปัญหานี้ก็คือนักพัฒนาซอฟต์แวร์อาจส่งข้อความไปยังบริการพุชที่ไม่น่าเชื่อถือ การเข้ารหัสเพย์โหลดจะทำให้บริการพุชอ่านข้อมูลที่ส่งไม่ได้ มีเพียงเบราว์เซอร์เท่านั้นที่สามารถถอดรหัสข้อมูลได้ วิธีนี้จะช่วยปกป้องข้อมูลของผู้ใช้
การเข้ารหัสของเพย์โหลดมีคำจำกัดความไว้ในข้อกำหนดจำเพาะของ Message Encryption
ก่อนที่เราจะดูขั้นตอนเฉพาะในการเข้ารหัสเพย์โหลดข้อความพุช เราควรพูดถึงเทคนิคบางอย่างที่จะใช้ในกระบวนการเข้ารหัส (เคล็ดลับสำคัญจาก Mat Scales สำหรับบทความที่ยอดเยี่ยมเกี่ยวกับการเข้ารหัสแบบพุช)
ECDH และ HKDF
มีการใช้ทั้ง ECDH และ HKDF ตลอดกระบวนการเข้ารหัสและให้ประโยชน์ในการเข้ารหัสข้อมูล
ECDH: การแลกเปลี่ยนคีย์ Elliptic Curve Diffie-Hellman
ลองนึกภาพว่าคุณมีสองคนที่อยากแชร์ข้อมูลกัน คืออลิสาและบัญชา ทั้งขวัญและบัญชามีคีย์สาธารณะและคีย์ส่วนตัวเป็นของตัวเอง อลิซและบ๊อบแชร์ คีย์สาธารณะให้แก่กันและกัน
พร็อพเพอร์ตี้ที่มีประโยชน์ของคีย์ที่สร้างด้วย ECDH คือ ขวัญใจสามารถใช้คีย์ส่วนตัวและคีย์สาธารณะของบัญชาเพื่อสร้างค่าลับ "X" บ๊อบสามารถทำแบบเดียวกันนี้ คือนำคีย์ส่วนตัวของเขาและคีย์สาธารณะของอลิซเพื่อสร้างค่า "X" เดียวกันได้อย่างอิสระ ซึ่งทำให้ "X" เป็นข้อมูลลับที่ใช้ร่วมกัน อลิซและบ็อบต้องแชร์เฉพาะคีย์สาธารณะเท่านั้น ตอนนี้บ็อบและอลิซสามารถใช้ "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 ในพุชจากเว็บไม่ควรยาวเกิน 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 ประการดังนี้
- ตัวเปย์โหลดนั้นเอง
- ข้อมูลลับ
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 ต้องเป็นข้อมูลแบบสุ่มขนาด 16 ไบต์ ใน NodeJS เราจะทำดังต่อไปนี้เพื่อสร้าง Salt
const salt = crypto.randomBytes(16);
คีย์สาธารณะ / ส่วนตัว
คีย์สาธารณะและคีย์ส่วนตัวควรสร้างขึ้นโดยใช้ P-256 Elliptic Curve ซึ่งเราจะทำในโหนด ดังนี้
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) คือการผสมผสานระหว่างข้อมูลลับในการตรวจสอบสิทธิ์ของการสมัครใช้บริการพุชกับข้อมูลลับที่ใช้ร่วมกันซึ่งเราเพิ่งสร้างขึ้น
const authEncBuff = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(subscription.keys.auth, sharedSecret, authEncBuff, 32);
คุณอาจสงสัยว่าสตริง Content-Encoding: auth\0
มีไว้เพื่ออะไร
กล่าวโดยสรุปคือมันไม่ได้มีจุดประสงค์ที่ชัดเจน แต่เบราว์เซอร์สามารถ
ถอดรหัสข้อความขาเข้าและมองหาการเข้ารหัสเนื้อหาที่คาดไว้
\0
จะเพิ่มไบต์ที่มีค่าเป็น 0 ถึงจุดสิ้นสุดของบัฟเฟอร์ กรณีนี้เกิดขึ้นได้เพราะเบราว์เซอร์จะถอดรหัสข้อความ ซึ่งคาดไว้ว่าการเข้ารหัสเนื้อหาจะมีไบต์จำนวนมาก ตามด้วย 1 ไบต์ที่มีค่า 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()]);
ตอนนี้เรามีเพย์โหลดที่เข้ารหัสแล้ว ไชโย
จากนั้นจึงกำหนดวิธีส่งเพย์โหลดนี้ไปยังบริการพุช
ส่วนหัวและเนื้อหาเพย์โหลดที่เข้ารหัส
หากต้องการส่งเพย์โหลดที่เข้ารหัสนี้ไปยังบริการพุช เราจำเป็นต้องกำหนดส่วนหัวที่แตกต่างกัน 2-3 ส่วนหัวในคำขอ POST ของเรา
ส่วนหัวการเข้ารหัส
ส่วนหัว "การเข้ารหัส" ต้องมี Salt ที่ใช้สำหรับการเข้ารหัสเพย์โหลด
Salt 16 ไบต์ควรเป็น URL แบบ base64 ที่เข้ารหัสและเพิ่มลงในส่วนหัวการเข้ารหัส ดังนี้
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-Encrypting" เป็นค่าคงที่
ดังที่แสดงด้านล่าง
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) เป็นจำนวนเต็มที่ระบุจำนวนวินาทีที่คุณต้องการให้ข้อความ Push แสดงในบริการ Push ก่อนนำส่ง เมื่อ TTL
หมดอายุ ระบบจะนำข้อความออกจากคิวบริการพุชและจะไม่นำส่ง
TTL: [Time to live in seconds]
หากคุณตั้งค่า TTL
เป็น 0 บริการพุชจะพยายามส่งข้อความทันที แต่หากไม่สามารถเข้าถึงอุปกรณ์ได้ ข้อความจะถูกตัดออกจากคิวบริการพุชทันที
ในทางเทคนิค บริการพุชสามารถลด TTL
ของข้อความพุชได้ตามต้องการ คุณจะทราบได้ว่าปัญหานี้เกิดขึ้นหรือไม่ โดยตรวจสอบส่วนหัว TTL
ในการตอบสนองจากบริการพุช
หัวข้อ
ไม่บังคับ
หัวข้อเป็นสตริงที่สามารถใช้เพื่อแทนที่ข้อความที่รอดำเนินการด้วยข้อความใหม่ หากชื่อหัวข้อไม่ตรงกัน
วิธีนี้มีประโยชน์ในสถานการณ์ที่มีการส่งข้อความหลายรายการขณะที่อุปกรณ์ออฟไลน์ และคุณต้องการให้ผู้ใช้เห็นข้อความล่าสุดเมื่อเปิดอุปกรณ์เท่านั้น
กรณีเร่งด่วน
ไม่บังคับ
ความเร่งด่วนจะบอกให้บริการพุชทราบว่าข้อความสำคัญต่อผู้ใช้เพียงใด บริการพุชจะใช้การตั้งค่านี้เพื่อช่วยยืดอายุการใช้งานแบตเตอรี่ของอุปกรณ์ของผู้ใช้ได้โดยปลุกระบบให้แสดงข้อความสำคัญเมื่อแบตเตอรี่เหลือน้อยเท่านั้น
ค่าของส่วนหัวจะกำหนดตามที่แสดงด้านล่าง ค่าเริ่มต้นคือ normal
Urgency: [very-low | low | normal | high]
รวมทุกอย่างเข้าด้วยกัน
หากคุณมีคำถามเพิ่มเติมเกี่ยวกับวิธีการทำงานนี้ คุณสามารถดูวิธีที่ไลบรารีทริกเกอร์ข้อความ Push ในองค์กร web-push-libs ได้เสมอ
เมื่อมีเพย์โหลดที่เข้ารหัสและมีส่วนหัวข้างต้นแล้ว คุณก็เพียงแค่ส่งคำขอ POST ไปยัง endpoint
ใน PushSubscription
สิ่งที่เราทำกับการตอบสนองต่อคำขอ POST นี้
การตอบกลับจากบริการพุช
เมื่อส่งคำขอไปยังบริการพุชแล้ว คุณจะต้องตรวจสอบรหัสสถานะของการตอบกลับเพื่อให้ทราบว่าคำขอสำเร็จหรือไม่
รหัสสถานะ | คำอธิบาย |
---|---|
201 | สร้างแล้ว ได้รับและยอมรับคำขอส่งข้อความพุชแล้ว |
429 | มีคำขอมากเกินไป ซึ่งหมายความว่าแอปพลิเคชันเซิร์ฟเวอร์ของคุณถึงขีดจำกัดอัตราด้วยบริการพุชแล้ว บริการพุชควรมีส่วนหัว "ลองอีกครั้ง-หลังจาก" เพื่อระบุระยะเวลาก่อนจะส่งคำขออีกได้ |
400 | คำขอไม่ถูกต้อง ซึ่งโดยทั่วไปหมายความว่าส่วนหัวอันใดอันหนึ่งของคุณไม่ถูกต้องหรือมีรูปแบบไม่ถูกต้อง |
404 | ไม่พบ ซึ่งแสดงว่าการสมัครใช้บริการหมดอายุแล้วและใช้งานไม่ได้ ในกรณีนี้ คุณควรลบ "PushSubscription" และรอให้ไคลเอ็นต์สมัครใช้บริการผู้ใช้อีกครั้ง |
410 | หมดแล้ว การสมัครใช้บริการไม่สามารถใช้ได้อีกต่อไปและควรนำออกจากแอปพลิเคชันเซิร์ฟเวอร์ ซึ่งทำซ้ำได้ด้วยการเรียกใช้ "unsubscribe()" ใน "PushSubscription" |
413 | เพย์โหลดมีขนาดใหญ่เกินไป เพย์โหลดขนาดขั้นต่ำที่บริการพุชต้องรองรับคือ 4096 ไบต์ (หรือ 4 KB) |
ขั้นตอนถัดไป
- ภาพรวมข้อความ Push ในเว็บ
- วิธีการทำงานของ Push
- การสมัครใช้บริการ
- UX ของสิทธิ์
- การส่งข้อความด้วยไลบรารีพุชจากเว็บ
- โปรโตคอลพุชจากเว็บ
- การจัดการเหตุการณ์พุช
- การแสดงการแจ้งเตือน
- ลักษณะการทำงานของการแจ้งเตือน
- รูปแบบการแจ้งเตือนทั่วไป
- คำถามที่พบบ่อยเกี่ยวกับข้อความ Push
- ปัญหาที่พบได้ทั่วไปและการรายงานข้อบกพร่อง