21 คะแนน โดย GN⁺ 2025-09-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • UUIDv47 ช่วยให้สามารถเก็บ UUIDv7 ที่เรียงลำดับได้ ไว้ในฐานข้อมูล ขณะเดียวกันก็ให้ค่า ที่ดูเหมือน UUIDv4 ผ่าน API ภายนอก
  • ทำการ XOR masking เฉพาะ ฟิลด์ timestamp เพื่อปกป้อง ข้อมูลเวลาใน UUIDv7 โดยคงฟิลด์สุ่มอื่น ๆ ไว้ตามเดิม
  • ใช้ SipHash-2-4 กับ คีย์ 128 บิต ในการ masking ทำให้ ปกป้องข้อมูลได้อย่างปลอดภัย โดยไม่เสี่ยงต่อการเปิดเผยคีย์
  • การ encode/decode เป็นแบบ กำหนดแน่นอนและย้อนกลับได้ และยังคงความสุ่มไว้ ทำให้ ความเสี่ยงการชนกันต่ำ
  • ผลการ benchmark แสดงให้เห็นถึงประสิทธิภาพที่รวดเร็วมากและวิธีการรวมระบบที่เรียบง่าย พร้อม เชื่อมต่อกับฐานข้อมูลอย่าง PostgreSQL ได้ง่าย

ภาพรวมและความสำคัญของโครงการ

  • UUIDv47 เป็นไลบรารี C แบบโอเพนซอร์สที่ทำให้ภายในฐานข้อมูลสามารถเก็บ UUIDv7 ซึ่งเหมาะกับการเรียงลำดับและทำดัชนี ได้ ขณะเดียวกันก็เปิดเผยค่า ที่ดูเหมือน UUIDv4 ให้กับ API และระบบภายนอก เพื่อให้บรรลุทั้ง การปกป้องข้อมูลส่วนบุคคล และ การประมวลผลประสิทธิภาพสูง ไปพร้อมกัน
  • เมื่อเทียบกับอัลกอริทึมแปลง UUID อื่น ๆ จุดเด่นที่แตกต่างคือ การแมปที่ย้อนกลับได้, ความเข้ากันได้กับ RFC, ความปลอดภัยแบบกู้คืนคีย์ไม่ได้, zero-deps และโครงสร้างแบบรวมเพียง header file เดียว

คุณสมบัติหลัก

  • Header-only C (C89) รวมเข้ากับระบบได้ง่ายโดย ไม่ต้องพึ่งพาไลบรารีภายนอก
  • ทำการ XOR masking เฉพาะฟิลด์ timestamp ของ UUIDv7 เพื่อ ป้องกันการเปิดเผยข้อมูลเวลา และ ไม่เปลี่ยนแปลง ฟิลด์สุ่มส่วนที่เหลือ
  • ใช้ keyed SipHash-2-4 สำหรับ masking จึงสามารถปกป้องข้อมูลได้อย่างปลอดภัยด้วย คีย์ 128 บิต
  • กระบวนการ encode/decode เป็นแบบกำหนดแน่นอนและย้อนกลับได้อย่างสมบูรณ์ (กู้คืนค่าเดิมได้อย่างถูกต้อง)
  • รองรับการแมประหว่าง UUID สำหรับ เก็บในฐานข้อมูล (v7) และ เปิดเผยภายนอก (v4) ได้อย่างรวดเร็ว
  • มีตัวอย่างครบถ้วน เช่น โค้ดทดสอบและเครื่องมือ benchmark

วัตถุประสงค์การใช้งานและข้อดี

  • ใช้งาน UUIDv7 ที่เรียงลำดับได้ เพื่อเพิ่ม index locality และ ประสิทธิภาพการแบ่งหน้า ภายใน DB ให้สูงสุด
  • ภายนอกจะเห็นเพียง รูปแบบที่ดูเหมือน UUIDv4 จึงช่วย ป้องกันการรั่วไหลของ timestamp และการติดตาม
  • ใช้ SipHash ทำให้ไม่สามารถกู้คืนคีย์ได้ และช่วยรับประกันความปลอดภัยของคีย์ลับ
  • จัดการบิตเวอร์ชัน/วาเรียนต์ให้เข้ากันได้กับ RFC
  • ทำงานได้รวดเร็ว จึงมีประสิทธิภาพแม้ใน งานประมวลผลแบบเรียลไทม์และสภาพแวดล้อมที่มีการสร้างจำนวนมาก

โครงสร้างหลักและหลักการทำงานภายใน

UUIDv7 Layout

  • ts_ms_be: timestamp แบบ big-endian 48 บิต
  • ver: high nibble ของไบต์ที่ 6 (0x7=DB, 0x4=ภายนอก)
  • rand_a: ค่าสุ่ม 12 บิต
  • var: RFC variant (0b10)
  • rand_b: ค่าสุ่ม 62 บิต

ตรรกะการ masking และการแมป (Façade mapping)

  • การเข้ารหัส: ts48 XOR mask48(R), ตั้งค่า version=4
  • การถอดรหัส: encTS XOR mask48(R), ตั้งค่า version=7
  • ไม่มีการเปลี่ยนแปลงฟิลด์สุ่ม
  • ใช้ฟิลด์สุ่ม 10 ไบต์เป็นอินพุตของ SipHash
  • การ XOR masking สามารถย้อนกลับได้ทันทีหากรู้คีย์

โมเดลความปลอดภัย

  • เป้าหมาย: แม้ผู้โจมตีจะเลือกอินพุตได้ ก็ต้องไม่สามารถเปิดเผยคีย์ได้
  • การใช้งานจริง: ใช้ SipHash-2-4 ซึ่งเป็น keyed pseudorandom function (PRF)
  • ใช้คีย์ 128 บิต และแนะนำให้ทำ key derivation ผ่าน HKDF เป็นต้น
  • เมื่อต้องหมุนเวียนคีย์ แนะนำว่าไม่ควรเก็บไว้ใน UUID ภายใน แต่ให้แยกเก็บ key ID ขนาดเล็กต่างหาก

Public API (C)

  • uuidv47_encode_v4facade : แปลง v7→v4
  • uuidv47_decode_v4facade : กู้คืน v4→v7
  • มีฟังก์ชันอื่น ๆ สำหรับการตั้งค่าเวอร์ชัน การ parse และการ format

ประสิทธิภาพและ benchmark

  • ในการคำนวณ SipHash masking (10B) ใช้เวลา ต่ำกว่า 14ns/op และรอบการทำงาน encode+decode แบบ round trip ทั้งหมดอยู่ที่ 33ns/op (อ้างอิง Apple M1)
  • รับประกันการประมวลผลที่รวดเร็วแม้ในงานสร้างและแมป UUID ปริมาณมาก
  • ได้ประสิทธิภาพสูงสุดเมื่อใช้ตัวเลือก -O3 -march=native

การรวมระบบและการขยาย

  • แนะนำให้ทำ encode/decode ที่ขอบเขตของ API
  • หากต้องการเชื่อมกับ PostgreSQL ให้เขียน C extension
  • เมื่อต้องทำ sharding สามารถ hash v4 façade ด้วย xxh3, SipHash เป็นต้น

อื่น ๆ

  • มีพอร์ตสำหรับภาษาอื่น เช่น Go (n2p5/uuid47)
  • แฮชที่แนะนำ: xxHash ไม่ใช่ PRF จึงอาจมีความเสี่ยงด้านการรั่วไหลของข้อมูล แนะนำให้ใช้ SipHash

ใบอนุญาต

  • ใบอนุญาต MIT (Stateless Limited, 2025)

1 ความคิดเห็น

 
GN⁺ 2025-09-19
ความคิดเห็นจาก Hacker News
  • สวัสดี ผมเป็นผู้เขียน uuidv47 เอง แนวคิดพื้นฐานคือ ภายในใช้ UUIDv7 เพื่อให้ได้ประโยชน์ด้านการทำดัชนีและการเรียงลำดับในฐานข้อมูล แต่ภายนอกจะแสดงค่าให้ดูเหมือน UUIDv4 เพื่อไม่ให้รูปแบบด้านเวลาเปิดเผยต่อไคลเอนต์
    วิธีทำงานคือเอา timestamp 48 บิตไป XOR mask กับสตรีม SipHash-2-4 ที่ได้มาจาก random field ของ UUID
    บิตสุ่มจะคงเดิม เวอร์ชันจะเป็น 7 ภายใน และสลับเป็น 4 ภายนอก โดยค่า RFC variant ก็ยังคงเดิม
    การแมปเป็นแบบ injective: โครงสร้างคือ (ts, rand) → (encTS, rand)
    การถอดรหัสใช้ encTS ⊕ mask จึงแปลงไปกลับได้สมบูรณ์
    ในแง่ความปลอดภัย SipHash เป็น PRF ดังนั้นถึงจะเห็นค่าที่แพ็กออกมาจากภายนอกก็ไม่ทำให้คีย์รั่ว
    ถ้าใช้คีย์ผิด timestamp ที่ได้ก็จะเพี้ยนไปทั้งหมด
    รองรับ key rotation ได้ด้วยการจัดการ key-ID ภายนอก
    ด้านประสิทธิภาพมี overhead ระดับนาโนวินาที เพราะเรียก SipHash แค่ครั้งเดียวต่อ 10 ไบต์ พร้อม load/store 48 บิตอีกไม่กี่ครั้ง เป็น C11 header-only ไม่มี dependency ภายนอก และไม่ต้อง allocate
    การทดสอบมีทั้ง SipHash reference vector, การ encode/decode แบบไปกลับ, และการทดสอบความคงตัวของ version/variant
    อยากฟังความคิดเห็นครับ

    • ชอบไอเดียนี้
      UUID มักถูกสร้างจากฝั่งไคลเอนต์ด้วย แต่วิธีนี้ดูเหมือนจะทำแบบนั้นไม่ได้
      หรือว่าถ้ารับ UUID ที่ไคลเอนต์สร้างมาแล้วคืนค่าเวอร์ชันที่ mask แล้วกลับไป จะเกิดช่องโหว่ได้ไหม เพราะมีคนอาจส่ง UUID สองตัวที่ ts ต่างกันแต่ rand เหมือนกันมาได้?
      สรุปแล้ววิธีนี้เหมาะเฉพาะกรณีที่ระบบเป็นฝ่ายสร้าง UUIDv7 เองเท่านั้นหรือเปล่า

    • มีความเห็นอยู่สองข้อ

      1. วิธีนี้ทำให้คนอื่นเอาประโยชน์จากคุณค่าของ UUID v7 ไปใช้ต่อไม่ได้ จึงน่าเสียดายสำหรับคนที่ใช้ API
      2. ถ้า API ภายนอกกับรูปแบบเก็บข้อมูลภายในต่างกัน ก็ต้องผ่านขั้นตอนแปลงแบบนี้ตลอด ทำให้การดูแลยุ่งขึ้นเล็กน้อย
        ไม่แน่ใจว่าความยุ่งยากระดับนี้คุ้มค่าพอหรือไม่
    • ความกังวลใหญ่สุดคือคุณภาพ entropy ของบิตสุ่ม
      UUIDv7 ให้ความสำคัญกับการหลีกเลี่ยงการชนกันมากกว่าเรื่องคาดเดาได้ยาก จึงเน้นความเป็นไปได้ของการชนมากกว่าความคาดเดาได้
      เพราะงั้นใน RFC จึงระบุเรื่องความไม่สุ่มแบบบังคับเป็นเพียง should ไม่ใช่ must และมี implementation ที่ใช้ PRNG อ่อน ๆ หรือใช้ตัวนับ หรือแม้แต่เอาข้อมูลนาฬิกาเพิ่มไปใส่แทนบิตสุ่มด้วยซ้ำ (อ้างอิง: RFC9562 s6.2 & s6.9)
      ดังนั้นการเอา rand_a, rand_b ของ v7 ไปใช้เป็น seed ของ PRF โดยตรง ถ้าข้อมูลนั้นมาจากนอกขอบเขตความเชื่อถือ ก็อาจเสี่ยงกว่าที่คิด
      แม้แต่ uuidv7() ใหม่ใน PostgreSQL 18 ก็เติม rand_a ทั้งหมดจาก high-precision timestamp ซึ่งตาม RFC ก็ยังถือว่าไม่ผิด
      ถ้าดู UUID ที่สร้างจากการ import จำนวนมาก สุดท้ายวิธี v7-to-v4 นี้ก็ยังอาจถูกจัดกลุ่มได้ จึงมีข้อมูลรั่วได้
      ถ้าเป็นข้อมูลอย่างการเก็บ telemetry ของชิ้นส่วนเครื่องยนต์ก็คงไม่เป็นไร แต่ถ้าเป็นข้อมูลตัวระบุที่ผูกกับคนโดยตรงต้องระวัง
      สรุปคือ ถ้าไม่ได้รับประกัน entropy ที่เชื่อถือได้ด้วยตัวเอง สคีมนี้ก็ยังอาจรั่วข้อมูลด้านเวลา ลำดับ หรือความสัมพันธ์ได้ ดังนั้นต้องตรวจดูซอร์สของ implementation v7 โดยตรงเสมอ

    • คิดว่าเป็นไอเดียที่ไม่ค่อยดี
      ใน PostgreSQL 18 พารามิเตอร์เสริม shift สามารถเลื่อน timestamp ตามช่วงที่กำหนดได้
      https://www.postgresql.org/docs/18/functions-uuid.html

  • เมื่อหลายปีก่อนผมเคยทำสคีมของตัวเอง โดยใช้เลข ID แบบเพิ่มขึ้นตามลำดับใน DB และเปิดเผยภายนอกเป็นสตริงสุ่มสั้น ๆ ยาว 4–20 ตัวอักษร
    ตอนนั้นใช้ custom instance ของตระกูลรหัสลับ Speck ซึ่งผมคิดว่ามันทนทานและค่อนข้างดูดี
    ทำเสร็จแล้วแต่โปรเจ็กต์ที่จะเอาไปใช้ถูกเลื่อนออกไป เลยยังไม่ได้เผยแพร่
    ปีนี้หรือปีหน้าวางแผนจะเปิดเอกสารพวกนั้นอย่างเป็นทางการ
    มีโน้ตที่สรุปวิธีทำ ข้อดีข้อเสียไว้ค่อนข้างดี ถ้าสนใจก็ดูได้
    https://temp.chrismorgan.info/2025-09-17-tesid/

    • ผมเองก็เคยพยายามใช้ Speck เพื่อทำให้ bigserial PKID อ่านยากขึ้น แต่ implementation แบบข้ามแพลตฟอร์มมีน้อย โดยเฉพาะใน pgcrypto รองรับไม่ค่อยดี เลยเลือกใช้
      base58(AES_K1(id{8} || HMAC_K2(id{8})[0..7]))
      ผลลัพธ์จะยาวขึ้นหน่อย ปกติประมาณ 22 ตัวอักษร แต่ทำได้แทบทุกสภาพแวดล้อมและประสิทธิภาพก็น่าพอใจมาก

    • เป็นไอเดียที่ดี
      ในแนวคิดคล้ายกัน sqids (ชื่อเดิม: hashids) ก็น่าดูเหมือนกัน
      https://sqids.org/

  • ผมเคยมีประสบการณ์คล้าย ๆ กัน เมื่อก่อนใช้สองคอลัมน์ คือ uuid สำหรับเผยแพร่ กับ bigint PK ที่ไม่เปิดออกทาง API (เป็นเรื่องก่อน uuidv7 จะออกมานานแล้ว)
    ความสะดวกของ uuid อาจน้อยลงหน่อย แต่ข้อดีคือถ้าดึงเฉพาะ PK ออกมาได้ ก็รวม DB dump จากคนละฐานได้ง่าย
    ต่อให้ lookup ด้วย hash สุดท้ายก็น่าจะยังต้องใช้สองคอลัมน์อยู่ดี แต่อาจเป็นเพราะผมเข้าใจกลไกของ hash ผิดก็ได้

    • การแปลงกลับทำได้ด้วยคีย์เข้ารหัสลับ
      ใน request สามารถเปลี่ยนค่า uuidv4 ให้กลับเป็น uuidv7 ในฐานข้อมูลได้
  • ตัวไอเดียน่าสนใจ แต่ก็หวังว่าฐานข้อมูลจะรองรับสิ่งนี้โดยตรง
    คือสามารถแปลง UUIDv7 เป็น “UUIDv4” และย้อนกลับได้ และใน query ก็ควรแยกใช้สองฟอร์แมตนี้อย่างชัดเจนได้ด้วย

  • เป็นโปรเจ็กต์ที่เจ๋งมาก
    ผมลองทำ implementation ภาษา Go โดยใช้ไลบรารี siphash ของ dchest
    https://github.com/n2p5/uuid47
    ดูเพิ่ม: https://github.com/dchest/siphash

  • โปรเจ็กต์น่าสนใจ แต่อยากเห็นตัวอย่างจริงว่าการเปิดเผยส่วน time ใน uuid v7 มีความเสี่ยงอย่างไร

    • อาจทำให้รูปแบบพฤติกรรมหรือ sequence ของผู้ใช้ถูกเปิดเผย จนเกิดสถานการณ์ไม่พึงประสงค์ได้

      • “อดีตสามี: ดูจาก userID ในเว็บหาคู่ของคุณ คุณสมัครตอนงานปาร์ตี้ของทอมแน่เลยใช่ไหม?”
      • “คุณบอกว่า timezone คือ XYZ แต่ log ของ imageID (ที่มีเวลาสร้างอยู่) ดูเหมือนจะออกมาตอนตี 3 ตลอดเลยนะ?”
        สำหรับข้อความรายบุคคลหรือธุรกรรมแบบเรียลไทม์อาจไม่สำคัญ แต่ถ้าเป็นการสร้างบัญชีผู้ใช้หรือข้อมูลระยะยาว ใครสักคนอาจเอาไปใช้ติดตามตัวตนได้
    • ผมเคย brute force คีย์ AES จากบางส่วนของ UUID ใน CTF มาก่อน
      เพราะคีย์ถูก derive มาจาก time source บางส่วน ถ้ารู้ system time ตอนสร้างคีย์ก็โจมตีได้
      อีกตัวอย่างง่าย ๆ คือบริการแชร์ไฟล์ที่เปิดเผยแค่โครงสร้าง เว็บไซต์.com/GUID โดยไม่ได้บอกเวลาอัปโหลดไฟล์แยกต่างหาก
      ถ้าใช้ UUIDv7 ก็สามารถประมาณเวลาอัปโหลดไฟล์ได้จากมันโดยตรง
      มันอาจไม่ใช่ภัยความปลอดภัยใหญ่โตเสมอไป แต่ก็เป็นการรั่วไหลของข้อมูลโดยไม่ได้ตั้งใจ

    • ลองนึกถึงระบบที่เก็บข้อมูลทางการแพทย์
      สมมติว่ามีการอัปโหลดผลทันทีหลังถ่าย MRI เพื่อใช้วิเคราะห์ และมีการลบข้อมูลส่วนบุคคลออกแล้ว
      แต่เพราะ timestamp ของ uuidv7 ยังอยู่ จึงอาจทำ correlation analysis จากภายนอกได้ เช่น “วันนั้นมีคนถ่าย MRI แค่คนเดียว งั้นก็รู้ได้เลยว่า MRI นี้เป็นของใคร”

  • สิ่งที่น่าหงุดหงิดที่สุดของ uuidv7 คือเวลาอยู่ในลิสต์แล้วคนดูด้วยตาเปล่าเพื่อเทียบความต่าง (diff) ได้ยากมาก
    ถ้าใน psql มี visual layer ที่เอาบิตสุ่มมาไว้ข้างหน้า แต่ยังคงการเรียงจริงตาม timestamp ไว้ได้ น่าจะช่วย UX ได้มาก

    • ผมแค่ฝึกนิสัยให้ดูเฉพาะท้าย UUID

    • สร้างฟังก์ชันเองแล้วใช้ใน query ได้เลย
      เช่น แปลงเป็น hex representation แล้วกลับสตริง หรือไม่ก็แสดงผลเป็น reversed base64 ก็จะสั้นและแยกความต่างได้ง่ายขึ้น

  • วิธีนี้ดูดีมาก
    แต่การตื่นตระหนกเกินเหตุเรื่อง timestamp รั่ว และการอ้างว่าการเปิดเผย sequential ID เท่ากับเปิดช่องโจมตีหรือเปิดเผยข้อมูลธุรกิจ ดูจะใกล้เคียงกับความกังวลเกินจำเป็นมากกว่าจะเป็นปัญหาความปลอดภัยจริง
    แค่บวกเลขสุ่มก้อนใหญ่เข้าไปในค่า int เป็นระยะ ๆ ก็ยังรักษาคุณสมบัติ monotonic ได้อยู่ ขณะเดียวกันก็ทำให้คนภายนอกมองรูปแบบได้ยากขึ้น
    ท้ายที่สุดผมรู้สึกว่ามันมีส่วนของการทำให้เรื่องใหญ่เกินจริงอยู่เหมือนกัน เหมือนทำเป็นกังวลเรื่องข้อมูลรั่วที่สำคัญมาก

    • สิ่งที่รั่วตรงนี้ไม่ใช่ข้อมูลธุรกิจ แต่เป็นข้อมูลของไคลเอนต์
      ตัวข้อมูลที่ระบบปล่อยออกมาอาจไม่มีความหมายมากนัก แต่ถ้าดูจำนวนมากหรือดูตามลำดับเวลา ก็อาจอนุมานข้อมูลเพิ่มได้
      ตัวอย่างเช่น SpiegelMining talk ของ David Kriesel แค่ scrape วันที่ของบทความกับชื่อผู้เขียน ก็สกัดรูปแบบได้แล้วว่าใครไปพักร้อนเมื่อไร
      ถ้าเอาข้อมูลของผู้เขียนหลายคนมาเทียบกันไปเรื่อย ๆ เรื่องอย่างความสัมพันธ์เชิงชู้สาวในที่ทำงานก็อาจถูกเปิดโปงได้หมด
  • ทำไมไม่ใช้คีย์เข้ารหัสต่างกันในแต่ละ session แล้วเปิดเผยแค่ id ที่เข้ารหัสออกไปล่ะ
    ถ้าเป็นแบบนั้น DB ก็ใช้แค่ sequential id ธรรมดาได้ไม่ใช่หรือ

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

    • ผมก็ไม่รู้คำตอบเหมือนกัน แต่ถ้า entropy สูงพอ มันก็อาจมองได้คล้าย PRNG ที่มี seed
      เป้าหมายของวิธีนี้คือทำให้ภายนอกดูเหมือนสุ่มอยู่แล้ว ดังนั้น v8 อาจกลับยิ่งสะดุดตากว่าก็ได้