Private Docs

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 จริงที่เขียนออกไปแล้วหา key data ⇒ ข้อบังคับเรื่องประกาศชนิดของ 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 object400 PayloadDecryptionError
body ใหญ่กว่า MaxRequestBodySizeBytes413 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 · ตัวตรวจติดตั้งเมื่อไหร่