← 全部文章

Base64 不是加密:藏得住什麼、藏不住什麼

Base64 是編碼不是加密。看看為何密碼、JWT 載荷、API 金鑰、Basic 驗證標頭、URL 資料與紀錄在 Base64 後仍可讀。

「存之前先 Base64 一下就好了嘛。」審過夠多的組態檔、內部後台、瀏覽器 bundle、Webhook 處理器與驗證程式碼之後,你早晚會看到這句話。因為輸出看起來雜亂無章,聽起來就像很有道理:

c2VjcmV0IHBhc3N3b3Jk

但 Base64 不是加密。它不是密碼保護。也不是在 URL 裡安全藏 API 金鑰、JWT claims、使用者 ID、Basic 驗證憑據或私密資料的辦法。同一個值一次呼叫就會解出來:

atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"

實務規則很直白:如果原值不用金鑰就能還原,那 Base64 就沒有保護它,只是換了個表示方式而已。

快速安全自檢

看到程式碼裡的 Base64,先問一句它在做什麼:

Base64 被用來做什麼 合理用途? 需要檢查什麼
把位元組塞進 JSON 大小上限保持合理
內嵌一個小的 data: URI 別無腦內嵌大檔案
把密文、nonce、鹽或簽章位元組以文字形式儲存 加密步驟必須在 Base64 之前
攜帶一個不透明的分頁游標 通常可 當作介面細節,不是存取控制
建構 Basic 驗證標頭 只能搭 HTTPS 保護請求的是 TLS,不是 Base64
在前端程式碼裡藏 API 金鑰 每個瀏覽器都會拿到值和解碼器
儲存密碼 用慢速密碼雜湊
藏 JWT claims 已簽名的 JWT 載荷若沒做 JWE 加密就是可讀的
在 URL 裡藏使用者 ID 或角色 伺服器端授權仍必須執行

如果你腦中冒出的話是「這樣就沒人能讀了」,那你講的不是 Base64,是加密、雜湊、簽章、存取控制或機密管理。

Base64 實際上在做什麼

Base64 是一種二進位到文字的編碼方案。RFC 4648 定義了常見的 Base64 字母表:

A-Z a-z 0-9 + / =

字元 = 是填補。Base64 把輸入位元組按 24 位一組分組:3 個輸入位元組變成 4 個可列印字元。所以 Base64 的輸出比壓縮前的原始位元組多約 33%。

btoa("hello");
// "aGVsbG8="

atob("aGVsbG8=");
// "hello"

同樣的往返在終端機也一樣:

printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk

printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password

沒有密碼提示、私鑰、解密步驟、鹽、nonce、IV、tag,也沒有工作因子。誰拿到編碼字串誰就能還原。

Base64 與 Base64url

標準 Base64 不一定 URL 安全,因為 +/= 放在 URL、檔名、Cookie、Token 段裡都不好處理。Base64url 換了字母表:

特性 標準 Base64 Base64url
第 62、63 個字元 +/ -_
填補 通常是 = 常常省略
常見場景 MIME、PEM、Basic 驗證、二進位欄位 JWT、URL Token、檔名
安全差異

除錯 JWT 時這個很重要。JWT 用的是 Base64url,不是帶填補的標準 Base64:

header.payload.signature

如果你把一段 JWT 貼進標準 Base64 解碼器失敗了,那段字串未必是秘密的、也未必被加密了,只是可能需要按 Base64url 處理:

function base64urlToBase64(part) {
  let out = part.replace(/-/g, "+").replace(/_/g, "/");
  while (out.length % 4 !== 0) out += "=";
  return out;
}

這個轉換只換字母表,不會多出機密性。

UTF-8:瀏覽器的坑

瀏覽器的 btoa()atob() 處理的是二進位字串。純 ASCII 的例子沒問題:

btoa("hello");
// "aGVsbG8="

但 Unicode 文字得先過一步轉位元組。用 hello 測過後再去編碼客戶姓名、中文、Emoji 或阿拉伯文,團隊常常會被打個措手不及:

btoa("你好");
// InvalidCharacterError in browsers

改用 UTF-8 位元組:

function utf8ToBase64(text) {
  const bytes = new TextEncoder().encode(text);
  const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
  return btoa(binary);
}

function base64ToUtf8(base64) {
  const binary = atob(base64);
  const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"

本站的 Base64 Encode & Decode 工具會在瀏覽器裡完成 UTF-8 的往返,非 ASCII 文字會照原文字回來,不會變成亂碼。

開發者為什麼會把 Base64 當加密

Base64 常出現在真安全機制附近:

  • TLS 憑證以 PEM 文字形式儲存。
  • JWT 使用 Base64url 段。
  • HTTP Basic 驗證用 Base64 編碼 username:password
  • SSH 公鑰裡帶有 Base64 編碼的金鑰素材。
  • 加密後的密文常常在儲存或傳輸前做 Base64 編碼。

最後這一點造成了最多混淆。被加密的資料可能是 Base64 編碼的,但 Base64 不是那份加密。安全來自 AES-GCM、XChaCha20-Poly1305、RSA-OAEP、JWE 或 TLS 這類密碼學運算,Base64 只是讓結果位元組更容易放進 JSON、電子郵件、標頭或資料庫欄位。

在程式碼與產品文案裡用精準的措辭:

encodedBackup      // 只是 Base64,OK
encryptedBackup    // 真的做過加密才 OK
signedState        // 有 HMAC 或簽章保護完整性時 OK
passwordHash       // 由密碼雜湊演算法產生時 OK

這裡的命名不是裝飾。誤導的名字會讓後面的審閱者以為存在一道安全邊界——實際上並沒有。

編碼 vs 加密 vs 雜湊 vs 簽章

這些運算解決的是不同問題:

運算 主要作用 是否需要祕密 是否可逆 例子
編碼 改變表示 Base64、Base64url、URL 編碼
加密 隱藏明文 有金鑰則可逆 AES-GCM、XChaCha20-Poly1305、JWE
雜湊 單向指紋 校驗和用的 SHA-256
密碼雜湊 慢速密碼驗證 鹽加成本參數 Argon2id、bcrypt、scrypt
簽章 / MAC 證明完整性與來源 是,或金鑰對 HMAC、JWS、Ed25519 簽章

Base64 回答的是「這串位元組能走文字通道嗎?」,不回答「誰能讀?」「誰改過?」「這個使用者可以存取那筆記錄嗎?」。

真實安全錯誤 1:把 API 金鑰藏在前端程式碼

這種模式在 React 應用、瀏覽器擴充功能、WordPress 主題與內部管理工具都會見到:

// 金鑰依舊會被送到每個瀏覽器。
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");

fetch("https://api.example.com/report", {
  headers: { Authorization: `Bearer ${apiKey}` },
});

Base64 沒有幫助,因為瀏覽器同時拿到編碼後的值和解碼器。誰都能打開 DevTools、看 JavaScript bundle、跑一次一樣的 atob(),把金鑰抄走。

修復要從架構上來:

  • 真的機密留在伺服器端。
  • 瀏覽器上的金鑰用嚴格的 scope 與網域限制。
  • 敏感的上游呼叫透過你的後端代理。
  • 已經發到使用者那邊的機密全部輪換。
  • 假設機密掃描器能解碼類 Base64 的字串。

編碼一把金鑰可能讓程式碼審查更麻煩,但金鑰不會更安全。

真實安全錯誤 2:把密碼存成 Base64

這特別糟,因為它可以在資料庫裡靜靜地待上好幾年:

// 可逆儲存,別這樣做。
const storedPassword = btoa(password);

// 之後:
const password = atob(storedPassword);

資料庫一外洩,每個密碼馬上都會還原。沒有破解成本,因為根本沒東西好破解。

密碼儲存必須是單向的:

// 用密碼雜湊函式庫時的流程樣貌。
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);

正常的登入系統從來不需要還原使用者的密碼,只要能拿候選密碼比對已儲存的雜湊就夠了。

真實安全錯誤 3:把 Basic 驗證誤認為加密

那個標頭看起來夠「編碼」,能唬住人:

Authorization: Basic dXNlcjpwYXNzd29yZA==

解碼出來是:

atob("dXNlcjpwYXNzd29yZA==");
// "user:password"

HTTP Basic 驗證只有在請求被 HTTPS 保護時才可以接受。TLS 負責傳輸中的加密。Base64 只是把 username:password 打包成適合放在標頭裡的 ASCII 字串。

在純 HTTP 上,網路觀察者能直接看到憑據。日誌、反向代理、APM 追蹤與技術支援匯出中的 Basic 驗證標頭都應該當成明文密碼處理。

真實安全錯誤 4:把 JWT 載荷當作私密

一個普通的簽名 JWT 有三段 Base64url:

header.payload.signature

前兩段解碼成 JSON,第三段是簽章。

eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0

解碼結果:

{
  "sub": "1234",
  "role": "admin"
}

這並不代表 token 是假的。簽名 JWT 可以同時做到防竄改與可讀。簽章保護的是完整性:告訴伺服器 token 是否由可信任方核發,以及標頭或載荷有沒有被改動過。它不隱藏載荷。

不要在普通簽名 JWT 的載荷裡放密碼、API 金鑰、Session 機密、完整信用卡號、醫療資訊或私人個人資料。如果 claim 需要機密性,用 JWE,或乾脆不要把這些資料放進 token。

還有一點:解碼不是驗證。開發者工具能顯示 claims;伺服器在信任它們之前,仍然必須驗證簽章、issuer、audience、過期時間、key ID、演算法策略以及應用自己的授權規則。

真實安全錯誤 5:看起來不透明的 URL 參數

舊系統常這樣傳狀態:

/profile?data=eyJ1c2VySWQiOjQyfQ==

解出來是:

{
  "userId": 42
}

跟著兩個問題就來了。誰都能讀,誰都能改了以後再重新編碼。如果 /profile?data=... 是決定要載入哪筆使用者資料的唯一依據,那 bug 就是缺少授權。

更安全的做法:

  • 把解碼出來的 URL 資料當成不可信任輸入。
  • 在伺服器再檢查一次權限。
  • 狀態敏感時優先使用短的、伺服器端隨機參照。
  • 客戶端如果得攜帶防竄改的狀態,用 HMAC 或簽章。
  • 私密狀態別放在 URL 裡,URL 會被複製到日誌、瀏覽器歷史、分析與技術支援截圖。

真實安全錯誤 6:只是編碼卻叫「加密匯出」

程式碼審查的另一種味道是這種函式:

function exportEncryptedBackup(data) {
  return btoa(JSON.stringify(data));
}

函式名說加密,程式碼說編碼。

若這個匯出只想做一段方便攜帶的文字 blob,就改個名字:

function exportBase64Backup(data) {
  return btoa(JSON.stringify(data));
}

如果真的要告訴使用者匯出是加密的,就用真正的認證加密,把金鑰管理明確寫清楚:經過審核的密碼函式庫、隨機的 nonce/IV、認證 tag、安全的金鑰生成,還有金鑰存放計畫。之後再用 Base64 包一層密文當然可以。

Base64 真正擅長的事

Base64 是有用的,錯的是給它派安全任務。

好的用途:

  • JSON、XML、HTML 或電子郵件裡的二進位資料。
  • MIME 電子郵件附件。
  • 圖片或字型的小 data: URI。
  • 密文、簽章、nonce、鹽與公鑰等密碼學輸出的格式化。
  • JWT 與 JOSE 的緊湊序列化各段。
  • 可讀性屬於產品層級關注、不是安全邊界的不透明分頁游標。
  • 除錯 API 欄位、JWT 載荷與設定 blob。

安全的措辭是「Base64 編碼」,不是「加密」。就這一個詞,能省掉在正式環境裡多得驚人的誤解。

該用什麼代替

如果你伸手去找 Base64 是想得到安全,那就挑一個切合場景的原語:

目標 該用什麼
對使用者或攻擊者隱藏資料 認證加密,例如 AES-GCM 或 XChaCha20-Poly1305
保護傳輸中的資料 HTTPS/TLS
儲存密碼 Argon2id、bcrypt 或 scrypt,搭配每筆密碼獨立鹽與調整後的成本設定
儲存 API 金鑰 機密管理服務、伺服器端環境、scope、稽核紀錄與輪換
證明某個值沒被改過 HMAC 或數位簽章
阻止使用者編輯 URL 狀態 伺服器端授權,加上簽章狀態或伺服器端 Session 參照
讓 JWT claim 保密 JWE,或乾脆不要把私密 claim 放進 token
把二進位塞進 JSON、電子郵件或標頭 Base64 就是對的選擇

難的部分很少是演算法名稱,而是威脅建模、金鑰管理、輪換、存取控制,以及一開始就要決定誰可以看見明文。

實用的程式碼審查清單

先找出 Base64 的使用點:

rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"

對每個命中的地方做分類:

  1. 傳輸:為 JSON、MIME、data URI、PEM 或儲存做位元組到文字的轉換。通常沒問題。
  2. 加密後的格式化:密文、nonce、簽章、鹽或公鑰。密碼學正確的話通常沒問題。
  3. 不透明介面:游標、邀請碼或客戶端攜帶的狀態。檢查授權與防竄改。
  4. 藏機密:API 金鑰、密碼、token、憑據、私密 claim 或使用者資料。這不 OK。
  5. 命名誤導:變數或 UI 文案宣稱加密、安全、受保護、機密、遮罩或隱藏。要嘛改名字,要嘛改實作。

接著問自己:

  • 編碼後的值會不會到達瀏覽器、行動 App、使用者、日誌收集器、代理或技術支援工具?
  • 旁邊是不是就放著解碼器?
  • 解碼後有沒有伺服器在檢查權限?
  • 如果客戶端可以修改,這個值是否被簽章過?
  • 如果是 JWT,做出信任決策前是否驗證過簽章?
  • 如果是密碼,為什麼它是可逆的?

這樣可以讓審查踩在地上。data URI 裡的 PNG 是無聊題材;JavaScript bundle 裡的正式環境 API 金鑰不是。

事件回應:發現 Base64「機密」怎麼辦

如果你在原始碼、日誌、工單、分析、URL 或資料庫匯出裡發現 Base64 編碼的機密:

  1. 本地解碼,確認到底是什麼。
  2. 把解碼後的值視為已經外洩。
  3. 輪換任何可能已經被看到的憑據、token 與金鑰。
  4. 盡量從原始碼、日誌、截圖、工單中移除編碼版本。
  5. 把設計換成伺服器端機密、帶 scope 的公鑰、簽章狀態、真正的加密或密碼雜湊。
  6. 加一條審查規則,讓同樣的模式別再回來。

不必花時間爭論那串 Base64「難不難被察覺」。自動化掃描器、瀏覽器使用者、攻擊者都能解開。

你真的需要加密 token 的時候

標準的簽名 JWT 用的是 JWS:可讀的載荷、防竄改的簽章。若你真的需要 claim 的機密性,用 JWE(JSON Web Encryption)。

一個緊湊 JWE 有五段 Base64url:

protected-header.encrypted-key.iv.ciphertext.authentication-tag

這些段仍是 Base64url 文字。機密性來自 JWE 的加密演算法和收件人的金鑰,而不是 Base64url 本身。

在許多 Web 應用裡,更簡單的設計比較好:把敏感狀態留在伺服器,只給瀏覽器一個短的隨機 Session 識別碼。不是每個可讀的 JWT 都要變成 JWE。

常見問題

Base64 是加密嗎?

不是。Base64 是沒有金鑰的可逆編碼。任何拿到那串字串的人,都能用瀏覽器函式、終端機命令、線上工具或幾行程式碼解開。

Base64 至少算混淆吧?

只在最弱的意義上算。它也許能擋住隨手一瞥,但擋不住開發者、攻擊者、掃描器、瀏覽器擴充功能、日誌閱讀者與技術支援工具。

Base64 對密碼或 API 金鑰安全嗎?

不安全。密碼需要 Argon2id、bcrypt、scrypt 這類慢速密碼雜湊。API 金鑰應該留在伺服器端,或放在具備 scope、可稽核性與輪換能力的機密管理服務。

大家都能讀 JWT 載荷嗎?

普通的簽名 JWT 是的。標頭與載荷是以 Base64url 編碼的 JSON。簽章在驗證之後能證明完整性,但並不隱藏 claim。

既然不安全,Basic 驗證為什麼要用 Base64?

Basic 驗證用 Base64 是為了讓 username:password 塞進 HTTP 標頭。傳輸層加密由 HTTPS/TLS 負責。沒有 HTTPS,Basic 驗證的憑據在網路上基本就是明文。

Base64url 比 Base64 更安全嗎?

不。Base64url 把 +/ 換成 -_,也常常省去填補,讓值可以在 URL 與 JWT 裡使用。它仍然是可逆編碼。

那要加密的話該用什麼代替 Base64?

用經過審查的認證加密設計,例如 AES-GCM、XChaCha20-Poly1305 或 JWE,加上妥當的金鑰生成、金鑰儲存、輪換與存取控制。不要自造密碼學格式。

那 Base64 到底是幹嘛的?

Base64 把位元組表達成文字:JSON 的二進位欄位、電子郵件附件、data URI、PEM 檔、簽章、密文的格式化,以及 URL 安全的 token 段。它是包裝層,不是安全邊界。

在瀏覽器裡編碼與解碼

現在就想看看某個 Base64 字串?fixjson.org 上的 Base64 Encode & Decode 可以在瀏覽器裡本地編碼或解碼文字,包含 UTF-8 內容。用它來看 JWT 載荷、API 回應欄位或 data: URI,但別把解碼當作驗證或解密。

相關工具與指南

參考

最後審閱:2026 年 7 月。