Security Uplift · D — Whole-body payload encryption
ติดตั้งการเข้ารหัสทั้ง body เป็น JWE ก้อนเดียว เลือกเป็นราย action — ต้องมีกลไก B อยู่ก่อน
อัปเดต: 2026-09-07
📘 แนวคิดเบื้องหลังกลไกนี้ → Encryption Field 101 (ใช้กุญแจและซองชุดเดียวกับ B ต่างกันที่ขอบเขต) 📕 สิ่งที่พังได้ในแต่ละขั้น → กับดัก D 📗 ภาพรวมทั้ง 6 กลไก · กฎ opt-in · การเปิดเป็นราย env → ติดตั้งใน service ของคุณ
D. Whole-body payload encryption — เข้ารหัสทั้ง body เป็น JWE ก้อนเดียว
ต่างจาก B ตรงที่ B เข้ารหัสทีละ field ส่วน D ห่อทั้ง body เป็น JWE ก้อนเดียว และ เลือกเป็นราย action ด้วย attribute บน method ไม่ใช่บน property
🔴 ต้องเปิด B ก่อน — D ไม่ register cipher ของตัวเองเลย มันหยิบ IPayloadCipher / IPayloadResponseCipher กับกุญแจชุดเดียวกับ B มาใช้ · ตั้ง FieldEncryption:Payload:WholeBody:Enabled = true โดยที่ FieldEncryption:Payload:Enabled ยัง false = แอปไม่ boot (PayloadWholeBodyConfigurationException)
🔴 เปิด B เพื่อจะเปิด D = [EncryptedField] ทุกตัวใน service เริ่มมีผลพร้อมกัน ทั้งขาเข้าและขาออก ⇒ ก่อนเปิด D ให้ grep -rn "EncryptedField" src/ ดูว่ามี mark อยู่กี่ที่ และ caller ของเส้นเหล่านั้นพร้อมรับ ciphertext แล้วหรือยัง
🔴 flag ตั้งต้นปิด และเป็น opt-in ราย action — ไม่แปะ attribute = ไม่ถูกแตะเลย แม้ flag เปิดอยู่ · ปิด flag = ไม่ถูกแตะแม้แปะ attribute ไว้แล้ว
รูปร่างบนสาย
ขาเข้า body ต้องเป็น envelope นี้เป๊ะ ๆ
{ "data": "<JWE compact 5 ส่วน>" }
ขาออก เอาเฉพาะสมาชิก data ของ envelope มาตรฐานไปเข้ารหัส แล้วแทนที่ด้วย JWE string ตัวเดียว · สมาชิกอื่นระดับบนสุด (เช่น meta) ไม่ถูกแตะ · data ที่เป็น null อยู่แล้วก็รองรับ
ทั้งเส้นทำงานยังไง
sequenceDiagram
autonumber
participant FE as Browser (auth-sdk)
participant SIG as UseClientSignatureVerification
participant WB as UsePayloadWholeBodyEncryption
participant H as Handler
participant R as Redis
Note over WB: ตอน startup ถ้า WholeBody Enabled เป็น true แต่ Payload Enabled ยัง false<br/>แอปไม่ boot — PayloadWholeBodyConfigurationException
FE->>FE: ปิดผนึก body ทั้งก้อนเป็น JWE ก้อนเดียว แล้วห่อเป็น envelope data
FE->>SIG: POST envelope พร้อมลายเซ็นที่เซ็นทับ ciphertext ตามที่อยู่บนสาย
SIG->>WB: ลายเซ็นผ่าน — ถอดรหัสต้องอยู่หลังการตรวจลายเซ็นเสมอ
alt action ไม่ได้แปะ EncryptedWholeBody หรือ flag ปิด
WB->>H: ไม่ถูกแตะเลย body เดิมไหลเข้า model binding
else action แปะไว้และ flag เปิด
WB->>WB: body ใหญ่เกิน MaxRequestBodySizeBytes — 413 PayloadTooLarge
WB->>WB: ไม่ใช่ envelope data หรือถอด JWE ไม่ออก หรือถอดแล้วไม่ใช่ JSON object
alt Mode Enforce
WB-->>FE: 400 PayloadDecryptionError
else Mode LogOnly
WB->>H: เขียน log แล้วปล่อย body เดิมผ่านไป ไม่ปฏิเสธ
end
WB->>H: ถอดสำเร็จ — เขียน body ที่ถอดแล้วกลับ handler bind DTO เดิม
end
Note over WB,R: ขาออกหากุญแจของ caller ก่อน handler ทำงาน ไม่ใช่หลัง
WB->>R: ถามกุญแจรับของที่ caller ลงทะเบียนไว้
alt Enforce และยังไม่มีกุญแจ
WB-->>FE: 500 PayloadEncryptionError และ handler ไม่ถูกเรียกเลย
else LogOnly และยังไม่มีกุญแจ
H-->>FE: ตอบ plaintext ตามเดิม พร้อม log
else มีกุญแจ
H->>WB: คืนค่าปกติ
WB->>WB: ปิดผนึกเฉพาะสมาชิก data ด้วย ECDH-ES+A256KW สมาชิกอื่นเช่น meta ไม่แตะ
WB-->>FE: envelope ที่ data เป็น JWE ก้อนเดียว
end
ติดตั้ง 4 ขั้น
1. DI
// ต้องมีบรรทัดของ B อยู่ก่อน — D ใช้ cipher กับกุญแจของตัวนี้
services.AddPayloadFieldDecryption(configuration);
// ตัว D เอง: อ่านแค่ flag กับ Mode ไม่ register cipher เพิ่ม
services.AddPayloadWholeBodyEncryption(configuration);
2. Pipeline
app.UseClientSignatureVerification();
app.UsePayloadFieldDecryption(); // B
app.UsePayloadWholeBodyEncryption(); // D — ต้องอยู่หลัง UseClientSignatureVerification()
ต้องอยู่หลัง UseClientSignatureVerification() ด้วยเหตุผลเดียวกับ B — ลายเซ็นครอบ ciphertext ตามที่มาบนสาย ถอดก่อนตรวจ = ลายเซ็นไม่ผ่านทุกอัน · ตำแหน่งเทียบกับ UsePayloadFieldDecryption() ไม่สำคัญ จะก่อนหรือหลังก็ได้ เพราะ action ของสองกลไกนี้ห้ามซ้อนกันอยู่แล้ว (บังคับตอน startup)
3. แปะ attribute บน action
using SupApp_util_lib.Abstractions.Security;
[HttpPost("submit")]
[EncryptedWholeBody] // ขาเข้า: body ที่รับมาเป็น {"data":"<JWE>"} ให้ถอดก่อน model binding
[EncryptedWholeBodyResponse] // ขาออก: เข้ารหัสสมาชิก data ของ response ก่อนส่งกลับ
public async Task<IActionResult> Submit(SubmitRequest req, CancellationToken ct) => ...
- สอง attribute แยกกันโดยตั้งใจ แปะตัวเดียวก็ได้ ขาไหนไม่แปะขานั้นไม่ถูกแตะ
- แปะได้ที่ method เท่านั้น (
AttributeTargets.Method) ไม่มีระดับ controller - handler ไม่ต้องแก้อะไรเลย DTO เดิม
[FromBody]เดิม Swagger schema เดิม - 🔴 ห้ามใช้คู่กับ
[EncryptedField]บน action เดียวกัน — ขาเข้า (DTO ของ body) หรือขาออก (response type ที่ประกาศไว้) มี[EncryptedField]แล้วยังแปะ whole-body ทับ = แอปไม่ boot (EncryptedFieldMapConfigurationException) - ⚠️ ขาออกของ D ไม่ได้อ่าน
[ProducesResponseType<T>]ต่างจาก B — มันอ่าน body จริงที่เขียนออกไปแล้วหา keydata⇒ ข้อบังคับเรื่องประกาศชนิดของ 2xx ในหัวข้อ B ไม่ใช้กับ D
4. config
ใส่ค่าปิดไว้ที่ base appsettings.json
"FieldEncryption": {
"Payload": {
// ของ B — D ต้องอาศัยตัวนี้ ต้องเป็น true ก่อนจะเปิด WholeBody ได้
"Enabled": false,
"WholeBody": {
// สวิตช์ของ D — false = ไม่แตะอะไรเลยแม้แปะ attribute ไว้ (ค่าตั้งต้น)
"Enabled": false,
// "LogOnly" (ค่าตั้งต้น) = ตรวจ + เขียน log แต่ไม่ปฏิเสธ ปล่อย body เดิมผ่านไป
// "Enforce" = ถอดไม่ได้ตอบ 400 · เข้ารหัสขาออกไม่ได้ตอบ 500 ไม่ยอมส่ง plaintext
"Mode": "LogOnly"
},
// เพดาน body ของ request ที่ middleware ยอมอ่านเข้า memory — ใช้ร่วมกับ B
// ไม่ตั้ง = 262144 (256 KB) · เกินแล้วตอบ 413 PayloadTooLarge
"MaxRequestBodySizeBytes": 262144
}
}
เช็คว่า D ทำงาน
| เช็ค | ผลที่ถูก |
|---|---|
WholeBody:Enabled=false · action แปะ attribute ไว้แล้ว | ไม่ถูกแตะเลย ทั้งขาเข้าขาออก |
| flag เปิด · action ไม่มี attribute | ไม่ถูกแตะ |
flag เปิด · LogOnly · ส่ง plaintext เข้า action ที่แปะไว้ | ผ่านเข้า handler ตามเดิม + มี log [PayloadWholeBodyDecryption] |
flag เปิด · Enforce · ส่ง plaintext หรือ envelope ผิดรูป | 400 PayloadDecryptionError |
flag เปิด · Enforce · ถอด JWE ไม่ออก | 400 PayloadDecryptionError |
flag เปิด · Enforce · ถอดออกแล้วไม่ใช่ JSON object | 400 PayloadDecryptionError |
body ใหญ่กว่า MaxRequestBodySizeBytes | 413 PayloadTooLarge |
ขาออก · Enforce · caller ยังไม่ลงทะเบียนกุญแจเข้ารหัส | 500 PayloadEncryptionError และ handler ไม่ถูกเรียกเลย (ไม่มีอะไร commit ค้าง) |
ขาออก · LogOnly · caller ยังไม่ลงทะเบียนกุญแจ | ตอบ plaintext ตามเดิม + log |
WholeBody:Enabled=true แต่ Payload:Enabled=false | แอปไม่ boot |
action เดียวมีทั้ง whole-body และ [EncryptedField] · มี flag ตัวใดตัวหนึ่งเปิด | แอปไม่ boot |
action เดียวมีทั้ง whole-body และ [EncryptedField] · ปิด flag ทั้งหมด | boot ผ่าน ไม่มีอะไรเตือน — ตัวตรวจติดตั้งเมื่อ flag เปิดเท่านั้น 📕 กับดัก D |
อยากตรวจ attribute ให้ครบโดยไม่ต้องเปิด B — เปิด RequestLog:Enabled=true บนเครื่องตัวเอง (ไม่ต้องตั้ง ServiceBus · Coverage ปล่อย OptIn ก็ได้) convention จะถูกติดตั้งและปฏิเสธทันทีตอน boot
📕 กับดัก D — ลืม
AddPayloadWholeBodyEncryptionแต่ใส่Use…·EnableBuffering· ขาออกต้องมีAddClientSignatureStores· ตัวตรวจติดตั้งเมื่อไหร่