- 技術 💻
シーザー暗号とは|仕組み・解き方とPython・JS・Javaの実装・練習問題
結論
シーザー暗号とは、文字をアルファベット順に決まった数だけずらして別の文字に置き換える、いちばん単純な古典暗号です。ずらし数は25通りしかないため、0〜25を順に試せば数ミリ秒で元の文に戻ります。守るための暗号ではなく、文字コード・剰余・ループを学ぶ題材として今も使われています。
この記事でわかること
- ずらして置き換える仕組みと、計算式(mod 26)の読み方
- Python・JavaScript・Javaで暗号化・復号・総当たりを動かした出力
- かな・記号・ROT13の扱い、負の剰余と文字コードのずれで起きる間違いの実物
シーザー暗号は、1文字ずつ「文字→番号→足す→26で割った余り→文字」の順に変換します。ずらし数3のときの流れです。
flowchart TD
A[平文の文字<br>H] --> B[番号にする<br>A=0 … H=7]
B --> C[ずらし数を足す<br>7 + 3 = 10]
C --> D[26で割った余り<br>10 mod 26 = 10]
D --> E[文字に戻す<br>10 → K]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| シーザー暗号とは | 文字を決まった数だけずらす換字式の古典暗号 | シーザー暗号とは |
| シーザー暗号の解き方 | ずらし数0〜25を全部試す(総当たり)か、頻度分析 | 総当たりで解く/頻度分析の入口 |
| シーザー暗号 python | ord・chr と % 26 で20行ほど。実行結果つき | Pythonで作る |
| JavaScript・Java で作るには | charCodeAt/char の計算と、負の剰余の補正が要る | JavaScript・Javaで作る |
| ROT13 との関係 | ずらし数13のシーザー暗号。2回かけると元に戻る | ROT13との関係 |
| 日本語(かな)は暗号化できるか | 五十音表の中でずらせばできる。文字コード直接は崩れる | かな・日本語の扱い |
| なぜ今は使われないのか | 鍵が25通り。AES-128 は 2 の 128 乗通り | 現代の暗号との違い |
| 動かない・文字化けする | 負の剰余・基準の引き忘れ・char と int の混同 | よくある間違い |
確認環境:Ubuntu 24.04.5 LTS の上で、Python 3.13.18・Node.js 22.23.4・OpenJDK 21.0.12.1 で、この記事に載せたコードをすべて実行し、出力をそのまま載せています。ROT13 は Python の codecs と GNU coreutils 9.4 の tr で、AES の暗号化は OpenSSL 3.0.13 の openssl enc で確かめました。負の剰余の仕様は Python 公式ドキュメント・MDN・Java 21 の API ドキュメントで、AES の鍵の長さは NIST の FIPS 197 で確かめています。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「シーザー暗号とは|仕組み・解き方とPython・JS・Javaの実装・練習問題」
シーザー暗号とは(仕組みと歴史)
この節では、シーザー暗号が何をする暗号で、どこから来た名前なのかが分かります。
シーザー暗号(Caesar cipher)は、平文の各文字を、アルファベット順に一定の数だけ後ろへずらした文字に置き換える暗号です。 ずらす数を「ずらし数(シフト数・鍵)」と呼びます。ずらし数3なら A は D、B は E になり、末尾の X・Y・Z は先頭に戻って A・B・C になります。
| 対応表(ずらし数3) | |
|---|---|
| 平文 | ABCDEFGHIJKLMNOPQRSTUVWXYZ |
| 暗号文 | DEFGHIJKLMNOPQRSTUVWXYZABC |
この表で「HELLO」を変換すると「KHOOR」になります。受け取った人は、同じ表を逆に引く(3つ前の文字に戻す)だけで元の文に戻せます。暗号化と復号で同じ鍵(ずらし数)を使うので、現代の分類では共通鍵暗号の仲間です。文字を別の文字に置き換える方式なので「換字式暗号」、その中でもずらすだけの方式なので「シフト暗号」とも呼ばれます。
鍵はずらし数
1〜25 のどれか。0 と 26 はずらしても元のままなので、実質25通り。
送る側と受け取る側で共有する
同じ文字は必ず同じ文字になる
平文の L が2つあれば暗号文も O が2つ。この性質が頻度分析の手がかりになる。
「HELLO」→「KHOOR」
英字以外はそのまま
空白・記号・数字は変換しないのが一般的。文の長さと区切りが暗号文に残る。
設計しだいで変えてよい
名前の由来
名前は古代ローマのユリウス・カエサル(英語読みでジュリアス・シーザー)にちなみます。カエサルが私的な手紙で3文字ずらしの暗号を使った、という記述が伝わっていることから、ずらし数3が「元祖」として紹介されることが多いです。実用の暗号として使われなくなって久しく、今は暗号の考え方を学ぶ入口、プログラミングの練習問題、パズルとして残っています。
暗号化と復号の計算式(mod 26 と文字コード)
この節では、プログラムに書くときの計算式と、「26で割った余り」が要る理由が分かります。
文字を A=0・B=1・…・Z=25 の番号に置き換えると、暗号化は「番号にずらし数を足して、26で割った余りを取る」、復号は「引いて、26で割った余りを取る」です。 式で書くと次のとおりです(x は文字の番号、k はずらし数)。
- 暗号化:
E(x) = (x + k) mod 26 - 復号:
D(x) = (x - k) mod 26
余りを取るのは、Z(25)に 3 を足した 28 を A・B・C のどれかに戻すためです。28 を 26 で割った余りは 2 なので C になります。余りを取らないと、英字の範囲を飛び出して記号になります(「よくある間違い」の節に実物があります)。
プログラムでは、文字そのものではなく文字コード(コンピューターが文字に割り当てている番号)で計算します。英字の文字コードは ASCII で次のように連続しているので、ord('A')(65)を引けば A=0 の番号になります。
| 文字 | 10進 | 16進 | A=0 の番号 |
|---|---|---|---|
| A | 65 | 0x41 | 0 |
| Z | 90 | 0x5A | 25 |
| a | 97 | 0x61 | 0(小文字の基準は 97) |
| z | 122 | 0x7A | 25 |
大文字と小文字は 32 離れた別の番号なので、基準を 65 にするか 97 にするかを、文字ごとに切り替える必要があります。文字コードの数え方そのものは、関連記事「Java charの使い方|int変換とCharacter判定メソッド一覧」で扱っています。
Pythonでシーザー暗号を作る(暗号化・復号の実行結果)
この節では、Python での最小の実装と、実際に動かした出力が分かります。
Python は ord() で文字コードに、chr() で文字に戻せます。 大文字・小文字・それ以外で分岐し、英字だけをずらします。
def encrypt(text, shift):
result = []
for ch in text:
if "A" <= ch <= "Z":
base = ord("A")
elif "a" <= ch <= "z":
base = ord("a")
else:
result.append(ch) # 英字以外はそのまま
continue
result.append(chr((ord(ch) - base + shift) % 26 + base))
return "".join(result)
def decrypt(text, shift):
return encrypt(text, -shift)
plain = "Hello, World! 2026"
enc = encrypt(plain, 3)
print("平文 :", plain)
print("暗号文:", enc)
print("復号 :", decrypt(enc, 3))
print("ずらし29(=3):", encrypt(plain, 29))
print("ずらし-1 :", encrypt(plain, -1))
$ python3 caesar.py
平文 : Hello, World! 2026
暗号文: Khoor, Zruog! 2026
復号 : Hello, World! 2026
ずらし29(=3): Khoor, Zruog! 2026
ずらし-1 : Gdkkn, Vnqkc! 2026
1復号は「マイナスで暗号化」
decrypt は encrypt に -shift を渡すだけ。Python の % は負の数でも 0〜25 を返すので補正が要らない。
2ずらし29は3と同じ
% 26 を取るので、26 を超えるずらし数も 0〜25 に収まる。
3記号・数字・空白は残る
「, ! 2026」はそのまま。単語の区切りが見えるので、解く側の手がかりにもなる。
1行で書くなら
仕組みが分かったあとは、対応表を str.maketrans で作って translate に渡す書き方も使えます。
import string
def encrypt2(text, shift):
up, lo = string.ascii_uppercase, string.ascii_lowercase
table = str.maketrans(up + lo, up[shift:] + up[:shift] + lo[shift:] + lo[:shift])
return text.translate(table)
print(encrypt2("Hello, World!", 3)) # Khoor, Zruog!
JavaScript・Javaでシーザー暗号を作る(負の剰余の補正つき)
この節では、JavaScript と Java での実装と、Python と違って「負の剰余の補正」が要る点が、実行結果で分かります。
JavaScript と Java の % は、左の数が負のときに負の値を返します。 復号(引き算)で番号が負になると、そのままでは文字の範囲を外れるので、補正を入れます。
JavaScript(Node.js 22)
function encrypt(text, shift) {
let out = "";
for (const ch of text) {
const code = ch.charCodeAt(0);
let base;
if (code >= 65 && code <= 90) base = 65; // A-Z
else if (code >= 97 && code <= 122) base = 97; // a-z
else { out += ch; continue; } // 英字以外はそのまま
out += String.fromCharCode(((code - base + shift) % 26 + 26) % 26 + base);
}
return out;
}
function decrypt(text, shift) { return encrypt(text, -shift); }
const plain = "Hello, World! 2026";
const enc = encrypt(plain, 3);
console.log("平文 :", plain);
console.log("暗号文:", enc);
console.log("復号 :", decrypt(enc, 3));
console.log("(-1) % 26 =", (-1) % 26, " / ((-1) % 26 + 26) % 26 =", ((-1) % 26 + 26) % 26);
$ node caesar.js
平文 : Hello, World! 2026
暗号文: Khoor, Zruog! 2026
復号 : Hello, World! 2026
(-1) % 26 = -1 / ((-1) % 26 + 26) % 26 = 25
((n % 26) + 26) % 26 が補正の定番です。MDN は、JavaScript の剰余演算子は常に被除数(左の数)の符号を取る、常に非負の値が要るときはこの式を使う、と説明しています。
Java(OpenJDK 21)
Java には Math.floorMod があり、除数(右の数)と同じ符号の余りを返すので、これを使えば補正の式を書かずに済みます。
public class Caesar {
static String encrypt(String text, int shift) {
StringBuilder sb = new StringBuilder();
for (char ch : text.toCharArray()) {
if (ch >= 'A' && ch <= 'Z') {
sb.append((char) (Math.floorMod(ch - 'A' + shift, 26) + 'A'));
} else if (ch >= 'a' && ch <= 'z') {
sb.append((char) (Math.floorMod(ch - 'a' + shift, 26) + 'a'));
} else {
sb.append(ch); // 英字以外はそのまま
}
}
return sb.toString();
}
static String decrypt(String text, int shift) { return encrypt(text, -shift); }
public static void main(String[] args) {
String plain = "Hello, World! 2026";
String enc = encrypt(plain, 3);
System.out.println("平文 : " + plain);
System.out.println("暗号文: " + enc);
System.out.println("復号 : " + decrypt(enc, 3));
System.out.println("-1 % 26 = " + (-1 % 26) + " / Math.floorMod(-1, 26) = " + Math.floorMod(-1, 26));
}
}
$ javac -encoding UTF-8 Caesar.java && java Caesar
平文 : Hello, World! 2026
暗号文: Khoor, Zruog! 2026
復号 : Hello, World! 2026
-1 % 26 = -1 / Math.floorMod(-1, 26) = 25
| 言語 | (-1) % 26 の結果 | 対処 |
|---|---|---|
| Python 3.13 | 25(除数の符号) | 補正不要 |
| JavaScript(Node 22) | -1(被除数の符号) | ((n % 26) + 26) % 26 |
| Java 21 | -1(被除数の符号) | Math.floorMod(n, 26) |
Java の整数の割り算と余りの決まりは、関連記事「Java 割り算の小数・切り捨て・余りまとめ|整数除算と0除算の挙動」で扱っています。
シーザー暗号の解き方①:総当たり(0〜25をすべて試す)
この節では、ずらし数が分からない暗号文を、全部のずらし数で復号して読める行を探す方法と、その出力が分かります。
ずらし数は 0〜25 の 26 通りしかないので、全部試して「意味の通る英文」になった行が答えです。 Python の decrypt を 26 回呼ぶだけです。
cipher = "Wkh txlfn eurzq ira mxpsv ryhu wkh odcb grj."
for k in range(26):
print(f"{k:2d}: {decrypt(cipher, k)}")
$ python3 brute.py
0: Wkh txlfn eurzq ira mxpsv ryhu wkh odcb grj.
1: Vjg swkem dtqyp hqz lworu qxgt vjg ncba fqi.
2: Uif rvjdl cspxo gpy kvnqt pwfs uif mbaz eph.
3: The quick brown fox jumps over the lazy dog.
4: Sgd pthbj aqnvm enw itlor nudq sgd kzyx cnf.
5: Rfc osgai zpmul dmv hsknq mtcp rfc jyxw bme.
6: Qeb nrfzh yoltk clu grjmp lsbo qeb ixwv ald.
7: Pda mqeyg xnksj bkt fqilo kran pda hwvu zkc.
8: Ocz lpdxf wmjri ajs ephkn jqzm ocz gvut yjb.
9: Nby kocwe vliqh zir dogjm ipyl nby futs xia.
10: Max jnbvd ukhpg yhq cnfil hoxk max etsr whz.
11: Lzw imauc tjgof xgp bmehk gnwj lzw dsrq vgy.
12: Kyv hlztb sifne wfo aldgj fmvi kyv crqp ufx.
13: Jxu gkysa rhemd ven zkcfi eluh jxu bqpo tew.
14: Iwt fjxrz qgdlc udm yjbeh dktg iwt apon sdv.
15: Hvs eiwqy pfckb tcl xiadg cjsf hvs zonm rcu.
16: Gur dhvpx oebja sbk whzcf bire gur ynml qbt.
17: Ftq cguow ndaiz raj vgybe ahqd ftq xmlk pas.
18: Esp bftnv mczhy qzi ufxad zgpc esp wlkj ozr.
19: Dro aesmu lbygx pyh tewzc yfob dro vkji nyq.
20: Cqn zdrlt kaxfw oxg sdvyb xena cqn ujih mxp.
21: Bpm ycqks jzwev nwf rcuxa wdmz bpm tihg lwo.
22: Aol xbpjr iyvdu mve qbtwz vcly aol shgf kvn.
23: Znk waoiq hxuct lud pasvy ubkx znk rgfe jum.
24: Ymj vznhp gwtbs ktc ozrux tajw ymj qfed itl.
25: Xli uymgo fvsar jsb nyqtw sziv xli pedc hsk.
3 の行だけが英文として読めます。手元で 26 回の復号にかかった時間は 0.373 ミリ秒でした。目で読める行を探す代わりに、「the」「and」のような単語が含まれる行を選ぶ、英語の文字の出現率に近い行を選ぶ、とすればプログラムだけで答えを出せます。
暗号文を用意する
英字だけをずらした文なら、空白と記号は残っているので単語の長さが見える。
0〜25 で復号して全部表示する
for ループで decrypt を呼ぶだけ。26 行出る。
読める行を選ぶ
人が読むか、よく出る単語(the・is・and)を含む行を数える。
そのずらし数が鍵
同じ鍵で送られた別の暗号文も、同じずらし数で読める。
シーザー暗号の解き方②:頻度分析の入口
この節では、総当たりをしなくても、文字の出現回数からずらし数を推定できる理由と、短い文では外れる実物が分かります。
英語の文章では E がいちばん多く出るので、暗号文でいちばん多い文字を「E がずれたもの」と仮定すると、ずらし数が推定できます。 272 文字の英文をずらし数 7 で暗号化し、出現回数を数えた結果です。
from collections import Counter
letters = [c for c in cipher.lower() if c.isalpha()]
cnt = Counter(letters)
for ch, n in cnt.most_common(6):
print(f" {ch}: {n:2d}回 ({n/len(letters)*100:4.1f}%) → e と仮定したずらし数 {(ord(ch)-ord('e'))%26}")
top = cnt.most_common(1)[0][0]
guess = (ord(top) - ord("e")) % 26
print("復号:", decrypt(cipher, guess)[:50], "...")
$ python3 freq2.py
英字の数: 272
l: 36回 (13.2%) → e と仮定したずらし数 7
h: 27回 ( 9.9%) → e と仮定したずらし数 3
a: 27回 ( 9.9%) → e と仮定したずらし数 22
y: 21回 ( 7.7%) → e と仮定したずらし数 20
v: 18回 ( 6.6%) → e と仮定したずらし数 17
t: 18回 ( 6.6%) → e と仮定したずらし数 15
最多の文字 = l → ずらし数の推定 = 7
復号: Programming is the art of telling another human be ...
最多の l を e と仮定して 7 が出て、そのまま読める文に戻りました。一方、107 文字の短い文(ずらし数3)で同じことをすると、最多は w(11回)で推定は 18 になり、外れました。 本来の答えである h(e+3)は 10 回で 3 番目でした。
$ python3 freq.py
英字の数: 107
w: 11回 (10.3%)
r: 11回 (10.3%)
h: 10回 (9.3%)
最多の文字 = w → e と仮定 → ずらし数の推定 = 18
推定で復号: Esp bftnv mczhy qzi ufxad zgpc esp wlkj ozr. ...
総当たりが向くとき
文が短い(数十文字)
26 行を目で見られる
確実に正解を含む
頻度分析が向くとき
文が長い(数百文字以上)
自動で鍵を当てたい
上位数文字を候補にして試す
頻度分析は、シーザー暗号に限らず「同じ文字が同じ文字に置き換わる」換字式暗号すべてに効く考え方で、現代の暗号が「同じ入力でも毎回違う出力になる」ように作られている理由につながります(後の節で実物を示します)。
大文字・小文字・記号・数字の扱い
この節では、英字以外の文字をどう扱うのが一般的か、設計の選択肢が分かります。
決まりはなく、設計しだいです。 この記事の実装は「大文字は大文字の中で、小文字は小文字の中でずらし、それ以外はそのまま残す」としました。練習問題や学習サイトで多い形です。
| 文字の種類 | この記事の扱い | 別の設計 |
|---|---|---|
| 大文字 A〜Z | 基準 65 で 26 文字の中をずらす | すべて大文字に揃えてから処理する |
| 小文字 a〜z | 基準 97 で 26 文字の中をずらす | 大文字に揃える(大小の情報は失われる) |
| 空白・記号 | そのまま | 削除する・空白も含めた表でずらす |
| 数字 0〜9 | そのまま | 10 で割った余りで数字の中をずらす |
| 全角英字・かな・漢字 | そのまま | 五十音表などの対応表を自作する |
「すべて大文字に揃える」設計にすると、復号したときに元の大文字・小文字が戻らないため、学習用なら大文字と小文字を別々にずらす形が扱いやすいです。
かな・日本語のシーザー暗号
この節では、日本語の文をシーザー暗号にする方法と、文字コードを直接ずらすと崩れる実物が分かります。
ひらがなは、五十音の清音 46 文字を1つの文字列(対応表)にして、その中の位置をずらせば、英字と同じ考え方で暗号化できます。 表に無い文字(濁音・半濁音・記号・漢字)はそのまま残します。
GOJUON = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん"
def encrypt_kana(text, shift):
out = []
for ch in text:
i = GOJUON.find(ch)
if i == -1:
out.append(ch) # 表に無い文字はそのまま
else:
out.append(GOJUON[(i + shift) % len(GOJUON)])
return "".join(out)
def decrypt_kana(text, shift):
return encrypt_kana(text, -shift)
$ python3 kana.py
平文 : あしたは はれ。
暗号文: えそてへ へを。
復号 : あしたは はれ。
表に無い文字: がうばわ
「がんばる」は、が・ば が表に無いので残り、ん・る だけがずれて「がうばわ」になりました。濁音も扱いたければ、表に「がぎぐげご…」を足すか、濁点を分けて処理します。
一方、英字のように文字コードに直接足す方法は、ひらがなでは崩れます。 Unicode のひらがなは U+3041〜U+3096 に並んでいますが、小さい「ぃ」「ぅ」や「ゔ」が混ざっているため、1 足すと五十音の次の文字にはなりません。
文字コードで+1: ぃぅぇ ['0x3043', '0x3045', '0x3047']
ん(U+3093)+1 : ゔ 0x3094
「あいう」に 1 を足すと「ぃぅぇ」(小書きの文字)になりました。英字で文字コードの計算ができたのは、A〜Z・a〜z が順に隙間なく並んでいるからです。文字コードと文字化けの関係は、関連記事「文字化けの原因8選|濁点分離・utf8mb4・絵文字の文字数」で扱っています。
ROT13との関係(2回かけると戻る)
この節では、ROT13 がシーザー暗号の特別な場合であることと、Python・コマンドでの実物が分かります。
ROT13 は、ずらし数を 13 に固定したシーザー暗号です。 26 文字の半分なので、暗号化と復号が同じ操作になります(13 足してさらに 13 足すと 26 で一周)。ネットの掲示板で、ネタバレや答えを「読もうと思えば読める」形で隠す目的で使われてきました。
import codecs
s = "Hello, World!"
print(codecs.encode(s, "rot13")) # Uryyb, Jbeyq!
print(codecs.encode(codecs.encode(s, "rot13"), "rot13")) # Hello, World!
print(encrypt(s, 13) == codecs.encode(s, "rot13")) # True
$ python3 rot13.py
Uryyb, Jbeyq!
Hello, World!
True
$ echo "Hello, World!" | tr 'A-Za-z' 'N-ZA-Mn-za-m'
Uryyb, Jbeyq!
Python の公式ドキュメントは、rot_13(別名 rot13)を「被演算子のシーザー暗号を返す」テキスト変換として載せており、str.encode() ではなく codecs.encode() で使います。自作の encrypt(s, 13) と結果が一致しました。tr コマンドは、A〜Z を N〜Z・A〜M に対応させるだけで同じことができます。
ROT13 を何かを守る目的で使う
ROT13 は「暗号化した」とは言えない
パスワードを保存するときの正しい扱いは、関連記事「パスワードのハッシュ化|bcrypt・Argon2の推奨値と実測・移行コード」で扱っています。
なぜ現代の暗号は違うのか(鍵空間の大きさを数で)
この節では、シーザー暗号が守る目的に使えない理由を、鍵の数と実行結果で比べて分かります。
暗号の強さの土台は「鍵の候補がいくつあるか(鍵空間)」です。 シーザー暗号は 25 通り、現代の共通鍵暗号 AES は鍵の長さが 128 ビットなら 2 の 128 乗通りです(NIST の FIPS 197 は AES の鍵長を 128・192・256 ビットと定めています)。
数だけではなく、性質も違います。同じ文を同じパスワードで AES で 2 回暗号化した結果です(OpenSSL 3.0.13、AES-256-CBC、PBKDF2 でパスワードから鍵を作成、Base64 で表示)。
$ echo -n "Hello, World!" | openssl enc -aes-256-cbc -pbkdf2 -k secret-pass -base64
U2FsdGVkX1/gwmUHcClgEqlOvdgpzKAt2nMogT0rd6M=
$ echo -n "Hello, World!" | openssl enc -aes-256-cbc -pbkdf2 -k secret-pass -base64
U2FsdGVkX1/X61Y6nREcyRiBbpWNJOplmPU+yIi+BEE=
$ echo -n "Hello, World!" | openssl enc -aes-256-cbc -pbkdf2 -k secret-pass -base64 | openssl enc -d -aes-256-cbc -pbkdf2 -k secret-pass -base64
Hello, World!
| 観点 | シーザー暗号 | AES(共通鍵) | RSA など(公開鍵) |
|---|---|---|---|
| 鍵の数 | 25 | 2 の 128〜256 乗 | 2048 ビット以上の数 |
| 同じ入力の出力 | 毎回同じ(頻度が残る) | 毎回違う(ソルト・IV) | 毎回違う(パディング) |
| 1文字の置き換え | 1文字→1文字 | 16 バイトのブロック単位で混ぜる | 大きな数の計算 |
| 鍵の渡し方 | ずらし数を事前に共有 | 共通鍵を事前に共有 | 公開鍵は公開してよい |
| 使われる場面 | 学習・パズル | ファイル・通信の本文 | 鍵の受け渡し・署名 |
AES の出力は、ランダムな値(ソルトと初期化ベクトル)を毎回混ぜるため、同じ平文でも違う暗号文になり、頻度分析の足がかりがありません。公開鍵暗号(RSA・楕円曲線)は、暗号化の鍵を公開しても復号できない仕組みで、鍵の受け渡しや署名に使われます。openssl コマンドの使い方は、関連記事「opensslコマンドの使い方|目的別一覧と実行結果・エラー対処【3系】」で扱っています。
もう1つ、Base64 と暗号を混同しないことも大切です。Base64 は誰でも戻せる「表記の変換」で、鍵がありません。関連記事「Base64エンコード各言語コード一覧|sha256ハッシュのコマンド早見表」で扱っています。
- 1シーザー暗号(ずらす)鍵 25 通り。総当たりで解ける
- 2単一換字暗号(並べ替える)鍵 26! 通りだが、頻度分析で解ける
- 3ヴィジュネル暗号(位置でずらし数を変える)鍵は単語。鍵の長さが分かれば頻度分析に戻る
- 4AES(ブロックごとに混ぜる)鍵 2 の 128 乗以上。同じ入力でも出力が変わる
- 5公開鍵暗号(鍵を分ける)鍵を事前に共有しなくてよい。TLS の土台
よくある間違い(負の剰余・文字コードのずれ・char と int)
この節では、シーザー暗号を書いたときに出る典型的な不具合を、実際の出力で確かめられます。
不具合の原因は、ほぼ「26 で割った余りを取っていない」「基準(65・97)を引いていない・足していない」「負の数の剰余」「char と int の混同」の4つです。
| 間違い | 出力(実物) | 原因と直し方 |
|---|---|---|
| 剰余を取らずに文字コードに足す | XYZ +3 → [\]、abc -3 → ^_` |
Z の次が記号になる。(番号 + k) % 26 にする |
| 基準の 65 を引かずに % 26 | ABC +3 → 文字コード 16・17・18(制御文字) |
ord(c) - 65 で 0 始まりにしてから余りを取り、最後に 65 を足す |
| 小文字を大文字の基準で計算 | hello +3 → QNUUX |
a〜z は基準 97。文字ごとに基準を切り替える |
| JS・Java で負の剰余を補正しない | ABC を 3 戻す → >?@(62・63・64) |
((n % 26) + 26) % 26 か Math.floorMod |
| Java で char + int をそのまま append | 'A' + 3 → 68 |
結果は int。(char)('A' + 3) で D になる |
境界判定が >(>= ではない) |
番号が 26 のときだけ範囲外 | 条件分岐ではなく常に % 26 を取る |
Python で確かめた出力です。
$ python3 mistakes.py
剰余なし 'XYZ' +3 : [\]
剰余なし 'abc' -3 : ^_`
基準を引かない 'ABC' +3 : '\x10\x11\x12' [16, 17, 18]
Python: (-1) % 26 = 25 / (0-3) % 26 = 23
大文字の基準で 'hello' +3 : QNUUX
ずらし 55 と 3 : def def
JavaScript と Java で確かめた出力です。
$ node caesar.js (補正なしの decryptBad)
補正なしで 'ABC' を3戻す: ">?@" [ 62, 63, 64 ]
$ java Caesar
'A' + 3 をそのまま append : 68
(char) ('A' + 3) : D
「XYZ」に 3 を足して [\] になるのは、Z(90)の次の 91・92・93 が ASCII では [・\・] だからです。文字コードの表を手元に置いておくと、崩れた出力から原因を逆算できます。
- 1余りは「番号」に対して取っているか文字コード(65〜90)のまま % 26 を取ると制御文字になる
- 2大文字と小文字で基準を切り替えているか65 と 97。isupper()・islower() か範囲比較で分ける
- 3復号(引き算)で負の数を試したかPython は補正不要、JavaScript と Java は補正が要る
- 4暗号化→復号で元に戻るテストを書いたかdecrypt(encrypt(s, k), k) == s を 0〜25 の k すべてで確かめる
- 5英字以外を含む入力で試したか空白・記号・数字・日本語が壊れずに残るか
練習問題3つと答え
この節では、仕組みを確かめるための練習問題と、その答えが分かります。答えはこの記事の Python コードで確かめています。
問1:「Tech Blog」をずらし数 5 で暗号化すると何になりますか。
問2:「Tmizv jg lwqvo」はずらし数 8 で暗号化されています。元の文は何ですか。
問3:「Cebtenzzvat vf sha」はずらし数が分かりません。総当たりか頻度の考え方で元の文を求め、ずらし数も答えてください。
考える手順です。
文字を番号にする
A=0 … Z=25。小文字も同じ番号で考える。
足す(暗号化)か引く(復号)
25 を超えたら 26 を引く。0 より小さくなったら 26 を足す。
ずらし数が不明なら 26 通り試す
3文字の単語に the・and・for などが当てはまる行を探すと早い。
| 問 | 答え | 確かめ方 |
|---|---|---|
| 問1 | Yjhm Gqtl |
encrypt("Tech Blog", 5) |
| 問2 | Learn by doing |
decrypt("Tmizv jg lwqvo", 8) |
| 問3 | Programming is fun(ずらし数 13) |
ROT13。codecs.encode(s, "rot13") でも戻る |
問3 は、「vf」が 2 文字の単語(is・of・to…)だと見当をつけると、v→i なら 13、f→s も 13 で一致するので、総当たりをしなくても答えにたどり着けます。
シーザー暗号を学習の題材にするときの進め方
この節では、プログラミングの練習としてシーザー暗号を扱うとき、何を順に学べるかが分かります。
シーザー暗号は、文字コード・ループ・条件分岐・剰余・関数・テストを、20 行のプログラムで一度に練習できる題材です。 次の順に広げると、1 つの題材で長く学べます。
| 段階 | やること | 身につくこと |
|---|---|---|
| 1 | 大文字だけを暗号化する | ord・chr、% 26、for ループ |
| 2 | 小文字と記号に対応する | 条件分岐、基準の切り替え |
| 3 | 復号を「負のずらし」で書く | 関数の再利用、負の剰余 |
| 4 | 0〜25 の総当たりを書く | ループと出力の整形 |
| 5 | 頻度分析で鍵を推定する | Counter・辞書・ソート |
| 6 | 暗号化→復号のテストを書く | unittest・pytest、往復の性質 |
| 7 | かな対応・ヴィジュネル暗号に広げる | 対応表の設計、鍵の長さ |
段階 6 のテストは、次の 1 行を 26 通りの k で回すだけで、往復の性質(復号すると必ず元に戻る)を確かめられます。
assert all(decrypt(encrypt("Hello, World! 2026", k), k) == "Hello, World! 2026" for k in range(26))
段階7:ヴィジュネル暗号は「位置ごとに違うシーザー暗号」
鍵を単語(例:KEY)にして、1 文字目は K の分(10)、2 文字目は E の分(4)、3 文字目は Y の分(24)だけずらし、4 文字目からは繰り返します。この記事の encrypt をそのまま使って書けます。
def vigenere(text, key, sign=1):
out, j = [], 0
for ch in text:
if ch.isalpha():
k = ord(key[j % len(key)].upper()) - ord("A")
out.append(encrypt(ch, sign * k)); j += 1
else:
out.append(ch)
return "".join(out)
$ python3 vig.py
平文 : attack at dawn
鍵 KEY: kxrkgi kx bkal
復号 : attack at dawn
シーザー3: dwwdfn dw gdzq
シーザー暗号(ずらし数3)では、平文の 3 つの a がすべて d になりました。ヴィジュネル暗号では同じ a が k・k・b と変わり、「同じ文字が同じ文字になる」性質が崩れています。これが、単純な頻度分析が効きにくくなる理由です。
どの解き方・どの書き方を選ぶか(判断表)
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 仕組みを理解したい | ord・chr と % 26 の for ループ | 1 文字ごとの変換が見える |
| 短く書きたい(理解したあと) | str.maketrans と translate | 対応表を 1 回作って置き換えるだけ |
| ずらし数が不明・文が短い | 0〜25 の総当たり | 26 行を目で見れば確実 |
| ずらし数が不明・文が長い | 頻度分析(上位数文字を候補に) | 自動で鍵を推定できる |
| ネタバレ隠しなど「読める前提」 | ROT13 | 同じ操作で戻せる。守る目的には使わない |
| 本当に守りたいデータ | AES などの現代の暗号・パスワードはハッシュ化 | シーザー暗号・ROT13・Base64 は守れない |
よくある質問
Q. シーザー暗号とは何ですか? A. 文字をアルファベット順に決まった数だけずらして置き換える古典暗号です。ずらし数が鍵で、受け取った人は逆向きに同じ数だけずらして元に戻します。古代ローマのカエサルの名前にちなみます。
Q. シーザー暗号の解き方は? A. ずらし数は 0〜25 の 26 通りしかないので、全部で復号して意味の通る行を選びます(総当たり)。文が長ければ、いちばん多く出る文字を E と仮定してずらし数を推定する頻度分析も使えます。手元では 26 通りの復号に 0.373 ミリ秒でした。
Q. ずらし数は 1〜25 だけですか? A. 26 で割った余りを取るので、29 や 55 を指定しても 3 と同じ結果になります。実質の鍵は 1〜25 の 25 通りです(0 と 26 は変化なし)。
Q. Python でシーザー暗号を書く最小のコードは?
A. ord() で文字コードにし、基準(65 か 97)を引いて、ずらし数を足し、% 26 を取り、基準を足して chr() で文字に戻します。英字以外はそのまま残します。この記事の encrypt がその形で、復号は encrypt(text, -shift) です。
Q. JavaScript で復号すると記号になります。
A. JavaScript の % は負の数に対して負を返すため、引き算で負になった番号がそのまま文字コードに足されています。((n % 26) + 26) % 26 で 0〜25 に補正します。Java なら Math.floorMod(n, 26) です。
Q. 日本語の文もシーザー暗号にできますか? A. できます。五十音の文字を 1 つの文字列にした対応表を作り、その中の位置をずらします。文字コードに直接足す方法は、ひらがなの Unicode に小書きの文字が混ざっているため、「あいう」が「ぃぅぇ」になり、五十音の順にはなりません。
Q. ROT13 とシーザー暗号の違いは?
A. ROT13 はずらし数を 13 に固定したシーザー暗号です。13 は 26 の半分なので、同じ操作を 2 回で元に戻ります。Python では codecs.encode(s, "rot13")、シェルでは tr 'A-Za-z' 'N-ZA-Mn-za-m' で同じ結果になります。
Q. シーザー暗号は安全ですか? A. 守る目的には使えません。鍵が 25 通りしかなく、総当たりで一瞬で解けます。AES-128 の鍵は 2 の 128 乗通りで、毎秒 10 億回試しても 10 の 22 乗年かかる計算です。学習・パズル用の暗号と考えます。
Q. 現代の暗号と何が違うのですか? A. 鍵の数が桁違いに多いことに加え、同じ平文を同じ鍵で暗号化しても毎回違う暗号文になるように作られています(手元の AES では 2 回の出力が違いました)。シーザー暗号は同じ文字が必ず同じ文字になるため、文字の出現回数から鍵が推定できます。
Q. ヴィジュネル暗号とは何ですか? A. 文字の位置ごとにずらし数を変えるシーザー暗号の発展形で、鍵は「KEY」のような単語です。1 文字目は K の分、2 文字目は E の分だけずらします。単純な頻度分析は効きにくくなりますが、鍵の長さが分かると、各位置ごとにシーザー暗号として解けます。
Q. 基本情報技術者試験などで出ますか? A. 換字式暗号の例として、共通鍵暗号・公開鍵暗号の説明の前置きに登場することがあります。「ずらし数が鍵」「鍵の数が少ないと総当たりで解ける」という考え方を押さえておくと、現代の暗号の話につながります。
まとめ
- シーザー暗号は、文字を決まった数だけずらして置き換える換字式の古典暗号。暗号化は
(x + k) mod 26、復号は(x - k) mod 26。 - Python は
ord・chrと% 26で 20 行ほど。JavaScript と Java は負の剰余を((n % 26) + 26) % 26かMath.floorModで補正する。 - 鍵は 25 通りなので、0〜25 の総当たりで解ける(手元で 0.373 ミリ秒)。長い文なら、最多の文字を E と仮定する頻度分析でも鍵が推定できる。短い文では外れた。
- かなは五十音の対応表の中でずらす。文字コードに直接足すと「あいう」が「ぃぅぇ」になる。ROT13 はずらし数 13 の特別な場合で、2 回で元に戻る。
- 現代の暗号(AES・公開鍵)は、鍵が 2 の 128 乗以上あり、同じ平文でも毎回違う暗号文になる。シーザー暗号は守る目的ではなく、文字コード・剰余・ループ・テストを学ぶ題材として使う。
参考・出典
確認日はいずれも 2026年10月29日です。
- Python 公式ドキュメント「codecs — codec レジストリと基底クラス」テキスト変換(rot_13):https://docs.python.org/ja/3/library/codecs.html#text-transforms
- Python 公式ドキュメント「式 (expression)」二項算術演算(% の結果の符号):https://docs.python.org/ja/3/reference/expressions.html#binary-arithmetic-operations
- Python 公式ドキュメント「組み込み関数」ord()・chr():https://docs.python.org/ja/3/library/functions.html
- MDN「剰余 (%)」:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Remainder
- MDN「String.prototype.charCodeAt()」:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt
- Oracle「Java SE 21 API: Math.floorMod」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Math.html#floorMod(int,int)
- NIST「FIPS 197: Advanced Encryption Standard (AES)」:https://csrc.nist.gov/pubs/fips/197/final
- GNU coreutils「tr invocation」:https://www.gnu.org/software/coreutils/manual/html_node/tr-invocation.html
- Unicode「Hiragana(U+3040〜U+309F)コード表」:https://www.unicode.org/charts/PDF/U3040.pdf
- OpenSSL「openssl-enc」:https://docs.openssl.org/3.0/man1/openssl-enc/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「シーザー暗号とは|仕組み・解き方とPython・JS・Javaの実装・練習問題」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。