การออกแบบตัวเข้ารหัสให้แข็งแกร่ง

Wasith T. (Bai-Phai)
กูโค้ด
Published in
2 min readMar 2, 2020

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

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

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_(ECB)

ไม่ได้พูดถึง mode การเข้ารหัสในบทความนี้ ผมบอกได้แค่ว่า

  • บาง mode เน้นเรื่อง performance
  • บาง mode สามารถถอดรหัสหลาย ๆ block พร้อม ๆ กันได้
  • บาง mode เน้นเรื่องความปลอดภัย
  • บาง mode ไม่สามารถถอดรหัส block ต่อ ๆ ไป ได้ถ้า block ก่อน ๆ หน้าพัง
  • บาง mode จะสามารถใช้ IV ได้
  • และบอกไม่ได้ว่า padding กับ nopadding ต่างกันยังไงนะ 555

แล้วทำยังไงให้ไม่เห็น pattern

IV (initialization vector) เป็นวิธีการหนึ่งที่ถูกสร้างมาเพื่อแก้ปัญหาเหล่านี้ ซึ่ง IV ควรจะถูกสร้างใหม่แบบสุ่ม ๆ ทุก ๆ ครั้งที่ใช้งาน หรือทุก ๆ กี่ block ของข้อมูลก็ว่าจะไปซึ่งถ้าจะ implement ให้มันสุ่มทุก n block อาจจะยากไปหน่อย (คนเขียนทำไม่ได้และขี้เกียจ 555) และควรใช้ SecureRandom หรืออะไรก็ได้ที่การสุ่มค่านั้น ๆ ได้มาตรฐานว่าเกิดการสุ่มจริง และถ้า IV ไม่ได้เกิดจากการสุ่มก็ไม่ต่างอะไรกับการเข้ารหัสด้วย AES แบบใช้ key อย่างเดียว

fun generateIv(random: SecureRandom): ByteArray {
val key = ByteArray(16)
random.nextBytes(key)
return key
}

เนื่องจาก key ถูกออกแบบมาให้เป็น private แล้วคือห้ามหลุดไปข้างนอกเด็ดขาด IV จึงยอมให้เป็น public ได้ ซึ่งโดยปกติเราจะแปะ IV ที่ข้อมูลที่เข้ารหัสแล้ว และมักแปะไว้ข้างหน้าของข้อมูลที่เข้ารหัส ทั้งนี้ใครจะออกแบบว่าจะแปะตรงไหนก็แล้วแต่จะตกลงกันเลย

fun encrypt(original: ByteArray): ByteArray {
val iv = generateIv()
val aes = AES(key, iv)
val encrypted = aes.encrypt(original)
return iv + encrypted
}

ซึ่งนั่นแปลว่าการถอด เราต้องแกะ IV ออกมาจากข้อมูลก่อน

fun decrypt(encrypted: ByteArray): ByteArray {
val iv = encrypted.sliceArray(0..15)
val encrypted = bytes.sliceArray(16..bytes.lastIndex)
val aes = AES(key, iv)
return aes.decrypt(encrypted)
}

การออกแบบ key

สมมุติว่าต้องสร้าง key ขนาด 256 bits เรามักจะใช้ string ยาว 32 ตัวอักษร ซึ่งเป็นเรื่องที่ไม่ควร เราไม่ควรใช้แค่ printable characters เพราะจะทำให้ความน่าจะเป็นของ key ลดลงมาก เพราะ non-printable characters จะไม่ถูกรวมในความน่าจะเป็นของ key

val key = "!WVPm`RJBa.4Xwo@z_*\"9qj8NbHe LT2"
val bytes = key.toByteArray()

ให้ดีถ้าต้องเก็บเป็น config และต้องเป็น stirng เท่านั้น อาจจะเก็บเป็น hex code ยาว 64 ตัวอักษร แล้วแปลงเป็น ByteArray แทนจะปลอดภัยกว่า และใช้ความน่าจะเป็นได้ทั้ง 256 bits

val key = "AA BB CC DD EE..."
val bytes = hexToBytes(key)

เจาะลึกลงไป

ว่าทำไมแค่เราเสก key จาก printable characters ให้ครบ 256 bits ไม่พอใช้ ถ้าเราดูตามตาราง ascii

  • 0-31 และ 127 รวม 33 ตัว เป็น control characters ซึ่งแสดงเป็นตัวอักษรไม่ได้ มันเอาไว้เป็น command ไปหาคอมพิวเตอร์ เช่นเลื่อน cursor สั่งลบตัวอักษรก่อนหน้า ฯลฯ ชุดนี้ไม่นับ
  • 32–47, 58–64, 91–96, 123-126 รวม 16+7+6+4 = 33 ตัว เป็นอักขระพิเศษ
  • 48–57 รวม 10 ตัว เป็นตัวเลข 0–9
  • 65–90 เป็นตัวพิมพ์ใหญ่ รวม 26 ตัว
  • 97–122 เป็นตัวพิมพ์เล็ก รวม 26 ตัว

รวมทั้งสิ้น 95 ตัว อักษร นั่นเท่ากับว่า key ที่ยาว 256 bits นั้นจะเท่ากับ log2(95³²) ซึ่งเท่ากับประมาณ 210 bits เท่านั้น

แล้วถ้าเราออกแบบ key แต่ใช้ พิมพ์เล็ก (26 รูปแบบ) และตัวเลข (10 รูปแปป) จะเท่ากับ log2(36³²) = ประมาณ 165 bits เท่านั้น

ปล.

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

ทั้งนี้ทีม audit และ security ควรจะเข้ามากำกับดูแลตั้งแต่เนิ่น ๆ เพราะของพวกนี้ส่วนใหญ่ทำครั้งเดียวและพอขึ้น production ไปแล้วจะแก้ไขได้ยาก

ถ้าทำไปแล้ว ก็ migrate ได้ แต่การ migrate ไม่ใช่เรื่องง่ายอยู่แล้ว

อ้างอิง

… ไม่มีอะ 555 อ่านมานานแล้วหาไม่ได้

ถ้าผิดโปรดบอก

--

--

Wasith T. (Bai-Phai)
กูโค้ด

ตบมือเป็นกำลังใจให้ผมด้วยนะครับ 😘