ทำตู้เซฟรหัสผ่านแบบ zero-knowledge: เซิร์ฟเวอร์เก็บข้อมูล แต่อ่านไม่ได้
เบื้องหลังการออกแบบตู้เซฟรหัสผ่านที่เข้ารหัสทุกอย่างในเบราว์เซอร์ ทั้งการแปลงรหัสผ่านหลักเป็นกุญแจ รหัสกู้คืน และการกันข้อมูลถูกเขียนทับ
ตู้เซฟรหัสผ่าน (Password Manager) เป็นเว็บแอปภาษาไทยที่ผมทำขึ้นเอง โจทย์หลักมีข้อเดียวคือ คนที่ดูแลเซิร์ฟเวอร์ต้องอ่านข้อมูลของผู้ใช้ไม่ได้ รวมถึงตัวผมเองด้วย
แนวคิดนี้เรียกว่า zero-knowledge ข้อมูลทุกอย่างถูกเข้ารหัสในเบราว์เซอร์ก่อนส่งออกไป ฐานข้อมูล (Supabase) กับโฮสต์ (Netlify) จึงเห็นแค่ข้อความที่เข้ารหัสแล้ว ไม่เคยเห็นรหัสผ่านหลักหรือข้อมูลจริง
จากรหัสผ่านหลักตัวเดียว ไปเป็นกุญแจสองดอก
ผู้ใช้จำรหัสผ่านแค่ตัวเดียว เบราว์เซอร์จะแปลงรหัสนั้นเป็นกุญแจแบบนี้
รหัสผ่านหลัก + อีเมล ─► PBKDF2-SHA256 600,000 รอบ ─► master key
master key ─► HKDF "login" ─► กุญแจล็อกอิน (ส่งให้ Supabase Auth)
master key ─► HKDF "wrap" ─► กุญแจห่อ (ไม่ออกจากเบราว์เซอร์)
- PBKDF2 600,000 รอบ ทำให้การเดารหัสผ่านแบบสุ่มไปเรื่อย ๆ ช้าลงมาก
- HKDF แตกกุญแจเป็นสองดอกที่ใช้คนละงาน เซิร์ฟเวอร์ได้แค่ “กุญแจล็อกอิน” ซึ่งย้อนกลับไปเป็นกุญแจห่อหรือรหัสผ่านหลักไม่ได้
ข้อมูลจริงล็อกด้วยกุญแจสุ่มอีกชั้น
ตัวข้อมูลในตู้เซฟเข้ารหัสด้วย AES-256-GCM โดยใช้ กุญแจตู้เซฟ ที่สุ่มขึ้นมา กุญแจนี้ถูก “ห่อ” เก็บไว้สองชุด
- ห่อด้วยกุญแจที่มาจากรหัสผ่านหลัก
- ห่อด้วย รหัสกู้คืน 25 ตัวอักษร ซึ่งแสดงให้ผู้ใช้ดูครั้งเดียวและอยู่ใน Emergency Kit
ข้อดีของการแยกชั้นแบบนี้คือ เวลาเปลี่ยนรหัสผ่านหลัก ระบบแค่ห่อกุญแจตู้เซฟใหม่ด้วยรหัสใหม่ ส่วนกุญแจที่ใช้เข้ารหัสข้อมูลยังเป็นดอกเดิม
ลืมรหัสผ่านหลักแล้วทำไง
ผู้ใช้กู้คืนได้ถ้ามี ทั้ง ลิงก์ที่ส่งไปทางอีเมล และ รหัสกู้คืน แล้วตั้งรหัสผ่านใหม่ได้เลย
แต่ถ้าไม่มีรหัสกู้คืน ก็กู้ไม่ได้จริง ๆ ไม่มีใครช่วยได้ รวมถึงผู้ดูแลระบบ ฟังดูโหด แต่นี่คือราคาของ zero-knowledge ถ้าผู้ดูแลกู้ให้ได้ แปลว่าผู้ดูแลก็เปิดอ่านข้อมูลได้เหมือนกัน
ฝั่งเซิร์ฟเวอร์ยังต้องป้องกันอะไรบ้าง
แม้ในฐานข้อมูลจะมีแต่ข้อความที่เข้ารหัส ฝั่งเซิร์ฟเวอร์ก็ยังมีหน้าที่ของมัน
- Row Level Security (RLS) ผู้ใช้แต่ละคนอ่านเขียนได้เฉพาะแถวของตัวเอง
- ตรวจเวอร์ชันก่อนบันทึก ถ้าเปิดแอปไว้หลายเครื่องแล้วแก้พร้อมกัน ข้อมูลที่ใหม่กว่าจะไม่ถูกเขียนทับด้วยข้อมูลเก่า
- Content Security Policy หน้าเว็บคุยได้กับ Supabase เท่านั้น ลดโอกาสที่สคริปต์แปลกปลอมจะส่งข้อมูลออกไปที่อื่น
- ไม่ใส่ secret key ในหน้าเว็บ ฝั่ง frontend ใช้แค่ anon key ซึ่งเปิดเผยได้ เพราะ RLS เป็นตัวกั้น และข้อมูลที่ได้ไปก็มีแต่ ciphertext
สิ่งที่ได้เรียนรู้
ส่วนที่ยากที่สุดไม่ใช่การเรียกใช้ Web Crypto แต่เป็นการคิดว่า กุญแจแต่ละดอกอยู่ที่ไหน และใครเห็นอะไรบ้าง ถ้าวาดผังนี้ให้ชัดตั้งแต่ต้น โค้ดที่เหลือจะตามมาเองได้ง่าย
ลองใช้งานได้ที่ pass.grocscode.com และดูโค้ดทั้งหมดได้ที่ GitHub