Mobile Security via Flutter — ตอนที่ 2 Strong Device/Strong Pin

Amorn Apichattanakul
KBTG Life
Published in
7 min readApr 28, 2021

เราจะสามารถทำให้ Security ของแอปพลิเคชันมือถือเราแข็งแกร่งมากขึ้นได้อย่างไรบ้าง? ลองมองง่ายๆ ครับ การที่บ้านของเราจะปลอดภัยได้ สิ่งที่เราคิดถึงเป็นลำดับต้นๆ อาจจะเป็นการติดกล้องวงจรปิด ติดสัญญาณกันขโมย ติดเซ็นเซอร์หลายๆ อย่าง เท่านี้ก็น่าจะแน่นหนาเพียงพอ แต่เจ้ากรรม ดันลืมไปว่าทิ้งกุญแจไว้ที่ลูกบิด! ดังนั้นสิ่งหนึ่งที่ต้องจำคือ Security ไม่ใช่ว่าเราทำอย่างเดียวแล้วแก้ได้ทุกอย่าง แต่เราจะต้องหาทางปิดความเป็นไปได้ทุกช่องทางไม่ให้เกิดรูรั่วหรือจุดอ่อนที่คนจะลอบเข้าบ้านได้ เราจึงต้องหาทางป้องกันทั้งจากภายนอกและภายในบ้านของเราครับ

บทความอีพีที่แล้วที่พูดถึง SSL Pinning เปรียบเสมือนการสร้างกำแพงสูงพร้อมด้วยลวดหนามที่ทำให้โจรปีนบ้านได้ยากขึ้น แต่ก็ใช่ว่าจะปีนไม่ได้อยู่ดี บทความนี้เราจึงจะมาดูกันต่อว่าทำยังไงไม่ให้ทุกคนลืมกุญแจค้างไว้ที่หน้าบ้าน เนื่องจาก iOS และ Android นั้นมีวิธีการป้องกันแอปของตัวเองเบื้องต้นให้แล้ว เราจะใช้ Native API เพื่อการณ์นี้กันครับ โดยขอแบ่งเนื้อหาออกเป็น 2 ส่วน คือส่วนที่ “จำเป็นต้องมี” กับส่วนที่ “มีก็ดี ไม่มีก็ได้”

จำเป็นต้องมี

1. Secure Data Storage

ในการบันทึกข้อมูลต่างๆ ลงเครื่องจะมีหลักสำคัญอยู่ 2 ข้อ

1.1 อย่าบันทึกข้อมูลสำคัญลงไป เช่น ชื่อ นามสกุล Password อีเมล เลขประจำตัวประชาชน หรือข้อมูลทุกประเภทที่ทำให้แฮกเกอร์สามารถย้อนรอยได้ว่าบุคคลที่ใช้แอปนี้คือใคร แต่ถ้าจำเป็นจริงๆ ให้บันทึกลงที่ Secure Storage เท่านั้น ซึ่งทั้ง 2 ระบบปฏิบัติการ (OS) ได้มีเตรียมพื้นที่ส่วนนี้ให้เรียบร้อย ผมเลือกใช้ Lib ตัวด้านล่างนี้สำหรับบันทึกครับ

ใน iOS จะใช้ตัวที่เรียกว่า Keychain ซึ่งจะไม่ถูกลบออก แม้ว่าผู้ใช้จะลบแอปแล้วก็ตาม ส่วน Android จะใช้ KeyStore คือจะเก็บ Key ที่ใช้ Encrypt Data ที่เราบันทึกไว้ใน Android OS

1.2 ต้องเข้ารหัสข้อมูลไว้เสมอ แม้จะบันทึกลง Secure Storage แล้วก็ตาม เพราะเราไม่รู้ว่าในอนาคตจะมีเครื่องมืออะไรมาแกะ Secure Storage ได้รึเปล่า และถ้าจะมีการเข้ารหัส ย่อมต้องมี Key เข้ามาเกี่ยวข้องด้วย Key ที่ใช้ควรเป็นแบบ Dynamic และ Unique สำหรับผู้ใช้งาน เราจึงเลือกเป็น Password เพื่อให้มั่นใจว่าถ้าผู้ใช้งานรู้ Password ก็ต้องสามารถแกะข้อมูลสำคัญเหล่านี้มาดูได้

แต่ความน่ากลัวคือถ้าแฮกเกอร์รู้ว่าเราทำแบบนี้และลองใช้ Password มา Brute Force ที่เข้ารหัสไว้ ก็จะรู้ Password นั้นแทน พอเป็นการเข้ารหัส Offline ที่อยู่ในเครื่อง แฮกเกอร์เพียงแค่รอแค่เวลาเท่านั้นว่าเมื่อไหร่จะถูกแกะได้ เราจึงต้องมีป้องกันเสริมอีกชั้น โดยเราจะอนุญาตให้แฮกเกอร์สามารถเอา Key ใดก็ได้มาแกะ แต่ข้อมูลจะไม่สามารถใช้ได้หากไม่มีการยืนยันจากทาง Backend ว่า Key อันไหนคืออันที่ถูก ซึ่งแฮกเกอร์สามารถลองกับทาง Backend ได้ 3 ครั้ง ถ้าผิดครบ เราก็จะล็อกบัญชีนั้นทันที เป็นการผสมผสานกระบวนการทำงานเพื่อปิดช่องโหว่นั่นเอง

2. ปิด Log บน Production

ผู้พัฒนาจำเป็นต้องใช้ Log ในการตรวจสอบว่าแอปของเราทำงานได้ถูกต้อง การใส่ Log นั้นไม่ถือว่าแปลกสำหรับ Non-Production แต่ไม่ควรทำใน Production ในทุกกรณี เราจะต้องปิดให้หมดด้วยวิธี Override debugPrint Function โดยนำโค้ดด้านล่างไปไว้ที่ main.dart

debugPrint = (String message, {int wrapWidth}) {};

การใส่โค้ดดังกล่าวหมายความว่าถ้าเราใช้ debugPrint เราจะไม่ทำการ Print ใดๆ ออกมา ผมจะแยกไว้ที่ส่วน main_dev.dart กับ main_prod.dart และนำฟังก์ชั่นด้านบนไปใส่ไว้ใน main_prod.dart เพื่อปิด Log นี้ แต่ไม่ต้องใส่ใดๆ สำหรับ Non-Prod หรือ main_dev.dart เหตุที่เราจำเป็นต้องปิด Log คือเพื่อไม่ให้ใครมาดู Device ได้ว่าเราส่งค่าอะไร หรือแอปเรากำลังทำงานใดอยู่บ้าง เพราะจะเป็นการบอกใบ้ให้แฮกเกอร์รู้ว่าจะต้องทำอะไรต่อ

ให้ใช้ debugPrint แทน Print Function เลยนะครับ

3. อย่าพยายาม Support ทุก OS เวอร์ชัน

เราสามารถปรับส่วนนี้ได้ที่ Native สำหรับ iOS ผมไม่ค่อยกังวลเรื่องเวอร์ชันเท่าไหร่ เพราะผู้ใช้ iOS มีแนวโน้มที่จะอัพเดต OS บ่อยๆ อ้างอิงจากบทความนี้

จะเห็นว่า 81% ของเครื่องทั่วโลกจะอัพเดตเป็น iOS 14 หลังออกมาได้ 3 เดือน เรื่อง Security เปรียบเสมือนเกมวิ่งไล่จับที่เราจะต้องพัฒนาและแก้ไขตลอดเวลา ไม่ใช่ทำครั้งเดียวแล้วเสร็จ ผู้พัฒนาจำเป็นต้องคอยปิด Security Exploit ที่เคยเจอมาใน OS ใหม่ๆ ผมจึงมองว่าสำหรับ iOS เรารองรับแค่ 2–3 เวอร์ชันล่าสุดพอ เช่น ปัจจุบันเป็น iOS 14 เรารองรับแค่ iOS12 จนถึง 14 ก็เพียงพอ แค่นั้นก็เท่ากับเรารองรับผู้ใช้งานกว่า 98% แล้ว ตรงนี้เราสามารถไปตั้งค่า Minimum Target ที่ Xcode ได้ครับ

ส่วน Android จะต่างออกไปครับ เนื่องจากเป็น Open Source ทำให้ Google ไม่สามารถควบคุมคนที่นำไป Implement ได้ ด้วยอัตราการอัพเดตค่อนข้างต่ำ เราจึงจำเป็นต้องรองรับ OS เก่าๆ ด้วย ปกติแล้วเราจะยึดตามประกาศ Google เช่น ถ้า Google บอกว่าเราจะไม่ออก Patch ใหม่สำหรับ Android 7.0 แล้ว แปลว่าเราต้องเริ่มเตรียมตัวเพื่อที่จะไม่รองรับ Android 7.0 เช่นกัน ดังนั้นของปัจจุบันจะแนะนำว่าให้ใช้ Android 8.0 ขึ้นไป โดยสามารถไปตั้งค่า minSdkVersion ใน build.gradle ให้เป็น 26

สรุปสั้นๆ คือ iOS 12 ขึ้นไป, Android build 26 ขึ้นไปครับ

4. ขอ Permission เท่าที่จำเป็น และตอนที่จะใช้เท่านั้น

อันนี้เป็นความรู้พื้นฐานในการเขียนแอปที่ทุกคนต้องทำตาม คือขอ Permission เท่าที่จำเป็นและตอนจะใช้เท่านั้น ยกตัวอย่างวิธีที่ผิด เช่น บางแอปขอ GPS Location ทั้งๆ ที่ไม่ได้มีฟีเจอร์ใดๆ ใช้เลย เพียงเพราะอยากเก็บ Location ของผู้ใช้ไว้ เป็นสิ่งที่ไม่ควรอย่างยิ่ง เพราะเรากำลังเก็บข้อมูลที่ไม่ได้มีประโยชน์ต่อผู้ใช้งาน และเผลอๆ แฮกเกอร์สามารถเข้าถึงข้อมูลเหล่านี้ ก่อให้เกิดการรั่วไหลไปอีก ส่วนที่บอกว่าควรจะขอ Permission เฉพาะตอนที่จะใช้เท่านั้น เคยเห็นบางแอปไหมครับ ขอ GPS, Bluetooth, Photo Album, Camera, Microphone, Push Notification ตั้งแต่เปิดแอปครั้งแรก ยังไม่ทันได้เริ่มใช้ก็โดนขอไป 5 อย่างแล้ว อย่างนี้จะมีผู้ใช้งานคนไหนอยากใช้ ให้ค่อยๆ ทยอยขอตอนที่คุณจะใช้เท่านั้น จะใช้ฟีเจอร์กล้องก็ค่อยขอ เป็นต้น

5. Jailbreak Detection

การใช้ Jailbreak Detection ของ Flutter เราจะไปใช้ Libs ส่วน Native ที่มีทำเอาไว้แล้ว เช่น Rootbeer สำหรับ Android และ DTTJailbreakDetection สำหรับ iOS ถือเป็น 2 ตัวที่คนนิยมใช้กัน สำหรับ Flutter เราจะใช้ Lib นี้ครับ

แม้ว่า Lib เหล่านี้จะไม่สามารถป้องกันแฮกเกอร์เก่งๆ ได้ แต่อย่างน้อยเอาไว้กันแฮกเกอร์ทั่วไป ซึ่งในท้องตลาดจะมี SDK สำหรับการตรวจเช็คพวกนี้อยู่แล้ว แต่ถ้าแอปของเราไม่ได้ต้องการ Security สูง ติดตั้งแค่ส่วนนี้ก็เพียงพอครับ

6. Biometrics กับ Cryptography

หลายแอปในปัจจุบันน่าจะมีการติดตั้ง Biometric Authentication เพื่อเสริมสร้าง User Experience ที่ดี แต่ส่วนใหญ่โค้ดจะรับ True/False มาจาก API แล้วทำเพียงแค่นั้น ซึ่งตาม Best Practice ยังไม่ถือว่าเพียงพอ ดูตัวอย่างได้จาก Apple ที่ออกมาแนะนำว่าเชื่อแค่ Flag จาก Apple ก็พอแล้ว

let reason = “Log in to your account”context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in 
if success {
// Login Succeed, do something next
} else {
// Failed, somebody else!!
}
}

แต่ในความเป็นจริงมีเครื่องมือชื่อ Frida Script ที่สามารถ Bypass ส่วนนี้ได้ ลองเข้าไปดูได้ครับ จะเห็นว่า Bypass ได้อย่างง่ายดายทีเดียว

ด้วยเหตุนี้เอง อย่าเชื่อ API เสมอไป เพราะแฮกเกอร์สามารถแก้ได้ ทาง Google เองก็มีการทำ Best Practice ให้ในรูปแบบของ Android ตามลิงก์ด้านล่างนี้

https://developer.android.com/training/sign-in/biometric-auth#crypto

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

7. อย่าส่ง Password เป็น Plain Text

ผมเคยอ่านที่เขาบอกว่าแค่ HTTPS เพียงพอแล้ว ไม่เห็นต้องสนใจส่วนนี้ แต่จริงๆ แล้วแฮกเกอร์สามารถทำ MITM (Man-in-the-Middle Attack) แกะข้อมูลที่ส่งออก จนได้ Password ของผู้ใช้คนนั้นไปเข้าบัญชีบนแอปอื่นๆ เพราะอย่างที่รู้กันว่าคนเรามักจะใช้ Password เดียวกันทุกที่ และสิ่งที่น่ากลัวไม่ได้มีแต่คนนอกเท่านั้น คนในที่สามารถเข้าถึงฐานข้อมูลก็เป็นอีกส่วนที่เราจะต้องป้องกันไว้ด้วย เพราะถ้าส่งเป็น Plain Text ไป ทาง Backend จะเห็นเลยว่าผู้ใช้ส่งอะไรมา ดังนั้นวิธีที่ดีที่สุดคือการส่ง Password ที่เป็น Hash และจะดียิ่งขึ้นถ้ามี Salt ด้วย จะได้ออกมาดังนี้

hash(password + dynamic salt)

Salt เค็มแต่ดี เอ้ย ไม่ใช่! 😔

Salt ต้อง Dynamic Unique User ครับ และในระยะเวลาสั้นๆ ด้วย แต่เราสามารถทำแบบ Basic แบบด้านล่างได้

hash(password + userID)

จะเห็นว่า userID นั้นก็เป็น Salt ที่ Dynamic และ Unique สำหรับผู้ใช้งาน แต่ยังไม่ตอบโจทย์เรื่องที่ต้องเปลี่ยนถี่ๆ ในเวลาสั้นๆ ทั้งนี้แบบเริ่มแรกสามารถใช้แบบนี้ไปก่อนได้ครับ แต่ถ้าต้องการ Security สูงๆ แนะนำให้เป็น Salt ที่มีอายุสั้นและเปลี่ยนตลอดเวลา และให้ใช้ Algorithms พวก Brcypt แทน SHA256 เพราะจะใช้เวลา Crack นานกว่า

หรือถ้าอยากให้ Secure สูงสุดคือ Body ต้องถูก Encrypt ไป แต่เราอาจจะไม่ได้ต้องการทำขนาดนั้นกับทุก API Call ทำแค่เฉพาะส่วนนี้ไปก่อนได้

ไม่ได้จำเป็นต้องมี แต่มีก็จะดีกว่า

ส่วนนี้จะเกี่ยวกับด้าน Financial Banking เป็นหลัก

8. Anti-Debugging

สำหรับ Android ผมจะแบ่งออกเป็นหลายๆ ฟังก์ชั่นดังนี้

8.1 Turn off Debuggable นำ Flag ไปใส่ใน build.gradle ในส่วนของ buildTypes โดยที่ผมจะเอา False ไปใส่ใน Release และ True ไปใส่ใน Debug แต่ถ้าต้องการใส่ False ทั้งคู่ก็ได้ครับ

buildTypes {
release {
debuggable
false
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable
true
signingConfig signingConfigs.release
}

}

8.2 Prevent Debugger

// Open ADB Debugging
if (Settings.Secure.getInt(this.applicationContext.contentResolver, Settings.Global.ADB_ENABLED, 0) == 1) {
}// Check by using `adb shell getprop ro.crypto.type`
if ((applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager).storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) {

}
// flag debuggable in gradle is true
if ((0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE || BuildConfig.DEBUG)) {

}
// Use Debugger in Android Studio to connect for getting log
if (Debug.isDebuggerConnected() || Debug.waitingForDebugger()) {

}

สามารถคัดลอกโค้ดด้านบนไปใส่ใน MainActivity.kt ได้เลยครับ ถ้าได้ค่า True ในฟังก์ชั่น แสดงว่าไม่ผ่าน Security ก็แล้วแต่ว่าต้องการจะไปทำอะไรต่อ ผมมักจะให้ Exit App อธิบายเป็นส่วนๆ ตามนี้ครับ

Open ADB Debugging ไว้ป้องกันการเปิด ADB อันนี้ไม่จำเป็นครับ แต่ใครอยากใส่ก็ได้ เพราะ Android Developer ส่วนใหญ่น่าจะเปิดโหมดนี้ไว้ทดสอบแอปอยู่แล้ว

ENCRYPTION_STATUS_UNSUPPORTED เช็คว่า Storage ในเครื่องสามารถ Encrypt ได้หรือไม่ โดยปกติเครื่อง Android จะเปิดเป็น Default อยู่แล้ว ถ้าไม่ได้เปิดอยู่ แสดงว่าเครื่องผิดปกติ เพราะคนทั่วไปจะปิดเองไม่ได้ พอสงสัยว่าน่าจะมีความเสี่ยง ก็ปิดไม่ให้ใช้เสียดีกว่า

ApplicationInfo.FLAG_DEBUGGABLE จะใช้คู่กับ ข้อ 8.1 ถ้าเราเช็คว่า Debugger Flag เปิดอยู่ เราก็ไม่ให้ผู้ใช้งานใช้เช่นกัน

Debug.isDebuggerConnected() เช็คว่าแอปถูก Debugger อยู่รึเปล่า โดยปกติจะต้องใช้คู่กับ Android Studio จึงจะเปิดตัว Debugger มาดูได้ เราเลือกปิดส่วนนี้เช่นกัน

ส่วน iOS ถึงจะยังไม่มี Solution ง่ายๆ ที่สามารถ implement ได้เอง แต่ก็มีพวก SDK แบบจ่ายเงินที่เข้ามาช่วยตรงส่วนนี้

9. เช็คว่า Device มีรหัสก่อนเข้าเครื่องไหม

ยังมีหลายคนที่ไม่ตั้งรหัสเข้าเครื่องครับ ไม่ทำ Finger Scan หรือ FaceID ใดๆ ทำให้แอปมีความเสี่ยงที่จะถูกแฮคได้ง่ายขึ้น เพราะเมื่อแฮกเกอร์ได้ Device ไป ขั้นแรกที่จะต้องแก้ให้ได้ก็คือการปลดล็อคเครื่อง แล้วถึงจะมาปลดล็อคแอปของเรา ตรงนี้เปรียบเสมือนกับบ้านที่กันไว้หมดทุกอย่าง แต่ดันไม่มีประตูรั้วบ้าน ใครก็เดินเข้ามาได้

สำหรับ Android ผมใช้ Flutter Channel เพื่อเรียก Native Function แล้วส่งค่ากลับไปให้ Flutter เพื่อเอาไปทำงานต่อ โดยปกติก็จะ Toast Message บอกว่าแอปมีความเสี่ยง กรุณามี Pin ก่อนเข้าเครื่องนะ

private val CHANNEL = "com.kbtg.flutter"
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getDeviceHasPasscode") {
result.success((getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager).isDeviceSecure)
} else {
result.notImplemented()
}
}

ใน Flutter ให้เรียกตามด้านล่าง

try {  final hasPasscode =await Storage.platform.invokeMethod('getDeviceHasPasscode');if (!hasPasscode) {Toast.show("No pin, DANGER DANGER",context,duration: 5,gravity: Toast.BOTTOM,);}} on PlatformException catch (e) {debugPrint("==== Failed to scan security '${e.message}' ====");}

ถ้าได้ hasPasscode เป็น False เราจะแจ้งเตือนผู้ใช้งานว่าเครื่องคุณมีความเสี่ยง

สำหรับ iOS เราจะใช้ฟังก์ชั่นนี้

private var isDeviceHasPasscode: Bool {   return LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)}

จากนั้นให้ Implement แบบนี้ใน AppDelegate.swift

if let controller = window?.rootViewController as? FlutterViewController {   let channel = FlutterMethodChannel(   name: "com.kbtg.flutter",   binaryMessenger: controller.binaryMessenger)   channel.setMethodCallHandler({ [weak self] (   call: FlutterMethodCall,   result: @escaping FlutterResult) -> Void in   switch call.method {
case "getSecure":
if let data = call.arguments as? [String: String], let value = data["data"] { self?.getSecureFileIO(result: result, from: value) } else { result(FlutterMethodNotImplemented) }
})
}

10. ปิด 3rd Party Keyboard

เนื่องจากเราไม่รู้ว่าเบื้องหลัง 3rd Party Keyboard เขาแอบส่งข้อมูล Password หรืออะไรที่เราพิมพ์ไปรึเปล่า Keyboard ชื่อดังๆ อาจจะไม่ต้องกังวล แต่บางทีเราอาจจะไปเจอ Keyboard สวยๆ เป็นตัวการ์ตูนแล้วนำไปใช้ อาจทำให้เสี่ยงได้ เราจะช่วยลดความเสี่ยงตรงนี้ด้วยการไม่อนุญาตให้ใช้ 3rd Party Keyboard ส่วนนี้สำหรับ iOS ทำได้ง่ายๆ ไม่มีปัญหาครับ แต่ Android จะยากหน่อย เพราะว่า Android มองทุก Keyboard เป็น 3rd Party ทั้งหมด แม้กระทั่งของ Android เอง วิธีแก้คือเราต้อง Implement Keyboard เอาเอง ซึ่งผมมองว่าการต้องทำ Keyboard ก-ฮ เองนั้นอาจจะ Overengineer เกินไปสักหน่อยสำหรับแอปทั่วๆ ไป

ของ iOS เราใช้ฟังก์ชั่นตามด้านล่างนี้ได้เลยครับ

override func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplicationExtensionPointIdentifier) -> Bool {   return extensionPointIdentifier != .keyboard}

11. ตรวจสอบ Code Integrity

เนื่องจากไฟล์ iPA และ APK นั้นสามารถ Decompile แก้โค้ดและ Build ใหม่อีกครั้งเพื่อแจกจ่ายได้ จึงทำให้แฮกเกอร์สามารถแก้โค้ดส่วน Logic เช่น ให้แอปแอบส่งข้อมูลไปที่เซิร์ฟเวอร์ปกติ แต่ก็ให้ส่งไปที่เซิร์ฟเวอร์ของแฮกเกอร์ด้วย สำหรับ iOS จะไม่ค่อยน่าเป็นห่วงเท่าไหร่ เพราะคนส่วนใหญ่จะโหลดจาก App Store อยู่แล้ว จะโหลดจาก Store อื่นๆ ก็ยุ่งยากในการติดตั้ง ผิดกับ APK ของ Android ที่สามารถติดตั้งแอปนอก Store ได้อย่างง่ายดาย เพียงแค่คลิก 2–3 ทีจบ

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

12. Code obfuscation

ไม่ว่าจะเป็น Business Logic หรือ Security Logic ที่พัฒนาและใส่ลงไปในแอป เราต้องมั่นใจว่าในกรณีที่ถูก Decompile และเจอแกะ Source Code แฮกเกอร์จะไม่สามารถอ่านและเข้าใจ Source Code ของเราได้ ไม่เช่นนั้นแฮกเกอร์ก็จะสามารถทำการแก้ไขได้ การที่เราทำ Code Obfuscation จะช่วยปิดปัญหาส่วนนี้ให้ ซึ่งโดยทั่วไปแล้ว Code Obfuscation ก็จะมี Commercial SDK ให้เช่นกัน

จะเห็นว่าหลายส่วนเราแก้ปัญหาได้ด้วย Commercial SDK อาจทำให้มีคำถามตามมาว่า “แล้วจะมาอ่านบทความนี้ทำไม อันนั้นก็ให้ใช้ อันนี้ก็ให้ใช้ มาอ่านเพราะไม่ต้องการใช้ ต้องการทำเอง”

ผมมองว่าเราไม่มีทางเก่งทุกเรื่องครับ ถ้าเก่งทุกเรื่องจริงก็จะได้ไม่สุด ดังนั้นการใช้ Commercial SDK หมายความว่าคุณมีกลุ่มที่เป็นผู้เชี่ยวชาญด้าน Security ทำงานให้กับคุณอยู่ คุณคนเดียวไม่สามารถปิดช่องว่างเรื่อง Security ได้หมด เพราะจะต้องมีการตามข่าวสารและแก้ไขตลอดเวลา ผมจึงมองว่าเราให้ผู้เชี่ยวชาญทำงานที่เขาเก่งไป แล้วเรามาทำงานที่เราเก่งดีกว่า คือการพัฒนาแอปนั่นเอง

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

หลักการตั้งรหัสผ่าน

รหัสผ่านเป็นตัวยืนยันว่าคนที่ใช้งานแอปของเราเป็นเจ้าของจริงๆ เพราะคนที่รู้รหัสดังกล่าวจะมีเพียงเจ้าของเท่านั้น และเราเลือกใช้ Pin ในการเข้าแอปเนื่องด้วย 2 เหตุผลหลักๆ คือ

1. Better User Experience

จริงๆ แล้ว Password นั้นมีความปลอดภัยกว่า Pin เพราะเป็นการผสมผสานของตัวอักษร A-Z ทั้งพิมพ์ใหญ่/พิมพ์เล็กและตัวเลข 0–9 ถ้าเรากำหนดให้ Password ต้องมีความยาวอย่างน้อย 8 ตัวอักษร ก็จะได้ 218 ล้านล้านรูปแบบ (26+26+10) กำลัง 8 (ฟังดูเยอะสุดๆ แต่ FYI ว่าคอมพิวเตอร์สมัยนี้สามารถแกะ 218 ล้านล้านรูปแบบได้ภายใน 24 ชั่วโมงเท่านั้น) ในเมื่อ Password เข้มแข็งกว่าตั้งเยอะ แล้วทำไมเรายังเลือกใช้ Pin? เพราะ Password มีปัญหาเรื่องการพิมพ์ที่ค่อนข้างยุ่งยาก ลองคิดดูสิครับ Keyboard เล็กๆ เราจะต้องมาพิมพ์ทุกครั้งที่เข้าแอป 1 วัน เข้าสัก 4–5 ทีก็ลำบากแล้ว เราจึงตัดสินใจว่า Pin เป็นทางเลือกที่ดีกว่า แต่เราเพียงต้องเพิ่ม Control บางอย่างให้ Pin ที่แม้ความน่าจะเป็นของการผสมเลข 6 ตัวจะมีแค่ 1,000,000 แบบก็ยังปลอดภัย

และยิ่งไปกว่านั้น Pin ใช้เวลาเพียงแค่ 2–3 วินาทีในการพิมพ์ ในขณะที่ Password อาจจะใช้เวลาถึง 15 วินาทีเลย

2. จำง่าย

แน่นอนเราต้องการบาลานซ์การใช้งานง่ายกับเรื่อง Security ครับ ไม่ใช่ว่าทำให้ยากจนนอกจากแฮกเกอร์แล้ว ผู้ใช้งานเองก็เข้าไม่ได้ เราต้องการให้แฮกยากแต่ยังใช้งานง่ายอยู่ ซึ่ง Pin ของทั้ง iOS/Android ใช้เป็นเรื่องทั่วไปอยู่แล้ว ผู้ใช้งานย่อมคุ้นเคยอยู่หากต้องมาใช้งานกับแอปด้วย

คำกล่าวที่ว่า ‘Pin ไม่ยาก’ อาจดูไม่ค่อยเป็นรูปธรรมสักเท่าไหร่ ต้องขนาดไหนถึงจะเรียกว่าไม่ยาก ณ ปัจจุบันก็ยังไม่มีมาตรฐานว่าทำยังไงให้ Pin ยาก แต่ลองดูข้อมูลตามผลวิจัยต่อไปนี้กันครับ

https://www.researchgate.net/publication/313823128_Understanding_Human-Chosen_PINs_Characteristics_Distribution_and_Security

Credit https://www.researchgate.net/figure/Top-ten-6-digit-PINs-in-each-PIN-dataset_tbl2_313823128

นักวิจัยได้แสดงผลให้ดูว่า Pin แบบไหนที่คนใช้เยอะ ซึ่งเราจะไม่ให้ใช้ ไม่อย่างนั้นแฮกเกอร์จะเดาได้ง่าย ทำให้ออกมาเป็นกฎมา 3 ข้อนี้

  1. ไม่ให้ใช้เลขเรียงกัน เช่น 123456, 234567, 345678 หรือแม้กระทั่งเลขเรียงย้อนหลัง เช่น 654321, 543210
  2. ไม่อนุญาตให้ใช้เลขบนแถวเดียวกัน เช่น 123123, 456456, 789789
  3. ต้องมีเลขไม่ซ้ำกันมากกว่า 3 เลขขึ้นไป เช่น 122112 ไม่ได้ แต่ 123321 อนุญาตแม้ว่าอาจจะดูขัดๆ กับด้านบน ตัวอย่างอื่นๆ เช่น 155115, 133133, 166661 จะไม่อนุญาต เพราะตัวเลขซ้ำกันมีแค่ 2 ตัวเท่านั้น
  4. (เสริม) ผมอ่านบางบทความมาว่าไม่อนุญาตให้ใช้วันเกิดของผู้ใช้งานเองด้วย ซึ่งผมว่าสมเหตุสมผลดี เช่น วันเกิด 8 มิถุนายน 1986 เราก็จะไม่ให้ใช้ 080686 หรือ 080629 แต่ผมละส่วนนี้เพราะแอปที่ทำไม่จำเป็นต้องรู้วันเกิด เราจึงไม่มีข้อมูลดังกล่าวเพื่อใช้ป้องกัน

แน่นอนครับว่าเราไม่ได้อยากใช้ข้อกำหนดเยอะเกินไป เพราะจะไปตัดความเป็นไปได้ของ Pin เยอะเกิน ถ้าข้อบังคับเยอะ Pin จาก 1,000,000 รูปแบบอาจจะเหลือเพียงแค่ 200–300 รูปแบบเท่านั้น กลายเป็นเดาได้ง่ายกว่าเดิมอีก

จากวิธีด้านบน เราตัดความเป็นไปได้จนเหลือประมาณ 60,000 กว่า ส่วนวิธี Implement จะเป็นดังนี้ครับ

static bool isPinComplexity(String pin) {const notAllowListPin = ["123123","456456","789789","012345","123456","234567","345678","456789","567890","098765","987654","876543","765432","654321","543210"];final pinSet = new Set.from(pin.split(""));final uniqueCharacter = pinSet.length > 2;return uniqueCharacter && !notAllowListPin.contains(pin);}

เราสามารถใช้ฟังก์ชัน isPinComplexity ตรวจสอบว่า Pin ที่ผู้ใช้ตั้งนั้นปลอดภัยพอไหม และต้องเช็คที่ Backend ด้วยนะครับเพื่อความถูกต้อง เราใส่ใน Frontend สำหรับ User Experience เท่านั้น แต่ที่ Backend เราทำไว้เพื่อเหตุผลทางด้าน Security ไม่ให้ใครมา Bypass Pin ได้

ถ้าใครอยากจะเพิ่มเงื่อนไข ก็สามารถไปใส่เพิ่มใน notAllowListPin ได้ครับ

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

สำหรับชาวเทคคนไหนที่สนใจเรื่องราวดีๆแบบนี้ หรืออยากเรียนรู้เกี่ยวกับ Product ใหม่ๆ ของ KBTG สามารถติดตามรายละเอียดกันได้ที่เว็บไซต์ www.kbtg.tech

--

--

Amorn Apichattanakul
KBTG Life

Google Developer Expert for Flutter & Dart | Senior Flutter/iOS Software Engineer @ KBTG