คำอธิบายเกี่ยวกับการเข้ารหัส Base64
(akshaykhot.com)- การเข้ารหัส Base64 เป็นวิธีแปลงข้อมูลไบนารีให้เป็นข้อความ ASCII เพื่อลดโอกาสที่ข้อมูลจะถูกตีความผิดระหว่างการจัดเก็บหรือการส่ง
- เนื่องจากไม่ใช่การเข้ารหัสลับ แต่เป็น การเปลี่ยนรูปแบบการแสดงผล ข้อมูลที่เข้ารหัสแล้วจึงสามารถแปลงกลับเป็นข้อความต้นฉบับหรือข้อมูลไฟล์ได้อย่างง่ายดาย
- เนื่องจากอักขระ 64 ตัวสามารถแทนได้ด้วย 6 บิต อักขระ Base64 หนึ่งตัวจึงเก็บ ข้อมูล 6 บิต และ 3 ไบต์ (24 บิต) จะถูกแปลงเป็นอักขระ Base64 4 ตัว
- มีประโยชน์ในสภาพแวดล้อมที่ไบนารีดิบอาจก่อปัญหาได้ เช่น Data URLs ของ HTML, การส่งข้อมูลไบนารีในอีเมล, หรือเครือข่ายและ URL ที่เน้นข้อความเป็นหลัก
- ภาษาและเครื่องมือต่าง ๆ เช่น Ruby, C#, PHP, JavaScript และคำสั่ง
base64บนเทอร์มินัล ต่างก็มีความสามารถสำหรับ การเข้ารหัสและถอดรหัส
สิ่งที่ Base64 เปลี่ยน
- การเข้ารหัส Base64 จะแปลงข้อมูลไบนารีให้เป็นข้อความ หรือเจาะจงกว่านั้นคือข้อความ ASCII
- ผลลัพธ์จะใช้เฉพาะอักขระ 64 ตัวด้านล่างนี้เท่านั้น
A-Za-z0-9+/
- ชุดอักขระนี้ถูกใช้เป็น ชุดอักขระที่ปลอดภัย เพื่อหลีกเลี่ยงสถานการณ์ที่อักขระอย่าง
<,>,\nอาจถูกตีความผิดในคอมพิวเตอร์หรือโปรแกรมรุ่นเก่า - หากเข้ารหัส
"Ruby on Rails"เป็น Base64 จะได้UnVieSBvbiBSYWlscw== - Base64 ไม่ใช่การเข้ารหัสลับ
- ข้อมูลที่เข้ารหัสแล้วสามารถแปลงกลับเป็นข้อความต้นฉบับได้อย่างง่ายดาย
- มันไม่ได้ซ่อนข้อมูล แต่เปลี่ยนเพียงวิธีการแสดงข้อมูลเท่านั้น
สถานการณ์ที่ใช้ Base64
- Data URLs ช่วยให้สามารถใส่ข้อมูลไฟล์อย่างรูปภาพลงใน HTML ได้โดยตรง และในกรณีนี้จะใช้ข้อความที่เข้ารหัสด้วย Base64
- รูปแบบตัวอย่างคือ
data:[<mime type>][;charset=<charset>][;base64],<encoded data> - ในอีเมล Base64 ถูกใช้มาอย่างต่อเนื่องเพื่อบรรจุ ข้อมูลไบนารี ได้อย่างปลอดภัย แม้อยู่ในสภาพแวดล้อมที่เซิร์ฟเวอร์อาจเปลี่ยนการขึ้นบรรทัดใหม่
- เมื่อต้องใส่ข้อมูลรูปภาพลงในซอร์ส HTML โดยตรง ก็จำเป็นต้องเข้ารหัสเพื่อไม่ให้อักขระอย่าง
<และ>ถูกตีความเป็นแท็ก - ยังสามารถใช้ได้เมื่อเก็บหรือส่งข้อมูลไบนารีผ่านเครือข่ายที่ออกแบบมาเพื่อจัดการข้อมูลแบบข้อความหรือ US-ASCII
- Base64 ยังอาจถูกใช้เมื่อต้องส่งข้อมูลที่มีอักขระซึ่งใส่ใน URL ได้ยาก
- การเข้ารหัสตระกูล Base ช่วยให้สามารถจัดการอ็อบเจ็กต์ด้วยโปรแกรมแก้ไขข้อความได้ จึงถูกนำไปใช้ในหลายแอปพลิเคชัน
อัลกอริทึมการเข้ารหัส
- การเข้ารหัส Base64 ดำเนินไปตามลำดับดังนี้
- แปลงข้อความเป็น การแทนค่าแบบไบนารี
- แบ่งบิตออกเป็นกลุ่มละ 6 บิต
- แปลงแต่ละกลุ่ม 6 บิตเป็นเลขฐานสิบตั้งแต่ 0 ถึง 63
- เปลี่ยนตัวเลขนี้ให้เป็นอักขระที่ตรงกันในอักษร Base64
- หากกลุ่มสุดท้ายมีบิตไม่ครบ อาจเติม
=หรือ==เป็น padding ได้ - ต้องใช้ 6 บิตเพื่อแทนอักขระ 64 ตัว
2^6 = 64- ตัวเลข Base64 หนึ่งตัวแทนข้อมูล 6 บิต
- หนึ่งไบต์มี 8 บิต และ ค.ร.น. ที่ใกล้ที่สุดของ 8 กับ 6 คือ 24
- 24 บิตเท่ากับ 3 ไบต์
- 24 บิตสามารถแทนได้ด้วยตัวเลข Base64 4 ตัวที่มีขนาด 6 บิต
ตัวอย่างการเข้ารหัส “Akshay”
"Akshay"เมื่อนำอักขระแต่ละตัวไปแปลงเป็นตัวเลข ASCII แล้วแปลงเป็นไบนารี จะได้ดังนี้01000001 01101011 01110011 01101000 01100001 01111001
- เมื่อนำมาแบ่งเป็นกลุ่มละ 6 บิต จะได้ดังนี้
010000 010110 101101 110011 011010 000110 000101 111001
- เมื่อแปลงแต่ละกลุ่มเป็นเลขฐานสิบ จะได้ค่าดังนี้
16 22 45 51 26 6 5 57
- เมื่อนำไปแปลงเป็นอักษร Base64 จะได้อักขระดังนี้
Q W t z a G F 5
- ดังนั้นการแทนค่า Base64 ของ
"Akshay"คือQWtzaGF5 - ในลักษณะเดียวกัน ไฟล์อย่างรูปภาพ, PDF, ข้อความ หรือวิดีโอ ก็สามารถแปลงเป็นไบนารีก่อนแล้วจึงเข้ารหัสด้วย Base64 เพื่อจัดเก็บหรือส่งในรูปแบบข้อความ ASCII ได้
การใช้งานในภาษาและเครื่องมือ
- Ruby จัดการการเข้ารหัสและถอดรหัสผ่านโมดูล
Base64Base64.encode64("Ruby on Rails")Base64.decode64(encoded)
- C# จะแปลงสตริงเป็นอาร์เรย์ไบต์ก่อน แล้วเข้ารหัสด้วย
Convert.ToBase64Stringและถอดรหัสด้วยSystem.Convert.FromBase64String - PHP มีฟังก์ชันระดับบนสุด
base64_encodeและbase64_decode - JavaScript เข้ารหัสด้วย
btoa()และถอดรหัสด้วยatob() - บนเทอร์มินัลก็สามารถเข้ารหัสและถอดรหัสได้ด้วยคำสั่ง
base64echo "akshay" | base64จะพิมพ์YWtzaGF5Cg==echo "YWtzaGF5Cg==" | base64 -dจะพิมพ์akshay
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ขอบคุณที่ย้ำว่าตรงนี้ไม่ได้เป็นการเข้ารหัสลับข้อความ นักพัฒนาจูเนียร์หลายคนมักโดนเล่นงานเพราะเรียนรู้ความแตกต่างเหล่านี้ช้าเกินไปว่า การเข้ารหัสลับ ต้องมีค่าลับจึงจะย้อนกลับได้, การแฮช ย้อนกลับไม่ได้ และ การเข้ารหัสข้อมูล มักย้อนกลับได้ง่ายเสมอ
อีกประเด็นที่ควรรู้คือ แม้ผลลัพธ์จะดูเหมือนสุ่ม แต่เอนโทรปีก็เท่ากับอินพุต กล่าวคือไม่ควรเอารหัสผ่านไปเข้ารหัสแบบ Base64 เพื่อทำให้มันแข็งแรงขึ้น
ถ้ารหัสผ่านถูกสร้างแบบสุ่มอย่างสมบูรณ์ การเข้ารหัสแบบ Base64 ก็ไม่มีผลอะไร แต่ถ้าเป็นรหัสผ่านที่สร้างจากระบบเอนโทรปีต่ำ เช่น คำในพจนานุกรมหรือกฎที่จำง่าย ผู้โจมตีจะต้องตั้งค่าเครื่องมือแคร็กรหัสผ่านอัจฉริยะให้คำนึงถึง กฎการเข้ารหัส Base64 ด้วย จึงเพิ่มความแข็งแรงได้ราวกับมีการคำนวณเพิ่มหนึ่งขั้นต่อความพยายามแต่ละครั้ง
แน่นอนว่าไม่ควรใช้ระบบรหัสผ่านแบบนี้ ผมมองว่ารหัสผ่านแนว “correct horse battery staple” ก็เพียงพอแล้ว
การแฮชมีวัตถุประสงค์มากมายที่ไม่ใช่เรื่องความปลอดภัย จึงมีไลบรารีแฮชหลากหลาย ถ้าจะใช้แฮชเพื่อความปลอดภัยหรือเกี่ยวกับการเข้ารหัสลับ ควรใช้แฮชที่ออกแบบมาเพื่อวัตถุประสงค์นั้น CRC hash เร็วก็จริง แต่ไม่เหมาะกับการใช้กับรหัสผ่านผู้ใช้
สิ่งที่น่าสนใจอย่างหนึ่งของ Base64 คือ หากเริ่มจากสตริงใด ๆ แล้วเข้ารหัสซ้ำไปเรื่อย ๆ ส่วนต้นของผลลัพธ์จะค่อย ๆ ลู่เข้าสู่ จุดตรึง สามารถตรวจสอบด้วย Bash ได้
ผมบังเอิญค้นพบเมื่อกว่าสิบปีก่อนและทวีตแบบเหมือนรหัสลับไว้ [1], จากนั้นมีคนเขียนบล็อกโพสต์เกี่ยวกับเรื่องนี้และเอามาโพสต์ที่นี่ด้วย แต่แทบไม่มีการถกเถียง [2] พออีกคนเอาไปโพสต์ใน Reddit /r/compsci ทางนั้นกลับมีการสนทนาเชิงสร้างสรรค์ที่ช่วยแก้ไขบล็อกโพสต์ [3] ตอนนี้บล็อกนั้นปิดไปแล้ว แต่ยังมีสำเนาอยู่ใน Internet Archive [4]
[1] https://twitter.com/p4bl0/status/298900842076045312
[2] https://news.ycombinator.com/item?id=5181256
[3] https://www.reddit.com/r/compsci/comments/18234a/the_base64_...
[4] https://web.archive.org/web/20130315082932/http://fmota.eu/b...
เวลาเข้ารหัสด้วย Bash ต้องใช้ตัวเลือก
-n:$ echo -n "abcde" |base64ถ้าไม่มี
-nคำสั่งechoจะเพิ่ม อักขระขึ้นบรรทัดใหม่ หนึ่งตัวท้ายสตริง และอักขระนั้นก็จะถูกเข้ารหัสไปด้วยechoจะดีกว่า https://linux.die.net/man/1/printfยังมี base64URL ด้วย ซึ่งเข้ารหัสโดยใช้อักขระ ASCII ชุดอื่นที่ปลอดภัยสำหรับ URL นักพัฒนาบางคนเรียก BASE64URL ว่า base64 เฉย ๆ จึงอาจทำให้คนที่ไม่รู้เกิดปัญหาได้
https://datatracker.ietf.org/doc/html/rfc4648#section-5
~และ.ไม่ถือเป็นตัวอักษร ดังนั้นดับเบิลคลิกค่าที่เข้ารหัสแล้วก็จะไม่เลือกทั้งค่า กลายเป็นแรงเสียดทานที่ไม่จำเป็นในหลายกรณีที่ต้องคัดลอก-วางการเข้ารหัส Base62 (
0-9A-Za-z) มีประสิทธิภาพใกล้เคียงกับ base64url มาก และยังปลอดภัยสำหรับ URL ขณะเดียวกันก็คัดลอก-วางได้ง่ายกว่า หากต้องการลดความกำกวมสำหรับคนอ่าน สามารถลดลงไปใช้ Base58 ได้ แต่โดยทั่วไปเมื่อถึงขั้นใช้การเข้ารหัส BaseXX ความยาวมักมากจนการคัดลอก-วางเป็นวิธีปกติ จึงไม่ใช่ปัญหาใหญ่https://en.wikipedia.org/wiki/Base62
สตริง Base64 ที่มี padding จะมีความยาวเป็นพหุคูณของ 4 เสมอ ดังนั้นถ้าได้รับสตริงที่ความยาวไม่ใช่พหุคูณของ 4 ก็จะรู้ได้ว่าควรมี padding เดิมอยู่เท่าไร และยังตัดสินได้ว่าจะถอดรหัส 3 ไบต์สุดท้ายอย่างไร
ดังนั้นจึงค่อนข้างสับสนว่าเดิมที Base64 จำเป็นต้องมี padding
==ไปทำไมทุกครั้งที่มีการพูดถึงการแปลงฐาน ผมก็จะโปรโมตตัวแปลงฐานใด ๆ ของตัวเองแบบหน้าด้าน ๆ: https://convert.zamicol.com
base64 ใต้ “useful alphabets” เป็นฐานแบบ “ธรรมชาติ” ที่ใช้การหารซ้ำด้วยฐาน ส่วนวิธีแปลงแบบ “bucket” ของ RFC อยู่ใต้ extras
ถ้าเข้ารหัสอะไรบางอย่างแล้วมนุษย์ต้องพิมพ์เอง แนะนำ https://en.wikipedia.org/wiki/Base32
ไม่มีอะไรน่าหงุดหงิดเท่าการสับสนว่าเป็น
lหรือ1,oหรือOหรือ0เพราะฟอนต์แย่ ๆถ้าพูดให้เคร่งขึ้นอีกนิด การบอกว่า Base64 เข้ารหัสข้อมูลไบนารีเป็น ชุดย่อยของ ASCII น่าจะแม่นยำกว่าการบอกว่าเป็นอักขระ ASCII ทั้งหมด
ASCII มีโค้ดพอยต์ 128 ตัว ในนั้นเป็นอักขระที่แสดงผลได้ 95 ตัว และอักขระควบคุม 33 ตัว แต่ Base64 ใช้แค่ 64 ตัวจากทั้งหมดนั้น หรือ 65 ตัวหากรวม padding
บทความไม่ได้อธิบายจุดประสงค์ของ padding ด้วย
=/==อย่างละเอียด และไม่ได้ยกตัวอย่างให้เห็นว่าข้อมูลที่แบ่งเป็นกลุ่ม 6 บิตได้ไม่ลงตัวนั้นจัดการอย่างไรเหมือนจะพอเข้าใจคร่าว ๆ แล้ว แต่อยากรู้ให้ชัดเจน ถ้าช่วยตอบแบบสั้น ๆ แต่ครบถ้วนได้ก็ดีว่า
=ใช้เมื่อไร==ใช้เมื่อไร ต้องเติมเสมอหรือมีกรณีที่ไม่เติมด้วย บิตที่เหลือของสตริงอย่าง"5byte"ถูกจัดการอย่างไรแน่ และตอน decode ต้องคำนึงถึงอะไรบ้างอักขระ Base64 หนึ่งตัวแทนค่า 6 บิต ดังนั้นบล็อกข้อมูล 3 ไบต์หนึ่งบล็อกจึงตรงกับบล็อกอักขระที่ encode แบบ Base64 จำนวน 4 ตัว ด้วยเหตุนี้ข้อมูล Base64 จึงสะดวกต่อการประมวลผลทีละ 4 อักขระ
=คือ padding ที่เติม 0 ตัว, 1 ตัว หรือ 2 ตัวตามจำเป็น เพื่อให้ความยาวของสตริงที่ encode แล้วเป็นพหุคูณของ 4 เช่น"543210"จะเป็น"543210==","6543210"จะเป็น"6543210=", ส่วน"76543210"ไม่ต้องมี padding ไม่มีกรณีที่ต้องใช้=เป็น padding 3 ตัว เพราะข้อมูล 1 ไบต์ก็ต้องใช้อักขระ Base64 อย่างน้อย 2 ตัวอยู่แล้วบิตที่เหลือให้เติมด้วย 0 ได้ และ decoder จะเห็นว่ามีบิตไม่พอที่จะสร้าง 1 ไบต์เต็ม แล้วสามารถทิ้งได้ ในกรณีสมัยใหม่ส่วนใหญ่ padding ไม่ได้จำเป็นอย่างเคร่งครัดนัก แต่ใกล้เคียงกับธรรมเนียมปฏิบัติมากกว่า บทความ Wikipedia ค่อนข้างละเอียด: https://en.wikipedia.org/wiki/Base64
อักขระ padding ที่ท้าย stream, ไฟล์ หรือสตริงสามารถอนุมานได้จากความยาวที่ประมวลผลไปแล้ว ดังนั้นถ้าพูดอย่างเคร่งครัดจึงไม่ใช่สิ่งจำเป็น
อย่างไรก็ตาม วิธีจัดการ padding มีรายละเอียดปลีกย่อยค่อนข้างมาก และความแตกต่างเหล่านั้นก็ทำให้เกิด implementation แบบแปลก ๆ ที่น่าสนใจ: https://eprint.iacr.org/2022/361.pdf
สุดท้ายแล้วก็คือ encode เป็น หน่วยละ 24 บิต นั่นเอง เมื่อข้อมูลสิ้นสุด ส่วน 24 บิตที่เหลือจะเติมด้วย
=ไม่ใช่AเพราะAในฐานะข้อมูลหมายถึง000000ผมเองก็อ่านทั้งหมดสองรอบเพื่อให้เข้าใจBase64 encoder shader ของผมอยู่ที่นี่: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
ผมย่อให้เหลือ GLSL ประมาณ 13 บรรทัด: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
ใช้ใน Cursed Mode ของ side project โดย render WebGL framebuffer เป็น BMP สีแบบ indexed color ขนาด 640x480 ที่ encode เป็น Base64 ประมาณ 15 ครั้งต่อวินาที: https://rezmason.github.io/excel_97_egg/?cursed=1
ถ้าเริ่มขุดลึกลงไป จะมีรายละเอียดเพิ่มเติมที่น่าสนใจ และ variation ของรายละเอียดเหล่านั้นก็มีมากจนน่าประหลาดใจ
หากความยาวข้อมูล input ไม่ได้เป็นพหุคูณของ 3 ไบต์พอดี จะใช้อักขระ Base64 2 ตัวหรือ 3 ตัวเพื่อ encode 1 ไบต์หรือ 2 ไบต์สุดท้าย เนื่องจากอักขระ Base64 หนึ่งตัวมี 6 บิต การจะแทน 8 บิตหรือ 16 บิตจึงต้องใช้ 12 บิตหรือ 18 บิต และผลคือมีบิตส่วนเกิน 4 บิตหรือ 2 บิตที่ไม่ได้ encode อะไรเลย
RFC กำหนดให้ encoder ต้องตั้งบิตเหล่านั้นเป็น 0 แต่บอกเพียงว่า decoder อาจปฏิเสธ input ที่บิตเหล่านั้นไม่เป็น 0 ได้ ในทางปฏิบัติ แทบไม่มี implementation ใดที่ปฏิเสธเป็นค่าเริ่มต้น และเท่าที่ผมรู้มีแค่ Ruby, Rust, Go ประมาณนี้ที่สามารถตั้งค่าให้ล้มเหลวกับ input แบบนั้นได้ Python มี option
validateแต่ไม่ได้ตรวจบิตเหล่านั้นความแตกต่างใหญ่อีกอย่างคือการจัดการ whitespace และอักขระที่ไม่ใช่ Base64 implementation จำนวนมากอย่างไม่น่าเชื่อ รวมถึง Python จะเงียบ ๆ ละเลยอักขระใด ๆ ใน input ถ้าเลือก alphabet ผิดจะเกิดปัญหาได้ เช่นใน Python คำสั่ง
base64.standard_b64decode(base64.urlsafe_b64encode(b'\xFF\xFE\xFD\xFC'))จะไม่แจ้ง error และให้ output ที่ผิดอย่างเงียบ ๆที่น่าสนใจอีกอย่างคือ Base64 encoder ของ Ruby ใส่ line break ทุก 60 อักขระ ไม่มี encoding มาตรฐานไหนนอกจาก PEM ที่ต้องการบรรทัดสั้นขนาดนั้น และ PEM ก็ต้องการบรรทัดละ 64 อักขระพอดี จึงเป็นตัวเลือกที่ค่อนข้างแปลก
ผมเขียนบทความสรุปความแตกต่างระหว่างภาษาโปรแกรมกับไลบรารี JavaScript บางตัวไว้ [1] และกำลังทำงานเพื่อเพิ่ม Base64 ที่ดีกว่าเข้าไปใน JS ด้วย [2]
[1] https://gist.github.com/bakkot/16cae276209da91b652c2cb3f612a...
[2] https://github.com/tc39/proposal-arraybuffer-base64