เช็คลิสต์การจัดเก็บอย่างปลอดภัยใน Kotlin สำหรับ tokens คีย์ และ PII
เช็คลิสต์การจัดเก็บอย่างปลอดภัยใน Kotlin เพื่อช่วยเลือกระหว่าง Android Keystore, EncryptedSharedPreferences และการเข้ารหัสฐานข้อมูลสำหรับ tokens คีย์ และ PII

สิ่งที่คุณพยายามปกป้อง (อธิบายง่าย ๆ)
การจัดเก็บอย่างปลอดภัยในแอปธุรกิจหมายถึงอย่างเดียว: ถ้ามีคนได้โทรศัพท์ (หรือไฟล์ของแอป) พวกเขายังไม่ควรอ่านหรือใช้ซ้ำสิ่งที่คุณบันทึกไว้ได้ นั่นรวมถึงข้อมูลที่พักอยู่บนดิสก์ และความลับที่รั่วผ่านการสำรองข้อมูล บันทึกข้อผิดพลาด (logs) รายงานการชน หรือเครื่องมือดีบัก
การทดสอบคิดง่าย ๆ: คนแปลกหน้าอาจทำอะไรได้บ้างถ้าเปิดโฟลเดอร์เก็บข้อมูลของแอปคุณ? ในหลายแอป ของมีค่าที่สุดไม่ใช่รูปหรือการตั้งค่า แต่เป็นสตริงสั้น ๆ ที่ปลดล็อกการเข้าถึง
การเก็บข้อมูลบนเครื่องมักจะมี session tokens (เพื่อให้ผู้ใช้คงสถานะล็อกอิน), refresh tokens, API keys, คีย์การเข้ารหัส, ข้อมูลส่วนบุคคล (PII) เช่น ชื่อและอีเมล, และข้อมูลธุรกิจที่แคชไว้เพื่อใช้งานแบบออฟไลน์ (คำสั่งซื้อ ตั๋ว หรือบันทึกลูกค้า)
ตัวอย่างความล้มเหลวในโลกจริง:
- อุปกรณ์สูญหายหรือถูกขโมยและโทเค็นถูกคัดลอกเพื่อแอบอ้างตัวผู้ใช้
- มัลแวร์หรือแอป "ผู้ช่วย" อ่านไฟล์ท้องถิ่นบนอุปกรณ์ที่รูทแล้วหรือใช้ช่องโหว่การเข้าถึง
- การสำรองข้อมูลอัตโนมัติย้ายข้อมูลแอปไปที่ที่คุณไม่ได้วางแผน
- บิลด์สำหรับดีบักบันทึกโทเค็น เขียนลงในรายงานการชน หรือลดการตรวจสอบความปลอดภัย
นั่นเป็นเหตุผลที่ "แค่เก็บใน SharedPreferences" ไม่พอสำหรับสิ่งที่ให้การเข้าถึง (tokens) หรือตีความว่าทำร้ายผู้ใช้และบริษัทได้ (PII). SharedPreferences ธรรมดาเหมือนการเขียนความลับลงบนโพสต์-อิทในแอป: สะดวก แต่ก็อ่านได้ง่ายหากมีคนเข้าถึง
จุดเริ่มต้นที่เป็นประโยชน์คือระบุชื่อแต่ละรายการที่เก็บและถามสองคำถาม: มันปลดล็อกอะไรหรือไม่ และจะเป็นปัญหาไหมถ้ามันรั่วไหล? ส่วนที่เหลือ (Keystore, EncryptedSharedPreferences, ฐานข้อมูลเข้ารหัส) ตามมาจากคำตอบนั้น
จัดประเภทข้อมูลของคุณ: tokens, keys, และ PII
การจัดเก็บอย่างปลอดภัยจะง่ายขึ้นเมื่อคุณไม่ปฏิบัติต่อ "ข้อมูลที่อ่อนไหว" ทั้งหมดเหมือนกัน เริ่มจากการลิสต์สิ่งที่แอปเก็บและจะเกิดอะไรขึ้นถ้ามันรั่วไหล
Tokens ไม่เหมือนรหัสผ่าน Access token และ refresh token ถูกออกแบบมาเพื่อเก็บไว้เพื่อให้ผู้ใช้คงสถานะล็อกอิน แต่ก็ยังเป็นความลับที่มีมูลค่าสูง รหัสผ่านไม่ควรถูกเก็บไว้เลย หากต้องมีการล็อกอิน เก็บเฉพาะสิ่งที่จำเป็นสำหรับเซสชัน (โดยปกติคือ tokens) และพึ่งพาเซิร์ฟเวอร์ในการตรวจสอบรหัสผ่าน
Keys เป็นชั้นข้อมูลที่ต่างออกไป API keys, signing keys, และคีย์เข้ารหัสสามารถปลดล็อกระบบทั้งระบบได้ หากมีคนสกัดออกมาจากอุปกรณ์ พวกเขาสามารถทำการละเมิดอัตโนมัติในระดับกว้าง กฎที่ดี: ถ้าค่าหนึ่งสามารถใช้ภายนอกแอปเพื่อแอบอ้างแอปหรือถอดรหัสข้อมูล ให้ถือว่ามีความเสี่ยงสูงกว่าท็อคเค็นของผู้ใช้
PII คือข้อมูลที่สามารถระบุตัวบุคคล: อีเมล เบอร์โทร ที่อยู่บ้าน บันทึกลูกค้า บัตรประจำตัวประชาชน ข้อมูลสุขภาพหรือการเงิน ฟิลด์ที่ดูไม่เป็นอันตรายก็กลายเป็นข้อมูลอ่อนไหวเมื่อรวมกัน
ระบบป้ายกำกับที่ใช้งานได้จริง:
- Session secrets: access token, refresh token, session cookie
- App secrets: API keys, signing keys, encryption keys (พยายามหลีกเลี่ยงการวางไว้บนอุปกรณ์เมื่อเป็นไปได้)
- User data (PII): รายละเอียดโปรไฟล์ ตัวระบุ เอกสาร ข้อมูลสุขภาพหรือการเงิน
- Device and analytics IDs: advertising ID, device ID, install ID (ยังถือว่าอ่อนไหวตามนโยบายหลายอย่าง)
Android Keystore: เมื่อใดควรใช้
Android Keystore เหมาะเมื่อคุณต้องปกป้องความลับที่ไม่ควรออกจากอุปกรณ์ในรูปแบบ plaintext มันเป็นตู้เซฟสำหรับคีย์คริปโต ไม่ใช่ที่เก็บฐานข้อมูลสำหรับข้อมูลของคุณ
สิ่งที่มันทำได้ดี: สร้างและถือคีย์ที่ใช้สำหรับการเข้ารหัส การถอดรหัส การลงลายมือชื่อ หรือการตรวจสอบ โดยทั่วไปคุณจะเข้ารหัสโทเค็นหรือข้อมูลออฟไลน์ที่อื่น แล้วใช้คีย์ใน Keystore เพื่อปลดล็อก
คีย์ที่สนับสนุนฮาร์ดแวร์: ความหมายที่แท้จริง
ในอุปกรณ์หลายรุ่น คีย์ใน Keystore อาจได้รับการสนับสนุนโดยฮาร์ดแวร์ นั่นหมายความว่าการดำเนินการกับคีย์เกิดขึ้นภายในสภาพแวดล้อมที่ป้องกันและวัสดุของคีย์ไม่สามารถสกัดออกมาได้ ซึ่งลดความเสี่ยงจากมัลแวร์ที่อ่านไฟล์แอป
การมีฮาร์ดแวร์แบบนี้ไม่ได้รับประกันในทุกอุปกรณ์ และพฤติกรรมแตกต่างตามรุ่นและเวอร์ชัน Android ออกแบบเผื่อสถานการณ์ที่การดำเนินการกับคีย์อาจล้มเหลว
การกั้นด้วยการยืนยันตัวผู้ใช้
Keystore สามารถกำหนดให้ต้องมีการยืนยันผู้ใช้ก่อนใช้คีย์ นั่นคือวิธีผูกการเข้าถึงกับไบโอเมตริกซ์หรือข้อมูลประจำเครื่อง ตัวอย่างเช่น คุณสามารถเข้ารหัสโทเค็นสำหรับส่งออกและถอดรหัสได้เฉพาะหลังผู้ใช้ยืนยันด้วยลายนิ้วมือหรือ PIN
Keystore เหมาะเมื่อคุณต้องการคีย์ที่ไม่สามารถส่งออกได้ ต้องการการอนุมัติด้วยไบโอเมตริกซ์หรือข้อมูลประจำเครื่องสำหรับการกระทำที่อ่อนไหว และต้องการความลับเฉพาะอุปกรณ์ที่ไม่ซิงก์หรือไม่รวมอยู่ในการสำรองข้อมูล
วางแผนรับกับปัญหา: คีย์อาจถูกยกเลิกหลังจากเปลี่ยนหน้าจอล็อก การเปลี่ยนไบโอเมตริกซ์ หรือเหตุการณ์ด้านความปลอดภัย คาดหวังความล้มเหลวและมีแผนสำรองที่ชัดเจน: ตรวจจับคีย์ที่ไม่ถูกต้อง ล้างบลอบที่เข้ารหัส และขอให้ผู้ใช้ลงชื่อเข้าใช้ใหม่
EncryptedSharedPreferences: เมื่อไหร่ถึงพอ
EncryptedSharedPreferences เป็นค่าเริ่มต้นที่ดีสำหรับชุดความลับขนาดเล็กในรูปแบบคีย์-ค่า มันคือ "SharedPreferences ที่ถูกเข้ารหัส" ดังนั้นใครจะมาเปิดไฟล์แล้วอ่านค่าไม่ได้ง่าย ๆ
เบื้องหลังมันใช้ master key ในการเข้ารหัสและถอดรหัสค่า คีย์หลักนั้นถูกปกป้องโดย Android Keystore ดังนั้นแอปของคุณจะไม่เก็บคีย์การเข้ารหัสแบบ plaintext
โดยทั่วไปพอเพียงสำหรับรายการเล็ก ๆ ที่อ่านบ่อย เช่น access และ refresh token, session ID, device ID, ธงสภาพแวดล้อม หรือสถานะเล็ก ๆ เช่น เวลาซิงค์ล่าสุด เหมาะกับชิ้นส่วนข้อมูลผู้ใช้เล็ก ๆ เท่านั้นถ้าจำเป็นจริง ๆ แต่ไม่ควรเป็นที่ทิ้ง PII
มันไม่เหมาะกับข้อมูลขนาดใหญ่หรือมีโครงสร้าง หากคุณต้องการรายการออฟไลน์ การค้นหา หรือการสอบถามตามฟิลด์ (customers, tickets, orders) EncryptedSharedPreferences จะช้าและไม่สะดวก จุดนั้นคุณควรไปหาฐานข้อมูลเข้ารหัส
กฎง่าย ๆ: ถ้าคุณสามารถลิสต์ทุกคีย์ที่เก็บได้บนหน้าจอเดียว EncryptedSharedPreferences น่าจะพอ ถ้าต้องการแถวและการค้นหา ให้เปลี่ยนไปใช้ฐานข้อมูล
การเข้ารหัสฐานข้อมูล: เมื่อจำเป็น
การเข้ารหัสฐานข้อมูลสำคัญเมื่อคุณเก็บมากกว่าการตั้งค่านิดหน่อย หากแอปของคุณเก็บข้อมูลธุรกิจในเครื่อง ให้ถือว่ามันอาจถูกสกัดจากโทรศัพท์ที่หายเว้นแต่คุณปกป้องมัน
ฐานข้อมูลเหมาะเมื่อคุณต้องการการเข้าถึงแบบออฟไลน์กับเรกคอร์ด แคชท้องถิ่นเพื่อประสิทธิภาพ ประวัติ/ตราประทับ หรือบันทึกและไฟล์แนบขนาดยาว
วิธีการเข้ารหัสสองแบบที่พบบ่อย
การเข้ารหัสทั้งไฟล์ฐานข้อมูล (มักแบบ SQLCipher) เข้ารหัสไฟล์ทั้งหมดตามที่เก็บไว้ แอปของคุณเปิดด้วยคีย์ นี่คิดง่ายเพราะไม่ต้องจำว่าคอลัมน์ไหนถูกปกป้อง
การเข้ารหัสที่ชั้นแอปในฟิลด์ เข้ารหัสเฉพาะฟิลด์ก่อนเขียนแล้วถอดรหัสหลังอ่าน ทำงานได้ถ้าส่วนใหญ่ของเรกคอร์ดไม่อ่อนไหว หรือถ้าคุณพยายามรักษาโครงสร้างฐานข้อมูลเดิมโดยไม่เปลี่ยนไฟล์
การแลกเปลี่ยน: ความลับเทียบกับการค้นหาและการเรียง
การเข้ารหัสทั้งฐานข้อมูลซ่อนทุกอย่างบนดิสก์ แต่เมื่อปลดล็อกแล้วแอปสามารถสอบถามปกติได้
การเข้ารหัสฟิลด์ปกป้องคอลัมน์เฉพาะ แต่คุณจะสูญเสียการค้นหาและการเรียงลำดับที่ง่าย การเรียงชื่อตามนามสกุลที่ถูกเข้ารหัสจะไม่ทำงานได้ดี และการค้นหาต้องเป็น "ค้นหาหลังถอดรหัส" (ช้า) หรือ "เก็บดัชนีเพิ่มเติม" (ซับซ้อนขึ้นและมีความเสี่ยงในการรั่ว)
พื้นฐานการจัดการคีย์
คีย์ฐานข้อมูลไม่ควรถูกฝังหรือส่งมากับแอป รูปแบบที่ใช้กันทั่วไปคือสร้างคีย์ฐานข้อมูลแบบสุ่ม แล้วเก็บไว้ในรูปแบบที่ห่อ (wrapped) ซึ่งเข้ารหัสด้วยคีย์ที่เก็บใน Android Keystore เมื่อออกจากระบบ คุณสามารถลบคีย์ที่ห่อไว้และถือว่าฐานข้อมูลท้องถิ่นทิ้งได้ หรือจะเก็บไว้ถ้าแอปต้องทำงานออฟไลน์ข้ามเซสชัน
วิธีเลือก: เปรียบเทียบเชิงปฏิบัติ
คุณไม่ได้เลือก "ตัวเลือกที่ปลอดภัยที่สุด" โดยรวม แต่เลือกตัวเลือกที่ปลอดภัยพอและเหมาะกับการใช้งานข้อมูลของแอป
คำถามที่ช่วยให้เลือกได้ถูกต้อง:
- ข้อมูลอ่านบ่อยแค่ไหน (เปิดแอปทุกครั้งหรือไม่บ่อย)?
- ข้อมูลมีขนาดเท่าไหร่ (ไม่กี่ไบต์หรือนับพันเรกคอร์ด)?
- จะเกิดอะไรขึ้นถ้ามันรั่ว (แค่รำคาญ มีค่าใช้จ่าย หรือต้องแจ้งตามกฎหมาย)?
- จำเป็นต้องเข้าถึงแบบออฟไลน์ ค้นหา หรือเรียงลำดับหรือไม่?
- มีข้อกำหนดด้านความสอดคล้อง (การเก็บรักษา การตรวจสอบ กฎการเข้ารหัส) หรือไม่?
แมปที่ใช้ได้จริง:
- Tokens (OAuth access และ refresh tokens) มักอยู่ใน
EncryptedSharedPreferencesเพราะมีขนาดเล็กและอ่านบ่อย - วัสดุคีย์ ควรอยู่ใน Android Keystore เมื่อเป็นไปได้เพื่อลดโอกาสที่ถูกคัดลอกออกจากอุปกรณ์
- PII และข้อมูลธุรกิจออฟไลน์ มักต้องเข้ารหัสฐานข้อมูลเมื่อเก็บมากกว่าบางฟิลด์หรือจำเป็นต้องกรอง/ค้นหา
ข้อมูลผสมเป็นเรื่องปกติในแอปธุรกิจ รูปแบบปฏิบัติคือสร้าง data encryption key (DEK) แบบสุ่มสำหรับฐานข้อมูลหรือไฟล์ท้องถิ่น เก็บเฉพาะ DEK ที่ห่อไว้ด้วยคีย์ที่มาจาก Keystore และหมุนคีย์เมื่อจำเป็น
ถ้าไม่แน่ใจ ให้เลือกเส้นทางที่ปลอดภัยและเรียบง่าย: เก็บน้อยลง หลีกเลี่ยงการเก็บ PII แบบออฟไลน์ถ้าไม่จำเป็น และเก็บคีย์ไว้ใน Keystore
ขั้นตอนทีละขั้นตอน: นำไปใช้ในแอป Kotlin
เริ่มจากเขียนลงว่าแต่ละค่าที่จะเก็บบนอุปกรณ์มีอะไรบ้างและเหตุผลที่ต้องเก็บ นี่เป็นวิธีเร็วสุดจะป้องกันการเก็บแบบ "กันไว้ดีกว่า"
ก่อนเขียนโค้ด ให้กำหนดกฎ: แต่ละไอเท็มอยู่ได้นานเท่าไร ควรถูกแทนที่เมื่อไร และ "ล็อกเอาต์" หมายถึงอะไร โทเค็นเข้าถึงอาจหมดเวลาใน 15 นาที refresh token อาจนานกว่า และ PII ออฟไลน์อาจต้องมีกฎ "ลบหลัง 30 วัน"
การนำไปใช้ที่ยังดูแลได้:
- สร้าง wrapper เดียว "SecureStorage" เพื่อให้ส่วนอื่นของแอปไม่ต้องสัมผัส
SharedPreferences, Keystore, หรือฐานข้อมูลโดยตรง - วางแต่ละไอเท็มในที่ที่เหมาะ: tokens ใน
EncryptedSharedPreferences, คีย์การเข้ารหัสปกป้องด้วย Android Keystore, และชุดข้อมูลออฟไลน์ใหญ่ในฐานข้อมูลเข้ารหัส - จัดการความล้มเหลวอย่างตั้งใจ หากการจัดเก็บอย่างปลอดภัยล้มเหลว ให้ล้มแบบปิด (fail closed) อย่ากลับไปเก็บแบบ plaintext โดยเงียบ ๆ
- เพิ่มการวินิจฉัยโดยไม่รั่วข้อมูล: บันทึกประเภทเหตุการณ์และรหัสข้อผิดพลาด อย่าบันทึก tokens คีย์ หรือรายละเอียดผู้ใช้
- ผูกเส้นทางการลบ: logout, การลบบัญชี, และ "clear app data" ควรไหลเข้า routine การลบเดียวกัน
จากนั้นทดสอบกรณีที่น่าเบื่อซึ่งมักทำให้การจัดเก็บอย่างปลอดภัยล้มเหลวในโปรดักชัน: การคืนค่าจากแบ็กอัพ การอัปเกรดจากเวอร์ชันเก่า การเปลี่ยนการตั้งค่าล็อกหน้าจอ การย้ายไปเครื่องใหม่ ตรวจสอบให้แน่ใจผู้ใช้ไม่ติดอยู่ในลูปที่ข้อมูลที่เก็บไม่สามารถถอดรหัสได้แต่แอปยังพยายามซ้ำ
สุดท้าย เขียนบันทึกการตัดสินใจในหน้าเดียวให้ทีมทั้งทีมตามได้: เก็บอะไรไว้ที่ไหน ระยะเวลาเก็บ และจะเกิดอะไรขึ้นเมื่อถอดรหัสล้มเหลว
ข้อผิดพลาดทั่วไปที่ทำให้การจัดเก็บล้มเหลว
ความล้มเหลวส่วนใหญ่ไม่ใช่เพราะเลือกไลบรารีผิด แต่เกิดจากทางลัดเล็ก ๆ ที่เงียบ ๆ คัดลอกความลับไปยังที่ที่คุณไม่ได้ตั้งใจ
สัญญาณเตือนที่ใหญ่ที่สุดคือ refresh token (หรือ token ยาว) ถูกเก็บเป็น plaintext ที่ไหนสักแห่ง: SharedPreferences, ไฟล์, แคชชั่วคราว, หรือคอลัมน์ฐานข้อมูล ท็อคเค็นนั้นสามารถมีชีวิตยืนยาวกว่ารหัสผ่านได้หากผู้โจมตีได้แบ็กอัพ ดัมพ์อุปกรณ์รูท หรือชิ้นงานดีบัก
ความลับยังรั่วผ่านการมองเห็นไม่ใช่แค่การจัดเก็บ การบันทึก header เต็มรูปแบบในคำขอ การพิมพ์โทเค็นระหว่างดีบัก หรือการแนบบริบทช่วยเหลือไปกับรายงานการชนและเหตุการณ์วิเคราะห์สามารถเปิดเผยข้อมูลรับรองนอกอุปกรณ์ได้ จงถือว่า logs เป็นสาธารณะ
การจัดการคีย์ก็เป็นช่องว่างบ่อยครั้ง การใช้คีย์เดียวกับทุกอย่างเพิ่มขนาดความเสียหาย การไม่หมุนคีย์ทำให้การละเมิดเก่ายังใช้ได้ มีแผนสำหรับการเวอร์ชันคีย์ การหมุน และการจัดการข้อมูลที่เข้ารหัสเก่า
อย่าลืมทางออกนอก "ตู้นิรภัย"
การเข้ารหัสไม่หยุดการแบ็กอัพบนคลาวด์จากการคัดลอกข้อมูลท้องถิ่น มันไม่หยุดการจับภาพหน้าจอหรือการบันทึกหน้าจอไม่ให้จับ PII ไม่หยุดบิลด์ดีบักที่ตั้งค่าหละหลวม หรือฟีเจอร์ส่งออก (CSV/share) ที่รั่วไหลฟิลด์สำคัญ การใช้คลิปบอร์ดยังสามารถรั่วรหัสครั้งเดียวหรือหมายเลขบัญชีได้
นอกจากนี้ การเข้ารหัสไม่แก้ปัญหา authorization ถ้าแอปโชว์ PII หลังผู้ใช้ล็อกเอาต์หรือเก็บแคชที่เข้าถึงได้โดยไม่ต้องยืนยัน นั่นคือบั๊กควบคุมการเข้าถึง ล็อก UI ลบแคชที่อ่อนไหวตอน logout และตรวจสอบสิทธิ์ก่อนแสดงข้อมูลปกป้อง
รายละเอียดเชิงปฏิบัติ: วงจรชีวิต ล็อกเอาต์ และกรณีชายขอบ
การจัดเก็บอย่างปลอดภัยไม่ใช่แค่วางความลับ มันคือพฤติกรรมของพวกมันตามเวลา: เมื่อแอปหลับ เมื่อผู้ใช้ล็อกเอาต์ และเมื่ออุปกรณ์ล็อก
สำหรับ token วางแผนวงจรชีวิตเต็มรูปแบบ Access token ควรมีอายุสั้น Refresh token ควรถูกปฏิบัติเหมือนรหัสผ่าน ถ้า token หมดอายุ ให้รีเฟรชเงียบ ๆ ถ้ารีเฟรชล้มเหลว (ถูกเพิกถอน รหัสผ่านเปลี่ยน อุปกรณ์ถูกลบ) หยุดการลองซ้ำและบังคับให้ลงชื่อเข้าใช้ใหม่ รองรับการเพิกถอนฝั่งเซิร์ฟเวอร์ การเก็บท้องถิ่นไม่มีทางช่วยได้หากคุณไม่เพิกถอนข้อมูลรับรองที่ถูกขโมย
ใช้ไบโอเมตริกซ์เพื่อยืนยันตัวใหม่ ไม่ใช่สำหรับทุกอย่าง กระตุ้นเมื่อการกระทำมีความเสี่ยงจริง (ดู PII, ส่งออกข้อมูล, เปลี่ยนรายละเอียดจ่ายเงิน, แสดงรหัสครั้งเดียว) อย่าแสดงคำขอทุกครั้งที่เปิดแอป
เมื่อ logout ให้เข้มงวดและคาดเดาได้:
- เคลียร์สำเนาในหน่วยความจำก่อน (tokens ที่แคชใน singleton, interceptor, หรือ ViewModel)
- ลบ tokens และสถานะเซสชันที่เก็บไว้ (รวม refresh tokens)
- ลบหรือเพิกถอนคีย์เข้ารหัสท้องถิ่นถ้าดีไซน์รองรับ
- ลบบันทึก PII ออฟไลน์และการตอบกลับ API ที่แคชไว้
- ปิดงาน background ที่อาจดึงข้อมูลกลับมา
กรณีชายขอบสำคัญในแอปธุรกิจ: หลายบัญชีบนเครื่องเดียว, โปรไฟล์งาน, backup/restore, การโอนเครื่อง, และการ logout แบบบางส่วน (เปลี่ยนบริษัท/workspace แทนออกจากระบบทั้งหมด) ทดสอบ force stop, อัปเกรด OS, และการเปลี่ยนเวลาเพราะความเบี้ยวของเวลาอาจทำให้การหมดอายุทำงานผิดพลาด
การตรวจจับการดัดแปลงเป็นการแลกเปลี่ยน จุดเช็คพื้นฐาน (บิลด์ debuggable, ธง emulator, สัญญาณรูทง่าย ๆ, คำตัดสิน Play Integrity) ลดการโจมตีแบบสมัครเล่น แต่ผู้โจมตีที่มุ่งมั่นจะหลบได้ ให้ใช้สัญญาณเหล่านี้เป็นอินพุตความเสี่ยง: จำกัดการเข้าถึงออฟไลน์, ขอการยืนยันตัวใหม่, และบันทึกเหตุการณ์
เช็คลิสต์ด่วนก่อนปล่อย
ใช้สิ่งนี้ก่อนรีลีส โฟกัสจุดที่การจัดเก็บล้มเหลวในแอปธุรกิจจริง
- ถือว่าอุปกรณ์อาจเป็นศัตรู. ถ้าผู้โจมตีมีอุปกรณ์รูทหรืออิมเมจเต็มของอุปกรณ์ พวกเขาอ่าน tokens, คีย์, หรือ PII จากไฟล์แอป, preferences, logs, หรือภาพหน้าจอได้หรือไม่? ถ้าคำตอบคือ "อาจจะ" ให้ย้ายความลับไปยังการป้องกันด้วย Keystore และเก็บ payload ให้เข้ารหัส
- ตรวจสอบการสำรองข้อมูลและการย้ายเครื่อง. เก็บไฟล์อ่อนไหวให้อยู่ห่างจาก Android Auto Backup, การสำรองข้อมูลบนคลาวด์, และการโอนเครื่อง หากการคืนค่าคีย์หายไปทำให้ไม่สามารถถอดรหัสได้ ให้วางแผนการกู้คืน (ขอให้ผู้ใช้ลงชื่อเข้าใหม่และดาวน์โหลดอีกครั้งแทนที่จะพยายามถอดรหัส)
- ค้นหา plaintext บนดิสก์โดยไม่ได้ตั้งใจ. หาชั่วคราว ไฟล์แคช HTTP รายงานการชน เหตุการณ์วิเคราะห์ และแคชภาพที่อาจมี PII หรือ tokens ตรวจสอบการบันทึกขณะดีบักและ JSON dumps
- กำหนดอายุและหมุน. Access token ควรสั้น Refresh token ควรถูกปกป้อง และเซสชันฝั่งเซิร์ฟเวอร์ควรถูกเพิกถอนได้ กำหนดการหมุนคีย์และพฤติกรรมเมื่อ token ถูกปฏิเสธ (ล้างข้อมูล, ลงชื่อเข้าใหม่, ลองครั้งเดียว)
- พฤติกรรมเมื่อติดตั้งใหม่และเปลี่ยนเครื่อง. ทดสอบถอนติดตั้งแล้วติดตั้งใหม่ แล้วเปิดแบบออฟไลน์ ถ้าคีย์ Keystore หาย แอปควรล้มอย่างปลอดภัย (ลบข้อมูลที่เข้ารหัส, แสดงหน้าล็อกอิน, หลีกเลี่ยงการอ่านบางส่วนที่ทำให้สถานะเสียหาย)
การทดสอบเร็ว ๆ คือการทดสอบ "วันแย่ ๆ": ผู้ใช้ล็อกเอาต์ เปลี่ยนรหัสผ่าน คืนค่าจากแบ็กอัพไปยังเครื่องใหม่ แล้วเปิดแอปบนเครื่องที่ออฟไลน์ ผลลัพธ์ควรคาดเดาได้: ข้อมูลถอดรหัสได้สำหรับผู้ใช้ที่ถูกต้อง หรือถูกลบและดาวน์โหลดใหม่หลังล็อกอิน
ตัวอย่างสถานการณ์: แอปธุรกิจที่เก็บ PII ออฟไลน์
สมมติแอปการขายภาคสนามที่ใช้ในพื้นที่สัญญาณไม่ดี ตัวแทนล็อกอินเช้าหนึ่งครั้ง เรียกดูลูกค้าที่มอบหมายแบบออฟไลน์ เพิ่มบันทึกการประชุม แล้วซิงค์ทีหลัง นี่คือจุดที่เช็คลิสต์หยุดเป็นทฤษฎีและเริ่มป้องกันการรั่วจริง
การแยกเชิงปฏิบัติ:
- Access token: สั้น ๆ เก็บใน
EncryptedSharedPreferences - Refresh token: ปกป้องเข้มงวดขึ้นและกั้นการเข้าถึงผ่าน Android Keystore
- Customer PII (ชื่อ เบอร์ โทร ที่อยู่): เก็บในฐานข้อมูลท้องถิ่นที่เข้ารหัส
- บันทึกออฟไลน์และไฟล์แนบ: เก็บในฐานข้อมูลเข้ารหัส โดยให้ระมัดระวังเป็นพิเศษกับการส่งออกและการแชร์
ตอนนี้เพิ่มสองฟีเจอร์แล้วความเสี่ยงเปลี่ยนไป
ถ้าเพิ่ม "จำฉันไว้" refresh token จะกลายเป็นทางกลับเข้าบัญชี ให้ปฏิบัติเหมือนรหัสผ่าน ขึ้นกับผู้ใช้ คุณอาจต้องขอปลดล็อกอุปกรณ์ (PIN/แพทเทิร์น/ไบโอเมตริกซ์) ก่อนถอดรหัส
ถ้าเพิ่มโหมดออฟไลน์ คุณจะปกป้องไม่ใช่แค่เซสชัน แต่รายการลูกค้าทั้งหมดซึ่งมีมูลค่าในตัวเอง นั่นส่วนใหญ่จะผลักดันให้ใช้การเข้ารหัสฐานข้อมูลพร้อมกฎการล็อกเอาต์ที่ชัดเจน: ลบ PII ท้องถิ่น เหลือเฉพาะสิ่งที่จำเป็นสำหรับการล็อกอินครั้งถัดไป และยกเลิกการซิงค์พื้นหลัง
ทดสอบบนอุปกรณ์จริง ไม่ใช่แค่อีมูเลเตอร์ อย่างน้อยตรวจสอบพฤติกรรมล็อก/ปลดล็อก การติดตั้งซ้ำ การสำรอง/คืนค่า และการแยกโปรไฟล์ผู้ใช้หรือโปรไฟล์งาน
ก้าวต่อไป: ทำให้เป็นนิสัยของทีม
การจัดเก็บอย่างปลอดภัยทำงานได้เมื่อเป็นนิสัย เขียนนโยบายการจัดเก็บสั้น ๆ ที่ทีมทำตามได้: อะไรไปที่ไหน (Keystore, EncryptedSharedPreferences, ฐานข้อมูลเข้ารหัส), อะไรห้ามเก็บ, และอะไรต้องลบเวลา logout
ทำให้เป็นส่วนหนึ่งของงานประจำวัน: definition of done, code review, และการเช็กก่อนปล่อย
เช็กลิสต์เบา ๆ สำหรับผู้ตรวจ:
- แต่ละไอเท็มที่เก็บถูกติดป้าย (token, วัสดุคีย์, หรือ PII)
- ทางเลือกการจัดเก็บได้รับการอธิบายในคอมเมนต์โค้ด
- การล็อกเอาต์และการสลับบัญชีลบข้อมูลที่ถูกต้อง (และเฉพาะข้อมูลนั้น)
- ข้อผิดพลาดและบันทึกไม่พิมพ์ความลับหรือ PII เต็มรูปแบบ
- มีคนรับผิดชอบนโยบายและอัปเดตให้ทันสมัย
ถ้าทีมของคุณใช้ AppMaster (appmaster.io) เพื่อสร้างแอปธุรกิจและส่งออกซอร์ส Kotlin สำหรับไคลเอนต์ Android ให้ใช้แนวทาง SecureStorage wrapper เดียวกันเพื่อให้โค้ดที่สร้างและโค้ดที่เขียนเองปฏิบัติตามนโยบายเดียวกัน
เริ่มจาก POC เล็ก ๆ
สร้าง POC เล็ก ๆ ที่เก็บ auth token หนึ่งอันและข้อมูล PII หนึ่งรายการ (เช่น เบอร์โทรลูกค้าที่ต้องใช้แบบออฟไลน์) แล้วทดสอบการติดตั้งใหม่ อัปเกรด ล็อกเอาต์ การเปลี่ยนการล็อกหน้าจอ และการล้างข้อมูลแอป ขยายต่อเมื่อพฤติกรรมการลบถูกต้องและทำซ้ำได้
คำถามที่พบบ่อย
เริ่มจากการระบุอย่างชัดเจนว่าคุณเก็บค่าอะไรและทำไมต้องเก็บไว้ในเครื่อง ใส่ความลับเซสชันขนาดเล็กเช่น access และ refresh token ลงใน EncryptedSharedPreferences เก็บคีย์เชิงคริปโตใน Android Keystore และใช้ฐานข้อมูลเข้ารหัสสำหรับบันทึกธุรกิจแบบออฟไลน์หรือ PII เมื่อมีมากกว่าบางฟิลด์หรือจำเป็นต้องค้นหา/กรอง
SharedPreferences แบบธรรมดาเก็บค่าเป็นไฟล์ที่ในบางกรณีสามารถอ่านได้จากการสำรองข้อมูลของอุปกรณ์ การเข้าถึงไฟล์บนเครื่องที่รูทแล้ว หรือเศษข้อมูลจากการดีบัก ถ้าค่านั้นเป็น token หรือ PII การเก็บไว้เหมือนการตั้งค่าทั่วไปทำให้มันง่ายต่อการคัดลอกและใช้นอกแอป
ใช้ Android Keystore เมื่อต้องการสร้างและเก็บคีย์คริปโตที่ไม่ควรถูกดึงออกมา คุณมักจะใช้คีย์เหล่านั้นเพื่อเข้ารหัสข้อมูลอื่น ๆ (tokens, คีย์ฐานข้อมูล, ไฟล์) และสามารถกำหนดให้ต้องยืนยันตัวผู้ใช้ (ไบโอเมตริกซ์หรือข้อมูลประจำเครื่อง) ก่อนใช้งานคีย์ได้
หมายความว่าการดำเนินการกับคีย์สามารถเกิดขึ้นในฮาร์ดแวร์ที่ป้องกัน ทำให้วัสดุของคีย์ยากต่อการสกัด แม้ว่าผู้โจมตีจะอ่านไฟล์แอปได้ก็ตาม อย่าคาดหวังว่าจะมีฮาร์ดแวร์แบบนี้เสมอไปหรือทำงานเหมือนกันทุกอุปกรณ์; ออกแบบให้รับมือกับความล้มเหลวและมีแนวทางกู้คืนเมื่อคีย์ไม่ใช้ได้
โดยปกติพอเพียงสำหรับชุดคีย์-ค่าเล็ก ๆ ที่อ่านบ่อย เช่น access/refresh token, session ID และค่าระดับสถานะเล็ก ๆ EncryptedSharedPreferences ไม่เหมาะกับข้อมูลขนาดใหญ่ ระเบียนเชิงโครงสร้าง หรือข้อมูลที่ต้องค้นหา/กรอง เช่น รายชื่อลูกค้า ตั๋ว หรือคำสั่งซื้อ
ควรใช้ฐานข้อมูลเข้ารหัสเมื่อคุณเก็บข้อมูลธุรกิจหรือ PII ในเครื่องในระดับที่มากขึ้น ต้องการการค้นหา/กรอง/การจัดเรียง หรือต้องเก็บประวัติการใช้งานแบบออฟไลน์ นั่นช่วยลดความเสี่ยงที่อุปกรณ์หายแล้วข้อมูลลูกค้าทั้งหมดหรือบันทึกจะหลุดออกมา ในขณะที่แอปยังสามารถทำงานออฟไลน์ได้ด้วยนโยบายคีย์ที่ชัดเจน
การเข้ารหัสทั้งไฟล์ฐานข้อมูล (full database encryption) ปกป้องไฟล์ทั้งหมดที่เก็บบนดิสก์และทำให้ง่ายต่อการคิดเหตุผลเพราะไม่ต้องตามว่าคอลัมน์ไหนถูกปกป้อง ส่วนการเข้ารหัสเฉพาะช่องข้อมูล (field encryption) เหมาะกับบางคอลัมน์ แต่ทำให้การค้นหาและการเรียงลำดับยากและเสี่ยงที่จะรั่วผ่านดัชนีหรือฟิลด์อนุพันธ์
สร้างคีย์ฐานข้อมูลแบบสุ่ม แล้วเก็บไว้เฉพาะในรูปแบบ "ห่อ" (wrapped) ที่ถูกเข้ารหัสด้วยคีย์ที่เก็บใน Android Keystore อย่าใส่คีย์ลงไปในโค้ดหรือส่งมากับแอป และกำหนดพฤติกรรมเมื่อผู้ใช้ล็อกเอาต์หรือคีย์หมดอายุ (บ่อยครั้ง: ลบคีย์ที่ห่อไว้และถือว่าข้อมูลท้องถิ่นทิ้งได้)
คีย์อาจถูกยกเลิกใช้งานเพราะการเปลี่ยนการล็อกหน้าจอ การเปลี่ยนไบโอเมตริกซ์ เหตุการณ์ด้านความปลอดภัย หรือการย้ายเครื่อง ตั้งรับกับกรณีเหล่านี้โดยตรวจจับความล้มเหลวในการถอดรหัส ลบข้อมูลที่เข้ารหัสหรือฐานข้อมูลท้องถิ่นอย่างปลอดภัย และขอให้ผู้ใช้ลงชื่อเข้าใช้ใหม่ แทนที่จะพยายามซ้ำหรือกลับไปเก็บแบบ plaintext
การรั่วไหลส่วนใหญ่เกิดนอก 'ตู้นิรภัย': บันทึก (logs), รายงานการชน (crash reports), เหตุการณ์วิเคราะห์, การพิมพ์ขณะดีบัก, แคช HTTP, ภาพหน้าจอ และคลิปบอร์ด อย่าถือว่าบันทึกเป็นส่วนตัว ห้ามบันทึก tokens หรือ PII เต็มรูปแบบ ปิดเส้นทางการส่งออกโดยไม่ตั้งใจ และทำให้การล็อกเอาต์ลบทั้งข้อมูลที่เก็บและสำเนาในหน่วยความจำ


