156 lines
		
	
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			156 lines
		
	
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from functools import partial
 | 
						||
import time
 | 
						||
import numpy as np
 | 
						||
import string
 | 
						||
import matplotlib.pyplot as plt
 | 
						||
from Crypto.PublicKey import RSA
 | 
						||
from Crypto.Cipher import AES, PKCS1_OAEP
 | 
						||
from Crypto.Util.Padding import pad, unpad
 | 
						||
from Crypto.Random import get_random_bytes
 | 
						||
from sympy import Matrix   # only for the 2×2 Hill inverse
 | 
						||
 | 
						||
 | 
						||
# 1.  Hill-Cipher utilities  (2×2 matrix [[3,3],[2,5]])
 | 
						||
 | 
						||
hillkey = np.array([[3, 3],
 | 
						||
                    [2, 5]], dtype=int)
 | 
						||
ALPH = string.ascii_uppercase
 | 
						||
m_len = 2                         # block size
 | 
						||
 | 
						||
def clean(txt):
 | 
						||
    return ''.join(ch.upper() for ch in txt if ch.isalpha())
 | 
						||
 | 
						||
def hill_encrypt(pt, key=hillkey):
 | 
						||
    pt = clean(pt)
 | 
						||
    if len(pt) % m_len:                       # padding
 | 
						||
        pt += 'X' * (m_len - len(pt) % m_len)
 | 
						||
    cipher = []
 | 
						||
    for i in range(0, len(pt), m_len):
 | 
						||
        block = np.array([ALPH.index(ch) for ch in pt[i:i+m_len]])
 | 
						||
        cipher.extend((key @ block) % 26)
 | 
						||
    return ''.join(ALPH[i] for i in cipher)
 | 
						||
 | 
						||
def hill_decrypt(ct, key=hillkey):
 | 
						||
    # modular inverse of the matrix
 | 
						||
    M = Matrix(key)
 | 
						||
    detinv = pow(int(M.det()) % 26, -1, 26)
 | 
						||
    adj = M.adjugate()
 | 
						||
    inv_key = (detinv * adj) % 26
 | 
						||
    inv_key = np.array(inv_key).astype(int)
 | 
						||
 | 
						||
    plain = []
 | 
						||
    for i in range(0, len(ct), m_len):
 | 
						||
        block = np.array([ALPH.index(ch) for ch in ct[i:i+m_len]])
 | 
						||
        plain.extend((inv_key @ block) % 26)
 | 
						||
    return ''.join(ALPH[i] for i in plain)
 | 
						||
 | 
						||
# Generic timing wrapper
 | 
						||
def timed(fn, *a, **kw):
 | 
						||
    t0 = time.perf_counter()
 | 
						||
    for _ in range(100_000):              # 100 000 loops → 1 000 000 calls
 | 
						||
        fn(*a, **kw)
 | 
						||
    delta = time.perf_counter() - t0
 | 
						||
    return delta
 | 
						||
 | 
						||
# Menu
 | 
						||
def menu():
 | 
						||
    while True:
 | 
						||
        print("\n===== Cryptographic Demo =====")
 | 
						||
        print("1. Hill-cipher demo")
 | 
						||
        print("2. RSA + envelope (share AES)")
 | 
						||
        print("3. AES-128 encrypt / decrypt")
 | 
						||
        print("4. Speed graph (100k it loops)")
 | 
						||
        print("0. Quit")
 | 
						||
        ch = input("Select >> ").strip()
 | 
						||
        if ch == '0': break
 | 
						||
        elif ch == '1': hill_demo()
 | 
						||
        elif ch == '2': rsa_env_demo()
 | 
						||
        elif ch == '3': aes_demo()
 | 
						||
        elif ch == '4': speed_graph()
 | 
						||
        else: print("Invalid choice")
 | 
						||
 | 
						||
# 1.  Hill demo
 | 
						||
def hill_demo():
 | 
						||
    msg = 'The  key is hidden under the mattress'
 | 
						||
    print("\n------- 1. Hill-Cipher Demo -------")
 | 
						||
    print("Key matrix:\n", hillkey)
 | 
						||
    ct = hill_encrypt(msg)
 | 
						||
    print("Cipher :", ct)
 | 
						||
    pt = hill_decrypt(ct)
 | 
						||
    print("Plain  :", pt)       # uppercase / padded – expected
 | 
						||
 | 
						||
# 2. RSA Envelope demo
 | 
						||
AES_KEY = b"0123456789ABCDEF"    # 16-byte AES-128 key (truncated from prompt)
 | 
						||
 | 
						||
def rsa_env_demo():
 | 
						||
    print("\n------- 2. RSA × AES envelope Demo -------")
 | 
						||
    # generate RSA pairs
 | 
						||
    encoder_priv = RSA.generate(2048)
 | 
						||
    encoder_pub  = encoder_priv.publickey()
 | 
						||
 | 
						||
    decoder_priv = RSA.generate(2048)   # strictly not needed, but shown
 | 
						||
    decoder_pub  = decoder_priv.publickey()
 | 
						||
 | 
						||
    # Use encoder_pub to encrypt the shared AES key
 | 
						||
    cipher_rsa = PKCS1_OAEP.new(encoder_pub)
 | 
						||
    enc_key = cipher_rsa.encrypt(AES_KEY)
 | 
						||
 | 
						||
    # Show keys
 | 
						||
    print("Encoder public key (PEM):\n", encoder_pub.export_key().decode())
 | 
						||
    print("Encoder private key (PEM):\n", encoder_priv.export_key().decode())
 | 
						||
    print("Encrypted AES key (hex):", enc_key.hex())
 | 
						||
 | 
						||
    # Decrypt back using encoder’s private key
 | 
						||
    dec_rsa = PKCS1_OAEP.new(encoder_priv)
 | 
						||
    dec_key = dec_rsa.decrypt(enc_key)
 | 
						||
    print("Decrypted AES key        :", dec_key.hex(), " match=", dec_key==AES_KEY)
 | 
						||
 | 
						||
# 3. AES-128
 | 
						||
def aes_demo():
 | 
						||
    msg = input("Enter AES plaintext:")
 | 
						||
    print("\n------- 3. AES-128 Demo -------")
 | 
						||
    cipher = AES.new(AES_KEY, AES.MODE_CBC)         # random IV
 | 
						||
    ct = cipher.encrypt(pad(msg.encode(), AES.block_size))
 | 
						||
    print("Raw key               :", AES_KEY.hex())
 | 
						||
    print("Ciphertext (incl. IV) :", cipher.iv.hex() + ct.hex())
 | 
						||
    decipher = AES.new(AES_KEY, AES.MODE_CBC, cipher.iv)
 | 
						||
    pt = unpad(decipher.decrypt(ct), AES.block_size).decode()
 | 
						||
    print("Recovered plaintext   :", pt)
 | 
						||
 | 
						||
# 4.  Speed test
 | 
						||
def speed_graph():
 | 
						||
    print("\n------- Benchmark (100 000 loops = 1 Mio calls) -------")
 | 
						||
    # prepare one big dummy block for AES
 | 
						||
    aes_cipher = AES.new(AES_KEY, AES.MODE_ECB)
 | 
						||
    dummy_aes = b"A" * 16
 | 
						||
 | 
						||
    # RSA encryption of AES key just once
 | 
						||
    pub_key = RSA.generate(2048).publickey()
 | 
						||
    rsa_cipher = PKCS1_OAEP.new(pub_key)
 | 
						||
    dummy_rsa = AES_KEY
 | 
						||
 | 
						||
    # Hill needs string
 | 
						||
    hill_string = "HELLOWORLD"
 | 
						||
    hill_ct = hill_encrypt(hill_string)   # pre-compute to keep test same
 | 
						||
 | 
						||
    # timing
 | 
						||
    t_hill = timed(hill_encrypt, hill_string)
 | 
						||
    t_rsa  = timed(rsa_cipher.encrypt, dummy_rsa)
 | 
						||
    t_aes  = timed(aes_cipher.encrypt, dummy_aes)
 | 
						||
 | 
						||
    methods = ['Hill-cipher\n(str encrypt)', 'RSA-OAEP\n(key wrap)', 'AES-128\n(block)']
 | 
						||
    timings = [t_hill, t_rsa, t_aes]
 | 
						||
    for name,val in zip(methods,timings):
 | 
						||
        print(f"{name:25s} : {val:9.4f} s (100 000 loops)")
 | 
						||
 | 
						||
    plt.bar(methods, timings, color=['gold','coral','dodgerblue'])
 | 
						||
    plt.ylabel("Time (s) for 100 000 iterations")
 | 
						||
    plt.title("Relative speed among 3 algorithms")
 | 
						||
    plt.tight_layout()
 | 
						||
    plt.show()
 | 
						||
 | 
						||
##############################################################
 | 
						||
# main
 | 
						||
##############################################################
 | 
						||
if __name__ == "__main__":
 | 
						||
    menu()
 |