"저장하기 전에 그냥 Base64 로 인코딩하면 되잖아." 설정 파일, 사내 대시보드, 브라우저 번들, 웹훅 핸들러, 인증 코드를 충분히 리뷰하다 보면 결국 이 문장을 만납니다. 결과가 뒤죽박죽으로 보이니 그럴싸하게 들리죠.
c2VjcmV0IHBhc3N3b3Jk
하지만 Base64 는 암호화가 아닙니다. 비밀번호 보호도 아닙니다. API 키, JWT 클레임, 사용자 ID, Basic 인증 자격 증명, URL 안의 비공개 데이터를 안전하게 감추는 방법도 아닙니다. 같은 값이 한 번의 호출로 디코딩됩니다.
atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"
현장 규칙은 단호합니다. 비밀 키 없이 원본이 그대로 돌아온다면, Base64 는 그것을 지키지 않은 겁니다. 표현만 바꿨을 뿐입니다.
빠른 보안 점검
코드에서 Base64 를 보면, 어떤 일을 시키려는 건지 물어보세요.
| Base64 가 하고 있는 일 | 좋은 용도? | 확인할 것 |
|---|---|---|
| 바이트를 JSON 에 담기 | Yes | 크기 상한을 합리적으로 |
작은 data: URI 임베드 |
Yes | 큰 파일을 무조건 인라인하지 말 것 |
| 암호문, nonce, 솔트, 서명 바이트를 텍스트로 저장 | Yes | 암호 처리는 Base64 앞에서 |
| 불투명한 페이지네이션 커서를 나르기 | 대개 OK | 접근 제어가 아니라 인터페이스 세부로 취급 |
| Basic 인증 헤더 구성 | HTTPS 와만 | 요청을 보호하는 건 TLS, Base64 아님 |
| 프런트엔드에서 API 키 감추기 | No | 모든 브라우저가 값과 디코더를 함께 받음 |
| 비밀번호 저장 | No | 느린 비밀번호 해싱 사용 |
| JWT 클레임 감추기 | No | 서명된 JWT 페이로드는 JWE 로 암호화되지 않는 한 읽힘 |
| URL 에서 사용자 ID 나 역할 감추기 | No | 서버 측 인가는 그대로 돌아야 함 |
머릿속에 떠오르는 문장이 "아무도 못 읽게"라면, 그건 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, 태그, 작업량 인자 어느 것도 없습니다. 인코딩된 문자열을 가진 사람이면 누구든 되돌릴 수 있습니다.
Base64 와 Base64url
표준 Base64 는 항상 URL 안전하지는 않습니다. +, /, = 는 URL, 파일명, 쿠키, 토큰 세그먼트에서 불편하기 때문입니다. Base64url 은 알파벳을 바꿉니다.
| 특성 | 표준 Base64 | Base64url |
|---|---|---|
| 62 번째와 63 번째 문자 | + 와 / |
- 와 _ |
| 패딩 | 보통 = |
흔히 생략 |
| 자주 쓰는 곳 | MIME, PEM, Basic 인증, 이진 필드 | JWT, URL 토큰, 파일명 |
| 보안상 차이 | 없음 | 없음 |
JWT 디버깅에서는 이게 중요합니다. JWT 는 패딩된 표준 Base64 가 아니라 Base64url 을 씁니다.
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="
하지만 유니코드 텍스트는 먼저 바이트 단계가 필요합니다. hello 로만 테스트하다가 나중에 고객 이름, 중국어, 이모지, 아랍어를 인코딩하면서 놀라는 팀이 많습니다.
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 인증은
username:password에 Base64 를 씁니다. - SSH 공개 키에는 Base64 로 인코딩된 키 자료가 들어 있습니다.
- 암호문은 저장이나 전송 전에 Base64 로 인코딩되는 경우가 많습니다.
마지막 항목이 가장 큰 혼란의 원인입니다. 암호화된 데이터가 Base64 로 인코딩될 수는 있지만, Base64 자체가 암호화는 아닙니다. 보안은 AES-GCM, XChaCha20-Poly1305, RSA-OAEP, JWE, TLS 같은 암호 처리에서 나옵니다. Base64 는 그 결과 바이트를 JSON, 이메일, 헤더, DB 컬럼에 담기 쉽게 만들어 줬을 뿐입니다.
코드와 제품 문구에서 정확한 단어를 쓰세요.
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 번들을 살펴본 뒤 같은 atob() 을 실행해 키를 복사할 수 있습니다.
해결은 아키텍처 차원입니다.
- 진짜 시크릿은 서버에 둡니다.
- 브라우저 키는 스코프와 도메인 제한을 좁게 겁니다.
- 민감한 상위 호출은 백엔드를 통해 프록시합니다.
- 이미 사용자에게 나간 시크릿은 모두 로테이트합니다.
- 시크릿 스캐너가 Base64 유사 문자열도 디코딩한다고 가정합니다.
키를 인코딩하면 코드 리뷰가 성가시게 됩니다. 키가 안전해지지는 않습니다.
실제 보안 실수 2: Base64 로 저장된 비밀번호
이건 DB 안에 몇 년씩 조용히 남아 있을 수 있어서 특히 해롭습니다.
// 가역 저장. 하지 마세요.
const storedPassword = btoa(password);
// 나중에:
const password = atob(storedPassword);
DB 가 유출되면 모든 비밀번호가 즉시 돌아옵니다. 깨야 할 것이 없으니 크래킹 비용도 없습니다.
비밀번호 저장은 단방향이어야 합니다.
// 비밀번호 해싱 라이브러리로 처리하는 흐름의 형태.
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 가 전송 중 HTTP 교환을 암호화합니다. Base64 는 username:password 를 헤더에 담기 좋은 ASCII 문자열로 포장해줄 뿐입니다.
평문 HTTP 위에서는 네트워크 관찰자가 자격 증명을 그대로 봅니다. 로그, 리버스 프록시, APM 트레이스, 지원용 덤프에서는 Basic 인증 헤더를 평문 비밀번호처럼 다뤄야 합니다.
실제 보안 실수 4: JWT 페이로드를 비공개로 오해
일반적인 서명된 JWT 는 세 개의 Base64url 섹션을 가집니다.
header.payload.signature
앞의 두 섹션은 JSON 으로 디코딩됩니다. 세 번째는 서명입니다.
eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0
디코딩 결과는 이렇습니다.
{
"sub": "1234",
"role": "admin"
}
이게 토큰이 가짜라는 뜻은 아닙니다. 서명된 JWT 는 변조 감지가 되면서도 읽힐 수 있습니다. 서명이 지키는 건 무결성입니다. 신뢰할 수 있는 주체가 발급했는지, 헤더나 페이로드가 바뀌었는지를 서버에 알려주죠. 페이로드를 감춰주지는 않습니다.
일반적인 서명된 JWT 페이로드에 비밀번호, API 키, 세션 시크릿, 완전한 카드 번호, 의료 정보, 비공개 프로필 데이터를 넣지 마세요. 클레임에 기밀성이 필요하면 JWE 를 쓰거나 아예 토큰에 그 데이터를 담지 마세요.
또 하나. 디코딩은 검증이 아닙니다. 개발자 도구가 클레임을 보여줄 수는 있어도, 서버는 신뢰 판단 전에 서명, 발급자, 대상, 만료, 키 ID, 알고리즘 정책, 애플리케이션 고유 인가 규칙을 반드시 검증해야 합니다.
실제 보안 실수 5: 불투명해 보이는 URL 파라미터
레거시 앱은 상태를 이렇게 넘기곤 합니다.
/profile?data=eyJ1c2VySWQiOjQyfQ==
디코딩 결과는 이렇습니다.
{
"userId": 42
}
두 가지 문제가 따라옵니다. 누구나 읽을 수 있고, 누구나 바꾼 뒤 다시 인코딩할 수 있습니다. /profile?data=... 만으로 어떤 사용자 레코드를 불러올지 결정된다면, 진짜 버그는 인가 부재입니다.
더 안전한 패턴.
- 디코딩된 URL 데이터는 신뢰할 수 없는 입력으로 취급합니다.
- 권한은 서버에서 다시 확인합니다.
- 상태가 민감하다면 짧고 무작위한 서버 측 참조를 선호합니다.
- 클라이언트가 변조 감지 가능한 상태를 지녀야 한다면 HMAC 이나 서명을 씁니다.
- 비공개 상태는 URL 에 넣지 마세요. URL 은 로그, 브라우저 기록, 애널리틱스, 지원 스크린샷에 복사됩니다.
실제 보안 실수 6: 인코딩일 뿐인 "암호화" 익스포트
또 하나의 코드 리뷰 냄새는 이런 함수입니다.
function exportEncryptedBackup(data) {
return btoa(JSON.stringify(data));
}
함수 이름은 암호화라고 말합니다. 코드는 인코딩이라고 말합니다.
익스포트가 그냥 이식 가능한 텍스트 덩어리면 되는 거라면, 이름을 바꾸세요.
function exportBase64Backup(data) {
return btoa(JSON.stringify(data));
}
사용자에게 익스포트가 암호화됐다고 안내하고 있다면, 진짜 인증 암호화를 쓰고 키 취급을 명시적으로 하세요. 검증된 암호 라이브러리, 무작위 nonce/IV, 인증 태그, 안전한 키 생성, 키 보관 계획을 갖추라는 뜻입니다. Base64 는 그 뒤에 암호문을 감쌀 때 여전히 쓸 수 있습니다.
Base64 가 실제로 잘하는 일
Base64 는 유용합니다. 문제는 그것에 보안 임무를 맡기는 겁니다.
좋은 용도.
- JSON, XML, HTML, 이메일 안의 이진 데이터.
- MIME 이메일 첨부.
- 이미지나 폰트를 위한 작은
data:URI. - 암호문, 서명, nonce, 솔트, 공개 키 등 암호 출력의 형식화.
- JWT 와 JOSE 컴팩트 직렬화 세그먼트.
- 가독성이 제품 관심사이지 보안 경계는 아닌 불투명한 페이지네이션 커서.
- API 필드, JWT 페이로드, 설정 덩어리 디버깅.
안전한 표현은 "Base64 인코딩된"이지 "암호화된"이 아닙니다. 이 한 단어가 놀랄 만큼 많은 프로덕션 오해를 예방합니다.
대신 무엇을 쓰면 되는가
보안이 필요해서 Base64 를 잡았다면, 목적에 맞는 프리미티브를 고르세요.
| 목표 | 대신 쓸 것 |
|---|---|
| 사용자나 공격자에게 데이터 감추기 | AES-GCM 이나 XChaCha20-Poly1305 같은 인증 암호화 |
| 전송 중 데이터 보호 | HTTPS/TLS |
| 비밀번호 저장 | Argon2id, bcrypt, scrypt 를 비밀번호별 솔트와 조정된 비용 설정으로 |
| API 키 저장 | 시크릿 매니저, 서버 측 환경, 스코프, 감사 로그, 로테이션 |
| 값이 변경되지 않았음을 증명 | HMAC 이나 디지털 서명 |
| URL 상태를 사용자가 편집 못 하게 | 서버 측 인가와 서명된 상태, 혹은 서버 측 세션 참조 |
| JWT 클레임을 비공개로 유지 | JWE, 또는 비공개 클레임을 토큰에 아예 담지 않기 |
| JSON, 이메일, 헤더에 이진 담기 | Base64 가 맞습니다 |
어려운 부분은 알고리즘 이름이 아니라 위협 모델링, 키 관리, 로테이션, 접근 제어, 그리고 애초에 누가 평문을 볼 수 있어야 하는가에 대한 판단입니다.
실용적인 코드 리뷰 체크리스트
Base64 사용처를 찾으세요.
rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"
각 결과를 분류합니다.
- 전송: JSON, MIME, data URI, PEM, 스토리지용 바이트-텍스트 변환. 대개 문제 없음.
- 암호 처리 후 형식화: 암호문, nonce, 서명, 솔트, 공개 키. 암호 처리가 올바르면 대개 문제 없음.
- 불투명 인터페이스: 커서, 초대 코드, 클라이언트가 지니는 상태. 인가와 변조 방지를 확인.
- 시크릿 숨김: API 키, 비밀번호, 토큰, 자격 증명, 비공개 클레임, 사용자 데이터. 문제 있음.
- 오해를 부르는 명명: 변수나 UI 문구가 암호화, 안전, 보호, 비밀, 마스킹, 숨김을 자칭. 이름이나 구현을 바꿉니다.
이어서 이렇게 자문하세요.
- 인코딩된 값이 브라우저, 모바일 앱, 사용자, 로그 수집기, 프록시, 지원 도구로 흘러갈 수 있나?
- 바로 옆에 디코더가 있나?
- 디코딩 이후 서버가 권한을 검증하나?
- 클라이언트가 편집할 수 있는 값이라면 서명되어 있나?
- JWT 라면 신뢰 판단 전에 서명을 검증하는가?
- 비밀번호라면 왜 애초에 가역인가?
이 질문들이 리뷰를 현실에 붙들어 둡니다. data URI 안의 PNG 는 지루한 얘기입니다. JavaScript 번들 안의 프로덕션 API 키는 그렇지 않습니다.
사고 대응: Base64 "시크릿"을 발견하면
소스 코드, 로그, 티켓, 애널리틱스, URL, DB 익스포트에서 Base64 로 인코딩된 시크릿을 발견했다면 이렇게 움직이세요.
- 로컬에서 디코딩해 무엇인지 확인합니다.
- 디코딩된 값은 노출된 것으로 취급합니다.
- 눈에 띄었을 수 있는 자격 증명, 토큰, 키를 로테이트합니다.
- 가능한 곳에서는 소스, 로그, 스크린샷, 티켓에서 인코딩된 사본을 제거합니다.
- 설계를 서버 측 시크릿, 스코프 있는 공개 키, 서명된 상태, 진짜 암호화, 비밀번호 해시로 대체합니다.
- 같은 패턴이 되돌아오지 않도록 리뷰 규칙을 추가합니다.
그 Base64 문자열이 "눈에 잘 안 띄었는지" 논쟁하는 데 시간을 쓰지 마세요. 자동 스캐너, 브라우저 사용자, 공격자 모두 디코딩할 수 있습니다.
진짜로 암호화 토큰이 필요할 때
표준 서명된 JWT 는 JWS 를 씁니다. 페이로드는 읽히고 서명은 변조 감지가 됩니다. 클레임의 기밀성이 정말로 필요하다면 JWE(JSON Web Encryption) 를 쓰세요.
컴팩트 JWE 는 다섯 개의 Base64url 섹션을 가집니다.
protected-header.encrypted-key.iv.ciphertext.authentication-tag
그 섹션들은 여전히 Base64url 텍스트입니다. 기밀성은 Base64url 자체가 아니라 JWE 의 암호화 알고리즘과 수신자의 키에서 옵니다.
많은 웹 앱에서는 더 단순한 설계가 낫습니다. 민감한 상태는 서버에 두고 브라우저에는 짧고 무작위한 세션 식별자만 보냅니다. 읽히는 JWT 라고 다 JWE 로 바꿔야 할 필요는 없습니다.
자주 묻는 질문
Base64 는 암호화인가요?
아닙니다. Base64 는 비밀 키가 없는 가역 인코딩입니다. 문자열을 가진 사람은 브라우저 함수, 터미널 명령, 온라인 도구, 몇 줄의 코드로 언제든 디코딩할 수 있습니다.
최소한 난독화 정도는 되나요?
가장 약한 의미에서만 그렇습니다. 슬쩍 보는 눈으로부터 값은 감출 수 있어도, 개발자, 공격자, 스캐너, 브라우저 확장, 로그 리더, 지원 도구를 막지는 못합니다.
비밀번호나 API 키에 Base64 가 안전한가요?
아닙니다. 비밀번호는 Argon2id, bcrypt, scrypt 같은 느린 비밀번호 해싱이 필요합니다. API 키는 서버 측에 두거나 스코프, 감사 가능성, 로테이션을 갖춘 시크릿 매니저에 넣어야 합니다.
누구나 JWT 페이로드를 읽을 수 있나요?
평범한 서명된 JWT 는 그렇습니다. 헤더와 페이로드는 Base64url 로 인코딩된 JSON 입니다. 서명은 검증 후 무결성을 증명할 수 있지만, 클레임을 감춰주지는 않습니다.
안전하지 않다면 왜 Basic 인증은 Base64 를 쓰나요?
Basic 인증은 username:password 를 HTTP 헤더에 담기 위해 Base64 를 씁니다. 전송 중 암호화는 HTTPS/TLS 가 맡습니다. HTTPS 가 없으면 Basic 인증 자격 증명은 네트워크상 사실상 평문입니다.
Base64url 이 Base64 보다 안전한가요?
아닙니다. Base64url 은 + 와 / 를 - 와 _ 로 바꾸고, URL 과 JWT 에서 잘 동작하도록 흔히 패딩을 생략합니다. 여전히 가역 인코딩입니다.
암호화에는 Base64 대신 무엇을 쓰나요?
AES-GCM, XChaCha20-Poly1305, JWE 같은 검증된 인증 암호화 설계에, 제대로 된 키 생성, 키 보관, 로테이션, 접근 제어를 곁들여 씁니다. 자체 암호 포맷은 설계하지 마세요.
그럼 Base64 는 원래 뭘 위한 건가요?
Base64 는 바이트를 텍스트로 표현합니다. JSON 의 이진 필드, 이메일 첨부, data URI, PEM 파일, 서명, 암호문 형식화, URL 안전 토큰 세그먼트. 감싸주는 것이지 보안 경계가 아닙니다.
브라우저에서 인코딩·디코딩
지금 당장 Base64 문자열을 확인해야 하나요? fixjson.org 의 Base64 Encode & Decode 는 브라우저 안에서 UTF-8 콘텐츠 포함해 로컬로 인코딩·디코딩할 수 있게 해 줍니다. JWT 페이로드, API 응답 필드, data: URI 확인에 쓰되, 디코딩을 검증이나 복호화로 착각하지 마세요.
관련 도구 및 가이드
- Base64 Encode & Decode — 브라우저 안에서 로컬로 Base64 인코딩·디코딩.
- Base64 문자열과 JWT 페이로드 디코딩 — 실전 Base64 및 Base64url 디코딩.
- JWT 디코딩 방법 — 클레임을 읽고 왜 디코딩이 검증이 아닌지 이해하기.
- 민감한 JSON 과 로컬 도구 — 브라우저 도구에 붙여도 되는지 판단하기.
- JSON Stringify — 중첩된 JSON 문자열 살펴보고 이스케이프하기.
- RFC 4648: Base64 표준 — Base64 와 Base64url 의 공식 배경.
- RFC 7519: JSON Web Token — JWT 구조와 보안 고려사항.
출처
- RFC 4648 — Base64, Base64url, 패딩, 알파벳, 보안 고려사항.
- RFC 7617 — HTTP Basic 인증과 Base64 자격 증명 형식.
- RFC 7519 — JWT 구조, 신뢰 판단, 프라이버시 고려사항.
- RFC 7516 — JSON Web Encryption 과 컴팩트 JWE 직렬화.
- MDN btoa 와 MDN atob — 브라우저 Base64 프리미티브와 UTF-8 주의점.
- OWASP Cryptographic Storage Cheat Sheet — 암호화, 인증 모드, 키 관리.
- OWASP Password Storage Cheat Sheet — 비밀번호 해싱 가이드.
마지막 검토: 2026 년 7 월.