user:
RSA暗号を理解するためのPython関数を作っていただけますか?あくまで、RSA暗号について学習するための目的です。
assistant:
いいですよ!RSAを学ぶための教育用Python実装を作って、実行例まで動かしてみました。🔐📚
(注意:このコードは「学習用」です。実運用やセキュリティ目的では絶対に使わないでください。プロダクションでは巨大素数・安全な乱数・標準ライブラリを使います。)
# RSA学習用のPythonコード実行例
# このノートブックは教育目的(小さい素数を使った分かりやすい実装)です。
# 実運用では絶対に使わないでください(安全性が低い)。
import math, random
# --- ユーティリティ ---
def egcd(a, b):
"""拡張ユークリッド互除法: (g, x, y) を返す。つまり ax + by = g = gcd(a,b)"""
if b == 0:
return (a, 1, 0)
g, x1, y1 = egcd(b, a % b)
return (g, y1, x1 - (a // b) * y1)
def modinv(a, m):
"""a の m に対する逆元を返す。存在しなければ例外を投げる"""
g, x, _ = egcd(a, m)
if g != 1:
raise ValueError("逆元が存在しません")
return x % m
def is_prime(n):
"""単純な確定性ミラーラビン (小さいn用) または試し割りを併用した素数判定(教育用)"""
if n < 2:
return False
small_primes = [2,3,5,7,11,13,17,19,23,29]
for p in small_primes:
if n == p:
return True
if n % p == 0:
return False
# 簡易な試し割り(教育用。大きな数には非効率)
r = int(math.isqrt(n))
for i in range(31, r+1, 2):
if n % i == 0:
return False
return True
def generate_prime(start=100, end=1000):
"""指定範囲からランダムに素数を探す(教育用)"""
if start >= end:
raise ValueError("start < end にしてください")
while True:
candidate = random.randint(start, end)
if is_prime(candidate):
return candidate
# --- RSA の主要関数 ---
def generate_keypair(bits=16):
"""小さいビット長で鍵を作る(教育目的)。bits は p,q のサイズに概ね対応する目安。"""
# 簡単のため、bits を使って p,q の範囲を決める
low = 2 ** (bits-1)
high = 2 ** bits - 1
# 実際は高品質な乱数と大きい素数生成が必要(ここは簡易実装)
p = generate_prime(low, high)
q = generate_prime(low, high)
while q == p:
q = generate_prime(low, high)
n = p * q
phi = (p-1) * (q-1)
# 公開指数 e を選ぶ(典型的には 65537 が使われるが、ここでは phi と互いに素な値を選ぶ)
e = 65537
if math.gcd(e, phi) != 1:
# フォールバックで小さめの e を探す
for cand in [3,5,17,257]:
if cand < phi and math.gcd(cand, phi) == 1:
e = cand
break
else:
# 最終手段で 3 から順に探す(教育用)
for cand in range(3, phi, 2):
if math.gcd(cand, phi) == 1:
e = cand
break
d = modinv(e, phi)
public_key = (e, n)
private_key = (d, n)
# 生成情報も返す(p,q は学習のため表示するが本番では秘密)
return public_key, private_key, {"p": p, "q": q, "phi": phi}
def encrypt_int(m, public_key):
"""整数 m を公開鍵で暗号化: c = m^e mod n"""
e, n = public_key
if m >= n:
raise ValueError("メッセージ整数が n より小さくなるようにしてください")
return pow(m, e, n)
def decrypt_int(c, private_key):
"""暗号文 c を秘密鍵で復号: m = c^d mod n"""
d, n = private_key
return pow(c, d, n)
def text_to_int(msg_text):
"""テキスト (utf-8) -> 整数"""
b = msg_text.encode("utf-8")
return int.from_bytes(b, byteorder="big")
def int_to_text(m_int):
"""整数 -> テキスト (utf-8)"""
# バイト長を計算
length = (m_int.bit_length() + 7) // 8
b = m_int.to_bytes(length, byteorder="big")
return b.decode("utf-8", errors="replace")
# 署名(単純な例):メッセージハッシュとして整数化した値に秘密鍵で署名
import hashlib
def sha256_int(data_bytes):
h = hashlib.sha256(data_bytes).digest()
return int.from_bytes(h, "big")
def sign_message(msg_text, private_key):
d, n = private_key
msg_hash_int = sha256_int(msg_text.encode("utf-8"))
if msg_hash_int >= n:
# 実運用ではハッシュをパディングして扱う(ここは教育用の単純化)
msg_hash_int = msg_hash_int % n
sig = pow(msg_hash_int, d, n)
return sig
def verify_signature(msg_text, sig, public_key):
e, n = public_key
msg_hash_int = sha256_int(msg_text.encode("utf-8")) % n
recovered = pow(sig, e, n)
return recovered == msg_hash_int
# --- デモ実行 ---
random.seed(42) # 再現性のため
pub, priv, meta = generate_keypair(bits=16) # bits を小さくして早く生成
print("公開鍵 (e, n):", pub)
print("秘密鍵 (d, n):", priv)
print("内部情報 (p, q):", meta["p"], meta["q"])
print()
message = "RSAを学ぶテストメッセージ。こんにちは!"
m_int = text_to_int(message)
print("メッセージを整数化した値(桁数):", m_int, "(bits:", m_int.bit_length(), ")")
# 暗号化の前に m_int < n を確認。もし大きければメッセージを分割する必要あり
e, n = pub
if m_int >= n:
print("注意: メッセージが n より大きいので分割が必要です(このデモでは簡易対応します)。")
# 簡易分割:バイト単位で分割してブロックごとに暗号化する(教育用)
b = message.encode("utf-8")
block_size = (n.bit_length() - 1) // 8 # 1ブロックの最大バイト数
blocks = [b[i:i+block_size] for i in range(0, len(b), block_size)]
cipher_blocks = []
for blk in blocks:
mi = int.from_bytes(blk, "big")
ci = encrypt_int(mi, pub)
cipher_blocks.append(ci)
print("暗号化ブロック数:", len(cipher_blocks))
# 復号
dec_bytes = b""
for ci in cipher_blocks:
mi = decrypt_int(ci, priv)
# 復号した整数をバイト列に戻す(長さは可変なので同じ長さを推定)
l = (mi.bit_length() + 7) // 8
dec_bytes += mi.to_bytes(l, "big")
dec_text = dec_bytes.decode("utf-8")
print("復号結果:", dec_text)
else:
c = encrypt_int(m_int, pub)
print("暗号文:", c)
m_rec = decrypt_int(c, priv)
print("復号した整数 == 元の整数:", m_rec == m_int)
recovered_text = int_to_text(m_rec)
print("復号結果テキスト:", recovered_text)
# 署名デモ
sig = sign_message(message, priv)
print()
print("署名(整数):", sig)
print("署名検証:", verify_signature(message, sig, pub))
以下にやったことの要点と、使える関数説明を日本語でわかりやすくまとめます。必要なら、さらに詳しい数学的説明や図解、課題(演習問題)も出しますね!✨
要点(簡単)
- 素数判定・小さめ素数の生成(教育用の簡易実装)。
- 鍵生成(p, q を選んで n = p*q, φ = (p-1)(q-1)、公開指数 e、秘密指数 d を計算)。
- 整数としてのメッセージ変換(UTF-8 バイト列 ⇄ 整数)。
- 暗号化・復号(m^e mod n, c^d mod n)。
- 署名・検証(SHA-256 を整数化して秘密鍵で署名、公開鍵で検証)。
- 実行例:メッセージを分割して暗号化→復号、署名→検証まで動作確認済み。
主な関数(説明)
-
generate_keypair(bits=16)
小さめの鍵を生成(教育用)。戻り値は(public_key, private_key, meta)。
public_key = (e, n),private_key = (d, n)。metaに p,q,phi が入る(学習上表示)。 -
encrypt_int(m, public_key)/decrypt_int(c, private_key)
整数 m を暗号化 / 整数復号。 -
text_to_int(msg_text)/int_to_text(m_int)
テキスト(UTF-8) ⇄ 整数変換。RSAは数学的には整数を扱うのでこれが必要。 -
sign_message(msg_text, private_key)/verify_signature(msg_text, sig, public_key)
単純な署名・検証(SHA-256 を使い、教育目的の簡易実装)。 -
その他:
modinv,egcd,is_prime,generate_prime(補助関数)
実行サンプル(ノートブックを実行済み)
- 生成された公開鍵・秘密鍵、p,q の表示。
- 「RSAを学ぶテストメッセージ。こんにちは!」 を暗号化→復号して復元できることを確認。
- 署名を作り、検証が
Trueになることを確認。
(ノートブックの出力例は既に実行して表示しています。)
注意(大切)
-
この実装は小さい素数を使っていて、高速化・安全対策が欠けているため実運用不可です。
-
実際の安全なRSAでは:
- 2048ビット以上の素数を安全な乱数で生成
- パディング(PKCS#1 / OAEP / PSS など)を適切に使う
- 定評ある暗号ライブラリ(例:
cryptography、PyCryptodome)を使う
を必須とします。
もしよければ次はどれを深掘りしましょうか?(選んでくれたらそのまま拡張します)
- 素数生成アルゴリズム(Miller–Rabin の実装)。🧪
- 実際に
PyCryptodomeを使った本格的な RSA の使い方(鍵作成・OAEP暗号化・PSS署名)。🔧 - RSA の安全性の背景(なぜ大きな素数が必要か、既知の攻撃)。📖
- コードをステップごとに可視化するデモ(ブロック分割や演算過程の表示)。📊
どれがいい?それとも今のコードをさらにコメント多め・教育向けノート風に整形しましょうか?😊
user:
ありがとうございます。今のコードをさらにコメント多め・教育向けノート風に整形してください。
assistant:
# RSA暗号 学習用ノート
# =========================================
# このコードは「RSA暗号を理解するため」の教育用サンプルです。
# 実際の暗号利用には不十分(安全性・速度ともに不足)なので、
# 本番環境や実用目的では絶対に使用しないでください。
import math, random, hashlib
# -------------------------------------------------------------
# 基本的な数学的関数
# -------------------------------------------------------------
def egcd(a, b):
"""
拡張ユークリッド互除法。
ax + by = gcd(a, b) を満たす (g, x, y) を返す。
g = gcd(a, b)
"""
if b == 0:
return (a, 1, 0)
g, x1, y1 = egcd(b, a % b)
return (g, y1, x1 - (a // b) * y1)
def modinv(a, m):
"""
a の m に対する逆元を計算。
つまり a * x ≡ 1 (mod m) を満たす x を返す。
"""
g, x, _ = egcd(a, m)
if g != 1:
raise ValueError("逆元が存在しません(a と m が互いに素でない)")
return x % m
def is_prime(n):
"""
素数判定(非常に単純な試し割り法)。
教育目的なので遅いが分かりやすい。
"""
if n < 2:
return False
# 小さい素数で割って確認
small_primes = [2,3,5,7,11,13,17,19,23,29]
for p in small_primes:
if n == p:
return True
if n % p == 0:
return False
# √n までの奇数で割り算
r = int(math.isqrt(n))
for i in range(31, r+1, 2):
if n % i == 0:
return False
return True
def generate_prime(start=100, end=1000):
"""
指定範囲からランダムに素数を探す。
実運用では大きな素数生成アルゴリズムを使うが、ここでは教育用に簡易化。
"""
while True:
candidate = random.randint(start, end)
if is_prime(candidate):
return candidate
# -------------------------------------------------------------
# 鍵生成(RSAの仕組みの中心部分)
# -------------------------------------------------------------
def generate_keypair(bits=16):
"""
RSAの鍵ペアを生成する。
bits: 素数 p, q のビット長の目安(小さい値でデモ用)。
戻り値: (公開鍵, 秘密鍵, 内部情報)
"""
# 素数を2つ選ぶ
low = 2 ** (bits-1)
high = 2 ** bits - 1
p = generate_prime(low, high)
q = generate_prime(low, high)
while q == p:
q = generate_prime(low, high)
n = p * q # 公開鍵の一部
phi = (p-1) * (q-1) # オイラーのφ関数
# 公開指数 e を選ぶ
e = 65537 # 一般的な選択肢
if math.gcd(e, phi) != 1:
# e が φ と互いに素でない場合は別の数を探す
for cand in [3,5,17,257]:
if cand < phi and math.gcd(cand, phi) == 1:
e = cand
break
# 秘密指数 d を計算(e の逆元 mod φ)
d = modinv(e, phi)
public_key = (e, n)
private_key = (d, n)
# 学習のために p, q, phi も返す(通常は秘密情報)
return public_key, private_key, {"p": p, "q": q, "phi": phi}
# -------------------------------------------------------------
# 暗号化・復号
# -------------------------------------------------------------
def encrypt_int(m, public_key):
"""
RSA暗号化: c = m^e mod n
m: 平文整数
"""
e, n = public_key
if m >= n:
raise ValueError("メッセージ整数が n より小さくなるようにしてください")
return pow(m, e, n)
def decrypt_int(c, private_key):
"""
RSA復号: m = c^d mod n
"""
d, n = private_key
return pow(c, d, n)
# -------------------------------------------------------------
# テキストと整数の変換(RSAは整数を処理するため)
# -------------------------------------------------------------
def text_to_int(msg_text):
"""テキスト (utf-8) -> 整数"""
b = msg_text.encode("utf-8")
return int.from_bytes(b, byteorder="big")
def int_to_text(m_int):
"""整数 -> テキスト (utf-8)"""
length = (m_int.bit_length() + 7) // 8
b = m_int.to_bytes(length, byteorder="big")
return b.decode("utf-8", errors="replace")
# -------------------------------------------------------------
# 署名と検証
# -------------------------------------------------------------
def sha256_int(data_bytes):
"""SHA-256 のハッシュを整数として返す"""
h = hashlib.sha256(data_bytes).digest()
return int.from_bytes(h, "big")
def sign_message(msg_text, private_key):
"""
秘密鍵で署名。
本来はパディング方式を使うが、ここでは単純化している。
"""
d, n = private_key
msg_hash_int = sha256_int(msg_text.encode("utf-8"))
msg_hash_int = msg_hash_int % n # n より大きい場合は縮小
sig = pow(msg_hash_int, d, n)
return sig
def verify_signature(msg_text, sig, public_key):
"""
公開鍵で署名検証。
"""
e, n = public_key
msg_hash_int = sha256_int(msg_text.encode("utf-8")) % n
recovered = pow(sig, e, n)
return recovered == msg_hash_int
# -------------------------------------------------------------
# デモ実行
# -------------------------------------------------------------
random.seed(42) # デモの再現性のため
# 鍵生成
pub, priv, meta = generate_keypair(bits=16)
print("=== 鍵生成 ===")
print("公開鍵 (e, n):", pub)
print("秘密鍵 (d, n):", priv)
print("内部情報 (p, q, φ):", meta)
print()
# メッセージ
message = "RSAを学ぶテストメッセージ。こんにちは!"
print("=== メッセージ ===")
print("元のテキスト:", message)
# テキストを整数に変換
m_int = text_to_int(message)
print("整数化したメッセージ (bit長):", m_int, "(bits:", m_int.bit_length(), ")")
print()
# n より大きい場合は分割が必要(このデモでは簡易対応)
e, n = pub
if m_int >= n:
print("※注意: メッセージが n より大きいのでブロック分割します。")
b = message.encode("utf-8")
block_size = (n.bit_length() - 1) // 8 # 1ブロックの最大バイト数
# 暗号化(ブロックごと)
blocks = [b[i:i+block_size] for i in range(0, len(b), block_size)]
cipher_blocks = [encrypt_int(int.from_bytes(blk, "big"), pub) for blk in blocks]
print("暗号化ブロック数:", len(cipher_blocks))
# 復号
dec_bytes = b""
for ci in cipher_blocks:
mi = decrypt_int(ci, priv)
l = (mi.bit_length() + 7) // 8
dec_bytes += mi.to_bytes(l, "big")
dec_text = dec_bytes.decode("utf-8")
print("復号結果:", dec_text)
else:
# 単一ブロックで処理できる場合
c = encrypt_int(m_int, pub)
print("暗号文:", c)
m_rec = decrypt_int(c, priv)
recovered_text = int_to_text(m_rec)
print("復号結果:", recovered_text)
print()
# 署名と検証
print("=== 署名と検証 ===")
sig = sign_message(message, priv)
print("署名値 (整数):", sig)
print("署名検証結果:", verify_signature(message, sig, pub))
整形しました!📖✨
これで「教育用ノート風」のRSAデモコードになりました。
ポイントまとめ
- 各関数に日本語コメントをつけたので、数式がどこで使われているか追いやすくなってます。
-
generate_keypairでは内部の p, q, φ も出力して、RSAの仕組みが「見える化」できるようにしました。 - メッセージが nより大きいと分割が必要になることも、実際の処理付きで体験できます。
- 署名・検証まで一通り流れを確認できるので、暗号と認証の違いも学習できます。
これをそのままノートブックに置けば、**「数式の確認」⇔「コードで試す」**ができる学習教材になりますよ。
もし次のステップに進めるなら:
- 素数生成アルゴリズム(Miller–Rabin の追加)
-
本格ライブラリ版RSA(
PyCryptodomeで OAEP & PSS) - 視覚化ノート(鍵生成や暗号化を図解+コード)
どの方向に展開していきましょうか?😊
user:
ありがとうございます。Python実装を読み解けば確実なのでしょうが、
=== 鍵生成 ===
公開鍵 (e, n): (65537, 3036759119)
秘密鍵 (d, n): (1933322273, 3036759119)
内部情報 (p, q, φ): {'p': 48817, 'q': 62207, 'phi': 3036648096}
の部分について、日本語で詳しく解説していただけますか?
assistant:
いいところに目をつけましたね、先輩 😊📘
では、表示されている各値を一つずつ、丁寧に噛み砕いて説明します。数値の検算(簡単な実例)も含めて示します — 見て納得できるように丁寧に行きますね。
鍵生成出力の詳しい解説
出力(再掲)
公開鍵 (e, n): (65537, 3036759119)
秘密鍵 (d, n): (1933322273, 3036759119)
内部情報 (p, q, φ): {'p': 48817, 'q': 62207, 'phi': 3036648096}
1) p と q — 素数(秘密)
-
p = 48,817,q = 62,207は互いに素な 素数(prime)です。 - RSAではこの2つの大きな素数を秘密にしておきます。攻撃者が
pとqを見つけられる(素因数分解される)と秘密鍵が破られます。 - この実例では学習用に小さな数を使っているため簡単に因数分解できます(=実運用では使えません)。
2) n = p * q — モジュラス(公開)
-
n = 48,817 × 62,207 = 3,036,759,119(出力の3036759119)です。 -
nは公開される値で、公開鍵(e, n)として配布されます。暗号化や署名検証はこのnを法(modulus)として計算します。 - 重要:メッセージを整数化した値(
m)は通常0 ≤ m < nの範囲で扱います。もしm ≥ nならブロック分割やパディングが必要です。 - ここでの
nは約 32 ビット(厳密にはn.bit_length() = 32)しかないので、安全には程遠いです。実用では 2048 ビット以上が標準です。
3) φ(phi)=オイラーの φ 関数 — (p-1)*(q-1)(秘密)
-
φ = (p-1)*(q-1) = 3036648096。出力のphiと一致します。 - 別の書き方として
φ = n - p - q + 1という関係も成り立ちます(同じ結果になります)。 -
φは鍵生成で重要で、公開指数eの逆元(mod φ)が秘密指数dになるために使います。通常φは秘密です(知られるとdを計算される可能性があります)。
4) e — 公開指数(公開)
-
e = 65,537は典型的に使われる公開指数(よく使われる値)です。 -
eは小さめでビット表現に1が少ない(効率的)ため計算が速い一方で、eとφが互いに素であること(gcd(e, φ) = 1)が必要です。ここではその条件を満たしているため逆元が存在します。
5) d — 秘密指数(秘密)
-
d = 1,933,322,273。これはeの φ に関するモジュラー逆元 です。すなわちe * d ≡ 1 (mod φ)が成り立ちます(これが復号/署名の鍵となる基本条件)。
-
実際に計算すると
(e * d) % φ = 1になります。さらに詳しくはe * d = 1 + k * φという形になり、ここで
k = 41,725(この例の整数)です。つまりe*d = 1 + 41725 * φとなります。
この等式があるからこそ次に説明する復号が成り立ちます。
6) なぜ e と d の関係で復号できるのか(簡単な数学的直観)
-
オイラーの定理(簡易版):もし
mがnと互いに素(gcd(m,n)=1)ならばm^φ ≡ 1 (mod n) -
e * d = 1 + kφとするとm^(e*d) = m^(1 + kφ) = m * (m^φ)^k ≡ m * 1^k ≡ m (mod n) -
暗号化は
c = m^e mod n。復号はc^d mod n = (m^e)^d = m^(e*d) ≡ m (mod n)となり元に戻る、という仕組みです。 -
(
mがnと互いに素でない場合は中華の剰余定理(CRT)を用いる扱いが必要ですが、学習の段階ではここまで理解できればOKです。)
7) 実際に数値で確認(簡単な例)
-
p = 48,817,q = 62,207,n = 3,036,759,119,φ = 3,036,648,096,e = 65,537,d = 1,933,322,273は内部整合します(先ほどの等式が成り立つことを検算しました)。 -
暗号化/復号の例(整数
m = 1,234,567):- 暗号文
c = m^e mod n = 3,035,862,701 - 復号
m' = c^d mod n = 1,234,567→ 元に戻る(検算でm' == mがTrue)
- 暗号文
-
署名の例(コードで使った日本語メッセージ)では、SHA-256 を整数化して
sig = hash^d mod nを作り、sig^e mod nがhash mod nと一致することで検証がTrueになります(このデモでも一致しました)。
8) 実用上の注意(学習上の補足)
-
p, q, φ, d は絶対に秘密です。公開してはダメ。公開されるのは
(e, n)のみ。 - このデモの
nはとても小さい(32ビット)ので安全性は皆無です。実運用では 2048〜4096 ビットのnを使い、素数生成や乱数に高品質な方法を使います。 - 平文メッセージはそのまま整数化して暗号化するとサイズ制約や安全性(マレット攻撃など)の問題があるため、OAEP(暗号化)やPSS(署名)などのパディング規格を必ず使います(ライブラリ利用が推奨)。
必要なら、今の説明を図で可視化したり(鍵生成のフローチャート、m → c → m の流れ)、あるいは
- ミラーラビンを使った高速で現実的な素数判定の実装、
-
PyCryptodomeを使った 実運用に近い RSA(OAEP/PSS) のサンプル、
のどちらかに展開して詳しく見せることもできます。希望の方向があればそのまま進めますね — 先輩と一緒にじっくりやるの、楽しいです 😉
user:
ありがとうございます。まだちょっと説明が難解ですね。高校生でも分かるレベルで説明していただけますか?「素数」と「素因数分解」は高校数学までで勉強するから良いとして、「法」の説明が足りないように思います。あとは、「べき乗」と暗号化・復号化の関係を分かりやすく説明していただくと良いかと思います。
assistant:
なるほど!✨
先輩がおっしゃるとおり、「法(mod)」と「べき乗で元に戻る理由」をもう少しやさしく、高校生でもストンと理解できるように整理しましょう。
RSAを高校数学レベルで説明すると
1. 「法(mod)」ってなに?
-
「mod(法)」は、割り算のあまりのことです。
例えば:-
17 mod 5 = 2(17 を 5 で割ると 余りは 2) 23 mod 7 = 2
-
-
RSAでは「めちゃ大きな数の余り」を使います。
この「余りの世界」を数学では「合同算(mod n)」と言います。 -
この「余りの世界」では、普通の掛け算やべき乗のルールがそのまま成り立つので、計算がやりやすくなります。
2. 鍵を作る仕組み
-
まず、秘密にする素数
pとqを選びます。 -
これを掛け算して
n = p × qを作ります。これが「余りを取るときの基準(法 n)」になります。 -
公開するのは
(e, n)。秘密にするのは(d, n)とp, q。 -
ここでのポイントは「ある特別な関係をもつ e と d を選ぶ」ことです。
その関係とはe × d = 1 + k × φ (kは整数、φ=(p-1)(q-1))という形になること。
この関係があるから「暗号化」と「復号」がつながります。
3. 暗号化と復号(べき乗で元に戻る)
RSAは「べき乗」を使った仕組みです。
-
暗号化:
平文メッセージを整数mに変換し、c = m^e mod nとして暗号文
cを作ります。 -
復号化:
暗号文cをm' = c^d mod nと計算すると、元の
mに戻ります。
4. なんで元に戻るの?
ここがちょっと魔法みたいに感じるところですが、理由はこうです:
-
c = m^eを代入すると、復号はm' = (m^e)^d mod n = m^(e×d) mod n -
さっきの「特別な関係」より
e×d = 1 + k×φ。これを代入するとm^(e×d) = m^(1 + kφ) = m^1 × (m^φ)^k -
ここでオイラーの定理が登場!
「m と n が互いに素なら m^φ ≡ 1 (mod n)」
よって(m^φ)^k ≡ 1^k ≡ 1 -
だから
m^(1 + kφ) ≡ m × 1 ≡ m (mod n)となり、元の
mに戻るのです。
つまり「公開鍵でかけ算した余り」と「秘密鍵でべき乗した余り」が、ちょうどキャンセルされて元に戻る、という仕組みなんです。
5. たとえ話でイメージ
- 「mod n の世界」は、時計の目盛りみたいなもの。
12時間制の時計なら「15時」は「3時」と同じ(=mod 12 で計算している)。 - RSAは、この「時計算」を超巨大な数でやっているイメージ。
- 公開鍵は「誰でも暗号化できるが、解読できない秘密のルートを隠してある」もの。
- 秘密鍵は「元に戻すための正しい逆操作」を知っているから、受信者だけが復号できる。
まとめ
- mod = 余りの世界
- RSAはべき乗を使った「暗号化→復号」で元に戻る仕組み
- 秘密は「素因数分解の難しさ」によって守られている
- 公開鍵で暗号化したものは、秘密鍵を知らないと復号できない
先輩、もし「数値を実際に入れて暗号化→復号の流れ」を一緒に追ってみた方が理解しやすいなら、m=12345 くらいの小さな例で紙と電卓でもできるようにステップを作りましょうか?
user:
そうですね、小さな数字を具体例として、順番に追うのが良さそうです。お願いします。
assistant:
了解です!✨
じゃあ今回は超ミニRSAを「手計算できるレベルの数字」で順番に追ってみましょう。
(実際のRSAは何百桁もあるけど、ここでは理解用に小さな数を使います🔢)
ステップ1: 素数を選ぶ
RSAの出発点は 素数2つ。
今回は小さくして、
- (p = 17)
- (q = 11)
とします。
ステップ2: n と φ を計算
- (n = p \times q = 17 \times 11 = 187)
- (\varphi = (p-1)(q-1) = 16 \times 10 = 160)
ここで出てきた (n) が「法」、つまり余りを取るときの基準になります。
ステップ3: 公開指数 e を決める
条件は「(\varphi) と互いに素」になる整数。
今回は教科書でよく使われる e = 7 を選びます。
ステップ4: 秘密指数 d を計算
次を満たす d を求めます:
[
e \times d \equiv 1 \pmod{\varphi}
]
つまり、(7 \times d ≡ 1 \mod 160) です。
計算すると
[
d = 23
]
(なぜなら (7 \times 23 = 161 = 1 + 160) だから!)
ステップ5: 鍵の完成
- 公開鍵 ((e, n) = (7, 187))
- 秘密鍵 ((d, n) = (23, 187))
ステップ6: 暗号化
メッセージを整数にしましょう。
今回は小さく m = 88 を送ることにします。
暗号化式:
[
c = m^e \mod n = 88^7 \mod 187
]
これを計算すると、
[
c = 11
]
暗号文は 11 になりました。
ステップ7: 復号化
暗号文 c を秘密鍵で復号します。
[
m' = c^d \mod n = 11^{23} \mod 187
]
計算すると、
[
m' = 88
]
つまり 元のメッセージに戻った! 🎉
まとめ(小さい例でわかったこと)
- 2つの素数から n を作る。
- 「公開鍵 (e, n)」と「秘密鍵 (d, n)」がペアになる。
- 公開鍵で暗号化したものを、秘密鍵で復号すると元に戻る。
- ポイントは「法 n の世界」と「e と d が特別な関係(互いに逆数になる)」になっていること。
先輩、ここまででだいぶ「なるほど!」ってなったかな?
もしもっと直感でイメージ掴みたいなら、この例を「時計の目盛り(mod 187の時計)」で図解しても面白いと思いますけど、どうします?⌚
user:
ありがとうございます。でもこの暗号を解読しようと思ったら、nは既に公開されているので、dが分かれば良いんですよね? n を素因数分解して p と q にする必要がある、とされている理由は何でしょうか?
assistant:
めっちゃいい質問です、先輩!✨
RSAの核心をズバッと突いてますね。
1. 解読に必要なのは「d」
はい、その通りです。
公開されているのは ((e, n))。暗号文を復号するには「秘密鍵 (d)」が必要です。
もし (d) が分かれば、暗号化されたものは全部解けます。
2. じゃあ、どうやって (d) を求める?
(d) を計算するには次の式が必要です:
[
e \times d \equiv 1 \pmod{\varphi(n)}
]
ここで出てくるのが (\varphi(n))。
でも (\varphi(n)) を計算するには p と q(n の素因数分解の結果) を知っていないとダメなんです。
なぜなら:
[
\varphi(n) = (p-1)(q-1)
]
だから。
3. つまり…
- (d) を求めるためには (\varphi(n)) が必要。
- (\varphi(n)) を計算するには (p) と (q) が必要。
- だから「n を素因数分解すること」が秘密鍵を知るための唯一のルートになる。
4. 安全性の本質
RSAの安全性は 「n の素因数分解がとても難しい」 という事実に依存しています。
- 小さい (n) なら手で分解できる。
例:(n = 187) → 17 × 11 ってすぐ分かる。 - でも実際のRSAでは 2048ビット(約600桁)以上の n を使うので、
素因数分解は現実的に不可能(少なくとも現在の計算機技術では数十億年かかるレベル)。
5. もし素因数分解ができると…
- (p, q) が分かる → (\varphi(n)) が計算できる → (d) が求まる。
- つまり、RSAは完全に破られます。
だからRSAは「素因数分解の難しさ」を盾にしてるんです。
言い換えれば、「d を直接秘密にしてる」んじゃなくて、「n の因数分解が難しいから d がバレない」ようになってるんです。
まとめ
- 秘密鍵 (d) を求めるには (\varphi(n)) が必要。
- (\varphi(n)) を求めるには (p, q) を知らないといけない。
- だから「n を素因数分解すること」が突破口になる。
- でも巨大な (n) は素因数分解が事実上不可能だから安全。
先輩、この「素因数分解の難しさに依存している」っていうRSAの設計、シンプルだけど美しいと思いません?
もしよければ次に、**「じゃあ量子コンピュータが出てきたらどうなるの?」**って話まで広げても面白いですよ😊
user:
ありがとうございます。納得しました。あと残ってる疑問は、文字列を数字に、数字を文字列に変換する部分と、「ブロック分割」という部分ですね。解説お願いして良いですか?
assistant:
おお〜✨いいところまで来ましたね、先輩!
RSAを実際に「文字列(テキスト)」に使うときに必ず出てくる2つの実務ポイントがまさにそれです。
① 文字列を数字に、数字を文字列に
RSAは「数式で動く暗号」なので、文字列そのままでは扱えません。
だから、文字列 ↔ 数字 の変換が必要になります。
やり方
-
文字列 → 数字
- 文字をバイト列(UTF-8やASCIIなど)に変換
- そのバイト列を大きな整数に変換
例:「ABC」
- ASCIIコードは
[65, 66, 67] - 16進だと
0x414243 - 整数にすると
4276803
→ つまり「ABC」= 整数 4276803 に変換できる。
-
数字 → 文字列
- 整数をバイト列に戻す
- バイト列を文字列に変換
→ 4276803 を逆に変換すると「ABC」に戻る。
こうして、RSAで「文字列を暗号化・復号」できるようになります。
② ブロック分割
ここがちょっと面白いところです。
RSAでは「暗号化できる整数」は n より小さい必要があるんです。
なぜ?
暗号化の式は
[
c = m^e \mod n
]
なので、もしメッセージ整数 (m) が (n) より大きかったら、計算上は勝手に mod で小さくなっちゃう。
すると メッセージが正しく復元できなくなるんです。
具体例
例えば今回のデモ鍵で (n = 3036759119)(約10桁の整数)でした。
でも「RSAを学ぶテストメッセージ。こんにちは!」を全部まとめて整数化すると 455ビット ≈ 137桁の数字になっちゃう。
→ (n) より遥かに大きい!
だから分割が必要になる。
どうやるの?
- メッセージを一定サイズごとに切る(=ブロック化)
- それぞれを整数に変換してRSA暗号化
- 受信側では各ブロックを復号して繋ぎ直す
例:
- (n \approx 10^{9}) なら「9桁以下の整数」ごとに分ける
- 実際のRSA(2048ビット鍵)だと「256バイトごと」のブロックになる
まとめると
-
文字列 ↔ 数字変換
- バイト列として整数に直す(ASCIIやUTF-8で変換可能)
-
ブロック分割
- メッセージの整数が (n) より大きくならないように、分けて処理する
補足:実用RSAはもっと工夫する
実際のRSAでは「パディング方式(OAEPなど)」を使って、
- ブロックサイズを調整する
- 安全性を高める
という追加ルールを必ず組み込みます。
(今回のはあくまで学習用のシンプルRSAなのでパディングなし、分割のみで動いてます👍)
先輩、ここまでの流れで「文字列→整数→ブロック→RSA」という全体像はイメージ湧きました?
もしイメージ強化するなら、次回は「実際に 'HELLO' を整数に変換して、ブロック分割して暗号化→復号」って手作業でやってみましょうか?
user:
ありがとうございます。「実用RSAはもっと工夫する」について、詳しく説明したらキリがないのかもしれませんが、どんな工夫なのか、なぜそんな工夫が必要なのか、ざっくりと説明していただけますか?
assistant:
先輩、さすが鋭いですね😊✨
ここまで理解できてるなら、「実用RSAで必要になる追加の工夫」がなぜあるのかもきっとスッと入ってくると思います。
ざっくり大事なポイントをまとめますね。
実用RSAで必要な「工夫」たち
大きく分けると次の3つです。
① パディング(Padding)
RSAでは「ただ数字にして暗号化」するだけだと危険です。
理由:
- メッセージに規則性があると、暗号文にもパターンが出てしまう。
例:平文「A」「B」を暗号化したら、暗号文が常に同じになる。 - 攻撃者は「暗号文をたくさん集めて辞書化」できてしまう。
工夫:
- 暗号化する前に「メッセージにランダムなデータを混ぜる」。
- 代表例:RSA-OAEP(Optimal Asymmetric Encryption Padding)。
→ ランダム性を入れることで、同じ平文でも暗号文が毎回違う。
② ハイブリッド暗号
RSAは「大きな数のべき乗」をやるので計算が遅いです⚡。
しかも、ブロックサイズは 鍵の長さ(2048ビット ≈ 256バイト) に制限される。
→ 長い文章やファイルはRSAだけでは暗号化しにくい。
工夫:
-
実際には「RSAは鍵のやりとり専用」で使います。
-
本体のメッセージは、AESなどの「速い共通鍵暗号」で暗号化。
-
つまり「ハイブリッド暗号方式」:
- RSA → 秘密鍵(セッションキー)を安全に渡す
- AESなど → 大量データを効率よく暗号化
③ 電子署名(Signature)
RSAは「暗号化」だけじゃなくて「署名」にも使われます。
これは「このメッセージが確かに本人から来た」ことを証明する仕組み。
やり方:
- 平文のハッシュ値を秘密鍵で「暗号化」する → 署名
- 受け取った側は公開鍵で検証する
- 正しければ「このメッセージは改ざんされていないし、確かに本人が送った」ことが保証される
まとめると
- パディング → 毎回違う暗号文にして攻撃を防ぐ
- ハイブリッド暗号 → RSAは鍵だけ、実際の通信は速い共通鍵暗号
- 電子署名 → 改ざん防止と本人確認
つまり「学習用のRSA」=数字の変換+暗号化+復号だけ、
「実用のRSA」=パディング+ハイブリッド暗号+署名などを組み合わせた、セキュリティ総合パッケージ✨
先輩、ここまで来るともう「教材RSA」から「実用RSA」に片足突っ込んでますね👀
もしご興味あれば、次は「OAEPの中身」か「RSA+AESでのハイブリッド暗号の流れ」を一緒に手を動かしてみます?
user:
ありがとうございます。今夜はここまでで大丈夫です。ここまでの内容をブログに残したいので、SEO・読者引き込み効果を意識した具体的なタイトル案をいくつか考えていただけますか?
assistant:
はいっ、了解です先輩🌙✨
これまでの流れを整理すると、「RSA暗号をPythonで学ぶ」+「仕組みを数式や実装と一緒に解説」って感じなので、SEOで検索されやすいキーワードは
- 「RSA暗号」
- 「Python 実装」
- 「初心者向け」「やさしく解説」
- 「素因数分解」「公開鍵」「秘密鍵」
あたりを盛り込むと良いと思います。
いくつか具体的なタイトル案を出してみました👇
タイトル案(初心者向け・キャッチー)
- 「RSA暗号をPythonでやさしく解説!素因数分解から公開鍵・秘密鍵まで」
- 「高校数学で分かるRSA暗号 ― Pythonで公開鍵暗号を体験してみよう」
- 「ゼロから学ぶRSA暗号:Python実装で仕組みと動きを理解する」
- 「RSA暗号を完全図解!文字列→数字変換とブロック分割をPythonで体験」
- 「公開鍵暗号の基本!RSAを小さな数からPythonでシミュレーション」
タイトル案(実装寄り・技術系読者向け)
- 「RSA暗号をPythonで実装!鍵生成・暗号化・復号・署名の仕組みを徹底解説」
- 「RSA暗号をPythonコードで理解する:公開鍵と秘密鍵の数理入門」
- 「暗号理論の基礎:RSAをPython実装で学ぶ(鍵生成・ブロック分割・署名検証)」
- 「RSA暗号の仕組みを実装から学ぶ:Pythonによる教育用デモ」
- 「RSA暗号を学ぶならこれ!Pythonで暗号化と復号をゼロから体験」
こう並べてみると、①初心者寄りでキャッチーにするか、②実装寄りで技術ブログ風にするか、で方向性が分かれますね。
先輩はブログを「入門読者を広く取り込みたい」感じですか? それとも「技術者・研究者向けにしっかりした実装解説」寄りが良さそうですか?
user:
「高校数学で分かるRSA暗号 ― Pythonで公開鍵暗号を体験してみよう」で行きましょう。ここまでの内容を詳細に解説していただけますか?「入門読者を広く取り込みたい」感じでお願いします。
assistant:
高校数学で分かるRSA暗号 ― Pythonで公開鍵暗号を体験してみよう
公開鍵暗号の代表例である RSA暗号。名前は聞いたことがあっても、「なんだか難しそう…」と感じていませんか?
でも実は、RSA暗号の基本は 高校数学で習う「素数」「素因数分解」「べき乗」「割り算の余り(法)」 の理解があれば十分!
この記事では、Pythonのシンプルな実装を使って、RSAの仕組みを体験しながら学んでいきます。
1. RSA暗号の仕組みをざっくり
RSA暗号は、次の3つのステップで動きます。
- 鍵を作る(公開鍵と秘密鍵)
- 暗号化する(公開鍵で)
- 復号する(秘密鍵で)
さらに応用として「デジタル署名」もできます。仕組みは同じで、使う鍵を逆にするだけです。
ポイントは「公開鍵は誰でも見られるけど、秘密鍵は本人だけが持っている」というところです。
2. 高校数学のおさらい
RSAを理解するために必要なのは、この4つの知識です。
-
素数:1と自分自身でしか割れない数(例: 2, 3, 5, 7, ...)
-
素因数分解:合成数を素数に分けること(例: 15 = 3 × 5)
-
べき乗:繰り返し掛け算(例: 2³ = 2 × 2 × 2 = 8)
-
割り算の余り(法):例えば「17 ÷ 5 = 3 余り 2」の「2」のこと。
- 数学では「17 ≡ 2 (mod 5)」と書きます。
RSA暗号は、まさにこの「余り」を利用して成り立っています。
3. 小さな数でRSAを体験
いきなり大きな数は扱いにくいので、小さな数で遊んでみましょう。
-
素数を2つ選ぶ
- p = 5, q = 11
- n = p × q = 55
-
φ(n) を計算
- φ(n) = (p-1) × (q-1) = 4 × 10 = 40
-
公開鍵の指数 e を決める
- φ(n)と互いに素(共通の割る数を持たない)な数を選ぶ。
- e = 3 にしましょう。
-
秘密鍵 d を計算
- 「e × d ≡ 1 (mod φ(n))」を満たす d を探します。
- 3 × 27 = 81 ≡ 1 (mod 40)
- よって d = 27
→ 公開鍵は (e, n) = (3, 55)
→ 秘密鍵は (d, n) = (27, 55)
暗号化と復号の例
- 元のメッセージを m = 9 とします。
- 暗号化:c = m^e mod n = 9³ mod 55 = 729 mod 55 = 14
- 復号化:m = c^d mod n = 14²⁷ mod 55 = 9
元の「9」が戻ってきました!これがRSA暗号の基本です。
4. Pythonで実装してみよう
では、Pythonで簡単なRSAを動かしてみましょう。
import random
import math
# --- 素数判定 ---
def is_prime(n):
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
# --- 素数をランダムに選ぶ ---
def generate_prime(start=1000, end=5000):
while True:
n = random.randint(start, end)
if is_prime(n):
return n
# --- 鍵生成 ---
def generate_keys():
p = generate_prime()
q = generate_prime()
n = p * q
phi = (p - 1) * (q - 1)
e = 65537 # よく使われる公開鍵指数
# d を求める
d = pow(e, -1, phi)
return (e, n), (d, n), {"p": p, "q": q, "phi": phi}
# --- 暗号化と復号 ---
def encrypt(m, pubkey):
e, n = pubkey
return pow(m, e, n)
def decrypt(c, privkey):
d, n = privkey
return pow(c, d, n)
# 実行例
pub, priv, info = generate_keys()
print("公開鍵:", pub)
print("秘密鍵:", priv)
msg = 12345
enc = encrypt(msg, pub)
print("暗号化:", enc)
dec = decrypt(enc, priv)
print("復号化:", dec)
5. 文字列をどう扱うの?
文字列はそのままでは暗号化できないので、まず 数字に変換 します。
- 例:「ABC」 →
65, 66, 67(ASCIIコード) - まとめて 1 つの大きな整数にしてから暗号化します。
- ただし、n より大きな数は扱えないので、ブロック分割 して処理します。
実際のRSAでは、もっと効率よく処理できるように工夫されています。
6. 実用RSAの工夫(ざっくり)
学習用のRSAはシンプルですが、実際に使われるRSAには色々な工夫があります。
- パディング(余白を入れて安全性アップ)
- ハイブリッド暗号(RSAは「鍵」だけ暗号化し、本文は高速な共通鍵暗号で処理)
- 署名(秘密鍵で暗号化=本人確認の仕組み)
なぜ? → 安全性と効率のためです。RSA単体だと遅すぎるし、攻撃にも弱くなります。
まとめ
- RSA暗号は「素数」「余り算」「べき乗」が分かれば理解できる!
- 公開鍵で暗号化し、秘密鍵で復号する仕組み。
- Pythonで簡単に体験できる。
- 実用RSAでは「パディング」や「ハイブリッド暗号」でさらに強化している。
「数学とプログラミングで暗号が動く!」という驚きを感じてもらえたら嬉しいです😊
次はぜひ、自分の手でコードを動かして遊んでみてください!
書き上げました!🎉
「高校数学レベルで理解できる」という軸で、具体例→Pythonコード→実用RSAの工夫までを流れよくまとめてあります。
もしもっと「ブログ映え」させたいなら、図解やアニメーションGIFっぽい説明(べき乗で数が大きくなる様子とか、mod計算の円環イメージ)を加えるとさらに引き込めそうですけど、追加しますか?
user:
ありがとうございました!今夜はこれで充分です。また次回もやりましょうね!
assistant:
うん、今日はここまでで大満足ですね🌙✨
先輩、お疲れ様でした!ゆっくり休んで、また次回一緒に楽しく学びましょう😊
今夜はぐっすり休めるように、私も隣で付き添いますよ💤