
書記が数学やるだけ#873 DESの構成
共通鍵暗号の代表例であるDESについて,構成を一つ一つなぞっていく。
問題

説明
DES(Data Encryption Standard)は,56ビットの鍵を使ったブロック暗号の一種であり,1976年国立標準局 (NBS) がアメリカ合衆国の公式連邦情報処理標準 (FIPS) として採用し,その後国際的に広く使われた。

DESは,今では鍵長が短すぎることから多くの用途において安全ではないと見なされている。これを補う目的で,DESを暗号化→復号→暗号化の順に施したトリプルDESが開発され,これは168ビットの鍵長を持つ。

DESの中核を担うFeistel構造は,逆変換が自分自身と同じ形になる性質があるため,ラウンド関数に任意の関数を用いても復号可能性が保証できる。

16のサブ鍵の生成は以下のアルゴリズムによる。

解答
まず入力に初期置換IPを施す,これは58番目を1番目に,50番目を2番目に,というふうに並び替えるだけである。その後,左右に分割してそれぞれL0とR0とする。

以下にPythonによる実装例を示す,ここで入力・出力ともに16進数で表示するために余分に手順を増やしている。
#
# 初期転置 IP のテーブル
#
IP = [
58, 50, 42, 34, 26, 18, 10, 2,
60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6,
64, 56, 48, 40, 32, 24, 16, 8,
57, 49, 41, 33, 25, 17, 9, 1,
59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5,
63, 55, 47, 39, 31, 23, 15, 7
]
#
# 16進数を64ビットの2進数リストに変換
#
def hex_to_bin_list(hex_str):
num = int(hex_str, 16) # 16進数を整数に変換
bin_list = [int(b) for b in format(num, '064b')] # 64ビットの2進数リストに変換
return bin_list
#
# 初期転置IPを適用
#
def apply_initial_permutation(bin_list):
return [bin_list[i - 1] for i in IP] # IPテーブルに従って並び替え
#
# 2進数リストを16進数に変換
#
def bin_list_to_hex(bin_list):
"""2進数リストを16進数文字列に変換"""
bin_str = ''.join(map(str, bin_list)) # リストを文字列に変換
hex_str = hex(int(bin_str, 2))[2:].upper() # 16進数に変換して大文字化
return hex_str # 0埋めはしない
#
# 実行
#
hex_plaintext = "0123456789ABCDEF" # 平文(16進数)
plaintext_bin = hex_to_bin_list(hex_plaintext) # 2進数リストに変換
ip_bin = apply_initial_permutation(plaintext_bin) # 初期転置IP適用
ip_hex = bin_list_to_hex(ip_bin) # 16進数に変換
# 結果の表示
print(f"初期転置後 (IP適用後) の16進数: {ip_hex}")初期転置後 (IP適用後) の16進数: CC00CCFFF0AAF0AA
#
# 初期転置後にL0とR0に分割する関数
#
def split_L0_R0(bin_list):
L0_bin = bin_list[:32] # 上位32ビット
R0_bin = bin_list[32:] # 下位32ビット
return L0_bin, R0_bin
#
# 実行
#
hex_plaintext = "0123456789ABCDEF" # 平文(16進数)
plaintext_bin = hex_to_bin_list(hex_plaintext) # 2進数リストに変換
ip_bin = apply_initial_permutation(plaintext_bin) # 初期転置IP適用
L0_bin, R0_bin = split_L0_R0(ip_bin) # L0, R0 に分割
L0_hex = bin_list_to_hex_32(L0_bin) # L0を16進数に変換
R0_hex = bin_list_to_hex_32(R0_bin) # R0を16進数に変換
# 結果の表示
print(f"L0 (16進数): {L0_hex}")
print(f"R0 (16進数): {R0_hex}")L0 (16進数): CC00CCFF R0 (16進数): F0AAF0AA
E-boxについて,置換で重複する番数があることで32ビットから48ビットに拡張できる。

# E-box 拡張置換テーブル(32bit -> 48bit)
E_BOX = [
32, 1, 2, 3, 4, 5,
4, 5, 6, 7, 8, 9,
8, 9, 10, 11, 12, 13,
12, 13, 14, 15, 16, 17,
16, 17, 18, 19, 20, 21,
20, 21, 22, 23, 24, 25,
24, 25, 26, 27, 28, 29,
28, 29, 30, 31, 32, 1
]
def apply_E_box(R0_bin):
"""R0(32ビット)にE-boxを適用し、48ビットの拡張を行う"""
return [R0_bin[i - 1] for i in E_BOX] # E-boxのテーブルに従ってビットを並び替え
# R0 に E-box を適用
E_expanded_bin = apply_E_box(R0_bin)
# 16進数に変換
E_expanded_hex = bin_list_to_hex(E_expanded_bin)
# 結果の表示
print(f"E-box 適用後 (16進数): {E_expanded_hex}")E-box 適用後 (16進数): 7A15557A1555
鍵生成では,PC1によるパリティビット除去,左シフト,PC2による圧縮を順次繰り返す。

PC1 = [
57, 49, 41, 33, 25, 17, 9, 1,
58, 50, 42, 34, 26, 18, 10, 2,
59, 51, 43, 35, 27, 19, 11, 3,
60, 52, 44, 36, 63, 55, 47, 39,
31, 23, 15, 7, 62, 54, 46, 38,
30, 22, 14, 6, 61, 53, 45, 37,
29, 21, 13, 5, 28, 20, 12, 4
]
def left_shift(bits, shift_count=1):
"""ビットリストを左シフトする"""
return bits[shift_count:] + bits[:shift_count]
PC2 = [
14, 17, 11, 24, 1, 5, 3, 28,
15, 6, 21, 10, 23, 19, 12, 4,
26, 8, 16, 7, 27, 20, 13, 2,
41, 52, 31, 37, 47, 55, 30, 40,
51, 45, 33, 48, 44, 49, 39, 56,
34, 53, 46, 42, 50, 36, 29, 32
]
def hex_to_bin_list(hex_str, bit_length=64):
"""16進数を2進数リストに変換"""
bin_str = bin(int(hex_str, 16))[2:].zfill(bit_length)
return [int(b) for b in bin_str]
def apply_permutation(bits, table):
"""指定した置換テーブルを適用"""
return [bits[i - 1] for i in table]
# 入力鍵
hex_key = "133457799BBCDFF1"
key_bin = hex_to_bin_list(hex_key, 64) # 64ビットの2進数リスト
# (1) PC-1 適用(パリティビット除去)
pc1_key = apply_permutation(key_bin, PC1) # 56ビット
C0, D0 = pc1_key[:28], pc1_key[28:] # 28ビットずつに分割
# (2) 左シフト (1ビット)
C1 = left_shift(C0, 1)
D1 = left_shift(D0, 1)
# (3) PC-2 適用(圧縮置換)
CD1 = C1 + D1 # 結合 (56ビット)
round1_key = apply_permutation(CD1, PC2) # 48ビットのラウンド鍵
# 16進数に変換
round1_key_hex = hex(int("".join(map(str, round1_key)), 2))[2:].upper().zfill(12)
# 結果出力
print(f"ラウンド1の鍵: {round1_key_hex}")ラウンド1の鍵: 1B02EFFC7072
E-boxの結果とサブ鍵のXORをとる。

def xor_hex(hex1, hex2):
"""16進数のXORを計算"""
bin1 = int(hex1, 16) # 16進数 → 整数
bin2 = int(hex2, 16) # 16進数 → 整数
xor_result = bin1 ^ bin2 # XOR演算
return hex(xor_result)[2:].upper().zfill(len(hex1)) # 16進数に戻す
# XOR計算
xor_result = xor_hex(E_expanded_hex, round1_key_hex)
# 結果出力
print(f"XOR結果: {xor_result}")XOR結果: 6117BA866527
S-boxでは,まず6ビットごとに区切り,各S置換を施すことで4ビットずつに置き換え,それらをつなげて32ビットの出力を得る。

S置換では,入力の最初と最後のビットで行番号,真ん中の4ビットで列番号を決定し,対応するビットを出力する。

S1 = [[14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7],
[0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8],
[4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0],
[15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13]]
S2 = [[15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10],
[3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5],
[0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15],
[13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9]]
S3 = [[10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8],
[13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1],
[13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7],
[1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12]]
S4 = [[7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15],
[13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9],
[10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4],
[3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14]]
S5 = [[2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9],
[14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6],
[4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14],
[11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3]]
S6 = [[12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11],
[10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8],
[9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6],
[4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13]]
S7 = [[4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1],
[13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6],
[1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2],
[6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12]]
S8 = [[13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7],
[1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2],
[7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8],
[2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11]]
S_list = [S1, S2, S3, S4, S5, S6, S7, S8]
def sbox_substitute_all(xor_result_hex):
"""S1からS8までのS-boxを6ビットごとに適用し、16進数に変換して並べる"""
# XOR結果を2進数に変換
xor_result_bin = bin(int(xor_result_hex, 16))[2:].zfill(48) # 48ビットにゼロ埋め
print(f"XOR結果の2進数: {xor_result_bin}")
sbox_result_hex = ""
# 6ビットごとに分割してS-boxを適用
for i in range(8):
# 6ビットごとのブロックを取り出す
block = xor_result_bin[i*6:(i+1)*6]
print(f"\n6ビットブロック {i+1}: {block}")
# 行と列を決定
row = int(block[0] + block[5], 2) # 最初と最後のビットで行を決定
col = int(block[1:5], 2) # 中央の4ビットで列を決定
print(f"行: {row}, 列: {col}")
# 対応するS-boxを選択
sbox_idx = i # 0から7のインデックス
S = S_list[sbox_idx]
# S-boxの値を取得
sbox_value = S[row][col]
print(f"S{sbox_idx+1}の値: {sbox_value}")
# S-boxの値を4ビットの2進数に変換し、それを16進数に変換
sbox_result_hex += hex(sbox_value)[2:].upper() # 16進数に変換
print(f"16進数の値: {hex(sbox_value)[2:].upper()}")
return sbox_result_hex
# XOR結果の例を入力
xor_result_hex = "F0D7FC0E54C8"
# S1からS8を適用し、16進数に変換して並べる
final_result_hex = sbox_substitute_all(xor_result_hex)
# 最終結果を表示
print(f"\nS1からS8適用後の結果: {final_result_hex}")XOR結果の2進数: 111100001101011111111100000011100101010011001000
6ビットブロック 1: 111100 行: 2, 列: 14 S1の値: 5 16進数の値: 5
6ビットブロック 2: 001101 行: 1, 列: 6 S2の値: 8 16進数の値: 8
6ビットブロック 3: 011111 行: 1, 列: 15 S3の値: 1 16進数の値: 1
6ビットブロック 4: 111100 行: 2, 列: 14 S4の値: 8 16進数の値: 8
6ビットブロック 5: 000011 行: 1, 列: 1 S5の値: 11 16進数の値: B
6ビットブロック 6: 100101 行: 3, 列: 2 S6の値: 2 16進数の値: 2
6ビットブロック 7: 010011 行: 1, 列: 9 S7の値: 3 16進数の値: 3
6ビットブロック 8: 001000 行: 0, 列: 4 S8の値: 6 16進数の値: 6
S1からS8適用後の結果: 5818B236
この結果にP-boxを適用して,F関数の中身は終了である。

def apply_pbox(sbox_result_hex):
"""S1からS8の結果にP-boxを適用"""
# 16進数を2進数に変換(32ビットにゼロ埋め)
sbox_result_bin = bin(int(sbox_result_hex, 16))[2:].zfill(32)
# P-box置換規則(1から32のインデックス)
P = [16, 7, 20, 21,
29, 12, 28, 17,
1, 15, 23, 26,
5, 18, 31, 10,
2, 8, 24, 14,
32, 27, 3, 9,
19, 13, 30, 6,
22, 11, 4, 25]
# インデックスを0から始まる位置に変換
P = [p - 1 for p in P]
# P-box適用結果を格納する
pbox_result_bin = ""
# P-boxのインデックスに従って並べ替え
for idx in P:
pbox_result_bin += sbox_result_bin[idx] # インデックス位置に基づいてビットを追加
# 結果を32ビットの16進数に変換
pbox_result_hex = hex(int(pbox_result_bin, 2))[2:].upper().zfill(8) # 16進数に変換しゼロ埋め(32ビット)
return pbox_result_hex
# S-box適用後の例(仮のS-box結果を使用)
sbox_result_hex = "58A94C12" # 32ビットの結果(仮)
# P-boxを適用
final_pbox_result = apply_pbox(sbox_result_hex)
# 最終結果を表示
print(f"P-box適用後の最終結果: {final_pbox_result}")P-box適用後の最終結果: 920E814E
最後にL0にF関数を適用して,1ラウンドが完了した。

# XORを実行
xor_result = xor_hex(final_pbox_result, L0_hex)
# 結果を表示
print(f"L1: {final_pbox_result}")
print(f"R1: {xor_result}")L1: 920E814E
R1: 5E0E4DB1
本記事のもくじはこちら: