JWT vs PASETO v2 vs TECTO: 2026년에 올바른 토큰 프로토콜 선택

발행: (2026년 2월 22일 오후 01:13 GMT+9)
14 분 소요
원문: Dev.to

Source: Dev.to

Tokens are everywhere in modern auth flows. But not all tokens are created equal.
In this post we compare three approaches side‑by‑side — classic JWT (HS256), the more modern PASETO v2, and the brand‑new TECTO — across security, ergonomics, and real‑code examples.

Quick Comparison Table

PropertyJWT (HS256)PASETO v2TECTO
Payload visible?✅ 예 (base64)✅ 예 (signed, not encrypted)❌ 완전 암호화
Cipher없음 (HMAC)Ed25519 (sign) / XChaCha20 (encrypt)XChaCha20‑Poly1305
NonceN/A토큰당 24‑byte토큰당 24‑byte CSPRNG
Key size가변가변정확히 256‑bit (강제)
Tamper detectionHMAC 서명Ed25519 / Poly1305 태그Poly1305 인증 태그
Error specificity이유 공개이유 공개일반 “Invalid token”
Algo‑confusion attacks⚠️ 예 (alg: none 문제)✅ 아니오✅ 아니오
Key rotation built‑in❌ DIY❌ DIY✅ 네이티브 (토큰에 kid 포함)

1️⃣ JWT – The Old Faithful

jsonwebtoken은 Node.js에서 가장 널리 사용되는 토큰 라이브러리입니다. 검증된 안정성, 방대한 생태계, 그리고 시작하기 쉬운 간단함이 특징입니다.

npm install jsonwebtoken
import jwt from "jsonwebtoken";

const SECRET = "my-secret-key"; // ← 이것이 문제입니다

// Sign
const token = jwt.sign(
  { userId: 42, role: "admin" },
  SECRET,
  { expiresIn: "1h", issuer: "my-app" }
);

console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMCwiaXNzIjoibXktYXBwIn0.SIGNATURE

// Verify
const payload = jwt.verify(token, SECRET) as { userId: number; role: string };
console.log(payload.userId); // 42

Payload는 읽을 수 있다

// 任意의 JWT의 중간 부분을 디코드
const [, payload] = token.split(".");
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
console.log(decoded);
// {"userId":42,"role":"admin","iat":1700000000,"exp":1700003600,"iss":"my-app"}

토큰을 가로채는 사람은 키 없이도 페이로드를 읽을 수 있습니다. 이는 설계상 JWT가 암호화된 것이 아니라 서명된 것이기 때문이며, 많은 개발자들이 처음에 이를 인식하지 못합니다.

알고리즘 혼동 문제

JWT는 헤더에 알고리즘을 지정할 수 있습니다. 이 때문에 alg: none 공격이 유명해졌으며, 공격자는 알고리즘을 none으로 설정해 토큰을 위조할 수 있었습니다. 현대 라이브러리들이 none을 차단하더라도, HMAC vs RSA 혼동 공격은 여러 발행자로부터 토큰을 받아야 할 경우 여전히 실질적인 위험이 됩니다.

JWT가 적합한 경우

  • 공개된 비민감 페이로드(예: 사용자 ID, 역할)
  • JWT를 요구하는 서드파티 서비스와 연동(OAuth, OIDC 등)
  • 팀이 이미 JWT 인프라를 보유하고 있는 경우

2️⃣ PASETO – 플랫폼에 구애받지 않는 보안 토큰

PASETO는 JWT의 함정을 해결하기 위해 설계되었습니다. 알고리즘 선택성을 완전히 없애고, 버전을 선택하면 고정된, 잘 선택된 알고리즘이 적용됩니다. alg: none도 없고, 혼동 공격도 없습니다.

npm install paseto
import { V2 } from "paseto";

const key = await V2.generateKey("local");

// ---- Encrypt (v2.local) ----
const token = await V2.encrypt(
  { userId: 42, role: "admin" },
  key,
  { expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

// Decrypt
const payload = await V2.decrypt(token, key);
console.log(payload.userId); // 42

// ---- Sign (v2.public) ----
const { privateKey, publicKey } = await V2.generateKey("public");

// Sign (payload is visible, like JWT)
const signed = await V2.sign({ userId: 42 }, privateKey, { expiresIn: "1h" });

// Verify
const verified = await V2.verify(signed, publicKey);

주요 특징

  • 알고리즘 혼동 없음 – 버전(v2)이 알고리즘을 고정합니다.
  • v2.local페이로드를 암호화합니다 (XChaCha20‑Poly1305).
  • 깔끔하고 현대적인 비동기 API.
  • 키 회전 스토리가 없음kid가 토큰 형식에 포함되지 않으므로, 키 버전 관리를 직접 해야 합니다.
  • 오류 메시지가 여전히 실패 원인을 드러낼 수 있습니다.
  • 키에 대한 엔트로피 검증이 없으며, 약한 키를 전달해도 라이브러리가 조용히 받아들입니다.

Source:

3️⃣ TECTO – Transport Encrypted Compact Token Object

TECTO는 다른 철학을 취합니다: 모든 토큰이 항상 완전히 암호화됩니다. “서명‑하지만‑읽을 수 있는” 모드는 없습니다.

bun add tecto
import {
  generateSecureKey,
  MemoryKeyStore,
  TectoCoder,
  InvalidSignatureError,
  TokenExpiredError,
  assertEntropy,
} from "tecto";

// 1️⃣ 암호학적으로 안전한 256‑bit 키 생성
const key = generateSecureKey();

// 2️⃣ 키 저장소 설정
const store = new MemoryKeyStore();
store.addKey("my-key-2026", key);

// 3️⃣ 코더 생성
const coder = new TectoCoder(store);

// 4️⃣ 암호화
const token = coder.encrypt(
  { userId: 42, role: "admin" },
  { expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// tecto.v1.my-key-2026.base64url_nonce.base64url_ciphertext

// 5️⃣ 복호화
const payload = coder.decrypt(token);
console.log(payload.userId); // 42

토큰 형식

tecto.v1...
  • kid(키 ID)가 토큰 자체에 포함되어 있어 네이티브 키 회전이 가능하며, 별도의 메타데이터나 헤더가 필요 없습니다.
// 키 회전
store.addKey("key-2026-01", oldKey);          // 오래된 키는 기존 토큰을 계속 복호화함
store.rotate("key-2026-06", newKey);          // 새 토큰은 새 키를 사용함
store.removeKey("key-2026-01");               // 더 이상 필요 없을 때 오래된 키 삭제

강력한 키‑엔트로피 검사

// 모두 KeyError를 발생시킴
assertEntropy(new Uint8Array(32));                 // 전부 0
assertEntropy(new Uint8Array(32).fill(0xaa));      // 반복 바이트
assertEntropy(new Uint8Array(16));                 // 길이 오류

// 이것은 안전함
const goodKey = generateSecureKey(); // 항상 고엔트로피, 256‑bit
assertEntropy(goodKey); // ✅ 통과

일관된 오류 처리

try {
  coder.decrypt(tamperedToken);
} catch (err) {
  if (err instanceof InvalidSignatureError) {
    // err.message === "Invalid token"
    // 왜 실패했는지 알 수 없습니다 – 이것이 의도된 동작입니다.
    // 공격자는 오류 메시지를 보고 시스템을 탐색할 수 없습니다.
  }
  if (err instanceof TokenExpiredError) {
    // 토큰이 단순히 만료되었습니다 – 클라이언트에 알려도 안전합니다.
  }
}

TECTO가 빛을 발하는 경우

  • 모든 클레임에 대한 완전한 기밀성이 필요할 때.
  • 별도 설정 없이 네이티브 키‑회전이 필요할 때.
  • 탐색 공격을 방지하기 위한 일관적이고 비공개적인 오류 메시지가 필요할 때.

TL;DR

기능JWT (HS256)PASETO v2TECTO
페이로드 읽을 수 있나요?✅ (base64)✅ (서명됨)❌ (암호화됨)
고정 알고리즘?❌ (alg header)✅ (버전 고정)✅ (내장)
네이티브 키 회전?✅ (kid in token)
강력한 키 크기 적용?✅ (256‑비트)
일관된 오류 메시지?
최적 사용 사례…공개이며 민감하지 않은 데이터; 기존 JWT 생태계서명된 토큰과 선택적 암호화를 원하는 최신 앱기밀을 유지해야 하고 내장 회전 기능을 활용해야 하는 모든 경우

보안 및 운영 요구 사항에 맞는 토큰 형식을 선택하세요 – 그리고 ‘서명 = 안전’이라고 가정하지 마세요. 암호화도 중요합니다.

토큰 형식 비교

아래는 JWT, PASETO v2, 그리고 TECTO에 대한 간단한 비교입니다.
세 가지 모두 동일한 페이로드—예: { userId: 42, role: "admin" }—를 전달할 수 있지만, 데이터가 보호되는 방식은 크게 다릅니다.

구체적인 예시

// 전달하려는 페이로드
{ userId: 42, role: "admin" }

JWT (서명만)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ   ← { userId: 42, role: "admin" }의 base64
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

누구든지 중간 세그먼트에 atob()을 실행해 페이로드를 읽을 수 있습니다 — 비밀 키가 필요하지 않습니다.

PASETO v2.local (암호화)

v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFULL_CIPHERTEXT

XChaCha20‑Poly1305암호화되었습니다. 키가 없으면 암호문은 불투명합니다.

TECTO (암호화 + 내장 키‑ID)

tecto.v1.my-key-2026.NONCE_BASE64URL.CIPHERTEXT_BASE64URL

XChaCha20‑Poly1305암호화되었습니다.
kid(my‑key‑2026)는 라벨 형태로만 표시되며, 키 없이는 페이로드를 읽을 수 없습니다.

기능 매트릭스

FeatureJWTPASETO v2TECTO
Key ID in token❌ 표준이 아님❌ 표준이 아님✅ Built‑in kid
Old token decryptable after rotationDIY (직접 구현)DIY (직접 구현)store.rotate()가 처리
Revoke old keyDIY (직접 구현)DIY (직접 구현)store.removeKey()가 메모리를 0으로 초기화
Entropy validation❌ 없음❌ 없음assertEntropy() 적용
Tamper‑evidence✅ HMAC❌ 페이로드가 누구에게나 읽히는✅ Authenticated encryption (XChaCha20‑Poly1305)
Algorithm agility❌ 알고리즘 혼동 공격 표면✅ 버전이 알고리즘 고정✅ 버전이 알고리즘 고정
Native key rotation❌ 없음❌ 없음kid 사용
Generic errors (no oracle attacks)
Timing‑safe comparisons
Memory zeroing on key removal
Payload size limits (DoS prevention)
Type‑checked registered claims

언제 어떤 토큰을 사용할까

  • Use JWT if you need:

    • Compatibility with OAuth / OIDC / existing infrastructure
      → OAuth / OIDC / 기존 인프라와의 호환성
    • Payloads that contain no sensitive data (just IDs, roles, etc.)
      → 페이로드에 민감한 데이터가 없고 (ID, 역할 등)만 포함될 때
    • Integration with third‑party services that only understand JWT
      → JWT만 이해하는 서드파티 서비스와의 통합
  • Use PASETO v2.local if you want:

    • A well‑audited, standardized encrypted token
      → 잘 감사된 표준화된 암호화된 토큰
    • Interoperability across many languages / platforms
      → 다양한 언어·플랫폼 간 상호 운용성
    • No built‑in native key rotation (you’ll handle rotation yourself)
      → 기본 제공되는 네이티브 키 회전 기능이 없으며(직접 회전 관리)
  • Use TECTO if you want:

    • Encryption‑by‑default with zero configuration mistakes possible
      → 기본 암호화이며 설정 실수가 전혀 불가능
    • Native key rotation without extra infrastructure
      → 별도 인프라 없이 네이티브 키 회전
    • A greenfield TypeScript/Bun project
      → 새로운 TypeScript/Bun 프로젝트
    • Defense‑in‑depth: entropy validation, generic errors, timing safety, memory zeroing, payload size limits, etc.
      → 방어 심화: 엔트로피 검증, 일반 오류, 타이밍 안전성, 메모리 영(0) 처리, 페이로드 크기 제한 등.

TECTO 빠른 시작

bun add tecto
import { generateSecureKey, MemoryKeyStore, TectoCoder } from "tecto";

// 1️⃣ 키 저장소를 생성하고 키를 추가합니다
const store = new MemoryKeyStore();
store.addKey("v1", generateSecureKey());

// 2️⃣ 저장소를 인식하는 코더를 만듭니다
const coder = new TectoCoder(store);

// 🔐 페이로드 암호화 (1시간 후 만료)
const token = coder.encrypt({ userId: 42 }, { expiresIn: "1h" });

// 🔓 복호화 (타입 검사됨)
const { userId } = coder.decrypt(token);

영구 키 저장소

TECTO는 SQLite, PostgreSQL, MariaDB용 어댑터를 제공하며, 모두 동일한 KeyStoreAdapter 인터페이스를 구현하므로 백엔드를 교체하는 것이 간단합니다.

요약

  • JWT는 연합 인증 및 OAuth 흐름의 사실상 표준으로 남을 것이며, 이는 공개적이고 민감하지 않은 클레임에 대해서는 괜찮다.
  • PASETO v2.local는 견고하고 표준화된 암호화 토큰을 제공하지만, 회전 및 엔트로피 관리를 직접 해야 한다.
  • TECTO는 한 단계 더 나아간다: 배터리 포함 키 회전, 필수 엔트로피 검사, 일반 오류 처리, 타이밍‑안전 비교, 자동 메모리 제로화.

최고의 토큰 프로토콜은 잘못 구성할 수 없는 것이다.
TECTO가 그 점을 강력히 입증한다.

0 조회
Back to Blog

관련 글

더 보기 »