2 คะแนน โดย GN⁺ 2023-10-24 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การเข้ารหัส Base64 เป็นวิธีแปลงข้อมูลไบนารีให้เป็นข้อความ ASCII เพื่อลดโอกาสที่ข้อมูลจะถูกตีความผิดระหว่างการจัดเก็บหรือการส่ง
  • เนื่องจากไม่ใช่การเข้ารหัสลับ แต่เป็น การเปลี่ยนรูปแบบการแสดงผล ข้อมูลที่เข้ารหัสแล้วจึงสามารถแปลงกลับเป็นข้อความต้นฉบับหรือข้อมูลไฟล์ได้อย่างง่ายดาย
  • เนื่องจากอักขระ 64 ตัวสามารถแทนได้ด้วย 6 บิต อักขระ Base64 หนึ่งตัวจึงเก็บ ข้อมูล 6 บิต และ 3 ไบต์ (24 บิต) จะถูกแปลงเป็นอักขระ Base64 4 ตัว
  • มีประโยชน์ในสภาพแวดล้อมที่ไบนารีดิบอาจก่อปัญหาได้ เช่น Data URLs ของ HTML, การส่งข้อมูลไบนารีในอีเมล, หรือเครือข่ายและ URL ที่เน้นข้อความเป็นหลัก
  • ภาษาและเครื่องมือต่าง ๆ เช่น Ruby, C#, PHP, JavaScript และคำสั่ง base64 บนเทอร์มินัล ต่างก็มีความสามารถสำหรับ การเข้ารหัสและถอดรหัส

สิ่งที่ Base64 เปลี่ยน

  • การเข้ารหัส Base64 จะแปลงข้อมูลไบนารีให้เป็นข้อความ หรือเจาะจงกว่านั้นคือข้อความ ASCII
  • ผลลัพธ์จะใช้เฉพาะอักขระ 64 ตัวด้านล่างนี้เท่านั้น
    • A-Z
    • a-z
    • 0-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 จัดการการเข้ารหัสและถอดรหัสผ่านโมดูล Base64
    • Base64.encode64("Ruby on Rails")
    • Base64.decode64(encoded)
  • C# จะแปลงสตริงเป็นอาร์เรย์ไบต์ก่อน แล้วเข้ารหัสด้วย Convert.ToBase64String และถอดรหัสด้วย System.Convert.FromBase64String
  • PHP มีฟังก์ชันระดับบนสุด base64_encode และ base64_decode
  • JavaScript เข้ารหัสด้วย btoa() และถอดรหัสด้วย atob()
  • บนเทอร์มินัลก็สามารถเข้ารหัสและถอดรหัสได้ด้วยคำสั่ง base64
    • echo "akshay" | base64 จะพิมพ์ YWtzaGF5Cg==
    • echo "YWtzaGF5Cg==" | base64 -d จะพิมพ์ akshay

1 ความคิดเห็น

 
GN⁺ 2023-10-24
ความคิดเห็นจาก Hacker News
  • ขอบคุณที่ย้ำว่าตรงนี้ไม่ได้เป็นการเข้ารหัสลับข้อความ นักพัฒนาจูเนียร์หลายคนมักโดนเล่นงานเพราะเรียนรู้ความแตกต่างเหล่านี้ช้าเกินไปว่า การเข้ารหัสลับ ต้องมีค่าลับจึงจะย้อนกลับได้, การแฮช ย้อนกลับไม่ได้ และ การเข้ารหัสข้อมูล มักย้อนกลับได้ง่ายเสมอ
    อีกประเด็นที่ควรรู้คือ แม้ผลลัพธ์จะดูเหมือนสุ่ม แต่เอนโทรปีก็เท่ากับอินพุต กล่าวคือไม่ควรเอารหัสผ่านไปเข้ารหัสแบบ Base64 เพื่อทำให้มันแข็งแรงขึ้น

    • ออกแนวจับผิดเล็กน้อยและไม่ค่อยเกี่ยวกับประเด็นหลัก แต่การเข้ารหัสรหัสผ่านด้วย 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 จะเพิ่ม อักขระขึ้นบรรทัดใหม่ หนึ่งตัวท้ายสตริง และอักขระนั้นก็จะถูกเข้ารหัสไปด้วย

    • ถ้าต้องการเอาต์พุตที่คาดเดาได้ ควรใช้ printf แทน echo จะดีกว่า https://linux.die.net/man/1/printf
  • ยังมี base64URL ด้วย ซึ่งเข้ารหัสโดยใช้อักขระ ASCII ชุดอื่นที่ปลอดภัยสำหรับ URL นักพัฒนาบางคนเรียก BASE64URL ว่า base64 เฉย ๆ จึงอาจทำให้คนที่ไม่รู้เกิดปัญหาได้
    https://datatracker.ietf.org/doc/html/rfc4648#section-5

    • ปัญหาของ base64url คือ ~ และ . ไม่ถือเป็นตัวอักษร ดังนั้นดับเบิลคลิกค่าที่เข้ารหัสแล้วก็จะไม่เลือกทั้งค่า กลายเป็นแรงเสียดทานที่ไม่จำเป็นในหลายกรณีที่ต้องคัดลอก-วาง
      การเข้ารหัส Base62 (0-9A-Za-z) มีประสิทธิภาพใกล้เคียงกับ base64url มาก และยังปลอดภัยสำหรับ URL ขณะเดียวกันก็คัดลอก-วางได้ง่ายกว่า หากต้องการลดความกำกวมสำหรับคนอ่าน สามารถลดลงไปใช้ Base58 ได้ แต่โดยทั่วไปเมื่อถึงขั้นใช้การเข้ารหัส BaseXX ความยาวมักมากจนการคัดลอก-วางเป็นวิธีปกติ จึงไม่ใช่ปัญหาใหญ่
      https://en.wikipedia.org/wiki/Base62
    • Base64url มักละเว้น padding ด้วย
      สตริง 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 จำเป็นเฉพาะตอนนำข้อมูลที่ encode แล้วมาต่อกันหรือทำ streaming กล่าวคือในกรณีที่มีอักขระ padding อยู่กลาง stream ที่ encode
      อักขระ padding ที่ท้าย stream, ไฟล์ หรือสตริงสามารถอนุมานได้จากความยาวที่ประมวลผลไปแล้ว ดังนั้นถ้าพูดอย่างเคร่งครัดจึงไม่ใช่สิ่งจำเป็น
      อย่างไรก็ตาม วิธีจัดการ padding มีรายละเอียดปลีกย่อยค่อนข้างมาก และความแตกต่างเหล่านั้นก็ทำให้เกิด implementation แบบแปลก ๆ ที่น่าสนใจ: https://eprint.iacr.org/2022/361.pdf
    • ตามบทความ เลข Base64 หนึ่งตัวแทนข้อมูล 6 บิต ไบต์มี 8 บิต และ common multiple ที่ใกล้ที่สุดของ 8 กับ 6 คือ 24 ดังนั้น 24 บิต หรือก็คือ 3 ไบต์ สามารถแทนด้วยเลข Base64 แบบ 6 บิตจำนวน 4 ตัวได้
      สุดท้ายแล้วก็คือ 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