รู้จักกับคำสั่งสำหรับ Security ที่อยู่ใน AndroidX

นักพัฒนาแอนดรอยด์หลายๆคนคงรู้จักกับ AndroidX กันอยู่แล้ว (ถ้าใครยังไม่รู้ถือว่าบาปแล้วล่ะ) ซึ่งหนึ่งในนั้นที่น่าสนใจไม่แพ้กันก็มีชื่อเรียกว่า Security นี่แหละ

AndroidX Security

เป็นหนึ่งใน Library ที่อยู่ในตระกูล AndroidX ที่ประกอบไปด้วยคำสั่งที่เกี่ยวข้องกับ Secutity สำหรับแอปฯเพื่อช่วยอำนวยความสะดวกให้กับนักพัฒนามากขึ้น อาจจะฟังดูเลิศหรูอลังการ แต่จริงๆแล้วใน Security นั้นมีให้ใช้แค่ 2 อย่างคือ EncryptedSharedPreferences กับ EncryptedFile

ทั้ง 2 คลาสนี้มีจุดประสงค์หลักก็คือการเข้ารหัสให้กับข้อมูลที่จะเก็บลง SharedPreferences หรือ Internal/External Storage นั่นเอง โดยที่นักพัฒนาไม่ต้องไปยุ่งกับเรื่องการเข้ารหัสให้วุ่นวาย

ในการใช้งาน Security ของ AndroidX จะต้องเพิ่ม Dependency ไว้ใน build.gradle แบบนี้

// build.gradle (Module: app)
implementation "androidx.security:security-crypto:1.0.0-rc02"

ข้อจำกัดสำคัญของ AndroidX Security ก็คือรองรับเฉพาะ Android 6.0 Marshmallow (API 23) ขึ้นไปเท่านั้น และถึงแม้ว่าตอนนี้จะยังเป็น Release Candidate อยู่ แต่จากที่ลองทดสอบดูก็สามารถใช้งานได้แล้วนะ

ก่อนเริ่มใช้งานมาทำความรู้จักกับ Master Key กันก่อน

ไม่ว่าจะเป็น EncryptedSharedPreferences หรือ EncryptedFile ก็ตาม นักพัฒนาจะต้องสร้างสิ่งที่เรียกว่า Master Key เพื่อให้คลาสทั้ง 2 ใช้ในการเข้ารหัสข้อมูล ซึ่งเป็นการสร้าง Key Alias ขึ้นมาใน Keystore ที่ฝังอยู่ในแอปฯนั่นเอง

ถ้านึกไม่ออกก็ให้นึกถึง Key Alias ที่นักพัฒนาต้องสร้างไว้ใน Keystore ตอนที่จะ Build APK หรือ AAB นั่นเอง ซึ่งในนั้นจะมี Unique Key ที่เอาไว้ใช้ในการเข้ารหัสข้อมูลที่ไม่สามารถปลอมแปลงได้ง่ายๆ และสามารถเอามาใช้กับภายในแอปฯได้ด้วย

แต่การเอา Key Alias ที่นักพัฒนาสร้างขึ้นมาเอง ก็ไม่ใช่วิธีที่ปลอดภัยที่สุด เพราะนักพัฒนาคนนั้นๆรู้รหัสผ่านของ Key Alias ตัวนั้นอยู่ดี ดังนั้น AndroidX Security จึงใส่คลาสที่ชื่อว่า MasterKeys ไว้เพื่อให้นักพัฒนาสามารถสร้าง Key Alias ขึ้นมาเพื่อใช้เป็น Master Key ในการเข้ารหัสข้อมูล ซึ่งเป็นคนละตัวที่ใช้กับตอน Build APK หรือ AAB จึงทำให้แม้แต่นักพัฒนาก็ไม่สามารถแอบลักลอบเพื่อเอาข้อมูลไปถอดรหัสได้ง่ายๆ

สำหรับการใช้งาน MasterKeys นั้นก็ง่ายมากๆ เพราะหน้าที่ของมันมีไว้แค่สร้าง Key Alias ถ้ายังไม่เคยสร้างมาก่อน แต่ถ้ามีอยู่แล้วก็จะได้ตัวเดิมที่มีอยู่ โดยเข้ารหัสแบบ AES256

val alias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

คำสั่งดังกล่าวจะได้ออกมาเป็น Key Alias เพื่อนำไปใช้งานใน EncryptedSharedPreferences หรือ EncryptedFile นั่นเอง

การใช้งาน EncryptedSharedPreferences

อยากจะเก็บข้อมูลสำคัญๆไว้ใน SharedPreferences แต่ห่วงเรื่องความปลอดภัย? ขอแนะนำ EncryptedSharedPreferences เลย

ในการสร้าง EncryptedSharedPreferences ขึ้นมาใช้งานจะคล้ายกับ SharedPreferences ปกติ แต่จะมีการกำหนด Key Alias และรูปแบบของ Encryption ที่จะใช้สำหรับเข้ารหัสข้อมูลทั้งส่วนที่เป็น Key และ Value

EncryptedSharedPreferences.create(
    fileName: String, 
    alias: String, 
    context: Context, 
    prefKeyEncryptionScheme: PrefKeyEncryptionScheme, 
    prefValueEncryptionScheme: PrefValueEncryptionScheme
): SharedPreferences

เวลาเรียกใช้งานจะมีหน้าตาประมาณนี้

val fileName = "user_info_prefs"
val alias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val preferences = EncryptedSharedPreferences.create(
    fileName,
    alias,
    context,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

สำหรับรูปแบบการเข้ารหัสในตอนนี้จะมีให้เลือกแค่ AES256 SIV สำหรับข้อมูลที่เป็น Key และ AES256 GCM สำหรับ Value เท่านั้น

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

preferences.edit {
    putString("username", "Akexorcist")
}

ที่สำคัญคือสามารถใช้ Android KTX ได้ด้วย เนื่องจากเป็น SharedPreferences นั่นเอง

จากตัวอย่างโค้ดข้างบนนี้ เจ้าของบล็อกได้เก็บคำว่า Akexorcist ไว้ใน Key ที่ชื่อว่า username และเมื่อลองเปิดเข้าไปดูไฟล์ที่ SharedPreferences สร้างไว้ภายในเครื่อง ก็จะพบว่าข้อมูลดังกล่าวถูกเข้ารหัสและกลายเป็นข้อมูลแบบนี้แทน

<!-- user_info_prefs.xml -->
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="ARiCfyob4/mjpPoHztIMTkKckE8ekUWvfg==">ARNNvv6nB/wpQmajOmGN//Rog1mke5aryVpirxofP1IC/aPQzv/RVmGD27z3e10Lt7pY</string>
    ...
</map>

และถ้าอยากจะดึงข้อมูลไปใช้งานก็ให้ใช้คำสั่งเหมือนกับ SharedPreferences ตามปกติเช่นกัน

val name = preferences.getString("username", null)

สะดวก รวดเร็ว และใช้ง่ายสุดๆ

การใช้งาน EncryptedFile

แต่ถ้ามีข้อมูลที่เป็นไฟล์ แต่ต้องการเข้ารหัสข้อมูลเพื่อไม่ให้เปิดไฟล์นั้นได้ง่ายๆ ก็ให้ใช้ EncryptedFile ได้เลย โดยคลาสดังกล่าวนักพัฒนาสามารถกำหนดได้ตามใจชอบว่าจะให้เก็บข้อมูลที่ว่าไว้ใน Internal Storage หรือ External Storage

สำหรับการใช้งาน EncryptedFile นั้นจะแตกต่างไปจากปกติเล็กน้อย เพราะต้องสร้างผ่าน Builder ที่ชื่อว่า EncryptedFile.Builder โดยมีรูปแบบคำสั่งดังนี้

EncryptedFile.Builder(
    file: File,
    context: Context,
    alias: String,
    fileEncryptionScheme: FileEncryptionScheme
): Builder

เวลาเรียกใช้งานจะเป็นแบบนี้

val alias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val file = File(context.filesDir, "confidential_info.txt")
val encryptedFile: EncryptedFile = EncryptedFile.Builder(
    file,
    context,
    alias,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

จะเห็นว่าคำสั่งดังกล่าวจะต้องกำหนดที่อยู่ของไฟล์ด้วยคลาส File ดังนั้นนักพัฒนาจึงสามารถสร้างได้ตามใจชอบว่าจะให้ไฟล์ดังกล่าวอยู่ที่ Internal Storage หรือ External Storage โดยในตัวอย่างจะสร้างไว้ใน Internal Storage (ถ้าใช้เป็น External Storage ก็อย่าลืมขอ Permission ให้เรียบร้อยก่อนล่ะ) และรูปแบบในการเข้ารหัสของข้อมูลจะมีให้เลือกแค่ AES256 GCM เท่านั้น

สำหรับการเขียนจากคลาส EncryptedFile จะทำผ่าน FileOutputStream เหมือนกับการเขียนไฟล์ทั่วๆไปนั่นเอง

val fos: FileOutputStream = encryptedFile.openFileOutput()
fos.use { it.write("Akexorcist".toByteArray()) }

จากตัวอย่างโค้ดข้างบนนี้ เจ้าของบล็อกได้สร้างไฟล์ confidential_info.txt ขึ้นมาและใส่ข้อมูลไว้ในนั้นเป็นข้อความว่า Akexorcist แต่เมื่อลองเปิดไฟล์ดังกล่าวขึ้นมาดูก็จะได้ข้อมูลที่เข้ารหัสไว้แบบนี้แทน

// confidential_info.txt
(–éhı¢@=[ùh:/Î ¢Ú[B’˘ Ì "–€ Õ⁄uı Eˇåb ÿ`çú_ìõ     eˇyÒ—{tUj%;2√j‰$7Ã

บ้าจริง มันถอดรหัสกลับมาเป็นคำว่า Akexorcist ได้ยังไงล่ะเนี่ย…

เมื่อใช้ FileOutputStream ในการเขียนข้อมูล นั่นหมายความว่าเวลาอ่านข้อมูลก็จะใช้ FileInputStream นั่นเอง

val fis: FileInputStream = encryptedFile.openFileInput()
val name = fis.use { String(it.readBytes()) }

ด้วยการใช้ FileOutputStream จึงทำให้นักพัฒนาสามารถใช้กับข้อมูลแบบไหนก็ได้ แต่ให้ระวังเรื่องขนาดของไฟล์ด้วย เพราะถ้าไฟล์มีขนาดใหญ่เกินไปก็จะใช้ระยะเวลาในการเข้ารหัสนานด้วยเช่นกัน

สรุป

AndroidX Security จะเข้ามาช่วยในเรื่องการเก็บข้อมูลสำคัญที่ต้องการเข้ารหัสเพื่อความปลอดภัย ช่วยป้องกันได้ดีในกรณีที่มีข้อมูลรั่วไหลออกไป เพราะข้อมูลดังกล่าวถูกเข้ารหัสด้วย Key Alias ของ Keystore ที่สามารถปลอมแปลงได้ยากมาก

ในขณะเดียวกัน ความสามารถของตัวนี้จะใช้ได้กับ Android 6.0 Marshmallow ขึ้นไปเท่านั้น เนื่องจากความสามารถทางด้านความปลอดภัยที่มีการปรับปรุงครั้งใหญ่ในเวอร์ชันนี้ จึงทำให้เวอร์ชันที่ต่ำกว่านั้นไม่สามารถใช้งาน AndroidX Security ได้