Tutorial

Hash Password ด้วย bcrypt บน Python

By Arnon Puitrakul - 14 กุมภาพันธ์ 2022 - 2 min read min(s)

Hash Password ด้วย bcrypt บน Python

เมื่อไม่กี่วันก่อน เราไปเห็นงานระบบนึง แล้วมีพวกส่วนของการเก็บ User Info ต่าง ๆ แล้วปรากฏว่า ไปขอดู Data Dictionary มา ก็คือโป๊ะแตกว่า หนูลูก.... หนูเก็บ Plain Password จังหวะนั้นก็คือ อารมณ์อยากโยกหน้าก็เข้ามาทันที ไม่ควรทำเนอะ ทำให้วันนี้เราจะมาสอนว่า จริง ๆ แล้ว เราจะเก็บ Password เราจะต้องทำอย่างไรกัน

ทำไม เราไม่ควรเก็บ Password เป็น Plain Text ?

มาเริ่มจากว่า ทำไม เราอยากจะโยกหน้าคนเขียนกันก่อน ด้วยคำถามที่ว่า ทำไมเราไม่ควรจะเก็บ Password เป็น Password จริง ๆ เลย เอาจริง ๆ ถ้าเราเก็บแบบนั้นเลย เวลาเราทำงาน ตอนเรา Register User เข้ามา เราก็แค่เก็บ Password ที่ได้รับมาเลย แล้วพอ เราจะ Login เราก็เทียบเอาตรง ๆ เลย เออ มันก็ดูตรงไปตรงมานิ แต่......

ลองคิดดูนะว่า ถ้าเกิด Database ของเราโดนเจาะขึ้นมา Hacker สามารถเข้าถึงข้อมูลได้หมดเลย นั่นแปลว่า Password ก็คือ หลุด ไปหมดเลยนะ เขาไม่ต้องเอาไปทำอะไรต่อเลย เพราะเขาได้ Password ไปเต็ม ๆ แล้ว ซึ่งเป็นเรื่องที่ไม่ดีแน่นอน

หรือถ้าเกิดวิธีการ Authenticate เรามีการส่งข้อมูลข้ามไปมาผ่านระบบ Network ด้วย ก็ยิ่งเพิ่มความเสี่ยงเข้าไปใหญ่ ที่จะโดนดักข้อมูล (Data Sniffing) ได้ เช่น เราบอกว่า เราส่ง Username และ Password ไปตรง ๆ เลย แล้วเราเป็นคนดักข้อมูล เราก็นั่งเช็คเลยว่า Request ไหนที่มีการส่ง Username และ Password ไป แล้ว Respond ต่อไปเป็นการ Redirect ไปหน้า Dashboard หรือหน้าอะไรก็ตามที่ Login ผ่านแล้วจะเข้าไป นั่นแหละ เป็น Username และ Password ที่ถูกต้องแน่นอน

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

เราจะทำยังไงดี ?

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

วิธีการนึงที่ถ้าคนที่อาจจะเคยผ่านพวก Computer Security มาบ้างน่าจะพอนึกออกนั่นคือ Hashing กับ Encryption ทั้ง 2 วิธีนี้ สามารถช่วยให้เราสามารถเก็บ หรือส่งข้อมูลผ่านตัวกลางที่ไม่ปลอดภัย อย่างปลอดภัยได้ ถามว่า 2 วิธีนี้ต่างกันอย่างไร เราเคยเขียนไว้แล้วในบทความด้านบนนี้เลย

ถ้ายาวไปไม่อ่าน สั้น ๆ คือ Hashing มันจะใช้ Function ทางคณิตศาสตร์ที่มีความสัมพันธ์แบบ Many-to-One นั่นหมายความว่า 1 ค่า Hash สามารถเกิดจากได้หลาย Password เลย ทำให้ เราไม่สามารถที่จะแปลงค่า Hash กลับไปเป็น Password ได้

แต่กลับกัน Encryption เราใช้ Function ทางคณิตศาสตร์เหมือนกัน แต่ความสัมพันธ์จะเป็น One-to-One นั่นหมายความว่า เราสามารถเอาข้อมูลที่ Encrypt แล้วแปลงกลับมาเป็นข้อมูลต้นฉบับได้อยู่นั่นเอง โดยผ่านสิ่งที่เรียกว่า Key เราไม่ขอเล่าต่อเนอะ เพราะมันก็จะมีพวก Symmetric Encryption และ Asymmetric Encryption อีก

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

นอกจากนั้น Hashing ยังดีกว่าในกรณีนี้ เพราะถ้าเกิดข้อมูลหลุด หรือถูกเข้าถึงข้อมูลอะไรก็ตาม Password ที่หลุดไป ก็ไม่สามารถที่จะแปลงคืนกลับมาเป็น Password จริง ๆ ได้ (จริง ๆ มันมีวิธีอยู่ แต่ยากละกัน) ก็ทำให้ยิ่งปลอดภัยเข้าไปใหญ่

bcrypt ตัวช่วยแสนดี

pip install bcrypt

แน่นอนว่า Python เป็นภาษาที่ได้รับความนิยมสูงมาก ๆ อยู่แล้วทำให้มี Library เยอะมาก ๆ หนึ่งในนั้นคือ Package ที่ชื่อว่า Bcrypt ที่เราสามารถติดตั้งผ่าน Pip หรือ Conda ได้เลย

import bcrypt

หลังจากเราลงเรียบร้อยแล้ว เราก็จะต้อง Import มันเข้ามาใช้งานผ่านคำสั่งด้านบนเลย

user_storage: dict = dict()

ในตัวอย่างตอนนี้เราจะมาลองทำเป็น User Table ปลอม ๆ ขึ้นมาผ่าน Dictionary ที่เราจะ Map Username ไปที่ Password ที่ Hash

from typing import ByteString

def hash_password (password: str) -> ByteString :
        byte_password = password.encode('utf-8')
        salt = bcrypt.gensalt()
        return bcrypt.hashpw(byte_password, salt)

Function ด้านบน เป็น Function ที่เราจะใช้ในการ Hash Password โดยที่เรารับ Password เป็น Plain Text เข้ามา ถ้าเราเข้าไปดูใน Document ของ Bcrypt เราจะเห็นว่า Function สำหรับการ Hash Password คือ hashpw() ซึ่งรับ Data Type ที่เป็น Byte แต่ Password เราเป็น String ทำให้เราจะต้องแปลงให้มันเป็น Byte ก่อน โดยใช้คำสั่ง Encode ตามปกติได้เลย โดยเราจะ Encode เป็น UTF-8 เลย

เพื่อความแข็งแกร่งของ Hashed Password ป้องกันการเดาสุ่ม ทำให้ Bcrypt มันบังคับ เราให้เติมอีกสิ่งที่เรียกว่า Salt ถ้าคิดง่าย ๆ เป็น String สุ่มสั้น ๆ ตัวนึงละกัน เพื่อเอามาต่อกับ Password เราแล้วค่อย Hash เพื่อป้องกันการเดาแล้ว Hash เช็คเอาอะไรแบบนั้น

และสุดท้ายเรามี Salt และ Password ที่เป็น Byte แล้ว เราก็เรียกคำสั่งสำหรับการ Hash Password คือ hashpw() โดยที่เราใส่ Password ที่เป็น Byte แล้ว กับ Salt ที่เราใช้คำสั่งของ Bcrypt ในการ Generate ขึ้นมานั่นเอง ก็เป็นอันเสร็จการ Hash Password

def register (self, user:str, password:str) -> bool :
    if user in user_storage :
        return False
    hash_password = hash_password(password)
    user_storage[user] = hash_password
    return True

เริ่มจาก Flow แรกกันก่อนคือการ Register หรือการเพิ่ม User กันก่อน เราทำการสร้าง Function ชื่อว่า Register เข้ามา โดยรับ Username และ Password เป็น String เข้ามา จากนั้น เราจะต้องทำการเช็คก่อนว่า Username ที่กรอกเข้ามา มันมีในระบบหรือยัง ถ้ามีแล้วเราก็ไม่ให้ลงทับ แล้วก็ Return False กลับไปเลย เพื่อเป็นการบอกว่า เราไม่ได้ Register ลงไปให้นะ

จากนั้น เราก็ทำการ Hash Password ผ่าน Function hash_password ที่เราพึ่งสร้างเมื่อครู่ และสุดท้าย เราก็ทำการเก็บ Password ที่เราพึ่ง Hash ไปเข้าไปใน Dictionary แล้วก็ Return True เพื่อเป็นการบอกว่า การ Register สำเร็จนั่นเอง

def login (self, user:str, password:str) -> bool :
    if user not in user_storage :
        return False
    
    byte_password = password.encode("utf-8")
    
    return bcrypt.checkpw(byte_password, user_storage[user])

มาที่อีก Flow นึงนั่นคือการ Login เราก็รับ Username และ Password เข้ามา แล้วก็เช็คก่อนว่า มันมี Username นี้อยู่มั้ย เพราะถ้าไม่มี มันก็ไม่มีค่าอะไรที่จะเช็คต่อแน่นอน ถ้าเกิดไม่มีก็คืนกลับไปเป็น False เพื่อเป็นการบอกว่าไม่ผ่านนะ จากนั้น ถ้าผ่าน เราก็ต้องแปลง Password ที่เป็น String ให้เป็น Byte ก่อนผ่านคำสั่ง Encode เหมือนเดิม

จากนั้น Bcrypt เตรียมคำสั่งสำหรับการเช็ค Password มาให้เราแล้วคือคำสั่งที่ชื่อว่า checkpw() แล้วเราก็ใส่ Password ที่แปลงเป็น Byte แล้วพร้อมกับ Password ที่เราต้องการจะเทียบได้เลย มันก็จะคืนค่ากลับมาเป็น Boolean True/False ถ้าตรงก็เป็น True ไม่ตรงก็ False แค่นั้นเลย

สรุป

จากที่เราเล่าไปคือ การเก็บ Password เป็น Plain Text เป็นเรื่องที่หยาบโลนมาก อย่าหาทำเด็ดขาด เพราะมันทำให้เกิดความเสี่ยงหลาย ๆ เรื่องตามมาเยอะมาก โดนที่หน้าโยกแน่นอน วิธีการเก็บ แนะนำให้ทำการ Hash Password แล้วค่อยเก็บจะดีกว่า ซึ่งในหลาย ๆ ภาษาก็มีพวก Library ที่ทำอะไรแบบนี้มาให้เราแล้วละ ตัวอย่างในวันนี้คือ Python เราสามารถใช้ Library bcrypt ในการทำได้เลย มันง่ายมาก ๆ ดังนั้น อย่าหาทำเก็บ Plain Text เด็ดขาดเลยนะ ถือว่า ขอ ร้อง !