Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[Cryptography] stage3_블록암호: DES 본문

Hack/DreamHack(로드맵)

[Cryptography] stage3_블록암호: DES

CIDY 2022. 7. 21. 00:14

*DES

Data Encryption Standard는 미국 국가 안보국에서 IBM의 루시퍼 알고리즘을 개량해 만든 대칭키 암호이다. 128비트의 루시퍼와 달리 56비트이다. 현대에는 DES에 대한 공격 기법이 많이 연구되어 더 이상 블록 암호의 표준으로 쓰이지 않는다.

 

8바이트(== 64비트)를 한 블록으로 하는 블록 암호로, 초기 순열(IP), 페이스텔 구조의 16라운드, 최종 순열(FP), 그리고 각 라운드에서 사용되는 48비트의 키를 생성하는 키 생성 함수로 구성된다.

 

 

*곱 암호

DES는 혼돈 성질 만족을 위해 치환을, 확산 성질 만족을 위해 순열을 사용한다. -> 치환과 순열을 여러번 교차해 반복 적용하면 혼돈과 확산의 성질을 모두 만족하게 될 수 있다. 이러한 특성을 이용해 한 라운드를 치환과 순열과 같은 단순한 연산으로 구성한 뒤, 각 라운드를 여러 번 반복해 암호학적 안정성을 확보하는 암호를 곱 암호(Product Cipher)라고 한다.

 

많은 현대 블록 암호들이 곱 암호 구조를 차용하고 있으며, DES도 그 중 하나이다.

 

 

*페이스텔 구조

DES에서 라운드 함수를 적용하는 전체 과정은 페이스텔 구조를 이루고 있다. (16라운드)

 

-> 입력으로 들어온 블록을 동일한 길이의 왼쪽 블록 L과 오른쪽 블록 R로 나눈다. 

-> 각 라운드의 오른쪽 블록은 다음 라운드의 왼쪽 블록으로 입력된다. 

-> 왼쪽 블록은 오른쪽 블록에 라운드 함수 F를 적용한 결과와 xor되어 다음 라운드의 오른쪽 블록으로 입력된다.

 

페이스텔 구조

 

위 과정을 수식으로 나타내면 다음과 같다. ↓

 

(1) L0 ​= P[:len(P)/2], R0 ​= P[len(P)/2:]

(2) Ln+1​ = Rn​

(3) Rn+1 ​= Ln ​⊕ F(Rn​,Kn​)

 

 

블록암호는 평문을 복호화할 수 있어야 하므로 일반적으로는 암호화를 구성하는 각 함수들에 역함수가 존재한다. 하지만 페이스텔 구조를 이용하게 되면, F가 복호화 과정에서 XOR연산으로 상쇄되므로 역함수가 존재하지 않아도 된다. 그리고 암호화와 복호화의 구조가 동일하게 때문에 암호화에 사용한 라운드 키를 역순으로 입력하면 그게 복호화이다.

 

하지만 위에서 말한 과정 중 2번 과정(n라운드의 오른쪽 블록이 그대로 n+1라운드의 왼쪽 블록으로 사용됨.) 때문에 페이스텔 암호는 비페이스텔 암호와 같은 안정성을 갖기 위해서는 두 배 정도 라운드를 사용해야 한다는 단점이 있다.

 

 

*DES의 시작과 끝

DES는 시작할 때 IP(초기 순열)를, 마지막에는 최종 순열(FP)을 수행한다. 초기 순열과 최종 순열은 정해진 테이블을 이용해 64비트 입력을 비트 단위로 전치한다. 테이블의 n번째 값이 m일 때, 출력의 n번째 비트는 입력의 m번째 비트가 된다.

 

초기 순열과 최종 순열은 각각 초기 순열 테이블(IPT)과 최종 순열 테이블(FPT)을 이용한다. 초기 순열과 최종 순열은 서로 역관계에 있다. -> 임의의 64비트 데이터에 초기 순열과 최종 순열을 차례로 적용하면 입력 값이 그대로 출력된다.

 

#!/usr/bin/python3
# Name: des.py
# Initial/Final Permutation Table
IPT = [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]
FPT = [40, 8, 48, 16, 56, 24, 64, 32,
       39, 7, 47, 15, 55, 23, 63, 31,
       38, 6, 46, 14, 54, 22, 62, 30,
       37, 5, 45, 13, 53, 21, 61, 29,
       36, 4, 44, 12, 52, 20, 60, 28,
       35, 3, 43, 11, 51, 19, 59, 27,
       34, 2, 42, 10, 50, 18, 58, 26,
       33, 1, 41, 9, 49, 17, 57, 25]
def plain2bitstring(plain: str):
    return "".join(format(ord(c), "0>8b") for c in plain)
def plain2bitarray(plain: str):
    bitstring = plain2bitstring(plain)
    encoded = bytearray([int(b) for b in bitstring])
    return encoded
def bitarray2plain(bitarray: bytearray):
    bitstring = bitarray2bitstring(bitarray)
    encoded = "".join([chr(int(bitstring[i*8:i*8+8], 2))
                       for i in range(len(bitstring)//8)])
    return encoded
def bitarray2bitstring(bitarray: bytearray):
    return "".join([str(b) for b in bitarray])
def permutation(block: bytearray, table: list[int], outlen: int):
    permutated = bytearray(outlen)
    for n in range(len(table)):
        m = table[n]-1
        permutated[n] = block[m]
    return permutated
plain = "DEScrypt"
key = "DREAMCRY"
bitarray = plain2bitarray(plain)
print(f"bitstring of '{plain}': {bitarray2bitstring(bitarray)}")
# Initial permutation
initial_permutated = permutation(bitarray, IPT, 64)
print(
    f"bitstring of initial_permutated: {bitarray2bitstring(initial_permutated)}")
# Final permutation
final_permutated = permutation(initial_permutated, FPT, 64)
print(f"bitstring of final_permutated: {bitarray2bitstring(final_permutated)}")
# plain == FP(IP(plain)) => FP = IP^{-1}
print(f"plaintext of final_permutated: {bitarray2plain(final_permutated)}")

 

위 permutation함수는 해당 과정을 파이썬 코드로 구현한 것이다. IP와 FP는 DES의 안정성을 증가시키지는 않지만, DES를 하드웨어에 구현하기 쉽도록 해 준다.

 

 

*라운드 함수

라운드 함수 F에는 오른쪽 블록만 입력됨 -> 입력 길이는 32비트.

라운드 함수는 확장 순열(Expansion Box) -> 라운드 키 결합(XOR) -> 치환 테이블(S-Box) -> 고정 순열(Straight P-Box)로 이루어져 있다.

 

확장 순열: 입력을 비트 단위로 전치하고, 전체 길이를 48비트로 확장한다. 32비트의 입력값을 4비트씩 8부분으로 나눈 뒤, 테이블을 참조해 각각을 6비트로 확장한다. (IP, FP와 테이블만 다르고 같은 방식으로 이루어진다.)

 

라운드 키 결합: 확장 순열로 나온 출력을 라운드 키와 XOR하는 것이다.

 

#!/usr/bin/env python3
# Name: DES
# Expansion P-Box Table
EPT = [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]
#Initial permutation
...
# Feistel
left_half = initial_permutated[:32]
right_half = initial_permutated[32:]
# Iterates 16 rounds
for round in range(16):
    # Expansion
    expansioned = permutation(right_half, EPT, 48)
    # XOR with round key
    for bit_idx in range(48):
        expansioned[bit_idx] ^= round_keys[round][bit_idx]
...
# Final permutation
...

 

치환 테이블(S-Box): 라운드 키 결합의 결과로 나온 48비트 값을 32비트로 축소한다. S-Box는 4개 행/16개 열로 이루어진 표를 사용하는데, 표의 각 값은 4비트로 표현되는 수이다. 

 

입력을 6비트씩 8부분으로 나눈 뒤, 6비트 중 첫 번째와 마지막 비트로 행을, 중간의 4비트로 열을 결정한다. 그리고 S-Box의 표에서 행과 열을 참조하여 그에 해당하는 4비트 값으로 치환한다. DES에서는 6비트로 자른 각 부분마다 다른 S-Box를 사용한다. (8개의 S-Box가 필요한 것)

 

 

고정 순열(Straight P-Box): 길이 축소 이후, 비트 단위로 전치한다. 

 

#!/usr/bin/env python3
# Name: des.py
# S-Boxes
S = [
    # 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]
    ],
     ...
    # 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],
    ]
]
# Straight P-Box Table
SPT = [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]
def substitution(block, table):
    row = (block[0] << 1) + block[5]
    column = (block[1] << 3) + (block[2] << 2) + (block[3] << 1) + block[4]
    val = table[row][column]
    binary = bin(val)[2:].zfill(4)
    return bytearray([int(b) for b in binary])
#Initial Permutation
...
# Feistel
...
# Iterate 16 rounds
for i in range(16):
   # Expansion
   expansioned = permutation(right_half, EPT, 48)
   # XOR with round key
   for j in range(48):
        expansioned[j] ^= round_keys[i][j]
   # Substitution
   substituted = bytearray(32)
   for block_idx in range(8):
       substituted[4*block_idx:4*block_idx+4] = substitution(expansioned[6*block_idx:6*block_idx+6], S[block_idx])
    # Straight
    straighted = permutation(substituted, SPT, 32)
...
# Final Permutation
...

 

 

 

*키 생성 함수

키 생성 함수는 각 라운드에 필요한 48비트짜리 라운드 키를 생성하는 함수이다. 

패리티 비트 제거(Parity Bir Drop) -> 쉬프트(Shift) -> 압축 순열(Compression P-Box) 과정으로 이루어진다.

 

패리티 비트 제거: 입력에서 패리티 비트를 제거하고 남은 56비트에 순열을 적용하는 과정이다. DES의 비밀키에서 각 바이트의 가장 오른쪽 비트는 해당 바이트의 나머지 7비트에 대한 홀수 패리티 비트(Odd Parity Bit)이다. (홀수 패리티 비트는 한 바이트를 이진수로 표현했을 때, 1의 개수가 홀수가 되도록 덧붙인 비트이다.)

 

패리티 비트는 통신 중에 비트 반전이 일어나지 않았음을 보증하는 역할을 한다. 홀수 패리티 비트를 이용해 통신할 때, 수신한 바이트 중 1의 개수가 짝수인 바이트가 있다면 해당 비트에 반전이 일어났음을 수신자가 인지할 수 있다.

 

 

쉬프트: 입력을 왼쪽 28비트와 오른쪽 28비트로 나누어 각각을 1비트나 2비트만큼 왼쪽으로 순환 쉬프트(Cyclic Shift)하는 과정이다. 1, 2, 9, 16라운드에서는 1비트를, 나머지 라운드에서는 2비트만큼 쉬프트한다.

 

 

압축 순열: 56비트 입력을 48비트로 압축하는 과정이다. 위에서 설명한 순열들과 같은 방법으로 진행한다.

 

 

 

#!/usr/bin/env python3
# Name: des.py
# Parity Bit Drop Table
PBDT = [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]
# Compression P-Box Table
CPBT = [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 key_scheduling(key: bytearray):
    shift_1 = [0, 1, 8, 15]
    round_keys = []
    # Drop parity bit
    parity_erased = permutation(key, PBDT, 56)
    left = parity_erased[:28]
    right = parity_erased[28:]
    # Shift
    for i in range(16):
        if i in shift_1:
            left = left[1:] + left[0:1]
            right = right[1:] + right[0:1]
        else:
            left = left[2:] + left[:2]
            right = right[2:] + right[:2]
        # Compression
        round_keys.append(permutation(left+right, CPBT, 48))
    return round_keys
...
key = "DreamCry"
key_bitarray = plain_to_bits(key)
# Key scheduling
round_keys = key_scheduling(key_bitarray)

 

 

 

*DES의 응용

DES는 더이상 안전한 암호 시스템이 아니게 되었다. 다중 DES는 서로 다른 키를 사용하는 DES모듈을 여러 개 이어붙여 DES의 약점을 보완한 암호 시스템이다. 이중 DES는 112비트, 삼중 DES는 168비트의 키를 사용한다.

 

p를 plaintext, c를 ciphertext라고 하면, 이중 DES를 식으로 표현하면 -> Ek2​​(Ek1​​(p)) = c 이고, 삼중 DES를 식으로 표현하면 -> Ek3​​(Dk2​​(Ek1​​(p))) = c가 된다. 중간에 E가 아니고 D가 들어가는 이유는 k2 = k3로 설정하면 Ek3와 Dk2가 상쇄되어 키가 k1인 단일 DES로도 사용할 수 있기 때문이다. 

 

이중, 삼중 DES는 키 길이가 늘어났으니 단일 DES보다 안전할 것 같지만. 중간 일치 공격(Meet-in-the-Middle-Attack)에 의해 안전성을 획기적으로 높이지는 못한다. (이중 DES는 단일 DES와 안전성이 비슷하고, 삼중 DES는 키 길이를 2배로 늘리는 효과 정도를 가질 뿐이다.)

 

 

중간 일치 공격: 평문 p과 해당 평문의 암호화 결과인 암호문 c를 알 수 있을 때 수행 가능한 공격이다. 

 

이중 DES에 대한 중간 일치 공격은 다음과 같은 방법으로 이루어진다. 

 

-> 56비트 키 공간 K에서 가능한 모든 키 k1으로 p를 암호화하여 집합 S1을 생성한다. S1​ = {Ek1​​(p) ∣ k1​∈K}

-> K에서 가능한 모든 키 k2로 c를 복호화한 집합 S2를 생성한다

-> S1과 S2의 모든 원소에 대해 Ek1​​(p) = Dk2​​(c)를 만족하는 k1, k2의 쌍으로 후보키의 집합 CK를 생성한다. 

    CK = {(k1​,k2​) ∣ Ek1​​(p) = Dk2​​(c), Ek1​​(p) ∈ S1​, Dk2​​(c) ∈ S2​}

-> 다른 평문 p`과 해당 평문을 암호화한 암호문 c`을 생선한다.

-> CK의 모든 원소에 대해 Ek1​​(p′) = Dk2​​(c′)를 만족하는 k1과 k2의 쌍으로 집합 CK를 갱신한다. 

    CK = {(k1​,k2​) ∣ Ek1​​(p′) = Dk2​​(c′), (k1​,k2​) ∈ CK}

-> CK의 원소가 하나가 될 때 까지 4번째와 5번째 과정을 반복 수행한다. 

 

위 과정 중 DES에 대해 중간 일치 공격을 수행할 때 연산량에 유의미한 영향을 미치는 구간은 첫 번째와 두 번째 과정에서 수행되는 2^56번의 DES 암/복호화이다. 세 번째의 후보키 생성과정은 퀵정렬과 이분탐색으로 빠르게 수행할 수 있으며, 이후 반복하는 과정은 반복 횟수 자체가 많지 않음이 증명되어 있다.

 

그렇기에 공격자가 어떠한 평문 p와 해당 평문의 암호문 c를 알 수 있다면 DES의 안전성은 DES의 두 배 정도에 그친다. 삽중 DES는 이 공격을 적용해도 이중 DES이상의 안전성을 가진다고 알려져 있다. 따라서 다중 DES이용 시 일반적으로 삼중 DES를 이용한다. 

 

#!/usr/bin/python3
# Name: des.py
from Crypto.Cipher import DES
# Initial/Final Permutation Table
IPT = [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]
FPT = [40, 8, 48, 16, 56, 24, 64, 32,
       39, 7, 47, 15, 55, 23, 63, 31,
       38, 6, 46, 14, 54, 22, 62, 30,
       37, 5, 45, 13, 53, 21, 61, 29,
       36, 4, 44, 12, 52, 20, 60, 28,
       35, 3, 43, 11, 51, 19, 59, 27,
       34, 2, 42, 10, 50, 18, 58, 26,
       33, 1, 41, 9, 49, 17, 57, 25]
# Expansion P-Box Table
EPT = [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]
# S-Boxs
S = [
    # 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],
    ]
]
# Straight P-Box Table
SPT = [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]
# Parity Bit Drop Table
PBDT = [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]
# Compression P-Box Table
CPBT = [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 plain2bitstring(plain: str):
    return "".join(format(ord(c), "0>8b") for c in plain)
def plain2bitarray(plain: str):
    bitstring = plain2bitstring(plain)
    encoded = bytearray([int(b) for b in bitstring])
    return encoded
def bitarray2plain(bitarray: bytearray):
    bitstring = bitarray2bitstring(bitarray)
    encoded = "".join([chr(int(bitstring[i*8:i*8+8], 2))
                       for i in range(len(bitstring)//8)])
    return encoded
def bitarray2bitstring(bitarray: bytearray):
    return "".join([str(b) for b in bitarray])
def bitarray2bytes(bitarray: bytearray):
    bitstring = bitarray2bitstring(bitarray)
    
    return bytes([int(bitstring[8*i:8*i+8], 2) for i in range(len(bitstring)//8)])
def permutation(block: bytearray, table: list[int], outlen: int):
    permutated = bytearray(outlen)
    for n in range(len(table)):
        m = table[n]-1
        permutated[n] = block[m]
    return permutated
def substitution(block: bytearray, table: list[int]):
    row = (block[0] << 1) + block[5]
    column = (block[1] << 3) + (block[2] << 2) + (block[3] << 1) + block[4]
    val = table[row][column]
    binary = bin(val)[2:].zfill(4)
    return bytearray([int(b) for b in binary])
def key_scheduling(key: bytearray):
    shift_1 = [0, 1, 8, 15]
    round_keys = []
    # Drop parity bit
    parity_erased = permutation(key, PBDT, 56)
    left = parity_erased[:28]
    right = parity_erased[28:]
    # Shift
    for i in range(16):
        if i in shift_1:
            left = left[1:] + left[0:1]
            right = right[1:] + right[0:1]
        else:
            left = left[2:] + left[:2]
            right = right[2:] + right[:2]
        # Compression
        round_keys.append(permutation(left+right, CPBT, 48))
    return round_keys
plain = "DEScrypt"
key = "DreamCry"
# Key scheduling
round_keys = key_scheduling(plain2bitarray(key))
bitarray = plain2bitarray(plain)
# Initial permutation
initial_permutated = permutation(bitarray, IPT, 64)
# Feistel
left_half = initial_permutated[:32]
right_half = initial_permutated[32:]
# Iterate 16 rounds
for round in range(16):
    # Expansion
    expansioned = permutation(right_half, EPT, 48)
    # XOR with round key
    for bit_idx in range(48):
        expansioned[bit_idx] ^= round_keys[round][bit_idx]
    # Substitution
    substituted = bytearray(32)
    for block_idx in range(8):
        substituted[4*block_idx:4*block_idx+4] = substitution(expansioned[6*block_idx:6*block_idx+6], S[block_idx])
    # Straight
    straighted = permutation(substituted, SPT, 32)
    tmp = bytearray(right_half)
    for i in range(32):
        right_half[i] = left_half[i] ^ straighted[i]
    left_half = tmp
# Final permutation
encrypted = permutation(right_half+left_half, FPT, 64)
encrypted = bitarray2bytes(encrypted)
assert(encrypted == DES.new(b"DreamCry", DES.MODE_ECB).encrypt("DEScrypt"))