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

ช่วงต้นปีที่ผ่านมามีข่าวครึกโครมอันหนึ่งที่บริษัทชื่อดัง ทำ 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 ของระบบเราแล้ว วิธีที่อธิบายมาในบทความนี้จึงน่าจะเป็นทางเลือกที่ค่อนข้างเหมาะสม

กู้ 2 factor authentication

ผมใช้ 2 factor authentication (2fa) มาได้สักสามสี่ปีแล้ว เนื่องจากวันนึงอีเมลผมโดนเด้งมาว่ามีการเข้าใช้จากประเทศแปลกๆ ผมก็รู้สึกไม่ปลอดภัย แต่ผมคิดรหัสผ่านใหม่เจ๋งๆ ไม่ออก กลัวจะลืมด้วย ก็เลยว่างั้นเปิด 2 factor เลยดีกว่า (ตอนนี้รหัสผมก็ใช้ password manager จัดการอีกที ก็ปลอดภัยขึ้นจากการเดารหัสแต่ถ้าไฟล์รหัสผมหลุดไปนี่ก็อีกเรื่องหนึ่ง ก็เก็บไฟล์ให้ดีๆ และตั้ง master password ที่ปลอดภัย)

ทีนี้วันนี้อยาก wipe rom ครับ ก็เลยกด wipe ไปโต้งๆ ด้วยความที่คิดว่า Authy มันมี backup ขึ้น cloud ให้อยู่ไม่ต้องกลัวอะไร ปรากฏว่า มัน backup ไม่ครบ และเป็นครั้งแรกที่ผมเจอว่ามันทำแบบนี้ (ครั้งก่อนผมได้คืนมาครบ) ก็เลยถึงคราวซวยที่ต้องมานั่งลิสต์แล้วครับว่าต้องทำอะไรบ้าง

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

– **Dropbox** อันนี้ไม่มีปัญหา ใช้ SMS รับได้ แต่ตอนนั้นผมเข้าค้างไว้อยู่แล้ว รู้สึกน่ากลัวมากๆ ตรงที่มันเปลี่ยนการตั้งค่า 2 Factor ได้โดยไม่ต้องกรอกรหัสซ้ำ
– **GitHub** ใช้ SMS รับรหัสได้แล้วก็เข้าไปเปลี่ยน (GitHub ใช้ sudo mode จะไม่ถามรหัสซ้ำในช่วงเวลาหนึ่ง ถึงจะตั้งให้ login ค้างไว้ก็อาจจะถามซ้ำได้)
– **Google** ใช้ SMS รับได้ ปัญหาคือ Android first boot รับ SMS ไม่ได้ ก็จะมีตัวเลือกคือให้โทรมาแทน โทรมาเป็นเบอร์ 081 เป็นภาษาอังกฤษและคุณภาพเสียงห่วยมาก

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

– **Facebook** ใช้ SMS รับรหัสได้ แต่ผมรู้สึกแปลกๆ ตรงที่ถ้า sign in ได้แล้วบนมือถือ มือถือตัวนั้นจะกลายเป็น code generator ไปเลยทันที ซึ่งผมว่าไม่ปลอดภัยตรงที่ถ้าผมให้อุปกรณ์หนึ่งๆ เป็น 2FA ผมถือว่าผม trust อุปกรณ์นั้นสุดๆ แต่เครื่องชาวบ้านผม trust แค่ครึ่งนึง ถ้าเกิดมันดักรหัสผมก็ไม่ได้ OTP ไป นี่กลายเป็นว่ามันจะได้ OTP ไปด้วยก็ไม่ใช่
– **Amazon Web Service** ใช้ SMS ไม่ได้เลย มีแต่ทิ้งเบอร์ไว้ซึ่งสิบนาทีต่อมา Amazon โทรหาผม (เป็นภาษาอังกฤษ) ถามอีเมล, security question และเมลมาฉบับหนึ่งมีรหัสให้ผมอ่านให้ฟัง แล้วก็ปลดออกให้
– **DigitalOcean** ใช้ SMS เข้าได้ แต่ถ้าใช้ SMS เข้าแล้วจะปลด 2FA ถาวร ต้องเข้าไปตั้งค่าใหม่อย่างเดียว

(นี่ถ้าสักสองปีก่อนงานเข้าโคตรๆ เลยครับเพราะ TOT3G ไม่มีบริการไหนรองรับเลย ขอ OTP ผ่าน SMS ไม่ได้)

อื่นๆ ที่เคยเจอมาคือ Blizzard (battle.net) ถาม serial เกมในไอดี ซึ่งผมไม่มีเกมในไอดีนั้น ก็เลยบาย ปิดทิ้งสมัครใหม่ง่ายกว่า

สรุปแล้วใช้ 2 factor ไม่ค่อยจะมีปัญหาตรงทำแอพพังเท่าไรครับ เพราะใช้ SMS ได้หมด ก็จะมีแค่ Amazon เท่านั้นแหละที่เป็นปัญหา

ปล. service หลายๆ ตัวจะมี backup code นะครับที่ใช้แทน 2FA แต่ผมไม่ได้ print มาเพราะไม่รู้จะเก็บไว้ที่ไหนให้ปลอดภัย คือไม่อยากไว้โต๊ะคอมมันเหมือนเขียนรหัสผ่านแปะหน้าจอ