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

Mat Scales

ก่อน Chrome 50 ข้อความ Push จะไม่มีข้อมูลเพย์โหลด เมื่อเหตุการณ์ "push" เริ่มต้นขึ้นใน Service Worker สิ่งเดียวที่คุณรู้คือเซิร์ฟเวอร์พยายามบอกอะไรบางอย่างกับคุณ แต่ไม่รู้ว่าเป็นอะไร จากนั้นคุณต้องส่งคําขอติดตามผลไปยังเซิร์ฟเวอร์และรับรายละเอียดการแจ้งเตือนเพื่อแสดง ซึ่งอาจไม่สําเร็จในสภาพเครือข่ายที่ไม่เอื้ออำนวย

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

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

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

การเปลี่ยนแปลงฝั่งไคลเอ็นต์

หากคุณได้ติดตั้งใช้งาน Push Notification โดยไม่มีพายโหลดแล้ว คุณต้องทำการเปลี่ยนแปลงเล็กๆ น้อยๆ เพียง 2 อย่างในฝั่งไคลเอ็นต์

ประการแรกคือเมื่อคุณส่งข้อมูลการสมัครใช้บริการไปยังเซิร์ฟเวอร์แบ็กเอนด์ คุณจะต้องรวบรวมข้อมูลเพิ่มเติม หากคุณใช้ JSON.stringify() ในออบเจ็กต์ PushSubscription เพื่อจัดรูปแบบการส่งไปยังเซิร์ฟเวอร์อยู่แล้ว ก็ไม่จำเป็นต้องเปลี่ยนแปลงอะไร ตอนนี้การติดตามจะมีข้อมูลเพิ่มเติมในพร็อพเพอร์ตี้คีย์

> JSON.stringify(subscription)
{"endpoint":"https://android.googleapis.com/gcm/send/f1LsxkKphfQ:APA91bFUx7ja4BK4JVrNgVjpg1cs9lGSGI6IMNL4mQ3Xe6mDGxvt_C_gItKYJI9CAx5i_Ss6cmDxdWZoLyhS2RJhkcv7LeE6hkiOsK6oBzbyifvKCdUYU7ADIRBiYNxIVpLIYeZ8kq_A",
"keys":{"p256dh":"BLc4xRzKlKORKWlbdgFaBrrPK3ydWAHo4M0gs0i1oEKgPpWC5cW8OCzVrOQRv-1npXRWk8udnW3oYhIO4475rds=",
"auth":"5I2Bu2oKdyy9CwL8QVF0NQ=="}}

ค่า 2 ค่า p256dh และ auth ได้รับการเข้ารหัสในรูปแบบ Base64 ที่เราเรียกว่า Base64 ที่ปลอดภัยสำหรับ URL

หากต้องการดูเฉพาะไบต์แทน ให้ใช้เมธอด getKey() ใหม่ในการสมัครใช้บริการซึ่งจะแสดงผลพารามิเตอร์เป็น ArrayBuffer พารามิเตอร์ 2 รายการที่คุณต้องการคือ auth และ p256dh

> new Uint8Array(subscription.getKey('auth'));
[228, 141, 129, ...] (16 bytes)

> new Uint8Array(subscription.getKey('p256dh'));
[4, 183, 56, ...] (65 bytes)

การเปลี่ยนแปลงที่ 2 คือพร็อพเพอร์ตี้ data ใหม่เมื่อเหตุการณ์ push เริ่มทํางาน โดยจะมีเมธอดแบบซิงค์หลายวิธีในการแยกวิเคราะห์ข้อมูลที่รับ เช่น .text(), .json(), .arrayBuffer() และ .blob()

self.addEventListener('push', function(event) {
  if (event.data) {
    console.log(event.data.json());
  }
});

การเปลี่ยนแปลงฝั่งเซิร์ฟเวอร์

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

รายละเอียดค่อนข้างซับซ้อน และเช่นเดียวกับทุกอย่างที่เกี่ยวข้องกับการเข้ารหัส เราขอแนะนำให้ใช้ไลบรารีที่พัฒนาอยู่ในปัจจุบันแทนที่จะพัฒนาขึ้นมาเอง ทีม Chrome ได้เผยแพร่ไลบรารีสำหรับ Node.js แล้ว และจะเพิ่มภาษาและแพลตฟอร์มอื่นๆ เร็วๆ นี้ ซึ่งจะจัดการทั้งการเข้ารหัสและโปรโตคอล Web Push เพื่อให้การส่งข้อความ Push จากเซิร์ฟเวอร์ Node.js เป็นเรื่องง่ายเพียง webpush.sendWebPush(message, subscription)

แม้ว่าเราจะขอแนะนำให้ใช้คลังอย่างจริงจัง แต่ฟีเจอร์นี้ยังเป็นฟีเจอร์ใหม่และยังมีภาษายอดนิยมหลายภาษาที่ไม่มีคลัง หากจำเป็นต้องติดตั้งใช้งานด้วยตนเอง โปรดดูรายละเอียดต่อไปนี้

เราจะอธิบายอัลกอริทึมโดยใช้ JavaScript รูปแบบ Node แต่หลักการพื้นฐานควรเหมือนกันในทุกภาษา

อินพุต

หากต้องการเข้ารหัสข้อความ เราต้องรับ 2 สิ่งจากออบเจ็กต์การสมัครใช้บริการที่ได้รับจากไคลเอ็นต์ก่อน หากคุณใช้ JSON.stringify() ในไคลเอ็นต์และส่งไปยังเซิร์ฟเวอร์ ระบบจะจัดเก็บคีย์สาธารณะของไคลเอ็นต์ไว้ในช่อง keys.p256dh ส่วนรหัสลับที่ใช้ร่วมกันเพื่อตรวจสอบสิทธิ์จะอยู่ในช่อง keys.auth ข้อมูลทั้ง 2 รายการนี้จะเข้ารหัส Base64 ที่เข้ากันได้กับ URL ตามที่ระบุไว้ข้างต้น รูปแบบไบนารีของคีย์สาธารณะของไคลเอ็นต์คือจุดเวกเตอร์รูปไข่ P-256 ที่ไม่ได้บีบอัด

const clientPublicKey = new Buffer(subscription.keys.p256dh, 'base64');
const clientAuthSecret = new Buffer(subscription.keys.auth, 'base64');

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

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

นอกจากนี้ เรายังต้องสร้างข้อมูลใหม่ด้วย เราต้องใช้เกลือแบบสุ่มที่เข้ารหัสอย่างปลอดภัยขนาด 16 ไบต์ และคู่คีย์เอลลิปติกโค้งแบบสาธารณะ/ส่วนตัว เส้นโค้งเฉพาะที่ใช้โดยข้อกำหนดการเข้ารหัสแบบพุชเรียกว่า P-256 หรือ prime256v1 คุณควรสร้างคู่คีย์ใหม่ทุกครั้งที่เข้ารหัสข้อความเพื่อให้มีความปลอดภัยสูงสุด และไม่ควรนำเกลือมาใช้ซ้ำ

ECDH

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

หลักการนี้เป็นพื้นฐานของโปรโตคอลการเข้ารหัสคีย์ Diffie-Hellman (ECDH) รูปไข่ ซึ่งช่วยให้ทั้ง 2 ฝ่ายมีความลับที่แชร์เดียวกันแม้ว่าจะแลกเปลี่ยนเฉพาะคีย์สาธารณะเท่านั้น เราจะใช้ข้อมูลลับที่ใช้ร่วมกันนี้เป็นพื้นฐานสำหรับคีย์การเข้ารหัสจริง

const crypto = require('crypto');

const salt = crypto.randomBytes(16);

// Node has ECDH built-in to the standard crypto library. For some languages
// you may need to use a third-party library.
const serverECDH = crypto.createECDH('prime256v1');
const serverPublicKey = serverECDH.generateKeys();
const sharedSecret = serverECDH.computeSecret(clientPublicKey);

HKDF

เป็นเวลาพักอีกรอบ สมมติว่าคุณมีข้อมูลที่เป็นความลับซึ่งต้องการใช้เป็นคีย์การเข้ารหัส แต่ข้อมูลดังกล่าวไม่ปลอดภัยจากการเข้ารหัสมากพอ คุณสามารถใช้ฟังก์ชันการสร้างคีย์ (HKDF) ที่อิงตาม HMAC เพื่อเปลี่ยนข้อมูลลับที่มีการรักษาความปลอดภัยต่ำให้กลายเป็นข้อมูลลับที่มีการรักษาความปลอดภัยสูง

ผลที่ตามมาอย่างหนึ่งของวิธีการทํางานคือช่วยให้คุณนําข้อมูลลับที่มีจำนวนบิตเท่าใดก็ได้ไปสร้างข้อมูลลับอีกรายการที่มีขนาดเท่าใดก็ได้สูงสุด 255 ครั้ง ตราบใดที่แฮชที่สร้างขึ้นโดยอัลกอริทึมการแฮชใดก็ตามที่คุณใช้ สำหรับ Push นั้น ข้อกำหนดกำหนดให้เราต้องใช้สัดส่วน SHA-256 ซึ่งมีความยาวแฮช 32 ไบต์ (256 บิต)

เราทราบดีว่าต้องสร้างคีย์ที่มีขนาดไม่เกิน 32 ไบต์ ซึ่งหมายความว่าเราสามารถใช้อัลกอริทึมเวอร์ชันที่เรียบง่ายซึ่งจัดการกับเอาต์พุตขนาดใหญ่ไม่ได้

เราได้ใส่โค้ดสำหรับเวอร์ชัน Node ไว้ด้านล่างแล้ว แต่คุณสามารถดูวิธีการทํางานจริงได้ใน RFC 5869

อินพุตของ HKDF คือเกลือ ข้อมูลการเข้ารหัสเริ่มต้น (ikm) บางส่วน ข้อมูลที่มีโครงสร้างซึ่งไม่บังคับสำหรับ Use Case ปัจจุบัน (info) และความยาวเป็นไบต์ของคีย์เอาต์พุตที่ต้องการ

// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
  if (length > 32) {
    throw new Error('Cannot return keys of more than 32 bytes, ${length} requested');
  }

  // 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);
}

การสร้างพารามิเตอร์การเข้ารหัส

ตอนนี้เราใช้ HKDF เพื่อเปลี่ยนข้อมูลที่เรามีให้เป็นพารามิเตอร์สําหรับการเข้ารหัสจริง

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

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

ประเภทข้อมูลสําหรับการเข้ารหัสเนื้อหาคือ "aesgcm" ซึ่งเป็นชื่อของการเข้ารหัสที่ใช้สําหรับการเข้ารหัสแบบพุช

const authInfo = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(clientAuthSecret, sharedSecret, authInfo, 32);

function createInfo(type, clientPublicKey, serverPublicKey) {
  const len = type.length;

  // The start index for each element within the buffer is:
  // value               | length | start    |
  // -----------------------------------------
  // 'Content-Encoding: '| 18     | 0        |
  // type                | len    | 18       |
  // nul byte            | 1      | 18 + len |
  // 'P-256'             | 5      | 19 + len |
  // nul byte            | 1      | 24 + len |
  // client key length   | 2      | 25 + len |
  // client key          | 65     | 27 + len |
  // server key length   | 2      | 92 + len |
  // server key          | 65     | 94 + len |
  // For the purposes of push encryption the length of the keys will
  // always be 65 bytes.
  const info = new Buffer(18 + len + 1 + 5 + 1 + 2 + 65 + 2 + 65);

  // The string 'Content-Encoding: ', as utf-8
  info.write('Content-Encoding: ');
  // The 'type' of the record, a utf-8 string
  info.write(type, 18);
  // A single null-byte
  info.write('\0', 18 + len);
  // The string 'P-256', declaring the elliptic curve being used
  info.write('P-256', 19 + len);
  // A single null-byte
  info.write('\0', 24 + len);
  // The length of the client's public key as a 16-bit integer
  info.writeUInt16BE(clientPublicKey.length, 25 + len);
  // Now the actual client public key
  clientPublicKey.copy(info, 27 + len);
  // Length of our public key
  info.writeUInt16BE(serverPublicKey.length, 92 + len);
  // The key itself
  serverPublicKey.copy(info, 94 + len);

  return info;
}

// Derive the Content Encryption Key
const contentEncryptionKeyInfo = createInfo('aesgcm', clientPublicKey, serverPublicKey);
const contentEncryptionKey = hkdf(salt, prk, contentEncryptionKeyInfo, 16);

// Derive the Nonce
const nonceInfo = createInfo('nonce', clientPublicKey, serverPublicKey);
const nonce = hkdf(salt, prk, nonceInfo, 12);

Padding

อีกเรื่องหนึ่งและถึงเวลาสําหรับตัวอย่างที่ไร้สาระและจงใจ สมมติว่าหัวหน้าของคุณมีเซิร์ฟเวอร์ที่ส่งข้อความ Push พร้อมราคาหุ้นของบริษัทให้เธอทุก 2-3 นาที ข้อความธรรมดาสำหรับการดำเนินการนี้จะเป็นตัวเลขที่สมบูรณ์ 32 บิตที่มีค่าเป็นสิบเซ็นเสมอ นอกจากนี้ เธอยังมีข้อตกลงลับๆ กับเจ้าหน้าที่จัดเลี้ยงด้วย ซึ่งหมายความว่าเจ้าหน้าที่สามารถส่งข้อความ "โดนัทในห้องพักผ่อน" มาให้เธอ 5 นาทีก่อนที่โดนัทจะมาถึงจริง เพื่อให้เธอ "บังเอิญ" อยู่ที่นั่นเมื่อโดนัทมาถึงและเลือกโดนัทที่ดีที่สุดได้

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

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

ค่าการเติมคือจำนวนเต็มแบบ Big Endian 16 บิตที่ระบุความยาวการเติม followed by that number of NUL bytes of padding. ดังนั้นการเติมค่าขั้นต่ำคือ 2 บิต - ตัวเลข 0 ที่เข้ารหัสเป็น 16 บิต

const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeroes, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);

เมื่อข้อความ Push มาถึงไคลเอ็นต์ เบราว์เซอร์จะตัดการถอดช่องว่างโดยอัตโนมัติเพื่อให้โค้ดไคลเอ็นต์ได้รับเฉพาะข้อความที่ไม่มีการตัดช่องว่าง

การเข้ารหัส

ตอนนี้เรามีทุกอย่างสําหรับการเข้ารหัสแล้ว การเข้ารหัสที่จําเป็นสําหรับ Web Push คือ AES128 โดยใช้ GCM เราใช้คีย์การเข้ารหัสเนื้อหาเป็นคีย์ และใช้ Nonce เป็นเวกเตอร์การเริ่มต้น (IV)

ในตัวอย่างนี้ ข้อมูลของเราคือสตริง แต่อาจเป็นข้อมูลไบนารีก็ได้ คุณส่งเพย์โหลดได้สูงสุด 4078 ไบต์ - 4096 ไบต์ต่อโพสต์ โดยมี 16 ไบต์สำหรับข้อมูลการเข้ารหัสและอย่างน้อย 2 ไบต์สำหรับการเติม

// Create a buffer from our data, in this case a UTF-8 encoded string
const plaintext = new Buffer('Push notification payload!', 'utf8');
const cipher = crypto.createCipheriv('id-aes128-GCM', contentEncryptionKey,
nonce);

const result = cipher.update(Buffer.concat(padding, plaintext));
cipher.final();

// Append the auth tag to the result - https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
return Buffer.concat([result, cipher.getAuthTag()]);

ข้อความ Push จากเว็บ

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

คุณต้องตั้งค่าส่วนหัว 3 รายการ

Encryption: salt=<SALT>
Crypto-Key: dh=<PUBLICKEY>
Content-Encoding: aesgcm

<SALT> และ <PUBLICKEY> คือ Salt และคีย์สาธารณะของเซิร์ฟเวอร์ที่ใช้ในการเข้ารหัส ซึ่งเข้ารหัสเป็น Base64 ที่ปลอดภัยสำหรับ URL

เมื่อใช้โปรโตคอล Web Push เนื้อหาของ POST จะเป็นไบต์ดิบของข้อความที่เข้ารหัส อย่างไรก็ตาม คุณสามารถใส่ข้อมูลไว้ในเพย์โหลด JSON ที่มีอยู่ได้โดยง่าย ดังนี้ ในระหว่างที่ Chrome และ Firebase Cloud Messaging ยังไม่รองรับโปรโตคอลนี้

{
    "registration_ids": [ "…" ],
    "raw_data": "BIXzEKOFquzVlr/1tS1bhmobZ…"
}

ค่าของพร็อพเพอร์ตี้ rawData ต้องเป็นข้อความที่เข้ารหัสฐาน 64

การแก้ไขข้อบกพร่อง / โปรแกรมตรวจสอบ

Peter Beverloo เป็นหนึ่งในวิศวกรของ Chrome ที่นำฟีเจอร์นี้มาใช้ (และเป็นหนึ่งในผู้เขียนข้อกำหนด) ได้สร้างโปรแกรมตรวจสอบ

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