เก็บไฟล์บัตรประชาชนยังไงดี?

ช่วงต้นปีที่ผ่านมามีข่าวครึกโครมอันหนึ่งที่บริษัทชื่อดัง ทำ bucket ที่เก็บบัตรประชาชนลูกค้าเป็นสาธารณะ และมีผู้ไปพบเข้า

หลายเดือนต่อมาผมก็ไม่คิดว่าที่ TipMe ก็จะต้องเก็บรูปบัตรประชาชนไว้เช่นกัน ก็เลยคิดว่าจะต้องเก็บให้ปลอดภัยแบบที่อ่านแล้วรู้สึกว่าแน่นหนา

Threat Model

ก่อนจะทำอะไรก็ต้องนึกถึง threat model ก่อน เพราะถ้า regulation ต้องการว่าไฟล์ถูก encrypt นั้น เนื่องจากเราใช้ Google Cloud Storage ก็จะทำ Encryption ให้อยู่แล้วโดยไม่สามารถปิดได้ (ต่างจาก AWS ที่ค่า default คือปิด)

แต่ข้อกังวลสำหรับผมไม่ใช่ regulation เพราะเรายังไม่มีใครมาตรวจส่วนนี้ สำหรับเคสนี้ผมคิดว่าปัญหาจะอยู่ที่

  1. เผลอทำ bucket หลุดไป ข้อมูลไม่ควรจะสามารถอ่านได้
  2. Application server ถูก hack แล้วใช้สิทธิ์ของ app มาอ่านได้ (เพราะ app มันต้องอ่านได้ด้วย ตอนที่ admin เข้าไปตรวจสอบ)
  3. เนื่องจากเราจะใช้ signed url มีความเสี่ยงที่ signed url จะหลุดไป
  4. admin แอบเซฟรูปไป อันนี้คิดว่าคงไม่น่าจะเป็นปัญหาในตอนนี้ เนื่องจากทีมเรามีจำกัด

Design

ปัญหาที่ผมค่อนข้างกังวลคือข้อ 2 เนื่องจากแอพเป็น monolithic จึงการันตีได้ว่าทั้งระบบปลอดภัยได้ยาก และไม่มีแผนที่จะแยกเป็น microservice ให้ยุ่งยากด้วย เพราะคนเขียนมีคนเดียวไม่จำเป็นจะต้องพยายามทำอะไรให้รองรับทีมขนาดใหญ่ได้ในตอนนี้

ลักษณะการใช้งานของข้อมูลบัตรประชาชนคือ

  1. User submit ข้อมูลมา
  2. Admin เข้าไปดูรูปและยึนยันว่าเป็นบัตรประชาชนที่ถูกต้อง อาจจะมีการอ่านข้อความที่ต้องใช้เก็บไว้
  3. หลังจากนั้นแล้วไม่มีใครควรจะดูได้อีก
  4. หากบริษัทถูกเรียกถาม ควรจะสามารถเรียกคืนมาให้ตรวจได้

จะเห็นว่าจากข้อ 3 แล้วทำให้เราจัดได้ว่าข้อมูลนี้เป็น cold storage หลังแอดมินยึนยันข้อมูลแล้ว

ระบบที่เรา Design ออกมาก็เลยเป็นดังนี้

(ขออภัยกับ UML ด้วย ไม่ค่อยชินกับ tool ที่ใช้วาดเท่าไร)

  1. พอ admin ยึนยันบัตรประชาชนแล้ว app จะ call ไปหา secure service เราเรียกมันว่า sealerd
  2. sealerd จะถือ Google Cloud service account token อีกชุดหนึ่ง แยกไปจากของ app ปกติ
  3. sealerd ทำดังต่อไปนี้
    1. Download file มาแล้ว encrypt ด้วย public key ของบริษัท
    2. แก้ไข ACL ของไฟล์นั้นใน Google Cloud Storage ให้สามารถเข้าได้เฉพาะ service account ของ sealerd (แม้แต่ผมก็เข้าไม่ได้ ต้อง assume role เป็น sealerd เท่านั้น)
  4. app จะ mark ว่าไฟล์เข้ารหัสแล้ว เพื่อประโยชน์ในการแสดงผล UI

พูดง่ายๆ ก็คือเรามี security 2 ชั้นคือ

  1. File level access control ทั้งหมดจะถูกล้างเสมอ
  2. ถ้ามีคนเอาไฟล์ไปได้ ไฟล์ก็จะถูกเข้ารหัสด้วย key ซึ่งกุญแจถอดรหัสเราเก็บไว้ offline เว็บเราไม่สามารถถอดรหัสได้

จาก threat model แล้วก็จะพบว่า

  1. ถ้า bucket เผลอเปิดเป็น public เราจะมี encryption ป้องกันไม่ให้อ่านไฟล์ได้
  2. สิทธิ์การใช้ของ application server จะถูกยกเลิกหลังไฟล์ถูก seal (ทั้งนี้การถอนสิทธิ์ใน Google Cloud Storage เป็น eventual consistent แอพอาจจะยังอ่านได้อยู่อีกประมาณ 1 นาที)
  3. เราตั้งให้ Signed URL มีวันหมดอายุอยู่แล้ว และเนื่องจากเรายกเลิกสิทธิ์ของ Application server หลัง seal แล้ว Signed URL ที่มีจึงใช้ไม่ได้

Implementation

คำถามถัดมาคือ แล้วจะ implement อย่างไร?

ภาษา

ภาษาแรกในหัวผมที่จะใช้เขียนคือ Rust แต่หลายๆ ครั้งที่พยายามจะใช้ผมพบว่า Rust มี library ไม่ค่อยดีพอเลยยังไม่กล้าเสี่ยง

ถัดมาคือ Python กับ JavaScript ที่ใช้อยู่แล้ว แต่เนื่องจาก encryption library มักเป็น synchronous และ blocking ด้วย ทั้ง Python และ JavaScript พวกนี้มี Global Lock ก็เลยไม่น่าจะเหมาะที่จะเอามาใช้ เพราะต้องลุ้นอีกทีว่า library ที่ใช้มันไปทำข้างนอก interpreter lock หรือไม่

อีกเหตุผลคือผมอยากจะใช้ Tink ที่ยังไม่รองรับภาษาเหล่านี้

สุดท้ายคือ Go ซึ่งผมคิดว่า Tink น่าจะรองรับ ก็เลยเลือกใช้ Go

RPC

ท่า RPC ก็เป็นเรื่องที่ต้องตัดสินใจเช่นกัน ตัวเลือกก็จะมี

  1. JSON on REST (HTTP) ง่ายๆ ใครๆ ก็ใช้
  2. gRPC
  3. Cap’nproto
  4. Flatbuffer

ส่วนตัวอยากลอง Cap’nproto/Flatbuffer แต่รู้สึกว่า RPC library มันยังไม่ค่อยดี ไว้โอกาสหน้าอาจจะ port ดูเล่นๆ

เหลือแค่ gRPC กับ REST ก็เลยเลือก gRPC

เหตุผลที่เลือกคือ

  1. มันมี type ชัดเจน ใน Go จะเขียนง่าย
  2. มันเร็วมาก ที่วงในผมเขียน integration test ให้ start gRPC server ใน python แล้วยิง test request เร็วอย่างกับยิงใน process เดียวกัน
  3. ไม่ต้องกังวลเรื่อง compatibility เพราะไม่มี clientside ยิงมาอยู่แล้ว เลยไม่จำเป็นจะต้องใช้ HTTP

Encryption

Project นี้รู้สึกว่าจะพลาดที่สุดก็ตรงนี้แหละ ผมเสียเวลาไป 3-4 วันในการ design implement ระบบที่ secure ที่สุดเท่าที่จะนึกออก พอเขียนจริงเลยไม่ได้ research ละเอียดว่าภาษาอะไรที่จะใช้ implement แล้วเหมาะสม

มารู้สึกตัวครั้งแรกคือตอนที่เขียน gRPC ให้ Go เสร็จแล้วกำลังจะเขียนส่วนที่เข้ารหัส ก็เจอกับ Surprise แรกคือ Tink ไม่มีใน Go… อ้าว!!

ก็เลยมาที่ plan B คือใช้ Sealed box เหมือนที่ผ่านๆ มา

Surprise ที่สองคือ Go มี nacl box ในตัวก็จริง แต่ไม่มี libsodium ทำให้ไม่มี sealed box…

สุดท้ายเลยต้องทำเหมือนใน JavaScript คือ implement nacl-sealed-box เสียเองเลย ซึ่งก็ไม่ยากเพราะ Go มี crypto primitive ให้หมดแล้ว แค่เอามาต่อกันให้เหมือนกับของ libsodium เป็นอันเรียบร้อย

Connect

เสร็จแล้วก็เขียนต่อจากฝั่งแอพ เนื่องจากเป็น gRPC ก็ทำให้การ implement ค่อนข้างง่าย แต่ตอน compile proto ก็อาจจะเหนื่อยหน่อย

สำหรับการ dev บนเครื่องเราก็ทำ version พิเศษที่ต่อกับ local filesystem และเพื่อความชัวร์จึงทำให้ option ในการเลือก driver นี้เป็น link time option จะได้ไม่พลาดบน production

สรุป

ในวงการ Cryptography มีกฎอยู่ข้อหนึ่งเรียกว่า Schneider’s Law บอกว่า

ใครๆ ก็สามารถคิดค้นระบบความปลอดภัยที่ที่เทพมากจนเค้าไม่สามารถคิดวิธีแหกได้
Any person can invent a security system so clever that she or he can’t think of how to break it

ผมว่าระบบอันนี้ค่อนข้าง fit กับ description นี้แล้ว คือผมคิดว่ามันแน่นหนาพอ และผมว่าถ้าไปเจอใครโม้เรื่องวิทยาการรหัสลับเสียดิบดีโดยไม่ได้มี disclaimer แบบนี้ ผมว่ามันน่ากลัวกว่านี้อีกเยอะ

จริงๆ แล้วคิดว่าในการใช้งานส่วนมากแล้วอาจจะแค่ download ไฟล์ที่อนุมัติแล้วเก็บเข้า offline cold storage ก็น่าจะปลอดภัยกว่าระบบนี้แล้ว แต่ก็จะ automate น้อยลง ความเสี่ยงในการสูญหายมากขึ้น ใน tradeoff ของระบบเราแล้ว วิธีที่อธิบายมาในบทความนี้จึงน่าจะเป็นทางเลือกที่ค่อนข้างเหมาะสม

Implementing libsodium Sealed Box

พอดีมี project ที่จะใช้ Sealed Box เลยได้นั่ง implement ให้กับภาษาที่ใช้งานอยู่ เลยอยากเอามาเล่าให้ฟังครับ

อะไรคือ libsodium

ถ้าเคยเขียนโปรแกรมที่ต้องใช้การเข้ารหัสข้อมูล น่าจะรู้จักกับ OpenSSL ครับ หรือถ้าใช้ PHP อาจจะเคยผ่านตากับ mcrypt ซึ่งสองตัวนี้เป็น library ที่รวบรวม algorithm เข้ารหัสไว้มากมายให้เราเลือกใช้

ปัญหาคือทางเลือกไม่ใช่เรื่องดีครับ เพราะกลายเป็นว่าโปรแกรมเมอร์จะต้องศึกษาดูก่อนว่า Algorithm ตัวไหนปลอดภัยบ้าง และแถมบางที library ก็ไม่ตรงปกซะเอง

นักวิทยาการรหัสลับ นำโดยคุณ Daniel J. Bernstein (djb) ผู้คิดค้น algorithm Ed25519/Poly1305/Salsa20 ที่กำลังมาแรงในช่วงนี้ ก็เลยคิดพัฒนาไลบรารีใหม่เข้ามาที่จะทำให้เข้ารหัสเป็นเรื่องง่าย และเร็ว มีชื่อว่า NaCl (อ่านว่า salt) ย่อมาจาก Networking and Cryptography Library (อ่านที่มาได้ใน Paper นี้ ครับ ไม่มีแมทอะไรมากอ่านสนุกดี)

นอกจากนี้เค้ายังพัฒนา TweetNaCl ซึ่งเป็นไลบรารีที่ใช้งานได้เหมือน NaCl แต่ขนาดเล็กเพียง 140 ตัว * 100 tweets เท่านั้น ทำให้สามารถอ่านโค้ดทั้งหมดเพื่อตรวจสอบได้ง่าย (ปัจจุบันมีการ port ไปหลายๆ ภาษาอีกด้วย เช่น Python, JavaScript) ทั้งนี้ความแตกต่างคือ TweetNaCl อาจจะทำงานช้ากว่า

ปัญหาคือ NaCl นั้นลงยากพอสมควร ทาง OpenDNS ก็เลยเอา NaCl ไปจัดระเบียบใหม่ให้นำไปใช้ได้ง่ายขึ้นอีก โดยเรียกไลบรารีใหม่นี้ว่า Sodium

สำหรับการใช้งานของ NaCl/Sodium นั้นจะเปลี่ยนคำถามใหม่จาก “ต้องการเข้ารหัสด้วย algorithm อะไร” เป็น “ต้องการความปลอดภัยแบบไหน” ตัวอย่างเช่น

  1. ถ้า Alice ต้องการส่งข้อความลับให้เฉพาะ Bob ก็ใช้ Box (authenticated encryption) ซึ่งเมื่อได้รับข้อความแล้ว Bob ยังสามารถยึนยันได้อีกด้วยว่า Alice เป็นผู้เขียนข้อความนี้จริง และข้อความไม่ถูกแก้ไขระหว่างทาง
  2. ถ้าต้องการส่งข้อความที่เข้ารหัสโดยใช้ password ก็ใช้ Secret box (authenticated encryption)
  3. ถ้าไม่ต้องการเข้ารหัสแต่ต้องการยึนยันว่าข้อความนี้เราเขียนจริง โดยใช้ระบบกุญแจสาธารณะ ก็ใช้ Sign (public-key signature)

จะเห็่นว่าเราไม่จำเป็นต้องทราบว่าด้านหลังมันทำงานยังไง ใช้ algorithm อะไรเลยด้วยซ้ำ อ่านรายละเอียดแต่ละ abstraction แล้วถ้าตรงกับโจทย์ก็เลือกใช้ได้ทันที

อะไรคือ Sealed Box

ทีนี้สิ่งที่ผมสนใจคือ Sealed box ครับ ซึ่งเป็นหนึ่งใน Security model ที่มีใน libsodium แต่มันพิเศษคือเป็นตัวที่ libsodium สร้างขึ้นมาเอง ไม่มีใน NaCl ปกติ

Sealed box จะเหมือนข้อ 1 ด้านบน คือใช้ส่งข้อความลับหาผู้รับที่เจาะจงและรู้ public key ของเค้า (เช่นส่งหาคุณ Bob) แต่ต่างกันคือเราไม่ต้องการยึนยันตัวผู้ส่งด้วย (ในตัวอย่างด้านบนคือ Alice ไม่ต้องการให้รู้) ซึ่งเราใช้ Box ปกติทำไม่ได้เพราะ Box จะต้องระบุ public key + secret key ของผู้รับและผู้ส่ง

ปัญหาคือพอมันเป็นสิ่งที่มีใน libsodium แล้ว ไลบรารีที่เอา NaCl ไปใช้ก็จะไม่ค่อยมี sealed box ให้ใช้ ซึ่งในหลายๆ ไลบรารีก็จะมี feature request เรื่องนี้อยู่

แกะ Sealed Box

วิธีแก้ปัญหาก็ไม่ยากครับ ไม่มีก็เขียนใช้เองซะสิ! แต่เราคงต้องแกะซอร์สซะก่อน

ซอร์ส libsodium จัดไว้ค่อนข้างง่ายครับ หาเจอไม่ยาก ไฟล์ที่เราต้องการคือ libsodium/src/libsodium/crypto_box/crypto_box_seal.c

พออ่านโค้ดใน crypto_box_seal แล้วก็จะพบว่าจริงๆ แล้ว Sealed box ก็คือ box ประเภทหนึ่งครับ ซึ่งจะสร้าง public และ private ของผู้ส่งขึ้นมาใหม่ทุกครั้ง แล้วก็ใช้ Box ปกติเข้ารหัสได้เลย จากนั้นเวลาส่งข้อมูลก็ให้ส่ง public key นำไปก่อน แล้วตามด้วย Box

ที่น่าสนใจคือ Nonce ครับ ใน docs ของ Box จะบอกว่าการส่งข้อความจะต้องมี Nonce ทุกครั้ง (ถ้าใครอ่านตำรา crypto มา อาจจะรู้จักกับ IV เจ้า Nonce นี่ก็คือ IV ล่ะครับ แต่ต่างกันคือ NaCl อนุญาตให้ใช้ string ใดๆ เป็น Nonce ได้ ขอแค่ห้ามใช้ซ้ำกัน ต่างกับ IV ตรงที่ IV จะต้องคาดเดาไม่ได้ด้วย) แต่การสร้าง Nonce ของ Sealed box น่าสนใจมากครับ คือแทนที่จะสุ่ม Nonce มา เราก็พบว่า Public key เราสุ่มใหม่ทุกรอบอยู่แล้ว ก็เลยเอา public key นั้น ต่อกับ public key ผู้รับ เข้า Hash function ก็จะได้ Nonce เลยที่ไม่ซ้ำแน่นอน และไม่จำเป็นต้องส่งไปด้วยเพราะผู้รับก็ทราบ public key ทั้งหมดอยู่แล้ว

อีกจุดนึงของ Nonce generation ที่น่าสนใจคือมันใช้ Hash function ใหม่ล่าสุดอย่าง BLAKE2 ครับ ข้อดีของเค้าคือสามารถกำหนดความยาวของแฮชเองได้ ไม่เหมือน SHA-256/512 ที่กำหนดความยาวไว้ชัดเจน แน่นอนว่าในเคสนี้เราก็กำหนดให้ยาวเท่ากับ Nonce ที่ Box ต้องการได้เลย

สุดท้ายแล้วผมก็พบว่าไม่ต้องอ่านโค้ดก็ได้ ใน docs เค้าก็เขียนไว้ชัดเจนแล้วว่ามันคือ ephemeral_pk ‖ box(m, recipient_pk, ephemeral_sk, nonce=blake2b(ephemeral_pk ‖ recipient_pk)) *facepalm* (‖ แปลว่าต่อ string)

Implement ใน JavaScript

ทีนี้ก็ได้เวลา implement ครับ ใน project ที่ผมจะทำ client side จะเข้ารหัสข้อความไปให้ server ซึ่งก็แน่นอนว่าฝั่ง client เราจะต้องใช้ JavaScript

ใน JavaScript ก็มีหลาย library ที่ใช้ libsodium ครับ แม้แต่ libsodium เองก็ยังมี official release ที่ใช้ Emscripten compile เป็น JavaScript ให้เลย แต่ปัญหาคือมันจะใหญ่มากและเปลือง memory ก็เลยตัดสินใจใช้ pure JavaScript implementation อย่าง TweetNaCl.js ซึ่งตัวนี้โดน Security audit มาแล้วด้วยซ้ำ และผลการ Audit คือ “ยอดเยี่ยม ไม่เจออะไรไม่ดีเลย” เป็นเรื่องที่หายากมากๆ

เวลาเขียนตัวนี้ก็ไม่ยากครับ JavaScript ยุคใหม่มี Fixed size array อย่าง Uint8Array แล้ว ก็ map จาก C ลงไปได้เลย แต่เขียนไปสักพักก็จะเจอปัญหาว่าไม่มี BLAKE2!

คือ NaCl ของแท้ๆ เนี่ยครับ hash function ที่เค้าเลือกใช้จะเป็น SHA-512 แต่ใน NaCl จะเปลี่ยนเป็น BLAKE2 ฉะนั้นก็ไม่มีทางเลือก เราต้องลง library ที่ทำ BLAKE2 ได้

ก็ปรากฏว่า BLAKE2 มีสองแบบครับ คือ BLAKE2s ที่มี library BLAKE2s.js และ BLAKE2b ที่มี library BLAKE.js ทำไว้ ตอนแรกก็เลือกผิดตัวไป เสียเวลาเขียนใหม่เล็กน้อย

(สำหรับความแตกต่างคือ BLAKE2s จะ optimize สำหรับ 32 bit ครับ และ BLAKE2b optimize สำหรับ 64 bit ผลลัพท์ไม่เหมือนกันใช้สลับกันไม่ได้นะครับ)

สุดท้ายก็ได้ Library ออกมาครับอย่าง tweetnacl-sealed-box ไปจิ้มจาก npm ได้เลย

Implement ใน Python

สำหรับฝั่ง server ผมใช้ Django ซึ่งสำหรับภาษา Python นั้นก็มี libsodium binding หลายตัวครับ เดิมทีผมใช้ PyNaCl อยู่ แต่อยากจะย้ายเป็นตัวอื่นที่มี wheel จะได้ build ไวๆ (มันเป็น Docker ครับ build นานเสียเวลา)

ตัวที่เลือกใช้ก็คือ libnacl ซึ่งเลือกจากว่ามันใช้ใน Salt ที่เป็น Server automation tool เจ้าดังตัวนึง (แต่เอาเข้าจริงเหมือนเค้าจะไม่ค่อย maintain libnacl เท่าไรนะ)

สำหรับ Implementation อันนี้ไม่ยากครับ เพราะ libnacl นั้นจะไปเรียก sodium.so ของแท้ๆ อยู่แล้ว เราก็แค่เขียน ctypes definition ให้มัน ก็จบ แต่ครั้นจะส่งแพทช์ไปแบบนี้เลยก็คงจะไม่ดีเท่าไร เลยต้องเขียน Abstraction ให้เค้าด้วย และ documentation ซึ่งก็ไม่รู้จะเขียนอะไรดี เลยก๊อปอีกหน้านึงมาแก้ซะเลย 555

สรุป

Sealed box เป็น design ที่น่าสนใจครับ คือเค้าพยายาม design ของใหม่ โดยใช้ของเดิมที่มีอยู่แล้ว เนื่องจากว่าของเดิมถูกพัฒนาโดยนักวิทยาการรหัสลับจึงมั่นใจได้ว่าปลอดภัย พอเป็นแบบนี้แล้วการจะ port ไปให้ภาษาอื่นๆ เลยค่อนข้างง่าย แต่ถ้า port ไป TweetNaCl ก็จะยุ่งยากนิดนึง

อ่านมาถึงตรงนี้ไม่รู้ว่าเจอศัพท์เทคนิคกันจนปิดไปหรือยัง แต่จะบอกว่า “ชนะโดยไม่ต้องรบจึงว่าเลิศ” นะครับ ถึง NaCl จะทำให้เราเข้ารหัสข้อมูลแบบปลอดภัยได้ง่ายๆ แต่ถ้าเราสามารถออกแบบโปรแกรมที่ไม่จำเป็นต้องเข้ารหัสเลยก็จะปลอดภัยกว่าแน่นอน