---
# System prepended metadata

title: Report decrypt ciphertext
tags: [NT219 - Mật mã học]

---

# Report break ciphertext

## Monoalphabetic Cipher

### Simple Substitution Cipher

#### Encrypt

**Simple Substitution Cipher** là loại mã hóa được sử dụng để ánh xạ 1-1 các kí tự chữ cái dựa trên key được tạo ra và các kí tự từ plaintext(chữ cái thống nhất hoa/thường).

**Nguyên lý hoạt động:**
+ Tạo khóa bằng cách tạo ra một chuỗi 26 kí tự chữ cái rồi hoán vị nó thành 1 thứ tự ngẫu nhiên, sau đó ánh xạ với mỗi kí tự với chữ cái theo thứ tự ALPHABET
+ Thực hiện thay đổi plaintext dựa trên khóa được ánh xạ
```python3=
def generate_substitution_key():
    """Generate a random substitution key (dict)."""
    letters = list(ALPHABET)
    random.shuffle(letters)
    return dict(zip(ALPHABET, letters))

def parse_substitution_key(key_str: str):
    """
    Parse a 26-character key string into a substitution mapping.
    Example: "QWERTYUIOPASDFGHJKLZXCVBNM"
    """
    key_str = key_str.upper()
    if len(key_str) != 26 or len(set(key_str)) != 26:
        raise ValueError("Substitution key must contain 26 unique letters.")
    return dict(zip(ALPHABET, key_str))

def substitution_encrypt(text: str, key: dict) -> str:
    """Encrypt text using a substitution key (dict)."""
    return ''.join(key.get(ch, ch) for ch in text.upper())
```

#### Decrypt

Để giải mã loại mã hóa này mà không biết `key` ta sẽ dựa trên thống kê tần suất xuất hiện của các chữ cái.
**Cụ thể như sau:**

+ **Điểm yếu:** dù các chữ cái đã bị thay thế nhưng **tần số xuất hiện** của các chữ cái vẫn không đổi. Ví dụ như trong tiếng Anh thì chữ `E` có tần suất xuật hiện nhiều nhất(12.2%)
+ Chính vì lí do trên, ta có thể đếm tuần suất xuất hiện của các kí tự chữ cái trong ciphertext rồi ánh xạ 1-1 theo thống kê tần suất của các chữ cái tiếng Anh.
+ Tất nhiên các làm trên sẽ không giải mã được 100% tuy nhiên nó sẽ làm rõ hơn phần nào, sau đó ta sẽ dựa vào vốn tiếng Anh đề dò ra phần bị sai còn lại.

Sau đây là bảng tần suất được thống kê:
![image](https://hackmd.io/_uploads/HyYtQlwoee.png)

Code break:
```python3=
import string
from collections import Counter

ALPHABET = string.ascii_uppercase
M = len(ALPHABET)

FREQ_ORDER = "ETAOINSHRDLCUMWFGYPBVKJXQZ"

ciphertext = "hzsrnqc klyy wqc flo mflwf ol zqdn nsoznj wskn lj xzsrbjnf, wzsxz gqv zqhhnf ol ozn glco zlfnco hnlhrn; nsoznj jnrqosdnc lj fnqj kjsnfbc, wzsxz sc xnjoqsfrv gljn efeceqr. zn rsdnb qrlfn sf zsc zlecn sf cqdsrrn jlw, wzsoznj flfn hnfnojqonb. q csfyrn blgncosx cekksxnb ol cnjdn zsg. zn pjnqmkqconb qfb bsfnb qo ozn xrep, qo zlejc gqozngqosxqrrv ksanb, sf ozn cqgn jllg, qo ozn cqgn oqprn, fndnj oqmsfy zsc gnqrc wsoz loznj gngpnjc, gexz rncc pjsfysfy q yenco wsoz zsg; qfb wnfo zlgn qo naqxorv gsbfsyzo, lfrv ol jnosjn qo lfxn ol pnb. zn fndnj ecnb ozn xlcv xzqgpnjc wzsxz ozn jnkljg hjldsbnc klj soc kqdlejnb gngpnjc. zn hqccnb onf zlejc leo lk ozn ownfov-klej sf cqdsrrn jlw, nsoznj sf crnnhsfy lj gqmsfy zsc olsrno."

count_ciphers = Counter(''.join(filter(str.isalpha, ciphertext.upper())))

sorted_by_freq = [pair[0] for pair in count_ciphers.most_common()]

matching = {cipher_char: plain_char for cipher_char, plain_char in zip(sorted_by_freq, FREQ_ORDER)}

matching["Z"] = "H"
matching["F"] = "N"
matching["V"] = "Y"
matching["L"] = "O"
matching["E"] = "U"
matching["Q"] = "A"
matching["R"] = "L"
matching["D"] = "V"
matching["S"] = "I"
matching["B"] = "D"
matching["X"] = "C"
matching["G"] = "M"
matching["A"] = "X"

def decrypt(ciphertext, matching):
    decrypted = []
    for char in ciphertext:
        if char.upper() in matching:
            new_char = matching[char.upper()]
            if char.islower():
                new_char = new_char.lower()
            decrypted.append(new_char)
        else:
            decrypted.append(char)  
    return ''.join(decrypted)

plaintext = decrypt(ciphertext, matching)
print("Ciphertext:", ciphertext)
print("Decrypted Plaintext:", plaintext)
print("Matching:", matching)
```

### Caesar Cipher

#### Encrypt
**Caesar Cipher** là loại mã hóa thay đổi chữ cái hiện tại bằng chữ cái cách nó **key** vị trí trên bảng chữ cái(Nối vòng bảng chữ cái lại(sau Z là A)).
**Công thức:** C = (P + key) mod 26

Code encrypt:
```python3=
def caesar(text: str, shift: int = 3, decrypt: bool = False) -> str:
    """
    Caesar cipher encryption/decryption.
    Key = shift (integer).
    """
    if not isinstance(shift, int):
        raise ValueError("Shift must be an integer.")
    shift = -shift if decrypt else shift
    return ''.join(
        ALPHABET[(ALPHABET.index(ch) + shift) % M] if ch in ALPHABET else ch
        for ch in text.upper()
    )
```
#### Decrypt

Cách break loại mã hóa này mà không biết key khá đơn giản, ta chỉ việc thử tất cả các key(vì key chỉ có thể có giá trị từ 1 -> 25) rồi xem xét đoạn giải mã nào có nghĩa.

```python3=
def caesar_bruteforce(cipher):
    """Try all Caesar shifts and return possible plaintexts."""
    results = []
    for shift in range(1, 26):
        pt = ''.join(
            ALPHABET[(ALPHABET.index(ch) - shift) % M] if ch in ALPHABET else ch
            for ch in cipher.upper()
        )
        results.append((shift, pt))
    return results
```

### Atbash Cipher

#### Encrypt

**Atbash Cipher** là loại mã hóa thay đổi chữ cái hiện tại với chữ cái đối xứng với nó trên bảng chữ cái. Ví dụ: A <-> Z, B <-> Y,...

Code encrypt:
```python3=
def atbash(text):
    return ''.join(ALPHABET[M - 1 - ALPHABET.index(ch)] if ch in ALPHABET else ch for ch in text.upper())
```

#### Decrypt

Để decrypt ta đơn giản chỉ cần thực hiện đổi chỗ lại như khi encrypt là xong.

Code decrypt:
```python3=
def decrypt_atbash(text):
    return ''.join(ALPHABET[M - 1 - ALPHABET.index(ch)] if ch in ALPHABET else ch for ch in text.upper())
```

### Affine Cipher

#### Encrypt

**Affine Cipher** là loại mã hóa substitution cổ điển nhưng phức tạp hơn so với **caesar cipher**. Cụ thể như sau:
+ Gán các chữ cái từ A tới Z lần lượt là 0, 1,..., 25
+ Mã hóa dựa trên hàm tuyến tính: `E(x) = (a * x + b) mod 26` với x là chỉ số của chữ cái gốc, a và b là khóa.

Code encrypt:
```python3=
import math
import string

ALPHABET = string.ascii_uppercase
M = len(ALPHABET)

def affine(text: str, a: int, b: int) -> str:
    if math.gcd(a, M) != 1:
        raise ValueError(f"a={a} must be coprime with 26 for the Affine cipher.")
    
    return ''.join(
        ALPHABET[(a * ALPHABET.index(ch) + b) % M] if ch in ALPHABET else ch
        for ch in text.upper()
    )
```

#### Decrypt

Về việc break loại mã hóa này, ta biết được công thức mã hóa chỉ tính toán với mod là 26 => a và b cũng sẽ nằm trong khoảng [0, 25]. Vậy nên ta sẽ bruteforce tất cả các giá trị của a và b rồi tìm kết quả.

Dựa trên công thức mã hóa, ta có công thức giải mã như sau:
`D(y) = a⁻¹ * (y - b) mod 26`, trong đó y là chỉ số của chữ cái đã bị mã hóa.

Code break:
```python3=
def affine_bruteforce(cipher):
    """Try all valid (a, b) pairs for Affine cipher."""
    results = []
    for a in range(1, M):
        if gcd(a, M) != 1:
            continue
        a_inv = pow(a, -1, M)
        for b in range(M):
            pt = ''.join(
                ALPHABET[(a_inv * (ALPHABET.index(ch) - b)) % M] if ch in ALPHABET else ch
                for ch in cipher.upper()
            )
            results.append(((a, b), pt))
    return results
```

### Keyword Cipher

#### Encrypt

**Keyword Cipher** là một dạng mã thay thế đơn bảng **(monoalphabetic substitution cipher)** cổ điển, dựa trên từ khóa. Cụ thể như sau:

Ví dụ khóa là `"HIDDEN"`:
+ Loại bỏ các kí tự lặp trong khóa `HIDDEN` -> `HIDEN`
+ Sau đó viết khóa đó vào đầu bảng chữ cái, rồi tiếp tục điền các chữ cái còn thiếu vào bảng
+ Như vậy là ta đã có bảng ánh xạ để mã hóa.

`Plain: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z`
`Encpt: H I D E N A B C F G J K L M O P Q R S T U V W X Y Z`

Code encrypt:
```python3=
import string

ALPHABET = string.ascii_uppercase
M = len(ALPHABET)

def keyword_cipher(text: str, keyword: str) -> str:

    # Clean keyword: uppercase, remove non-letters, drop duplicates
    keyword = ''.join(ch for ch in keyword.upper() if ch in ALPHABET)
    keyword = ''.join(sorted(set(keyword), key=keyword.index))
    
    # Build cipher alphabet
    rest = ''.join(ch for ch in ALPHABET if ch not in keyword)
    cipher_alphabet = keyword + rest

    # Build mappings
    mapping = dict(zip(ALPHABET, cipher_alphabet))

    # Encrypt
    return ''.join(mapping.get(ch, ch) for ch in text.upper())
```

#### Decrypt

Với loại mã hóa, để break mà không có khóa, ta sẽ sử dụng phương pháp giống với phương pháp break **Simple Substitution Cipher**

Code decrypt:
```python3=
import string
from collections import Counter

ALPHABET = string.ascii_uppercase
M = len(ALPHABET)

FREQ_ORDER = "ETAOINSHRDLCUMWFGYPBVKJXQZ"

ciphertext = "hzsrnqc klyy wqc flo mflwf ol zqdn nsoznj wskn lj xzsrbjnf, wzsxz gqv zqhhnf ol ozn glco zlfnco hnlhrn; nsoznj jnrqosdnc lj fnqj kjsnfbc, wzsxz sc xnjoqsfrv gljn efeceqr. zn rsdnb qrlfn sf zsc zlecn sf cqdsrrn jlw, wzsoznj flfn hnfnojqonb. q csfyrn blgncosx cekksxnb ol cnjdn zsg. zn pjnqmkqconb qfb bsfnb qo ozn xrep, qo zlejc gqozngqosxqrrv ksanb, sf ozn cqgn jllg, qo ozn cqgn oqprn, fndnj oqmsfy zsc gnqrc wsoz loznj gngpnjc, gexz rncc pjsfysfy q yenco wsoz zsg; qfb wnfo zlgn qo naqxorv gsbfsyzo, lfrv ol jnosjn qo lfxn ol pnb. zn fndnj ecnb ozn xlcv xzqgpnjc wzsxz ozn jnkljg hjldsbnc klj soc kqdlejnb gngpnjc. zn hqccnb onf zlejc leo lk ozn ownfov-klej sf cqdsrrn jlw, nsoznj sf crnnhsfy lj gqmsfy zsc olsrno."

count_ciphers = Counter(''.join(filter(str.isalpha, ciphertext.upper())))

sorted_by_freq = [pair[0] for pair in count_ciphers.most_common()]

matching = {cipher_char: plain_char for cipher_char, plain_char in zip(sorted_by_freq, FREQ_ORDER)}

matching["Z"] = "H"
matching["F"] = "N"
matching["V"] = "Y"
matching["L"] = "O"
matching["E"] = "U"
matching["Q"] = "A"
matching["R"] = "L"
matching["D"] = "V"
matching["S"] = "I"
matching["B"] = "D"
matching["X"] = "C"
matching["G"] = "M"
matching["A"] = "X"

def decrypt(ciphertext, matching):
    decrypted = []
    for char in ciphertext:
        if char.upper() in matching:
            new_char = matching[char.upper()]
            if char.islower():
                new_char = new_char.lower()
            decrypted.append(new_char)
        else:
            decrypted.append(char)  
    return ''.join(decrypted)

plaintext = decrypt(ciphertext, matching)
print("Ciphertext:", ciphertext)
print("Decrypted Plaintext:", plaintext)
print("Matching:", matching)
```

### Homophonic Substitution

#### Encrypt

**Homophonic Substitution** là loại mã hóa thay thế đơn bảng, nhưng một chữ cái trong bản rõ có thể được mã hóa thành nhiều ký hiệu khác nhau thay vì chỉ 1 như các loại ở trên.

Code encrypt:
```python3=
def load_homophonic_mapping(file_path: str = None):
    """
    Load homophonic mapping from a JSON file.
    If no file is given, use a default built-in mapping.
    """
    if file_path:
        with open(file_path, "r", encoding="utf-8") as f:
            mapping = json.load(f)
    else:
        # Default demo mapping (same as before)
        mapping = {
            'E': ['1', '!', 'Q', '9', 'X', '3'],
            'T': ['2', '$', 'Y', '+'],
            'A': ['@', '4', '7'],
            'O': ['0', 'O', '*'],
            'I': ['|', 'i', '8'],
            'N': ['~', 'n', '^'],
            'S': ['$', '5', '%'],
            'H': ['#', 'h', '&'],
            'R': ['R', '?', '4'],
            'D': ['D', ')'],
            'L': ['L', '/'],
            'C': ['C', '(', '['],
            'U': ['U', '_'],
            'M': ['M', '='],
            'W': ['W', '{'],
            'F': ['F', '}'],
            'G': ['G', ':'],
            'Y': ['Y', ';'],
            'P': ['P', '.'],
            'B': ['B', ','],
            'V': ['V', '<'],
            'K': ['K', '>'],
            'J': ['J', '`'],
            'X': ['X', '"'],
            'Q': ['Q', "'"],
            'Z': ['Z', '\\']
        }

    # Ensure all 26 letters exist (fallback self-mapping)
    for ch in ALPHABET:
        if ch not in mapping:
            mapping[ch] = [ch]
    return mapping

def homophonic_encrypt(text: str, mapping: dict) -> str:
    """Encrypt using homophonic substitution."""
    result = []
    for ch in text.upper():
        if ch in mapping:
            result.append(random.choice(mapping[ch]))
        else:
            result.append(ch)  # keep non-letters as is
    return ''.join(result)
```

#### Decrypt

Với việc ánh xạ ra nhiều kí tự khác nhau, điều này sẽ làm giảm đáng kể khả năng sử dụng kĩ thuật thống kê tần suất xuất hiện của các chữ cái.

Trong trường có bảng ánh xạ, ta có code decrypt sau:
```python3=
def homophonic_decrypt(cipher: str, mapping: dict) -> str:
    """Decrypt homophonic ciphertext (many-to-one mapping)."""
    reverse = {}
    for k, vals in mapping.items():
        for v in vals:
            reverse[v] = k
    return ''.join(reverse.get(ch, ch) for ch in cipher)
```
### Pigpen Cipher

#### Encrypt

**Pigpen cipher** (còn gọi là Masonic cipher, Freemason's cipher hay tic-tac-toe cipher) là một dạng **monoalphabetic substitution cipher**:

+ Mỗi chữ cái A–Z được thay bằng một ký hiệu hình học.
+ Không dùng số, phép cộng, hay công thức toán học.
+ Hoàn toàn dựa trên sơ đồ (grid) gồm các ô dạng “chuồng lợn” (pigpen).

Code encrypt:
```python3=
# Default simple placeholder Pigpen map (A=∆, B=⊡, ... etc.)
DEFAULT_PIGPEN_SYMBOLS = [
    "∆", "⊡", "□", "⊞", "⊟", "⊠", "◈", "◇", "◆", "◉", 
    "○", "◍", "◎", "●", "◐", "◑", "◒", "◓", "◔", "◕",
    "★", "✦", "✧", "✩", "✪", "✫"
]
DEFAULT_PIGPEN_MAP = dict(zip(ALPHABET, DEFAULT_PIGPEN_SYMBOLS))


def build_pigpen_map(custom_symbols: str = None):
    """
    Build Pigpen mapping. If custom_symbols is provided,
    it must contain 26 unique characters.
    """
    if custom_symbols:
        if len(custom_symbols) != 26:
            raise ValueError("Pigpen key must have exactly 26 symbols/characters.")
        return dict(zip(ALPHABET, list(custom_symbols)))
    else:
        return DEFAULT_PIGPEN_MAP


def pigpen_encrypt(text: str, mapping: dict) -> str:
    """Encrypt using Pigpen cipher with mapping."""
    return ''.join(mapping.get(ch, ch) for ch in text.upper())
```

#### Decrypt

Trong trường hợp có bảng hoặc các kí tự toán học dễ đoán, ta có thể dựa trên nó để decrypt, ta có code decrypt sau:
```python3=
DEFAULT_PIGPEN_SYMBOLS = [
    "∆", "⊡", "□", "⊞", "⊟", "⊠", "◈", "◇", "◆", "◉", 
    "○", "◍", "◎", "●", "◐", "◑", "◒", "◓", "◔", "◕",
    "★", "✦", "✧", "✩", "✪", "✫"
]
DEFAULT_PIGPEN_MAP = dict(zip(ALPHABET, DEFAULT_PIGPEN_SYMBOLS))


def build_pigpen_map(custom_symbols: str = None):
    """
    Build Pigpen mapping. If custom_symbols is provided,
    it must contain 26 unique characters.
    """
    if custom_symbols:
        if len(custom_symbols) != 26:
            raise ValueError("Pigpen key must have exactly 26 symbols/characters.")
        return dict(zip(ALPHABET, list(custom_symbols)))
    else:
        return DEFAULT_PIGPEN_MAP

def pigpen_decrypt(cipher: str, mapping: dict) -> str:
    """Decrypt Pigpen ciphertext (symbol-to-letter)."""
    inv = {v: k for k, v in mapping.items()}
    return ''.join(inv.get(ch, ch) for ch in cipher)
```

Nếu không, ta tiếp tục sử dụng kĩ thuật thống kê tần suất xuất hiện của các chữ cái, nhưng thay vì thống kê chữ cái thì ta thống kê kí hiệu toán học.

### Rot-N Variants

#### Encrypt

Đây là loại mã hóa khá giống với Caesar Cipher, nó thay đổi chữ cái hiện tại bằng chữ cái cách nó N vị trí trên bảng chữ cái(Nối vòng bảng chữ cái lại(sau Z là A)).
Công thức: `C = (P + N) mod 26`
Code encrypt:
```python3=
def rotN(text, n:int):
    return ''.join(ALPHABET[(ALPHABET.index(ch) + n) % M] if ch in ALPHABET else ch for ch in text.upper())
```
#### Decrypt

Trong trường hợp biết N, ta chỉ việc trừ đi số vị trí bị dịch đi là N, công thức như sau: `D = (C - N) mod 26`
Code decrypt:
```python3=
def rotN(text, n:int):
    return ''.join(ALPHABET[(ALPHABET.index(ch) - n) % M] if ch in ALPHABET else ch for ch in text.upper())
```
Nếu không biết N, thực hiện bruteforce N vì N chỉ nằm trong khoảng từ [1->25]

Code bruteforce:
```python3=
def rotN_bruteforce(cipher):
    """Try all Caesar shifts and return possible plaintexts."""
    results = []
    for shift in range(1, 26):
        pt = ''.join(
            ALPHABET[(ALPHABET.index(ch) - shift) % M] if ch in ALPHABET else ch
            for ch in cipher.upper()
        )
        results.append((shift, pt))
    return results
```

### Numeric Cipher

#### Encrypt

```python3=
# --- 9. Numeric Ciphers ---
def a1z26(text, decrypt=False):
    if decrypt:
        return ''.join(ALPHABET[int(p)-1] if p.isdigit() else p for p in text.split())
    else:
        return ' '.join(str(ALPHABET.index(ch) + 1) if ch in ALPHABET else ch for ch in text.upper())
```

Đơn giản là nó chuyển chữ cái hiện tại về số thứ tự của nó ở bảng chữ cái.

#### Decrypt

Nếu là chữ hoa ta chỉ cần cộng thêm 65, nếu là chữ thường ta cộng thêm 97.

## Polyalphabetic Cipher

### Vigenere Cipher

#### Encrypt

```python3=
# --- 1. Vigenere Cipher ---
def vigenere(text: str, key: str, decrypt: bool = False) -> str:
    key = ''.join(ch for ch in key.upper() if ch in ALPHABET)
    if not key:
        raise ValueError("Key must contain alphabetic characters.")
    key_indices = [ALPHABET.index(k) for k in key]
    result = []
    j = 0
    for ch in text.upper():
        if ch in ALPHABET:
            shift = key_indices[j % len(key_indices)]
            if decrypt:
                shift = -shift
            idx = (ALPHABET.index(ch) + shift) % M
            result.append(ALPHABET[idx])
            j += 1
        else:
            result.append(ch)
    return ''.join(result)
```

Mật mã **Vigenère** là một phương pháp mã hóa văn bản thay thế đa bảng chữ cái sử dụng một chuỗi khóa để xác định các phép dịch chuyển Caesar khác nhau cho từng chữ cái trong văn bản gốc, chia bản gốc thành từng đoạn bằng key rồi sub dựa trên khóa, thay vì sử dụng một phép dịch chuyển đơn lẻ như trong mật mã Caesar truyền thống. Phương pháp này che giấu tần suất xuất hiện của các chữ cái trong văn bản mã hóa, làm cho việc phá mã khó khăn hơn. 

#### Decrypt

Code attack vigenere:
```python3=
def vigenere_attack(cipher: str, max_keylen: int = 16) -> Tuple[str, str, int]:
    # Combine Kasiski + Friedman, then try candidates up to max_keylen
    cand_lens = list(dict.fromkeys(kasiski_candidates(cipher) + [round(friedman_estimate_keylen(cipher))]))
    cand_lens = [L for L in cand_lens if 1 <= L <= max_keylen]
    if not cand_lens:
        cand_lens = list(range(1, min(12, max_keylen)+1))
    best_plain, best_key, best_score = "", "", float('inf')
    for L in cand_lens:
        key = vigenere_recover_key(cipher, L)
        pt = vigenere_decrypt(cipher, key)
        sc = chi_squared_score(pt)
        if sc < best_score:
            best_plain, best_key, best_score = pt, key, sc
    return best_plain, best_key, len(best_key)
```

### Beaufort Cipher

Code attack Beaufort:
```python3=
def beaufort_attack(cipher: str, max_keylen: int = 16) -> Tuple[str, str, int]:
    # Same key-length pipeline; reuse Vigenere recovery (columns solved by Caesar)
    cand_lens = list(dict.fromkeys(kasiski_candidates(cipher) + [round(friedman_estimate_keylen(cipher))]))
    cand_lens = [L for L in cand_lens if 1 <= L <= max_keylen]
    if not cand_lens:
        cand_lens = list(range(1, min(12, max_keylen)+1))
    best_plain, best_key, best_score = "", "", float('inf')
    for L in cand_lens:
        # For Beaufort, the best Caesar shift per column corresponds to key value k that makes P English:
        # P = K - C => for a guess k, P_i = (k - C_i). We choose k that minimizes chi2 over column.
        idx = to_indices(cipher)
        cols = split_columns_by_len(idx, L)
        key_vals = []
        for col in cols:
            best_k, best_col_score = 0, float('inf')
            for k in range(26):
                pt_col = from_indices([(k - c) % 26 for c in col])
                sc = chi_squared_score(pt_col)
                if sc < best_col_score:
                    best_col_score, best_k = sc, k
            key_vals.append(best_k)
        key = from_indices(key_vals)
        pt = beaufort_decrypt(cipher, key)
        sc_total = chi_squared_score(pt)
        if sc_total < best_score:
            best_plain, best_key, best_score = pt, key, sc_total
    return best_plain, best_key, len(best_key)
```

#### Encrypt

Mật mã **Beaufort** là một mật mã hoán vị đa bảng chữ cái, sử dụng các bảng chữ cái dịch chuyển khác nhau dựa trên một khóa lặp lại, giống với mật mã Vigenère, nhưng sử dụng khóa làm cơ sở thay vì văn bản gốc để xác định các dịch chuyển. Điểm khác biệt chính là thuật toán mã hóa và giải mã của nó là như nhau, làm cho nó trở thành một mật mã tương hỗ.

```python3=
def beaufort(text: str, key: str) -> str:
    key = ''.join(ch for ch in key.upper() if ch in ALPHABET)
    if not key:
        raise ValueError("Key must contain alphabetic characters.")
    key_indices = [ALPHABET.index(k) for k in key]
    result = []
    j = 0
    for ch in text.upper():
        if ch in ALPHABET:
            shift = key_indices[j % len(key_indices)]
            idx = (shift - ALPHABET.index(ch)) % M
            result.append(ALPHABET[idx])
            j += 1
        else:
            result.append(ch)
    return ''.join(result)
```

#### Decrypt

```python3=
def beaufort_attack(cipher: str, max_keylen: int = 16) -> Tuple[str, str, int]:
    # Same key-length pipeline; reuse Vigenere recovery (columns solved by Caesar)
    cand_lens = list(dict.fromkeys(kasiski_candidates(cipher) + [round(friedman_estimate_keylen(cipher))]))
    cand_lens = [L for L in cand_lens if 1 <= L <= max_keylen]
    if not cand_lens:
        cand_lens = list(range(1, min(12, max_keylen)+1))
    best_plain, best_key, best_score = "", "", float('inf')
    for L in cand_lens:
        # For Beaufort, the best Caesar shift per column corresponds to key value k that makes P English:
        # P = K - C => for a guess k, P_i = (k - C_i). We choose k that minimizes chi2 over column.
        idx = to_indices(cipher)
        cols = split_columns_by_len(idx, L)
        key_vals = []
        for col in cols:
            best_k, best_col_score = 0, float('inf')
            for k in range(26):
                pt_col = from_indices([(k - c) % 26 for c in col])
                sc = chi_squared_score(pt_col)
                if sc < best_col_score:
                    best_col_score, best_k = sc, k
            key_vals.append(best_k)
        key = from_indices(key_vals)
        pt = beaufort_decrypt(cipher, key)
        sc_total = chi_squared_score(pt)
        if sc_total < best_score:
            best_plain, best_key, best_score = pt, key, sc_total
    return best_plain, best_key, len(best_key)
```

### 

## Bài tập

Giải mã cipher sau:
```
◔◆●□⊟ ◕◇⊟ ◓⊟◍⊟∆◔⊟ ◐⊠ ◕◇⊟ ⊠◆◓◔◕ ●◐✦⊟◍, ◇∆◓◓✪ ◑◐◕◕⊟◓ ∆●⊞ ◕◇⊟ ◑◇◆◍◐◔◐◑◇⊟◓'◔ ◔◕◐●⊟, ◐● 26 ◉★●⊟ 1997, ◕◇⊟ ⊡◐◐○◔ ◇∆✦⊟ ⊠◐★●⊞ ◆◎◎⊟●◔⊟ ◑◐◑★◍∆◓◆◕✪ ∆●⊞ □◐◎◎⊟◓□◆∆◍ ◔★□□⊟◔◔ ✧◐◓◍⊞✧◆⊞⊟. ◕◇⊟✪ ◇∆✦⊟ ∆◕◕◓∆□◕⊟⊞ ∆ ✧◆⊞⊟ ∆⊞★◍◕ ∆★⊞◆⊟●□⊟ ∆◔ ✧⊟◍◍ ∆◔ ✪◐★●◈⊟◓ ◓⊟∆⊞⊟◓◔ ∆●⊞ ∆◓⊟ ✧◆⊞⊟◍✪ □◐●◔◆⊞⊟◓⊟⊞ □◐◓●⊟◓◔◕◐●⊟◔ ◐⊠ ◎◐⊞⊟◓● ◍◆◕⊟◓∆◕★◓⊟,[3][4] ◕◇◐★◈◇ ◕◇⊟ ⊡◐◐○◔ ◇∆✦⊟ ◓⊟□⊟◆✦⊟⊞ ◎◆✩⊟⊞ ◓⊟✦◆⊟✧◔ ⊠◓◐◎ □◓◆◕◆□◔ ∆●⊞ ◍◆◕⊟◓∆◓✪ ◔□◇◐◍∆◓◔. ∆◔ ◐⊠ ⊠⊟⊡◓★∆◓✪ 2023, ◕◇⊟ ⊡◐◐○◔ ◇∆✦⊟ ◔◐◍⊞ ◎◐◓⊟ ◕◇∆● 600 ◎◆◍◍◆◐● □◐◑◆⊟◔ ✧◐◓◍⊞✧◆⊞⊟, ◎∆○◆●◈ ◕◇⊟◎ ◕◇⊟ ⊡⊟◔◕-◔⊟◍◍◆●◈ ⊡◐◐○ ◔⊟◓◆⊟◔ ◆● ◇◆◔◕◐◓✪, ∆✦∆◆◍∆⊡◍⊟ ◆● ⊞◐✫⊟●◔ ◐⊠ ◍∆●◈★∆◈⊟◔. ◕◇⊟ ◍∆◔◕ ⊠◐★◓ ⊡◐◐○◔ ∆◍◍ ◔⊟◕ ◓⊟□◐◓⊞◔ ∆◔ ◕◇⊟ ⊠∆◔◕⊟◔◕-◔⊟◍◍◆●◈ ⊡◐◐○◔ ◆● ◇◆◔◕◐◓✪, ✧◆◕◇ ◕◇⊟ ⊠◆●∆◍ ◆●◔◕∆◍◎⊟●◕ ◔⊟◍◍◆●◈ ◓◐★◈◇◍✪ 2.7 ◎◆◍◍◆◐● □◐◑◆⊟◔ ◆● ◕◇⊟ ★●◆◕⊟⊞ ○◆●◈⊞◐◎ ∆●⊞ 8.3 ◎◆◍◍◆◐● □◐◑◆⊟◔ ◆● ◕◇⊟ ★●◆◕⊟⊞ ◔◕∆◕⊟◔ ✧◆◕◇◆● ◕✧⊟●◕✪-⊠◐★◓ ◇◐★◓◔ ◐⊠ ◆◕◔ ◓⊟◍⊟∆◔⊟. ◆◕ ◇◐◍⊞◔ ◕◇⊟ ◈★◆●●⊟◔◔ ✧◐◓◍⊞ ◓⊟□◐◓⊞ ⊠◐◓ ⊡⊟◔◕-◔⊟◍◍◆●◈ ⊡◐◐○ ◔⊟◓◆⊟◔ ⊠◐◓ □◇◆◍⊞◓⊟●.
```

Sử dụng frequency analyst, em được:

```
AOSCE IDE NEHERAE TM IDE MONAI STVEH, DRNNY KTIIEN RSL IDE KDOHTATKDEN'A AITSE, TS 26 JUSE 1997, IDE PTTBA DRVE MTUSL OWWESAE KTKUHRNOIY RSL CTWWENCORH AUCCEAA FTNHLFOLE. IDEY DRVE RIINRCIEL R FOLE RLUHI RULOESCE RA FEHH RA YTUSGEN NERLENA RSL RNE FOLEHY CTSAOLENEL CTNSENAITSEA TM WTLENS HOIENRIUNE,[3][4] IDTUGD IDE PTTBA DRVE NECEOVEL WOXEL NEVOEFA MNTW CNOIOCA RSL HOIENRNY ACDTHRNA. RA TM MEPNURNY 2023, IDE PTTBA DRVE ATHL WTNE IDRS 600 WOHHOTS CTKOEA FTNHLFOLE, WRBOSG IDEW IDE PEAI-AEHHOSG PTTB AENOEA OS DOAITNY, RVROHRPHE OS LTQESA TM HRSGURGEA. IDE HRAI MTUN PTTBA RHH AEI NECTNLA RA IDE MRAIEAI-AEHHOSG PTTBA OS DOAITNY, FOID IDE MOSRH OSAIRHWESI AEHHOSG NTUGDHY 2.7 WOHHOTS CTKOEA OS IDE USOIEL BOSGLTW RSL 8.3 WOHHOTS CTKOEA OS IDE USOIEL AIRIEA FOIDOS IFESIY-MTUN DTUNA TM OIA NEHERAE. OI DTHLA IDE GUOSSEAA FTNHL NECTNL MTN PEAI-AEHHOSG PTTB AENOEA MTN CDOHLNES.
```

Vẫn đã rõ ràng 1 số từ, tiếp tục ta sẽ chỉnh sửa lại, em có full script sau:

```python3=
import string
from collections import Counter
text = """◔◆●□⊟ ◕◇⊟ ◓⊟◍⊟∆◔⊟ ◐⊠ ◕◇⊟ ⊠◆◓◔◕ ●◐✦⊟◍, ◇∆◓◓✪ ◑◐◕◕⊟◓ ∆●⊞ ◕◇⊟ ◑◇◆◍◐◔◐◑◇⊟◓'◔ ◔◕◐●⊟, ◐● 26 ◉★●⊟ 1997, ◕◇⊟ ⊡◐◐○◔ ◇∆✦⊟ ⊠◐★●⊞ ◆◎◎⊟●◔⊟ ◑◐◑★◍∆◓◆◕✪ ∆●⊞ □◐◎◎⊟◓□◆∆◍ ◔★□□⊟◔◔ ✧◐◓◍⊞✧◆⊞⊟. ◕◇⊟✪ ◇∆✦⊟ ∆◕◕◓∆□◕⊟⊞ ∆ ✧◆⊞⊟ ∆⊞★◍◕ ∆★⊞◆⊟●□⊟ ∆◔ ✧⊟◍◍ ∆◔ ✪◐★●◈⊟◓ ◓⊟∆⊞⊟◓◔ ∆●⊞ ∆◓⊟ ✧◆⊞⊟◍✪ □◐●◔◆⊞⊟◓⊟⊞ □◐◓●⊟◓◔◕◐●⊟◔ ◐⊠ ◎◐⊞⊟◓● ◍◆◕⊟◓∆◕★◓⊟,[3][4] ◕◇◐★◈◇ ◕◇⊟ ⊡◐◐○◔ ◇∆✦⊟ ◓⊟□⊟◆✦⊟⊞ ◎◆✩⊟⊞ ◓⊟✦◆⊟✧◔ ⊠◓◐◎ □◓◆◕◆□◔ ∆●⊞ ◍◆◕⊟◓∆◓✪ ◔□◇◐◍∆◓◔. ∆◔ ◐⊠ ⊠⊟⊡◓★∆◓✪ 2023, ◕◇⊟ ⊡◐◐○◔ ◇∆✦⊟ ◔◐◍⊞ ◎◐◓⊟ ◕◇∆● 600 ◎◆◍◍◆◐● □◐◑◆⊟◔ ✧◐◓◍⊞✧◆⊞⊟, ◎∆○◆●◈ ◕◇⊟◎ ◕◇⊟ ⊡⊟◔◕-◔⊟◍◍◆●◈ ⊡◐◐○ ◔⊟◓◆⊟◔ ◆● ◇◆◔◕◐◓✪, ∆✦∆◆◍∆⊡◍⊟ ◆● ⊞◐✫⊟●◔ ◐⊠ ◍∆●◈★∆◈⊟◔. ◕◇⊟ ◍∆◔◕ ⊠◐★◓ ⊡◐◐○◔ ∆◍◍ ◔⊟◕ ◓⊟□◐◓⊞◔ ∆◔ ◕◇⊟ ⊠∆◔◕⊟◔◕-◔⊟◍◍◆●◈ ⊡◐◐○◔ ◆● ◇◆◔◕◐◓✪, ✧◆◕◇ ◕◇⊟ ⊠◆●∆◍ ◆●◔◕∆◍◎⊟●◕ ◔⊟◍◍◆●◈ ◓◐★◈◇◍✪ 2.7 ◎◆◍◍◆◐● □◐◑◆⊟◔ ◆● ◕◇⊟ ★●◆◕⊟⊞ ○◆●◈⊞◐◎ ∆●⊞ 8.3 ◎◆◍◍◆◐● □◐◑◆⊟◔ ◆● ◕◇⊟ ★●◆◕⊟⊞ ◔◕∆◕⊟◔ ✧◆◕◇◆● ◕✧⊟●◕✪-⊠◐★◓ ◇◐★◓◔ ◐⊠ ◆◕◔ ◓⊟◍⊟∆◔⊟. ◆◕ ◇◐◍⊞◔ ◕◇⊟ ◈★◆●●⊟◔◔ ✧◐◓◍⊞ ◓⊟□◐◓⊞ ⊠◐◓ ⊡⊟◔◕-◔⊟◍◍◆●◈ ⊡◐◐○ ◔⊟◓◆⊟◔ ⊠◐◓ □◇◆◍⊞◓⊟●"""
special_symbols = sorted(set(ch for ch in text if not (ch.isdigit() or ch.isspace() or ch in string.punctuation)))
latin_letters = list(string.ascii_uppercase)
mapping_latin = {sym: latin_letters[i] for i, sym in enumerate(special_symbols)}
decoded_latin = ''.join(mapping_latin.get(ch, ch) for ch in text)
print(decoded_latin,'\n')
FREQ_ORDER = "ETAOINSHRDLCUMWFGYPBVKJXQZ"
alphabet = string.ascii_uppercase
letter_count = {char : 0 for char in alphabet}

for letter in decoded_latin:
    if letter in alphabet:
        letter_count[letter] += 1
    else:
        continue
sorted_by_freq = sorted(letter_count.items(), key=lambda x: x[1], reverse=True)
mathching = {}
for (cipher_char, _), plain_char in zip(sorted_by_freq, FREQ_ORDER):
    mathching[cipher_char] = plain_char
mathching = {
    "N": "N",
    "O": "O",
    "R": "S",
    "G": "I",
    "S": "T",
    "H": "H",
    "B": "D",
    "L": "L",
    "Q": "R",
    "E": "B",
    "I": "G",
    "K": "K",
    "X": "Y",
    "P": "P",
    "C": "L",
    "A": "A",
    "T": "E",
    "F": "O",
    "V": "W",
    "Y": "M",
    "C":"E",
    "T":"U",
    "D":"F",
    "F" : "C",
    "U" : "V",
    "M" : "M",
    "W" : "X",
    "Y" :"Z"
}
def decrypt(ct,map):
    text = []
    for ch in ct:
        if ch in map:
            text.append(map[ch])
        else:
            text.append(ch)
    return ''.join(text)
print(decrypt(decoded_latin,mathching),"\n")
```

Plaintext:
```
SINCE THE RELEASE OF THE FIRST NOVEL, HARRY POTTER AND THE PHILOSOPHER'S STONE, ON 26 JUNE 1997, THE BOOKS HAVE FOUND IMMENSE POPULARITY AND COMMERCIAL SUCCESS WORLDWIDE. THEY HAVE ATTRACTED A WIDE ADULT AUDIENCE AS WELL AS YOUNGER READERS AND ARE WIDELY CONSIDERED CORNERSTONES OF MODERN LITERATURE,[3][4] THOUGH THE BOOKS HAVE RECEIVED MIXED REVIEWS FROM CRITICS AND LITERARY SCHOLARS. AS OF FEBRUARY 2023, THE BOOKS HAVE SOLD MORE THAN 600 MILLION COPIES WORLDWIDE, MAKING THEM THE BEST-SELLING BOOK SERIES IN HISTORY, AVAILABLE IN DOZENS OF LANGUAGES. THE LAST FOUR BOOKS ALL SET RECORDS AS THE FASTEST-SELLING BOOKS IN HISTORY, WITH THE FINAL INSTALMENT SELLING ROUGHLY 2.7 MILLION COPIES IN THE UNITED KINGDOM AND 8.3 MILLION COPIES IN THE UNITED STATES WITHIN TWENTY-FOUR HOURS OF ITS RELEASE. IT HOLDS THE GUINNESS WORLD RECORD FOR BEST-SELLING BOOK SERIES FOR CHILDREN 
```