[เล่าประสบการณ์] หรรษาไปกับปัญหาของ RSA บน Android API 23–24
เรื่องราวของ RSA ที่สามารถทำงานได้เกือบทุกเวอร์ชัน ยกเว้น Android 6
เกริ่นแนะนำพอเป็นพิธี
สวัสดีครับ ผมชื่อ “หมี” เป็น นิสิตฝึกงานที่ Nextzy ในตำแหน่งแอนดรอยด์
โดยเรื่องที่เล่าในวันนี้ คือ เกิดขึ้นจากพี่ที่ดูแลผมในระหว่างที่กำลังฝึกงานอยู่ได้ให้โจทย์ว่า ทำไมโค้ดที่เขียนขึ้นมาเกี่ยวกับ RSA เมื่อทดสอบบน Android API 23–24 กลับเจอปัญหาแปลก ๆ คือไม่สามารถรันเทสผ่านได้ แต่ในเวอร์ชันที่สูงกว่า เช่น 25 เป็นต้นไป กลับรันเทสผ่านปกติ
ต้องทำอะไรบ้าง?
หลัก ๆ คือ แก้ไฟล์ RSAUtil ที่ใช้ในการเข้าและถอดรหัส ให้สามารถรันเทสผ่านตั้งแต่ Android API 23 ขึ้นไป ทั้งนี้ TestCase มี 2 อัน
- TestCase1 จะเป็นการเอาข้อความต้นฉบับมาเข้ารหัส RSA ด้วย PublicKey จากนั้นจับชุดข้อความที่เข้ารหัสแล้วมา ถอดรหัสด้วย PrivateKey อีกทีนึง โดยเปรียบเทียบความถูกต้องกับข้อความต้นฉบับ เพื่อที่จะเช็คว่า กระบวนการเข้ารหัสสามารถทำงานได้อย่างถูกต้อง เพราะหากทำงานไม่ถูกต้อง ข้อความที่ออกมาจากกระบวนการจะไม่ตรงกับข้อความที่ใส่ไปนั้นเอง
TestCase2 เหมือนกับเคสแรก เพียงแค่เทสเคสนี้มีข้อความที่เข้ารหัสมาแล้ว จับมาถอดรหัสด้วย PrivateKey แล้วผลลัพท์ที่ออกมาต้องเหมือนกับข้อความต้นฉบับ
พังจริงไหม ต้องพิสูจน์!!
หลังจาก Clone โปรเจคมาจาก Github ก็จัดการรันเทสบน API เวอร์ชันที่คิดว่าผ่านก่อน อย่างน้อยก็ยืนยันว่ามันทำงานได้จริงนะ จัดไป API 29 หมุน ๆๆๆ ปั๊ง! ผลคือ ผ่านทั้งสองอันเลยจ้า!!
ในเมื่อผ่านแล้ว ก็ไปดูปัญหาที่เกิดขึ้นใน API 23 กันต่อ โดยสร้าง Emulator ขึ้นมาแล้วก็รันเทสซะ หมุน ๆๆๆ ตู้ม!!!
จาก Dying message ของผู้ตาย มันบอกว่าได้รับ Null กลับมา ทั้งสอง TestCase เลย แสดงว่า RSAUtil มันถอดรหัสไม่ได้ หรือต้องมีอะไรพลาดไป?
It’s time to be Conan!!
ลิสรายชื่อผู้ต้องสงสัย
- Cipher ทำงานพลาดรึเปล่า?
- ByteArray ที่ได้มาจาก StringKey นั้น Decode ถูกต้องรึเปล่า?
- String ที่ผ่าน Encode จาก ByteArray นั้นถูกต้องรึเปล่า?
- Base64 flag พวก Default, No wrap, No padding มีส่วนเกี่ยวข้องไหม?
จากนั้นก็ค้นหาข้อมูลจากผู้ต้องสงสัยเหล่านั้น เริ่มจากปัจจุบัน ไล่ไปจนถึงจุดที่ต้องย้อนเวลาหาอดีต ไปในปี 2015–2016 ปีที่ API 23–24 ออกมาและกำลังขึ้น API 25
สิ่งที่พบคือ โค้ดไม่แตกต่างกับใน RsaUtil ที่เทสเลย..
ด้วยความมึนงงกับสิ่งที่เจอตรงหน้า ขอลองอะไรหน่อยละกัน ก็อปโค้ดนั้นเลยจ้า! แล้วก็เขียนฟังก์ชันว่าง ๆ ขึ้นมาตัวนีง วางโค้ดลงไป จากนั้นก็ยิงมัน บน API 23
รางวัลที่ออกคือ พัง!
มองซ้ายมองขวา บ่ายหนึ่งแล้วสิ กองทัพต้องเดินด้วย KFC ฝากไว้ก่อนนะเดี๋ยวกลับมา!!
ถ้าเป็น Key ที่มันสร้างใหม่ล่ะ จะพังรึเปล่า?
หลังจากกลับมาจากการไปหาผู้พันชุดขาว ก็อยากลองอะไรบางอย่าง..
ถ้าเขียนฟังก์ชันนึง โดยให้มันสร้าง PrivateKey กับ PublicKey ขึ้นมาใหม่ทั้งหมด จากเข้ากระบวนการของ Encrypt และ Decrypt เพื่อทดสอบ Cipher
เริ่มจากให้มันสร้าง PrivateKey กับ PublicKey ขึ้นมา
จากนั้นแก้พารามิเตอร์ให้รับ Key และลบโค้ดแปลง String เป็น Key เพราะเราไม่จำเป็นต้องแปลงอีกแล้ว โดยส่วนที่เหลืออย่าง Cipher และ Base64 ยังคงเดิม
เขียนเทสอีกตัวมาเพื่อทดสอบ
แล้วเลือก API 29 รัน! หมุน ๆๆ ผ่าน!
ยัง ๆๆ ต้องลองเทสบน API 23 ดูบ้าง จากนั้นก็ รัน! หมุน ๆๆๆ (คาดหวังว่า แดงแน่นอน ยังไงก็แดง)
เขียว ผ่านจ้า!!
เริ่มหัวร้อนล่ะ เกิดอะไรขึ้นว่ะ? ทำไมวิธีนี้เข้ารหัสถอดรหัสได้ เท่ากับว่า Cipher คือผู้บริสุทธ์ … แน่นอนต้อง Base64 แน่ ๆ เลย เริ่มจับผิดด้วยการไล่ Print ค่า ByteArray ทุกตัว ทั้งสอง SDK เปรียบเทียบกันเลย แต่งตั้ง Python มาเป็นคนกลางเปรียบเทียบ ผลที่ได้คือ ค่า ByteArray ตรงกันทุกตัวและ Base64 รอด ..
หมดผู้ต้องสงสัย แต่คนร้ายยังลอยนวล?
ณ เช้าวันที่ 2 จะเรียกได้ว่าเจอทางตันก็ว่าได้ ผู้ต้องสงสัยไม่มีเหลือแล้ว ลองขุดคุ้ยทุกเว็บทุกเรื่องที่คิดว่าเกี่ยวข้อง จนมาสงสัยเรื่องการ Generate Key ผู้รับผิดชอบคือ KeyFactory
ลงมือสืบ และแล้วก็มาสะดุดกับเว็บนี้
แสงสว่างก็เรืองรอง ไม่มีรู้ว่าจะเกี่ยวรึเปล่า น่าจะไม่ก็ได้ … ข้างในมีการกล่าวถึง Provider ชื่อ “BC”
“BC” หรือ “BouncyCastle” ก็คือหนึ่งใน API ที่ใช้ในเข้ารหัสข้อมูล รองรับทั้ง Java และ C# พัฒนาโดย “Legion of the Bouncy Castle Inc” ประเทศออสเตรเลีย
พึ่งรู้ว่ามันใส่ Provider ได้ .. ╮( ̄ω ̄;)╭
จัดการใส่ “BC” ลงไปใน KeyFactory แล้วรันเทสบน 23!
เห้ย! API 23 ผ่านแล้ว!!! เย้! .. เดี๋ยว ๆ แต่ทำไม API 29 แดงแทนฟ่ะ!?
ลองลบ Provider เปลี่ยนกลับแบบเดิม ผลคือ API 29 ผ่าน แต่ API 23 แดงอีกแล้ว
ฉะนั้น! ต้องแยก 2 ชุดนี้ออกจากกัน โดยให้ API ที่สูงกว่า 23–24 หรือสูงกว่า Version Code “M” ใช้คำสั่ง
KeyFactory.getInstance(“RSA”);
นอกนั้นก็ ใช้คำสั่งที่มีการใส่ BC Provider แทน
KeyFactory.getInstance(“RSA”,”BC”);
ได้เงื่อนไขแล้ว ก็อัญเชิญ if-else มา! บรึ่ม!
เพื่อความแน่ใจว่าทุก SDK ตั้งแต่ API 23 ไปจนถึง API 29 สามารถรันได้ ต้องเทสให้หมดสิ
รันผ่านหมด!!
แต่ เมื่อคิดจะทดสอบทุกเวอร์ชัน ต้องมีการเสียสละ.. SSD!
สรุป
ที่มันพังคือตอนที่ เอา ByteArray จากได้จาก String ไปทำเป็น PrivateKey แล้วใน Android 6.X มันเข้าเงื่อนไขอะไรสักอย่าง นี่น่าจะเกี่ยวกับ Provider ที่ชื่อว่า “AndroidOpenSSL” ซึ่งเป็นค่า Default ทำให้มันส่ง Null กลับมา
โยน Null ไปให้ Cipher ใช้ถอดรหัส ผลคือ มันถอดไม่ได้ (ถ้าเกิดมันถอดได้ก็เตรียม ShipLost)
ส่วนใน Android ที่สูงกว่า M ตัว KeyFactory สามารถสร้าง PrivateKey ออกมาได้ถูกต้อง ใช้งานได้ปกติ! ( ` ω ´ )
ฝากของทิ้งท้าย
นี่เป็นบทความ Medium อันแรกที่ผมได้เขียน คิดว่าหลังจากนี้ น่าจะมีเพิ่มเรื่อย ๆ มีของหมักดองไว้อีกเพียบ ไม่ว่าจะเป็น
- Swift Vapor 3 + PostgreSQL +Heroku ที่ยังไม่มีใครเขียนเลย ดำดิ่งอยู่หลายเดือนมาก _(:3 」∠)_
- Android Jetpack Compose ส่วนของ Components ที่แยกย่อยเยอะมาก ๆ!
- Kotlin Ktor + Exposed + PostgreSQL + Heroku อันนี้ยังไม่เจอคนเขียนเลย
- และอื่น ๆ
สวัสดีขอบคุณและบายครับ~ ( * ́꒳`*)੭