2 điểm bởi GN⁺ 2023-10-24 | 1 bình luận | Chia sẻ qua WhatsApp
  • Mã hóa Base64 là phương thức chuyển dữ liệu nhị phân thành văn bản ASCII, giúp giảm khả năng dữ liệu bị diễn giải sai trong quá trình lưu trữ hoặc truyền tải
  • Đây không phải là mã hóa bảo mật mà là thay đổi cách biểu diễn, nên dữ liệu đã được mã hóa có thể dễ dàng được chuyển ngược lại thành văn bản hoặc dữ liệu tệp ban đầu
  • 64 ký tự có thể được biểu diễn bằng 6 bit, nên một ký tự Base64 chứa 6 bit dữ liệu, và 3 byte (24 bit) được chuyển thành bốn ký tự Base64
  • Hữu ích trong các môi trường mà dữ liệu nhị phân thô có thể gây vấn đề, như Data URLs trong HTML, truyền dữ liệu nhị phân qua email, các mạng thiên về văn bản hoặc URL
  • Nhiều ngôn ngữ và công cụ như Ruby, C#, PHP, JavaScript, lệnh base64 trong terminal cung cấp chức năng mã hóa và giải mã

Base64 thay đổi điều gì

  • Mã hóa Base64 chuyển dữ liệu nhị phân thành văn bản, cụ thể hơn là văn bản ASCII
  • Kết quả chỉ sử dụng 64 ký tự dưới đây
    • A-Z
    • a-z
    • 0-9
    • +
    • /
  • Tập ký tự này được dùng như một tập ký tự an toàn để tránh các tình huống những ký tự như <, >, \n bị diễn giải sai trên các máy tính hoặc chương trình cũ
  • Khi mã hóa "Ruby on Rails" bằng Base64, kết quả là UnVieSBvbiBSYWlscw==
  • Base64 không phải là mã hóa bảo mật
    • Dữ liệu đã được mã hóa có thể dễ dàng được chuyển ngược lại thành văn bản ban đầu
    • Nó không che giấu dữ liệu, mà chỉ thay đổi cách biểu diễn dữ liệu

Khi nào dùng Base64

  • Data URLs cho phép đưa trực tiếp dữ liệu tệp như hình ảnh vào trong HTML, và khi đó sử dụng văn bản đã được mã hóa Base64
  • Định dạng ví dụ là data:[<mime type>][;charset=<charset>][;base64],<encoded data>
  • Trong email, Base64 đã được dùng để chứa dữ liệu nhị phân một cách an toàn ngay cả trong môi trường máy chủ có thể thay đổi ký tự xuống dòng
  • Khi đưa trực tiếp dữ liệu hình ảnh vào mã nguồn HTML, cần mã hóa để các ký tự như <> không bị diễn giải thành thẻ
  • Cũng có thể dùng khi lưu trữ hoặc truyền dữ liệu nhị phân qua các mạng được thiết kế để xử lý dữ liệu văn bản hoặc US-ASCII
  • Base64 cũng có thể được dùng khi truyền dữ liệu chứa các ký tự khó đưa vào URL
  • Các kiểu mã hóa họ Base được dùng trong nhiều ứng dụng vì cho phép xử lý đối tượng bằng trình soạn thảo văn bản

Thuật toán mã hóa

  • Mã hóa Base64 diễn ra theo thứ tự sau
    • Chuyển văn bản thành biểu diễn nhị phân
    • Chia các bit thành từng nhóm 6 bit
    • Chuyển mỗi nhóm 6 bit thành một số thập phân từ 0 đến 63
    • Chuyển số đó thành ký tự tương ứng trong bảng chữ cái Base64
  • Nếu nhóm cuối thiếu bit, có thể thêm = hoặc == làm phần đệm
  • Cần 6 bit để biểu diễn 64 ký tự
    • 2^6 = 64
    • Một số Base64 biểu diễn 6 bit dữ liệu
  • Một byte có 8 bit, và bội chung gần nhất của 8 và 6 là 24
    • 24 bit là 3 byte
    • 24 bit được biểu diễn bằng bốn số Base64, mỗi số 6 bit

Ví dụ mã hóa “Akshay”

  • "Akshay" khi đổi từng ký tự thành số ASCII rồi chuyển sang nhị phân sẽ như sau
    • 01000001 01101011 01110011 01101000 01100001 01111001
  • Chia thành các nhóm 6 bit sẽ được như sau
    • 010000 010110 101101 110011 011010 000110 000101 111001
  • Chuyển từng nhóm thành số thập phân sẽ nhận được các giá trị sau
    • 16 22 45 51 26 6 5 57
  • Chuyển sang bảng chữ cái Base64 sẽ thành các ký tự sau
    • Q W t z a G F 5
  • Vì vậy, biểu diễn Base64 của "Akshay"QWtzaGF5
  • Theo cách tương tự, các tệp như hình ảnh, PDF, văn bản, video cũng có thể được chuyển thành nhị phân rồi mã hóa bằng Base64 để lưu trữ hoặc truyền tải dưới dạng văn bản ASCII

Sử dụng trong ngôn ngữ và công cụ

  • Ruby xử lý mã hóa và giải mã bằng module Base64
    • Base64.encode64("Ruby on Rails")
    • Base64.decode64(encoded)
  • C# chuyển chuỗi thành mảng byte rồi mã hóa bằng Convert.ToBase64String, và giải mã bằng System.Convert.FromBase64String
  • PHP cung cấp các hàm cấp cao nhất base64_encodebase64_decode
  • JavaScript mã hóa bằng btoa() và giải mã bằng atob()
  • Trong terminal cũng có thể thực hiện mã hóa và giải mã bằng lệnh base64
    • echo "akshay" | base64 xuất ra YWtzaGF5Cg==
    • echo "YWtzaGF5Cg==" | base64 -d xuất ra akshay

1 bình luận

 
GN⁺ 2023-10-24
Ý kiến trên Hacker News
  • Cảm ơn vì đã nhấn mạnh rằng ở đây không phải là mã hóa theo nghĩa bảo mật văn bản. Rất nhiều lập trình viên junior đến quá muộn mới học được sự khác nhau rằng mã hóa cần có giá trị bí mật mới đảo ngược được, băm thì không thể đảo ngược, còn encoding thì luôn có thể đảo ngược dễ dàng, nên hay bị “dính đòn”
    Cũng đáng nhớ rằng dù đầu ra trông có vẻ ngẫu nhiên, entropy vẫn giống với đầu vào. Nói cách khác, đừng encode mật khẩu bằng Base64 để cố làm nó mạnh hơn

    • Hơi bắt bẻ và không liên quan nhiều đến ý chính, nhưng encode mật khẩu bằng Base64 cũng có thể làm nó mạnh hơn. Độ mạnh mật khẩu không chỉ là vấn đề entropy; entropy cao chỉ là cách hiệu quả nhất mà thôi
      Nếu mật khẩu được tạo hoàn toàn ngẫu nhiên thì Base64 encoding không có tác dụng gì. Nhưng nếu mật khẩu được tạo theo một hệ thống entropy thấp, như từ trong từ điển hoặc quy tắc dễ nhớ, thì kẻ tấn công phải cấu hình trình bẻ khóa mật khẩu thông minh để tính thêm cả quy tắc encode Base64, nên mỗi lần thử sẽ có thêm khoảng một phép tính, tức là cũng tăng độ mạnh đôi chút
      Tất nhiên không nên dùng kiểu hệ thống mật khẩu như vậy. Tôi nghĩ các mật khẩu kiểu “correct horse battery staple” là đủ tốt
    • Tôi nghĩ nếu là người học ngành khoa học máy tính thì ai cũng biết những khác biệt này. Ngay cả người quan tâm đến lập trình cũng có thể học các khái niệm này trong một buổi chiều
    • Một điểm liên quan luôn đáng nhấn mạnh là hash không nhất thiết an toàn về mặt mật mã học
      Băm có nhiều mục đích ngoài bảo mật, nên thư viện hash cũng rất đa dạng. Nếu dùng hash cho mục đích bảo mật hoặc liên quan đến mật mã, phải dùng loại hash được thiết kế cho mục đích đó. Hash CRC thì nhanh, nhưng không phù hợp để dùng cho mật khẩu người dùng
  • Một điều thú vị về Base64 là nếu bắt đầu từ một chuỗi nào đó rồi encode lặp đi lặp lại, phần đầu của kết quả sẽ dần hội tụ về một điểm cố định. Có thể kiểm chứng bằng Bash
    Hơn 10 năm trước tôi tình cờ phát hiện ra và tweet như một mật mã [1], rồi có người viết bài blog về chủ đề đó và cũng đăng lên đây, nhưng không có nhiều thảo luận [2]. Khi người khác đăng lên Reddit /r/compsci thì ở đó đã có một cuộc thảo luận hữu ích để sửa lại bài blog [3]. Hiện blog đã gỡ xuống, nhưng Internet Archive vẫn còn bản sao [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...

  • Khi encode bằng Bash, nên dùng tùy chọn -n: $ echo -n "abcde" |base64
    Nếu không có -n, echo sẽ thêm một ký tự xuống dòng ở cuối chuỗi, và cả ký tự đó cũng sẽ bị encode

  • Cũng có base64URL, dùng các ký tự ASCII khác an toàn cho URL để encode. Một số lập trình viên cũng gọi BASE64URL đơn giản là base64, điều này có thể gây rắc rối cho người không biết
    https://datatracker.ietf.org/doc/html/rfc4648#section-5

    • Vấn đề của base64url là ~. không được xem là chữ, nên khi nhấp đúp vào giá trị đã encode thì không chọn được toàn bộ. Điều này tạo ra ma sát không cần thiết trong nhiều trường hợp muốn sao chép-dán
      Encoding Base62 (0-9A-Za-z) gần như hiệu quả tương đương base64url, vẫn an toàn cho URL, đồng thời dễ sao chép-dán hơn. Nếu muốn giảm nhập nhằng khi con người đọc, có thể hạ xuống Base58, nhưng thường đã dùng encoding BaseXX thì chuỗi khá dài và việc sao chép-dán là phổ biến, nên không phải vấn đề lớn
      https://en.wikipedia.org/wiki/Base62
    • Base64url thường cũng bỏ qua padding
      Chuỗi Base64 có padding luôn có độ dài là bội số của 4, nên nếu nhận được chuỗi không phải bội số của 4 thì có thể biết ban đầu cần bao nhiêu padding, và cũng xác định được cách decode 3 byte cuối
      Vì vậy tôi hơi không hiểu vì sao Base64 ban đầu lại cần padding ==
  • Mỗi khi có chuyện về chuyển đổi cơ số, tôi lại trơ trẽn quảng bá bộ chuyển đổi cơ số tùy ý của mình: https://convert.zamicol.com
    base64 dưới mục “useful alphabets” là hệ cơ số “tự nhiên” dùng phép chia lặp theo cơ số, còn cách chuyển đổi kiểu “bucket” của RFC nằm dưới mục extras

  • Nếu bạn encode thứ gì đó mà con người phải tự gõ lại, tôi khuyên dùng https://en.wikipedia.org/wiki/Base32
    Không gì khó chịu bằng việc phải phân vân đó là l hay 1, o hay O hay 0 vì font chữ tệ

  • Nói chính xác hơn một chút, sẽ đúng hơn nếu nói Base64 encode dữ liệu nhị phân thành một tập con của ASCII, chứ không phải toàn bộ ký tự ASCII
    ASCII có 128 code point, trong đó 95 ký tự in được và 33 ký tự điều khiển; Base64 chỉ dùng 64 ký tự trong số đó, hoặc 65 nếu tính cả padding

  • Bài viết chưa nói kỹ về mục đích của padding = / ==, cũng không minh họa bằng ví dụ cách xử lý dữ liệu không chia vừa thành các nhóm 6 bit
    Có vẻ tôi đã hiểu đại khái, nhưng muốn nắm chắc hơn. Sẽ rất tốt nếu có thể trả lời ngắn gọn mà đầy đủ: khi nào dùng =, khi nào dùng ==, có phải luôn thêm vào hay cũng có trường hợp không gắn, các bit dư của chuỗi như "5byte" chính xác được xử lý thế nào, và khi giải mã cần lưu ý gì không

    • Hai câu hỏi này liên quan với nhau
      Mỗi ký tự Base64 biểu diễn 6 bit, nên một khối dữ liệu 3 byte tương ứng với một khối 4 ký tự mã hóa Base64. Vì vậy dữ liệu Base64 rất thuận tiện để xử lý theo từng nhóm 4 ký tự
      = là padding được thêm 0, 1 hoặc 2 ký tự khi cần để đưa độ dài chuỗi đã mã hóa về bội số của 4. Ví dụ "543210" thành "543210==", "6543210" thành "6543210=", còn "76543210" thì không cần padding. Không có trường hợp cần 3 ký tự = làm padding, vì ngay cả 1 byte dữ liệu cũng cần tối thiểu 2 ký tự Base64
      Các bit còn dư chỉ cần điền bằng 0, và decoder có thể nhận ra rằng không có đủ bit để tạo thành một byte hoàn chỉnh rồi bỏ chúng đi. Trong đa số trường hợp hiện đại, padding không hẳn là bắt buộc mà gần với quy ước hơn. Bài trên Wikipedia khá chi tiết: https://en.wikipedia.org/wiki/Base64
    • Padding chỉ cần thiết khi nối dữ liệu đã mã hóa hoặc khi streaming, tức là khi ký tự padding xuất hiện ở giữa luồng mã hóa
      Các ký tự padding ở cuối stream, file hoặc chuỗi có thể được suy ra từ độ dài đã xử lý, nên nói nghiêm ngặt thì không bắt buộc
      Tuy nhiên cách xử lý padding khá tinh tế, và chính khác biệt đó đã tạo ra những biến thể triển khai thú vị: https://eprint.iacr.org/2022/361.pdf
    • Theo bài viết, một chữ số Base64 biểu diễn 6 bit dữ liệu. Byte là 8 bit, và bội chung gần nhất của 8 và 6 là 24, nên 24 bit, tức 3 byte, có thể được biểu diễn bằng 4 chữ số Base64 6 bit
      Rốt cuộc có thể xem là mã hóa theo đơn vị 24 bit. Khi dữ liệu kết thúc, phần còn lại của khối 24 bit được lấp bằng = chứ không phải A, vì A mang nghĩa dữ liệu 000000. Tôi cũng đã phải đọc toàn bộ hai lần để hiểu
  • Base64 encoder shader của tôi ở đây: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
    Tôi đã rút gọn GLSL xuống còn khoảng 13 dòng: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
    Nó được dùng trong Cursed Mode của side project, nơi framebuffer WebGL được render khoảng 15 lần mỗi giây thành BMP màu chỉ mục 640x480 pixel được mã hóa Base64: https://rezmason.github.io/excel_97_egg/?cursed=1

  • Khi bắt đầu đào sâu thì còn có thêm những chi tiết thú vị, và các biến thể của những chi tiết đó cũng nhiều đến đáng ngạc nhiên
    Nếu độ dài dữ liệu đầu vào không đúng bằng bội số của 3 byte, thì Base64 dùng 2 hoặc 3 ký tự để mã hóa 1 hoặc 2 byte cuối. Vì mỗi ký tự Base64 là 6 bit, ta sẽ dùng 12 bit hoặc 18 bit để biểu diễn 8 bit hoặc 16 bit, và kết quả là có thêm 4 bit hoặc 2 bit dư không mã hóa gì cả
    RFC yêu cầu encoder phải đặt các bit đó thành 0, nhưng chỉ nói rằng decoder có thể từ chối đầu vào nếu các bit đó không phải 0. Trên thực tế hầu như không có triển khai nào mặc định từ chối; theo tôi biết thì chỉ Ruby, Rust và Go có thể được cấu hình để thất bại với loại đầu vào đó. Python có tùy chọn validate, nhưng không kiểm tra các bit đó
    Một khác biệt lớn nữa là cách xử lý khoảng trắng và các ký tự không thuộc Base64. Có khá nhiều triển khai, bao gồm Python, âm thầm bỏ qua các ký tự tùy ý trong đầu vào. Điều này sẽ gây vấn đề nếu chọn sai bảng chữ cái; ví dụ trong Python, base64.standard_b64decode(base64.urlsafe_b64encode(b'\xFF\xFE\xFD\xFC')) không báo lỗi mà âm thầm cho ra kết quả sai
    Việc encoder Base64 của Ruby chèn xuống dòng sau mỗi 60 ký tự cũng thú vị. Ngoài PEM thì không có mã hóa chuẩn nào yêu cầu dòng ngắn như vậy, mà PEM lại yêu cầu đúng 64 ký tự mỗi dòng, nên đây là một lựa chọn khá lạ
    Tôi đã viết một bài tổng hợp khác biệt giữa các ngôn ngữ lập trình và một số thư viện JavaScript [1], đồng thời cũng đang làm việc để bổ sung Base64 tốt hơn vào JS [2]
    [1] https://gist.github.com/bakkot/16cae276209da91b652c2cb3f612a...
    [2] https://github.com/tc39/proposal-arraybuffer-base64