Base64 là gì: không phải mã hóa bảo mật, và các cái bẫy

Base64 biến byte thành văn bản thế nào, vì sao không phải mã hóa, dung lượng tăng 33%, Base64 an toàn cho URL, dấu =, bẫy UTF-8 với btoa và data URI.

Trong bài này

Base64 là cách viết bất kỳ chuỗi byte nào, văn bản hay nhị phân, chỉ bằng 64 ký tự thông thường, để dữ liệu đi qua được những hệ thống chỉ tin cậy văn bản: email, JSON, URL, thuộc tính HTML và tệp cấu hình. Nó không phải mã hóa bảo mật và không che giấu được gì, nó làm dữ liệu to thêm khoảng 33%, và có vài cái bẫy: biến thể an toàn cho URL, dấu = đệm, và việc văn bản phải được chuyển thành byte UTF-8 trước. Bài này cho thấy cách mã hóa hoạt động qua một ví dụ tự tay kiểm chứng được, các cái bẫy với giá trị thật bạn có thể dán vào công cụ Base64, và khi nào nên dùng data URI, khi nào không.

Cách hoạt động

Base64 đọc dữ liệu từng ba byte một, tức 24 bit, rồi cắt 24 bit đó thành bốn nhóm 6 bit. Mỗi nhóm 6 bit là một số từ 0 đến 63, và mỗi số được viết bằng một ký tự trong bảng sau: A đến Z là 0 đến 25, a đến z là 26 đến 51, 0 đến 9 là 52 đến 61, + là 62 và / là 63.

Ví dụ làm tay với ba chữ cái Man:

BướcGiá trị
Byte (ASCII)M = 77, a = 97, n = 110
Dạng nhị phân 8 bit01001101 01100001 01101110
Gom lại theo 6 bit010011 010110 000101 101110
Dạng số19, 22, 5, 46
Dạng ký tựT, W, F, u

Vậy Man là TWFu. Ba byte luôn thành bốn ký tự.

Phần đệm

Khi dữ liệu không chia hết cho ba, nhóm cuối bị thiếu. Base64 điền thêm = để độ dài kết quả chia hết cho bốn:

  • Man (3 byte) là TWFu: không đệm.
  • Ma (2 byte) là TWE=: một dấu =.
  • M (1 byte) là TQ==: hai dấu =.

Phần đệm không mang thông tin gì, vì độ dài đã cho biết thiếu bao nhiêu byte. Đó là lý do bộ giải mã có thể chấp nhận chuỗi thiếu nó, và nhiều hệ thống, trong đó có JWT, bỏ nó đi. Dấu = chỉ hợp lệ ở ngay cuối chuỗi.

Base64 không phải mã hóa bảo mật

Ai cũng giải mã được Base64, không cần khóa và chẳng tốn công. Một yêu cầu web mang Authorization: Basic dXNlcjpwYXNz đang gửi đi chuỗi user:pass được viết bằng Base64. Vì vậy xác thực Basic chỉ được đi qua HTTPS. Secret của Kubernetes cũng lưu giá trị bằng Base64, đó là cách để chứa giá trị nhị phân trong một tệp văn bản, không phải cách để bảo vệ chúng.

Nếu cần giấu một thứ gì, hãy dùng mã hóa thật. Nếu cần kiểm tra dữ liệu không bị đổi, hãy dùng mã băm; bài kiểm tra tệp tải về bằng SHA-256 chỉ cách làm, và kết quả ở đó là hệ thập lục phân, không phải Base64. Mã hóa dạng này được thiết kế để đảo ngược được. Mã băm thì không.

Cái giá về dung lượng

Cứ 3 byte thành 4 ký tự, nên kết quả lớn hơn khoảng một phần ba: 33,3%, cộng thêm tối đa hai dấu = ở cuối.

Kích thước đầu vàoĐộ dài Base64Tăng
3 byte4 ký tự33%
100 byte136 ký tự36%
1.000 byte1.336 ký tự33,6%
3.000 byte4.000 ký tự33,3%
1.000.000 byte1.333.336 ký tự33,3%

Công thức là 4 × phần nguyên làm tròn lên của (số byte ÷ 3). Ngắt kết quả thành dòng 64 ký tự như trong chứng chỉ PEM, hoặc 76 ký tự như trong tệp đính kèm email, sẽ thêm một dấu xuống dòng cho mỗi dòng.

Cái bẫy UTF-8

Base64 mã hóa byte, không phải chữ cái. Văn bản phải được chuyển thành byte trước, và bộ mã nào được dùng là điều quan trọng. Trên web đó là UTF-8. Từ café trong UTF-8 là năm byte 63 61 66 c3 a9, nên Base64 của nó là Y2Fmw6k=.

Hàm btoa cũ của JavaScript coi mỗi ký tự là một byte, nên với café nó cho Y2Fm6Q==. Một hệ thống khác giải mã chuỗi đó theo UTF-8 sẽ thấy caf theo sau là một byte không hợp lệ. Với Việt thì nó còn không thử: btoa("Việt") ném lỗi InvalidCharacterError, emoji cũng vậy. Cách đúng trong trình duyệt là chuyển văn bản thành byte UTF-8 trước:

const bytes = new TextEncoder().encode("Việt");
const base64 = btoa(String.fromCharCode(...bytes)); // "Vmnhu4d0"

const back = new TextDecoder().decode(
  Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)),
); // "Việt"

Việc trải các byte vào String.fromCharCode ổn với văn bản ngắn; với dữ liệu lớn hãy chuyển đổi theo từng đoạn hoặc đọc tệp bằng FileReader, vì danh sách đối số quá dài có thể làm tràn ngăn xếp gọi hàm. Câu mẫu dài hơn Hello, Việt Nam! 😀 café được mã hóa thành SGVsbG8sIFZp4buHdCBOYW0hIPCfmIAgY2Fmw6k= và giải mã ra y nguyên. Trên dòng lệnh, byte là thứ terminal của bạn gửi, thường là UTF-8:

printf 'café' | base64          # Y2Fmw6k=
printf 'Y2Fmw6k=' | base64 -d   # café

Trên các bản macOS cũ hãy dùng -D thay cho -d. base64 của GNU ngắt kết quả ở 76 ký tự trừ khi bạn thêm -w 0. Trong PowerShell: [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("café")).

Nếu văn bản sau khi giải mã hiện ký tự lạ như é thay vì é, nghĩa là văn bản được mã hóa UTF-8 nhưng bị đọc như Latin-1, hoặc ngược lại. Vấn đề nằm ở bộ mã, không phải ở Base64.

Base64 an toàn cho URL

Bảng ký tự chuẩn có + và /, và cả hai đều có ý nghĩa trong URL: + có thể bị hiểu là dấu cách trong chuỗi truy vấn, còn / là dấu phân cách đường dẫn. Biến thể an toàn cho URL, gọi là Base64URL, viết - thay cho + và _ thay cho /, và thường bỏ phần đệm =. Bốn byte fb ff fe ff là +//+/w== trong Base64 chuẩn và -__-_w trong Base64 an toàn cho URL không đệm. Hãy dùng dạng an toàn cho URL với mọi thứ đi vào URL, tên tệp hay token. Phần đầu {"alg":"HS256","typ":"JWT"} là eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, trùng hợp là không cần đệm; xem cách đọc một JWT để biết Base64URL được dùng ngoài thực tế ra sao.

Data URI

Data URI đặt cả một tệp vào trong một giá trị văn bản: data:image/png;base64, theo sau là Base64 của tệp. Tệp PNG hợp lệ nhỏ nhất, một ảnh trong suốt 1 × 1, nặng 70 byte và thành 96 ký tự:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

Bạn có thể dùng nó làm src của thẻ <img>, hoặc trong url() của CSS. Lợi ích là bớt một yêu cầu mạng, điều có thể có ý nghĩa với một biểu tượng rất nhỏ. Cái giá thì có thật:

  • Dữ liệu to hơn một phần ba.
  • Trình duyệt không lưu đệm riêng được ảnh tách khỏi trang hoặc tệp CSS chứa nó.
  • Trang nặng lên trước khi hiện được gì.
  • Nhiều ứng dụng email không hiển thị ảnh data URI.

Nguyên tắc chung: chỉ dùng data URI cho ảnh vài kilobyte trở xuống. Với thứ gì lớn hơn, một tệp bình thường tốt hơn; hãy thu nhỏ nó trước bằng bộ công cụ ảnh.

Dùng công cụ Base64

  1. Chọn Mã hóa hoặc Giải mã.
  2. Gõ hoặc dán, bấm "Mở tệp", hoặc thả tệp vào trang (tối đa 20 MB). Kết quả cập nhật khi bạn gõ.
  3. Ở Bảng ký tự, chọn Chuẩn hoặc An toàn cho URL. Ở Đệm bằng =, bật hoặc tắt phần đệm. Ở Độ dài dòng, chọn một dòng, 64 ký tự (PEM) hoặc 76 ký tự (MIME).
  4. Bật Dạng data: URI để có giá trị dán ngay được với tiền tố đúng.
  5. Bấm Sao chép hoặc Tải xuống. "Dùng làm đầu vào" chuyển kết quả sang thẻ còn lại để bạn kiểm tra vòng đi và về.

Giải mã khá dễ tính. Khoảng trắng và dấu xuống dòng bị bỏ qua, = thiếu được tự thêm, - và _ được chấp nhận, và cả một data: URI được tự bóc ra. Công cụ cho biết nó đã làm điều nào. Nếu chuỗi không phải Base64, công cụ chỉ ra ký tự sai đầu tiên và vị trí của nó. Khi thứ bạn giải mã không phải văn bản, như ảnh, PDF hay ZIP, bạn nhận được một tệp tải về đúng loại thay vì rác trên màn hình, và ảnh còn được xem trước. Mọi thứ chạy trong trình duyệt, nên token hay tài liệu không bị tải lên.

Những lỗi thường gặp

  • Coi Base64 là cách giấu mật khẩu hay khóa. Không phải. Ai cũng đảo ngược được.
  • Vô tình mã hóa hai lần. Một chuỗi giải mã ra lại là chuỗi trông như Base64 nghĩa là đã bị mã hóa hai lần. Hãy giải mã thêm lần nữa.
  • Lẫn lộn hai bảng ký tự. + và / trong token lẽ ra phải an toàn cho URL, hoặc - và _ trong token không phải vậy, làm bộ giải mã nghiêm ngặt báo lỗi.
  • Cắt hoặc thêm ký tự. Một dấu nháy thừa, dấu cách do sao chép, hay thiếu ký tự cuối đều làm đổi kết quả. Độ dài dư một ký tự lẻ thì không thể là Base64 hợp lệ.
  • Dùng `btoa` với văn bản có dấu. Hãy chuyển sang byte UTF-8 trước.
  • Tưởng kết quả sẽ nhỏ hơn. Nó luôn lớn hơn. Nén, chứ không phải Base64, mới làm dữ liệu nhỏ đi.

Danh sách kiểm tra ngắn

Nhớ rằng Base64 là cách biểu diễn, không phải mã hóa bảo mật. Chuyển văn bản thành byte UTF-8 trước khi mã hóa. Dùng bảng ký tự an toàn cho URL với URL và token, và xem phần đệm có được mong đợi không. Chờ đợi dung lượng tăng 33%, và chỉ dùng data URI cho tệp rất nhỏ. Khi kết quả giải mã trông sai, hãy kiểm tra bộ mã ký tự trước khi nghi ngờ Base64.

Công cụ được nhắc trong bài

Về trang blog