Data Encryption & Masking 101 — ปกป้อง column ข้อมูล 2 แบบ
อธิบายแบบง่ายที่สุด: ข้อมูลอ่อนไหว 1 field ต้องป้องกัน 2 จังหวะ — เข้ารหัสตอนเก็บลง DB (data encryption at rest — โจทย์ PDPA) กับเซ็นเซอร์/mask ตอนส่งออกไปให้คนเห็น — ต่างกันยังไง กันอะไรได้ ทำไมต้องใช้คู่กัน และทำไม mask ที่ FE = ไม่ได้ป้องกันอะไรเลย
อัปเดต: 2026-08-06
Data Encryption & Masking 101 — ปกป้อง column ข้อมูล 2 แบบ
เล่มก่อนหน้า (Encryption Field 101) พูดถึงการปกป้องข้อมูลตอนเดินทาง (browser ↔ backend) — เล่มนี้พูดถึงอีก 2 จังหวะที่เหลือของชีวิตข้อมูล: ตอนนอนอยู่ใน database และตอนถูกส่งออกไปให้คนเห็น
ทั้งสองแบบทำงานที่ backend ล้วนๆ — ไม่เกี่ยวกับ browser, ไม่เกี่ยวกับชั้น 1-3 ของขา browser (แต่เสริมกัน — ดูข้อ 5)
TL;DR — อ่าน 30 วิ
- ข้อมูลอ่อนไหว 1 field (เลขบัตรประชาชน, เลขบัญชี) ต้องคิด 2 คำถามแยกกันเสมอ: “เก็บยังไงให้หลุดแล้วอ่านไม่ออก” (encryption at rest) กับ “ส่งออกยังไงให้เห็นเท่าที่จำเป็น” (masking)
- Encryption at rest = เข้ารหัสก่อนเก็บลง DB, ถอดได้ด้วย key ที่อยู่ Key Vault — กัน DB dump/backup หลุด, กันคน query ตรง · เป็นมาตรการ security ตาม PDPA โดยตรง
- Masking = ส่งค่าบางส่วน (
1-2345-xxxxx-67-8) เมื่อผู้รับไม่ต้องใช้ค่าเต็ม — กันตาคน, กัน log, ลดการเปิดเผยเกินจำเป็น (data minimization) - สองอย่างนี้ไม่ใช่ตัวเลือกให้เลือกอันเดียว — ปกป้องคนละจุด ใช้คู่กันเสมอ
- ⚠️ กฎเหล็กข้อเดียวที่ห้ามพลาด: mask ต้องทำที่ backend — ส่งค่าเต็มมาแล้วให้หน้าจอปิดดาวเอง = ค่าเต็มอยู่ใน Network tab เรียบร้อยแล้ว ไม่ได้ป้องกันอะไรเลย
1. ชีวิตของข้อมูล 1 field — และจุดที่แต่ละเครื่องมือปกป้อง
graph LR
subgraph transit1["🚚 ตอนเดินทาง (ขาเข้า)"]
A["Browser"] -->|"TLS + ชั้น 3<br/>(เล่ม Encryption Field 101)"| B["Backend"]
end
subgraph rest["💾 ตอนนอนอยู่"]
B -->|"① encrypt ก่อนเก็บ<br/>(data encryption at rest)"| DB[("Database<br/>ciphertext")]
DB -->|"ถอดด้วย key จาก KV"| B2["Backend"]
end
subgraph egress["📤 ตอนถูกส่งออก"]
B2 -->|"② mask ก่อนส่ง<br/>ถ้าปลายทางไม่ต้องใช้ค่าเต็ม"| C["หน้าจอ / รายงาน / log /<br/>ระบบภายนอก"]
end
| จังหวะ | ภัยที่เจอ | เครื่องมือ |
|---|---|---|
| เดินทาง (browser ↔ backend) | คนดักฟัง / คนกลางที่ terminate TLS | TLS + ชั้น 3 (เล่มก่อนหน้า) |
| นอนอยู่ใน DB | DB dump หลุด, backup หลุด, คนมีสิทธิ์ query ตรง, SQL injection อ่านทั้ง table | ① Data encryption at rest |
| ถูกส่งออกไปให้คนเห็น | ตาคน (หน้าจอ, screenshot), log, admin/support ที่ไม่จำเป็นต้องเห็นค่าเต็ม | ② Masking (เซ็นเซอร์) |
2. แบบที่ ① — Data Encryption at Rest (โจทย์ PDPA)
ทำงานยังไง
ตอนเขียน: plaintext → [AES encrypt ด้วย key จาก Key Vault] → เก็บ ciphertext ลง column
ตอนอ่าน: ciphertext → [AES decrypt ด้วย key เดียวกัน] → ได้ plaintext ใน memory ของ app
- key อยู่ Key Vault เท่านั้น — ไม่อยู่ใน DB, ไม่อยู่ใน source code, ไม่อยู่ใน appsettings (อยู่เป็น KV secret)
- คนที่ได้ DB ไปทั้งก้อน (dump/backup/query ตรง) เห็นแค่ ciphertext — ไม่มี key = อ่านไม่ออก
- เป็นการเข้ารหัสแบบ ถอดกลับได้ (reversible) โดยตั้งใจ — เพราะ app ต้องใช้ค่าจริงทำงานต่อ (ต่างจาก password ที่ต้อง hash ทิ้งถาวร)
มุม PDPA — ทำไมข้อนี้คือ “ต้องมี” ไม่ใช่ “ควรมี”
- PDPA กำหนดให้ผู้ควบคุมข้อมูลมีมาตรการรักษาความมั่นคงปลอดภัยที่เหมาะสม — การเข้ารหัสข้อมูลส่วนบุคคลอ่อนไหวใน storage คือมาตรการมาตรฐานที่ผู้ตรวจถามหาเป็นข้อแรกๆ
- ถ้าเกิดเหตุข้อมูลรั่ว: หลุดเป็น ciphertext ที่ไม่มี key ≠ หลุด plaintext — ระดับความเสียหายและภาระหน้าที่หลังเกิดเหตุต่างกันมาก
- ช่วยบังคับ least privilege ในตัว: DBA/คนมีสิทธิ์อ่าน DB ไม่เห็นข้อมูลส่วนบุคคลอัตโนมัติอีกต่อไป — เห็นได้เฉพาะผ่าน app ที่มี key และมี audit trail
ของที่มีอยู่แล้วใน stack เรา
Backend_Package มี AES field encryption อยู่แล้ว (config Encryption:Key — เป็น KV secret) — งานนี้จึงไม่ใช่สร้างกลไกใหม่ แต่คือเลือกว่า column ไหนต้องผ่านมัน ตาม PII inventory (ตัวเดียวกับที่ชั้น 3 ใช้ — มติ 14)
จุดที่ต้องคิดก่อนเลือก column (ราคาที่ต้องจ่าย — รู้ก่อน ไม่เจ็บทีหลัง)
| ประเด็น | ผลกระทบ | ทางออกมาตรฐาน |
|---|---|---|
| ค้นหา/filter ไม่ได้ | เข้ารหัสแล้ว WHERE citizen_id = ? ใช้ไม่ได้ (ciphertext ไม่เหมือนกันแม้ค่าเดิม) | เพิ่ม column คู่เก็บ hash (HMAC-SHA256 ด้วย key แยก) ไว้ใช้ค้นแบบ exact-match — ค้น partial/LIKE ไม่ได้โดยธรรมชาติ ต้องยอมรับตั้งแต่ออกแบบ |
| index / join | index บน column ที่เข้ารหัส = ไร้ความหมาย | ใช้ hash column เป็นตัว index/join แทน |
| ความยาว column | ciphertext + IV ยาวกว่า plaintext | ขยาย column เป็น text/varbinary ตามจริง — คิดตอน migration ไม่ใช่ตอนพัง |
| Key rotation | เปลี่ยน key = ข้อมูลเก่าถอดไม่ออก | เก็บ key version กำกับ (v1:...) — rotate แล้ว re-encrypt แบบ background ได้ทีละแถว |
| ของเก่าใน DB | แถวเดิมเป็น plaintext อยู่แล้ว | migration แบบ dual-read (อ่านได้ทั้งสองแบบ) → backfill → ปิดทาง plaintext — ห้าม big-bang |
3. แบบที่ ② — Masking (เซ็นเซอร์ขาส่งออก)
ทำงานยังไง
Backend แปลงค่าก่อนใส่ response — ผู้รับได้ค่าที่เอากลับเป็นค่าเต็มไม่ได้:
เลขบัตรประชาชน 1-2345-67890-12-3 → 1-2345-xxxxx-12-3
เลขบัญชี 123-4-56789-0 → xxx-x-xx789-0
เบอร์โทร 081-234-5678 → 08x-xxx-5678
อีเมล somchai@exim.go.th → som***@exim.go.th
⚠️ กฎเหล็ก: mask ที่ backend เท่านั้น — ทำไม FE mask เอง = ศูนย์
ความเข้าใจผิดคลาสสิกที่สุดของเรื่องนี้: “ส่งค่าเต็มมา แล้วให้หน้าจอแสดงเป็นดาวเอง”
- ค่าเต็มออกจาก backend แล้ว = อยู่ใน Network tab ของ DevTools เรียบร้อย (กด F12 ดูได้ทุกคน)
- อยู่ในมือของทุกคนกลางที่มองเห็น response (จนกว่าชั้น 3 ขา response จะมา — มติ 11)
- อยู่ใน memory/state ของ FE — extension/สคริปต์ในหน้าอ่านได้
- สรุป: FE mask = การตกแต่งหน้าจอ ไม่ใช่มาตรการ security — ค่าที่ไม่ควรเห็นต้องไม่ออกจาก backend ตั้งแต่แรก
กันอะไร — และหลักคิดตอนตัดสินใจ
- กันการเปิดเผยเกินจำเป็น (data minimization ตาม PDPA): จอ list รายชื่อไม่จำเป็นต้องได้เลขบัตรเต็มทุกแถว — ได้ 4 ตัวท้ายพอให้ user ยืนยันว่าใช่คนนี้
- กันตาคน: screenshot, จอ support/admin, คนเดินผ่านหลังโต๊ะ
- กัน log รั่ว: response ที่ mask แล้ว ต่อให้หลุดไปอยู่ใน log/monitoring ก็เสียหายจำกัด
- หลักตัดสินต่อ endpoint มีข้อเดียว: “หน้าจอนี้ต้องใช้ค่าเต็มเพื่อทำงานไหม” — ไม่ต้อง = mask เสมอ · ต้องจริง (เช่น จอแก้ไขข้อมูลตัวเอง, เอกสารทางการ) = ส่งเต็มได้ แต่ต้องรู้ตัวว่า endpoint นั้นกลายเป็นจุดอ่อนไหว → เป็นตัวเต็งอันดับแรกที่ชั้น 3 ขา response ต้องครอบ (มติ 11)
4. เทียบกันชัดๆ — และทำไมต้องมีทั้งคู่
| ① Encryption at rest | ② Masking | |
|---|---|---|
| ปกป้องตอนไหน | ข้อมูลนอนอยู่ใน DB | ข้อมูลออกจาก backend |
| กันใคร | คนได้ DB ไป (dump/backup/query ตรง) | คนเห็น response (หน้าจอ, log, คนกลาง) |
| ถอดกลับได้ไหม | ได้ — โดย app ที่มี key (ตั้งใจ) | ไม่ได้ — ผู้รับได้ค่าบางส่วนถาวร |
| ใครทำ | Backend_Package (AES + KV key) ตอนเขียน/อ่าน DB | Backend ตอนประกอบ response |
| โจทย์หลัก | PDPA security safeguard | Data minimization + ลด exposure ระหว่างรอชั้น 3 |
| ถ้ามีแค่ตัวเดียว | DB ปลอดภัย แต่ค่าเต็มยังไหลออกทุก response | ขาออกสะอาด แต่ DB หลุดครั้งเดียว = plaintext ทั้ง table |
จำง่ายๆ: encryption กันขโมยงัดตู้เซฟ · masking กันคนเห็นของบนโต๊ะ — ล็อกตู้เซฟแน่นแค่ไหน ถ้าวางของเต็มโต๊ะก็ไม่มีประโยชน์ และเก็บโต๊ะเรียบร้อยแค่ไหน ตู้เซฟไม่ล็อกก็จบเหมือนกัน
5. ต่อภาพกับของที่ทำอยู่ (3 ชั้นขา browser)
- PII inventory ตัวเดียว ใช้ร่วมทุกเรื่อง (มติ 14): field ไหนอ่อนไหว → ตัวเดียวกันนี้คือรายชื่อ column ที่ต้อง encrypt at rest, รายชื่อ field ที่ชั้น 3 ต้อง encrypt ตอนเดินทาง, และรายชื่อที่ต้องมี mask rule ขาออก — จุดตัดสินใจเดียว ไม่ต้องไล่สามรอบ
- Masking คือกติกาชั่วคราวของมติ 11 อยู่แล้ว: ระหว่างที่ชั้น 3 ขา response ยังไม่มา backend ต้อง mask field อ่อนไหวใน response — เล่มนี้คือเนื้อหาเต็มของคำว่า “mask” ในมติน้้น และต่อให้ชั้น 3 มาแล้ว masking ก็ไม่หายไป (จอที่ไม่ต้องใช้ค่าเต็มก็ยังไม่ควรได้ค่าเต็ม — encrypt ค่าที่ไม่จำเป็นต้องส่ง แพ้การไม่ส่งเสมอ)
- ลำดับความสำคัญตอน implement: ทั้งสองแบบเป็นงาน backend ล้วน ไม่พึ่ง browser ไม่พึ่ง ShareLib — เดินคู่ขนานกับ POC ชั้น 2 ได้โดยไม่ชนกัน (คนละไฟล์ คนละ layer: at-rest อยู่ Infrastructure/entity config, masking อยู่ response mapping)
6. Checklist ต่อ 1 field (ใช้ตอนเดิน PII inventory)
- field นี้เป็นข้อมูลส่วนบุคคล/อ่อนไหวไหม → ใช่ = เข้า inventory
- ระบบต้องค้นหาด้วยค่านี้ไหม → ใช่ = ต้องมี hash column คู่ (ค้น exact-match เท่านั้น) · ไม่ = encrypt เดี่ยวๆ จบ
- หน้าจอ/ผู้รับไหนบ้างต้องใช้ค่าเต็ม → ระบุเป็นรายการ endpoint ที่ส่งเต็ม (ให้สั้นที่สุด) — ที่เหลือ mask หมด
- ค่า mask หน้าตาเป็นยังไง (กี่ตัวท้าย/รูปแบบ) → กำหนดเป็น rule กลางต่อประเภทข้อมูล ไม่ใช่ต่อ endpoint (เลขบัตรต้อง mask เหมือนกันทุกจอ)
- ข้อมูลเก่าใน DB มีอยู่แล้วไหม → มี = ต้องมีแผน backfill (dual-read → migrate → ปิดทาง plaintext)