Python 다중 치환 암호(Polyalphabetic Cipher) 코드 분석
분석 대상 코드
upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lower = "abcdefghijklmnopqrstuvwxyz"
# ===== 암호화 =====
def encrypt_with_process(plain_text, shifts):
result = ""
print("\n🔐 암호화 과정:")
print("-" * 40)
for i, (ch, shift) in enumerate(zip(plain_text, shifts), 1):
print(f"\n[{i}] 문자: {ch}, 회전값: {shift}")
if ch in upper:
idx = upper.index(ch)
new_idx = (idx + shift) % 26
encoded_char = upper[new_idx]
print(f" → {idx} + {shift} → {new_idx} → '{encoded_char}'")
elif ch in lower:
idx = lower.index(ch)
new_idx = (idx + shift) % 26
encoded_char = lower[new_idx]
print(f" → {idx} + {shift} → {new_idx} → '{encoded_char}'")
else:
encoded_char = ch
print(" → 그대로 유지")
result += encoded_char
print(f" 👉 현재까지 암호문: {result}")
print("\n✅ 최종 암호문:", result)
return result
# ===== 복호화 =====
def decrypt_with_process(cipher_text, shifts):
result = ""
print("\n🔓 복호화 과정:")
print("-" * 40)
for i, (ch, shift) in enumerate(zip(cipher_text, shifts), 1):
print(f"\n[{i}] 문자: {ch}, 회전값: {shift}")
if ch in upper:
idx = upper.index(ch)
new_idx = (idx - shift) % 26
decoded_char = upper[new_idx]
print(f" → {idx} - {shift} → {new_idx} → '{decoded_char}'")
elif ch in lower:
idx = lower.index(ch)
new_idx = (idx - shift) % 26
decoded_char = lower[new_idx]
print(f" → {idx} - {shift} → {new_idx} → '{decoded_char}'")
else:
decoded_char = ch
print(" → 그대로 유지")
result += decoded_char
print(f" 👉 현재까지 평문: {result}")
print("\n✅ 복호화 결과:", result)
return result
# ===== 사용자 입력 =====
plain = input("평문 입력 (예: LASAGNA): ")
# ⭐ 핵심: 공백 없이 입력
shift_input = input("회전값 입력 (예: 3517248): ")
# 문자열을 한 글자씩 숫자로 변환
shifts = [int(s) for s in shift_input]
# 길이 검사
if len(plain) != len(shifts):
print("❗ 오류: 길이가 맞지 않습니다.")
else:
cipher = encrypt_with_process(plain, shifts)
decrypt_with_process(cipher, shifts)
코드의 목적: 사용자에게 평문과 각 글자에 해당하는 숫자 '회전값' 리스트를 입력받아, 다중 치환 암호 방식으로 암호화 및 복호화를 수행하고 그 과정을 단계별로 보여줍니다.
[1] 상세 개념 5가지
1. 다중 치환 암호 (Polyalphabetic Substitution Cipher)
하나의 문자가 항상 동일한 문자로 암호화되는 단일 치환 암호(예: 시저 암호)와 달리, 다중 치환 암호는 여러 개의 치환 규칙(여기서는 '회전값' 리스트)을 사용하여 암호화합니다. 평문의 각 문자 위치에 따라 다른 회전값이 적용되므로, 동일한 글자라도 위치에 따라 다른 문자로 암호화될 수 있습니다. (예: 평문이 'Apple'이고 회전값이 '12345'라면 첫 'p'와 두 번째 'p'는 다른 문자로 암호화됨) 이로 인해 암호의 강도가 훨씬 높아집니다.
2. 모듈러 연산 (%)과 알파벳 순환
이 코드의 핵심 연산입니다. 알파벳은 총 26개(A=0, ..., Z=25)이므로, 회전 결과가 25를 넘어가면 다시 처음(A)으로 돌아와야 합니다. 모듈러(나머지) 연산자 %가 이 역할을 합니다.
- 암호화:
(기존 인덱스 + 회전값) % 26. 예를 들어 'Y'(인덱스 24)를 3만큼 회전하면 (24 + 3) % 26 = 27 % 26 = 1이 되어, 인덱스 1에 해당하는 'B'가 됩니다.
- 복호화:
(암호 인덱스 - 회전값) % 26. 예를 들어 'B'(인덱스 1)를 3만큼 역회전하면 (1 - 3) % 26 = -2 % 26 = 24가 되어, 인덱스 24에 해당하는 'Y'가 됩니다.
3. zip() 함수를 이용한 병렬 처리
zip(plain_text, shifts)는 두 개의 시퀀스(평문과 회전값 리스트)를 묶어, 각 시퀀스의 같은 위치에 있는 요소들을 하나의 튜플(tuple)로 만들어줍니다.
예를 들어 zip("ABC", [1, 2, 3])는 ('A', 1), ('B', 2), ('C', 3)을 차례로 생성합니다. 이를 통해 for 루프에서 평문의 각 문자와 그에 해당하는 회전값을 동시에 가져와 처리할 수 있습니다.
4. enumerate() 함수를 이용한 순서 매기기
enumerate(iterable, start=1) 함수는 반복 가능한 객체(iterable)를 순회하면서 각 요소에 순번(index)을 부여해줍니다. start=1 옵션은 순번을 0이 아닌 1부터 시작하게 합니다.
이 코드에서는 암/복호화 과정의 각 단계를 [1], [2], ... 와 같이 보기 좋게 출력하기 위해 사용되었습니다.
5. 문자열 index() 메소드와 인덱싱
upper.index(ch)는 upper 문자열("ABC...")에서 문자 ch가 몇 번째 위치(인덱스)에 있는지를 찾아 숫자로 반환합니다. (예: upper.index('C')는 2를 반환)
반대로, upper[new_idx]는 upper 문자열의 new_idx 위치에 있는 문자를 가져옵니다. (예: upper[2]는 'C'를 반환)
이 두 가지를 조합하여 문자를 숫자로, 숫자를 다시 문자로 자유롭게 변환합니다.
[2] OX 퀴즈 5개
문제 1
이 코드는 평문 'Hello'와 회전값 '11111'을 입력하면, 두 개의 'l'이 서로 다른 문자로 암호화된다.
정답: X
상세 풀이: 이 암호 방식은 '다중 치환'이지만, 회전값이 모두 동일하면 '단일 치환(시저 암호)'처럼 동작합니다. 두 개의 'l' 모두 동일한 회전값 1을 적용받으므로, 같은 문자인 'm'으로 암호화됩니다.
문제 2
암호화 시, 문자 'Z'에 회전값 1을 적용하면 결과는 'A'가 된다.
정답: O
상세 풀이: 'Z'의 인덱스는 25입니다. (25 + 1) % 26은 26 % 26이므로 결과는 0입니다. 인덱스 0에 해당하는 문자는 'A'이므로 올바른 설명입니다.
문제 3
평문에 포함된 공백이나 숫자는 알파벳 'a' 또는 'A'로 암호화된다.
정답: X
상세 풀이: 코드의 if-elif-else 구문에서, 문자가 upper나 lower에 속하지 않는 경우(공백, 숫자, 특수문자 등) else 블록이 실행되어 아무런 변환 없이 원래 문자를 그대로 유지합니다.
문제 4
복호화는 암호화와 동일하게 회전값을 더하는 방식으로 이루어진다.
정답: X
상세 풀이: 복호화는 암호화의 역과정입니다. 따라서 회전값을 더하는 것이 아니라 빼는 연산((idx - shift) % 26)을 통해 원래의 문자를 찾아냅니다.
문제 5
zip 함수 때문에 평문과 회전값의 길이가 다르면 오류가 발생하여 프로그램이 즉시 종료된다.
정답: X
상세 풀이: zip 함수 자체는 길이가 다를 경우 짧은 쪽 길이에 맞춰 동작하고 오류를 내지 않습니다. 하지만 이 코드에서는 if len(plain) != len(shifts): 라는 조건문으로 길이를 미리 검사하여, 길이가 다를 경우 "오류: 길이가 맞지 않습니다." 라는 메시지를 출력하고 암/복호화 과정을 시작하지 않도록 설계되었습니다.
[3] 5지선다형 문제 5개
문제 1. 평문 "PYTHON"과 회전값 "701984"를 입력했을 때, 세 번째 문자인 'T'는 어떤 문자로 암호화되는가?
정답: 2번
상세 풀이: 세 번째 문자 'T'에 해당하는 회전값은 1입니다. 'T'의 인덱스는 19입니다. 암호화 공식에 따라 (19 + 1) % 26 = 20이 됩니다. upper 문자열의 20번째 인덱스에 있는 문자는 'U'입니다.
문제 2. 이 코드의 암호화 방식에 대한 가장 정확한 설명은?
- 1) 모든 문자를 3칸씩 미는 시저 암호
- 2) 문자의 순서를 뒤섞는 치환 암호
- 3) 각 문자에 다른 회전값을 적용하는 다중 치환 암호
- 4) 문자를 아스키 코드로 변환하는 암호
- 5) 평문과 키를 XOR 연산하는 스트림 암호
정답: 3번
상세 풀이: shifts 리스트를 통해 평문의 각 문자 위치마다 다른 이동 거리를 지정하여 암호화를 수행하므로, 이는 다중 치환 암호(Polyalphabetic Cipher)의 일종입니다.
문제 3. 복호화 과정에서 암호문 'C'를 회전값 5를 이용해 복호화하면 어떤 문자가 되는가?
정답: 3번
상세 풀이: 'C'의 인덱스는 2입니다. 복호화 공식 (idx - shift) % 26에 따라 (2 - 5) % 26 = -3 % 26이 됩니다. Python에서 음수 모듈러 연산의 결과는 23입니다. upper 문자열의 23번째 인덱스에 있는 문자는 'X'입니다.
문제 4. 코드에서 zip(plain_text, shifts)가 하는 역할로 가장 적절한 것은?
- 1) 평문과 회전값을 한 줄의 문자열로 합친다.
- 2) 평문의 각 문자를 회전값만큼 반복하여 출력한다.
- 3) 평문의 길이와 회전값의 길이가 같은지 검사한다.
- 4) 평문의 각 문자와 그에 대응하는 회전값을 짝지어 준다.
- 5) 평문과 회전값 중 더 긴 것을 기준으로 반복한다.
정답: 4번
상세 풀이: zip 함수는 여러 개의 반복 가능한 객체들을 인자로 받아, 각 객체의 동일한 인덱스에 있는 요소들을 하나의 튜플로 묶어주는 역할을 합니다. 이 코드에서는 문자와 회전값을 짝지어 for문에서 함께 사용하기 위해 쓰였습니다.
문제 5. 다음 중 이 암호화 코드를 깨뜨리기 가장 어렵게 만드는 요소는 무엇인가?
- 1) 대문자와 소문자를 구분해서 처리하는 점
- 2) 공백이나 특수문자를 그대로 유지하는 점
- 3) 암호화 과정을 단계별로 출력해주는 점
- 4) 회전값(키)의 길이가 길고, 패턴이 불규칙한 점
- 5) 26으로 나누는 모듈러 연산을 사용하는 점
정답: 4번
상세 풀이: 다중 치환 암호의 강도는 키(회전값 리스트)의 길이와 복잡성에 따라 결정됩니다. 키가 길고, 사용된 숫자에 일정한 패턴이 없을수록 통계적 분석(빈도수 분석 등)이 어려워져 해독하기가 훨씬 힘들어집니다. 다른 보기들은 부수적인 특징일 뿐, 암호의 핵심 강도에 직접적인 영향을 주지는 않습니다.