欢迎来到7N的个人博客!

如何手搓自己的对称加密算法?————ECB&CBC学习笔记


avatar
7ech_N3rd 2024-12-04 252

闲来无事,在js逆向中见过了些牛鬼蛇神的加密方法,不禁对对称加密产生了兴趣,自己入门学学对称加密

Xor特性

说到对称加密肯定绕不开异或,因为异或有三大美妙的特性:

1. 自反性(对称性)

这个性质的意思是:用同一个东西翻转两次,结果就回到原样了。

举个例子:
假设你有一个数字 A=5(用二进制表示是 0101),还有一个“密钥” B=3(用二进制表示是 0011)。
现在我们用 B 对 A 做一次异或(加密):

  • A⊕B=0101⊕0011=0110(结果是 6)。

这时候,6 就是加密后的结果。
如果再用 B 对 6 做一次异或(解密):

  • 6⊕B=0110⊕0011=0101(结果又回到了 5)。

所以,同一个密钥 B 不管是加密还是解密都能用,这就是异或的自反性!简单来说,异或两次就能还原原来的数据。


2. 结合性

结合性就是说,异或的计算顺序不重要,先算谁都一样。

比如你有三个数字 A=5、B=3、C=2:

  • 如果你先算 B⊕C,再用结果去异或 A:

    A⊕(B⊕C)=5⊕(3⊕2)=5⊕1=4

  • 如果你先算 A⊕BA⊕B,再用结果去异或 C:

    (A⊕B)⊕C=(5⊕3)⊕2=6⊕2=4

不管你先算谁,结果都是 4。这就是结合性,异或的顺序不影响结果。


3. 交换性

交换性就更简单了,意思是异或的顺序可以随便换,结果不会变。

比如:

  • A⊕B=5⊕3=6,
  • 换个顺序 B⊕A=3⊕5=6。

结果一样!所以,异或的操作是可以随便调换顺序的。
那为啥我们用异或来加密呢?
加密的目标是将原始数据(明文)通过某种算法和密钥变换为不可读的数据(密文),并且这个过程必须是可逆的。异或运算正好满足这些要求只要我们对明文A异或密钥得到密文,再用密文异或一次密钥就可以得到明文了。这个特性极大的方便了我们的加密

EBC

首先异或要求明文长度和密钥长度相同,所以我们得对密钥进行拓展和缩减长度的操作,例如简单的重复:

def xor_encrypt_decrypt(input_string, key):
    """
    使用简单的异或加密/解密字符串
    :param input_string: 需要加密/解密的字符串
    :param key: 加密/解密的密钥(字符)
    :return: 加密/解密后的结果
    """
    # 将字符串转换为字符列表,逐字符进行异或运算
    result = ''.join(chr(ord(char) ^ ord(key)) for char in input_string)
    return result

# 示例
if __name__ == "__main__":
    original_text = "Hello, World!"  # 原始字符串
    encryption_key = "KKEY"  # 密钥(单字符)

    # 加密
    encrypted_text = xor_encrypt_decrypt(original_text, encryption_key)
    print("加密后的字符串:", encrypted_text)

    # 解密
    decrypted_text = xor_encrypt_decrypt(encrypted_text, encryption_key)
    print("解密后的字符串:", decrypted_text)

这就是EBC(电子密码本)有如下两个特点:
一, 每个明文分组独立加密:将明文分成固定大小的分组(如 128 位),然后每个分组单独使用密钥加密。
二,
相同的明文分组会生成相同的密文分组
这样就解决长度不同的问题,但是ECB也有自己的安全缺陷:一,如果明文中存在重复的数据块,密文会直接暴露出这种模式,从而导致安全性降低,例如:000000数据异或任何密钥都是密钥本身,这样直接暴露了密钥,同理如果明文中有很多特定的模式,在异或处理后也会保持相同的形式,由于每个分组的加密是独立的,如果不同位置的分组内容相同,则加密后密文也会相同。二,因为密钥的自反性,攻击者知道加密后的密文和一部分明文(已知明文攻击),可以通过简单的异或运算直接推导出密钥:密钥=密文⊕明文
那如果我使用轮回异或如下呢?


def cir_xor(text, key, action):
    if action == "decrypt":
        text = decode_from_base64(text)
    text_list = list(text)
    key_list = list(key)
    key_length = len(key_list)
    encrypted_text = []
    for i in range(len(text_list) - key_length):
        for j in range(key_length):
            key_char = key_list[j]
            encrypted_char = chr(ord(text_list[i + j]) ^ ord(key_char))
            text_list[i + j] = encrypted_char
    result = ''.join(text_list)
    if action == "encrypt":
        return encode_to_base64(result)
    return result

还记得异或的第二个性质吗?异或运算是满足结合律的,虽然这里采用了循环操作,而且不同位置的字符异或次数不一样从而就抹去了原始数据的结构特征,但是根据异或运算的结合律,就是相当于异或了一次。只要我们输入很多00 00 00...字符串还是能够已知明文,逐步算出密钥。例如密钥为key 00 异或出的第一个字符肯定是k,第二个字符是'k'^'e'='\x0e' 我们只要通过 '\x0e'^ 'k'就能够算出密钥的第二字符e了同理第三个字符w,y='w'^'\x0e'。就成功破解了。
那如何防范?

CBC

我们现在的所有被攻击的点都是因为密钥是没有办法更新,所以说我们就可以通过不同的数据对相同的密钥异或来逐步分析推算出来密钥。那如果我们并不使用固定的密钥,而是根据上一组的明文的内容来更新密钥的内容。攻击者就会因为无法推算出上一组明文的异或密钥,从而没有办法实现推算出密钥
这里我给出了我的算法demo--7Nencryption v0.0

import random
import string
import hashlib
import base64
from itertools import cycle

class TNEncryption:
    def __init__(self, key: str):
        self.key = key

    def get_sha256_hash_string(self, input_string: str) -> int:
        sha256_hash = hashlib.sha256()
        sha256_hash.update(input_string.encode('utf-8'))
        return int(sha256_hash.hexdigest(), 16)

    def generate_random_string(self, seed: int) -> str:
        random.seed(seed)
        characters = string.ascii_letters + string.digits
        return ''.join(random.choice(characters) for _ in range(16))

    def split_bytes_into_groups(self, input_bytes: bytes, group_length: int) -> list:
        return [input_bytes[i:i + group_length] for i in range(0, len(input_bytes), group_length)]

    def encrypt(self, bytes_data: bytes) -> str:
        seed = self.get_sha256_hash_string(self.key)
        random_str = self.generate_random_string(seed)
        groups = self.split_bytes_into_groups(bytes_data, len(random_str))
        encrypted_bytes = b''

        for i in range(len(groups)):
            xored_group = bytes([a ^ b for a, b in zip(groups[i], cycle(random_str.encode()))])
            encrypted_bytes += xored_group

            if i % 2 == 0:
                seed += groups[i][0]
            else:
                seed -= groups[i][0]

            random_str = self.generate_random_string(seed)

        return base64.b64encode(encrypted_bytes).decode('utf-8')

    def decrypt(self, encrypted_base64: str) -> bytes:
        try:
            encrypted_bytes = base64.b64decode(encrypted_base64)
        except base64.binascii.Error as e:
            raise ValueError("Invalid Base64 input") from e

        groups = self.split_bytes_into_groups(encrypted_bytes, 16)
        seed = self.get_sha256_hash_string(self.key)
        decrypted_bytes = b''

        for i, group in enumerate(groups):
            random_str = self.generate_random_string(seed)
            random_bytes = random_str.encode()
            decrypted_group = bytes([a ^ b for a, b in zip(group, cycle(random_bytes))])
            decrypted_bytes += decrypted_group

            if len(decrypted_group) == 0:
                continue

            first_byte = decrypted_group[0]
            if i % 2 == 0:
                seed += first_byte
            else:
                seed -= first_byte

        return decrypted_bytes

# 示例用法
if __name__ == "__main__":
    key = "my_secret_key"
    tn_encryption = TNEncryption(key)

    # 原始字节数据
    original_data = b"Hello, this is a secret message!"

    # 加密
    encrypted_data = tn_encryption.encrypt(original_data)
    print(f"Encrypted (Base64): {encrypted_data}")

    # 解密
    decrypted_data = tn_encryption.decrypt(encrypted_data)
    print(f"Decrypted message: {decrypted_data.decode('utf-8')}")

这个算法通过密钥+哈希的处理生成了随机字符串作为初始密钥,并且通过不断地取上一组明文中的前两个字符来不断更新密钥种子,再用种子生成下一组的异或密钥,这样纵使黑客截取到了一对明密文,他还是无从下手,很好的杜绝了已知明文攻击了。。。




吗?

当然没有,如果黑客能有加密的权限,但是没有解密的权限,他能先使用16个0先和初始的随机字符密钥异或得到随机密钥,再用这16个随机字符密钥,解密出第一组密文的明文。
再用这第一组的明文,拼接上16个0使用相同的密钥生成的种子的加密,这时候生成的第二对密文长度便是32位,由于后16位的异或密钥是通过上一个16位明文更新后的随机数种子生成的对第二组的临时异或密钥,在和16个0异或之后就暴露了,接着,黑客就解密得到了第二个临时异或密钥。
黑客解密获得了第二组的明文后,再去拿第一第二组的明文接着拼接16个0去获得第三组的密钥和明文,以此类推,经过反复的已知明文攻击便可破解所有的密文,在不知道密钥的情况下。
我们发现上面这种攻击形式就是利用了首个明密文对在相同密钥加密的情况下永远都是相同的,那我们改进的办法也很简单,就是在前面加入随机生成的64个字符,在解密的时候把这64个字符去掉你会发现,当黑客在尝试用上面的手段去攻击的时候,这64个字符是永远都不知道的就好了。
你会发现,当黑客在尝试用上面的手段去攻击的时候,这64个字符是永远都不知道的。因为每次加密都会让这64个字符变化,就没办法推出一个恒定的密钥流最终实现解密
于是,就有了7NEncryption v1.0 👇

import random
import string
import hashlib
import base64
from itertools import cycle

class TNEncryption:
    def __init__(self, key: str):
        self.key = key

    def get_sha256_hash_string(self, input_string: str) -> int:
        sha256_hash = hashlib.sha256()
        sha256_hash.update(input_string.encode('utf-8'))
        return int(sha256_hash.hexdigest(), 16)

    def generate_random_string(self, seed: int, length: int = 16) -> str:
        random.seed(seed)
        characters = string.ascii_letters + string.digits
        return ''.join(random.choice(characters) for _ in range(length))

    def split_bytes_into_groups(self, input_bytes: bytes, group_length: int) -> list:
        return [input_bytes[i:i + group_length] for i in range(0, len(input_bytes), group_length)]

    def encrypt(self, bytes_data: bytes) -> str:
        # Generate a random 64-byte string as padding
        padding = ''.join(random.choices(string.ascii_letters + string.digits, k=64)).encode('utf-8')
        # Combine padding and original data
        data_to_encrypt = padding + bytes_data

        # Encryption process
        seed = self.get_sha256_hash_string(self.key)
        random_str = self.generate_random_string(seed, length=16)
        groups = self.split_bytes_into_groups(data_to_encrypt, len(random_str))
        encrypted_bytes = b''

        for i in range(len(groups)):
            xored_group = bytes([a ^ b for a, b in zip(groups[i], cycle(random_str.encode()))])
            encrypted_bytes += xored_group

            if i % 2 == 0:
                seed += groups[i][0]
            else:
                seed -= groups[i][0]

            random_str = self.generate_random_string(seed, length=16)

        return base64.b64encode(encrypted_bytes).decode('utf-8')

    def decrypt(self, encrypted_base64: str) -> bytes:
        try:
            encrypted_bytes = base64.b64decode(encrypted_base64)
        except base64.binascii.Error as e:
            raise ValueError("Invalid Base64 input") from e

        groups = self.split_bytes_into_groups(encrypted_bytes, 16)
        seed = self.get_sha256_hash_string(self.key)
        decrypted_bytes = b''

        for i, group in enumerate(groups):
            random_str = self.generate_random_string(seed, length=16)
            random_bytes = random_str.encode()
            decrypted_group = bytes([a ^ b for a, b in zip(group, cycle(random_bytes))])
            decrypted_bytes += decrypted_group

            if len(decrypted_group) == 0:
                continue

            first_byte = decrypted_group[0]
            if i % 2 == 0:
                seed += first_byte
            else:
                seed -= first_byte

        # Remove the first 64 bytes of padding
        if len(decrypted_bytes) < 64:
            raise ValueError("Decrypted data is too short to contain padding")
        original_data = decrypted_bytes[64:]  # Remove padding

        return original_data

# 示例用法
if __name__ == "__main__":
    key = "my_secret_key"
    tn_encryption = TNEncryption(key)

    # 原始字节数据
    original_data = b"Hello, \r\nthis is a secret message!"

    # 加密
    encrypted_data = tn_encryption.encrypt(original_data)
    print(f"Encrypted (Base64): {encrypted_data}")

    # 解密
    decrypted_data = tn_encryption.decrypt(encrypted_data)
    print(f"Decrypted message: {decrypted_data.decode('utf-8')}")

这前64个字符,我们就叫它初始向量iv,而这个就是广义上的CBC(密码块链模式)

但是这个模式还有问题:如果我们加密的是这样的数据

{
    "orderId":"123123123",
    "money":"5"
}

加密后:ficeBjozNDluPwkwAyhzSQZ/OQUzTkt7fBkUC0EuUTp5fQU7Nh4OYj4gJhkhPSgbWl8wH0hxW3hePAQAFAErAi9jXxMIHVcRQ34pWwlQRENgfGcEBFdhZmhyWmYIAw8SPVNvc3hpZS8=base64解一下

b'k\x1aGz51\x1d\x10\x02g>\x17\x1dsY5\x19\x029\r2#\x0ca\x06$\x1c))}\x7ft2D]\x17+-\x14s/Z"d;\x00I*8\x05{5\n\x1d>V\x12\x17p?E\x1eG:\tD84"\x12\x1b#\x13kMnt^Y@KWKa@D\x7ft+\x00\r\x13M\x17bf]E\n'

其中money的具体值5在第31位置上也就是对应E如果黑客把这个关键字改了,由于这个不位于16的整数位置上,所以也不会影响后续进程的加密。而且,就算所有的关键数据都在这个16n字节的位数上,也会遭到重放攻击。
于是我们就有了第三版算法:

import random
import string
import hashlib
import base64
import hmac
import time
from itertools import cycle

class TNEncryption:
    def __init__(self, key: str, hmac_secret_key: str):
        self.key = key  # 加密密钥
        self.hmac_secret_key = hmac_secret_key  # HMAC 密钥

    def get_sha256_hash_string(self, input_string: str) -> int:
        sha256_hash = hashlib.sha256()
        sha256_hash.update(input_string.encode('utf-8'))
        return int(sha256_hash.hexdigest(), 16)

    def generate_random_string(self, seed: int, length: int = 16) -> str:
        random.seed(seed)
        characters = string.ascii_letters + string.digits
        return ''.join(random.choice(characters) for _ in range(length))

    def split_bytes_into_groups(self, input_bytes: bytes, group_length: int) -> list:
        return [input_bytes[i:i + group_length] for i in range(0, len(input_bytes), group_length)]

    def generate_hmac(self, data: bytes) -> str:
        """生成数据的 HMAC 签名"""
        hmac_obj = hmac.new(self.hmac_secret_key.encode('utf-8'), data, hashlib.sha256)
        return base64.b64encode(hmac_obj.digest()).decode('utf-8')

    def verify_hmac(self, data: bytes, received_hmac: str) -> bool:
        """验证 HMAC 签名是否匹配"""
        expected_hmac = self.generate_hmac(data)
        return hmac.compare_digest(expected_hmac, received_hmac)

    def get_simple_hash(self,bytes):
        r=0
        for i in bytes:
           r+=i
        return r 

    def encrypt(self, bytes_data: bytes) -> str:
        # 1. 生成当前时间戳
        timestamp = int(time.time())
        timestamp_bytes = str(timestamp).encode('utf-8')

        # 2. 生成随机 64 字节的填充数据
        padding = ''.join(random.choices(string.ascii_letters + string.digits, k=64)).encode('utf-8')

        # 3. 组合时间戳、填充数据和原始数据
        data_to_encrypt = timestamp_bytes + b'||' + padding + bytes_data

        # 4. 加密数据
        seed = self.get_sha256_hash_string(self.key)
        random_str = self.generate_random_string(seed, length=16)
        groups = self.split_bytes_into_groups(data_to_encrypt, len(random_str))
        encrypted_bytes = b''

        for i in range(len(groups)):
            xored_group = bytes([a ^ b for a, b in zip(groups[i], cycle(random_str.encode()))])
            encrypted_bytes += xored_group

            if i % 2 == 0:
                seed += self.get_simple_hash(groups[i])
            else:
                seed -= self.get_simple_hash(groups[i])

            random_str = self.generate_random_string(seed, length=16)

        # 5. 生成 HMAC 签名
        hmac_signature = self.generate_hmac(encrypted_bytes)

        # 6. 拼接 HMAC 签名和加密数据
        combined_data = hmac_signature.encode('utf-8') + b'||' + encrypted_bytes

        # 7. 返回 Base64 编码的结果
        return base64.b64encode(combined_data).decode('utf-8')

    def decrypt(self, encrypted_base64: str) -> bytes:
        try:
            # 1. 解码 Base64
            combined_data = base64.b64decode(encrypted_base64)
        except base64.binascii.Error as e:
            raise ValueError("Invalid Base64 input") from e

        # 2. 分离 HMAC 签名和加密数据
        try:
            hmac_signature, encrypted_bytes = combined_data.split(b'||', 1)
        except ValueError:
            raise ValueError("Invalid encrypted data format")

        # 3. 验证 HMAC 签名
        if not self.verify_hmac(encrypted_bytes, hmac_signature.decode('utf-8')):
            raise ValueError("HMAC signature verification failed")

        # 4. 解密数据
        groups = self.split_bytes_into_groups(encrypted_bytes, 16)
        seed = self.get_sha256_hash_string(self.key)
        decrypted_bytes = b''

        for i, group in enumerate(groups):
            random_str = self.generate_random_string(seed, length=16)
            random_bytes = random_str.encode()
            decrypted_group = bytes([a ^ b for a, b in zip(group, cycle(random_bytes))])
            decrypted_bytes += decrypted_group

            if len(decrypted_group) == 0:
                continue

            first_byte = self.get_simple_hash(decrypted_group)
            if i % 2 == 0:
                seed += first_byte
            else:
                seed -= first_byte

        # 5. 分离时间戳、填充数据和原始数据
        try:
            timestamp_bytes, rest = decrypted_bytes.split(b'||', 1)
            timestamp = int(timestamp_bytes.decode('utf-8'))
            original_data = rest[64:]  # 去除填充数据
        except (ValueError, IndexError):
            raise ValueError("Decrypted data format is invalid")

        # 6. 验证时间戳是否在有效范围内(30 秒内)
        current_time = int(time.time())
        if abs(current_time - timestamp) > 30:
            raise ValueError("Timestamp is invalid or expired")

        return original_data

# 示例用法
if __name__ == "__main__":
    key = "my_secret_key"
    hmac_secret_key = "my_hmac_secret_key"
    tn_encryption = TNEncryption(key, hmac_secret_key)

    # 原始字节数据
    original_data = b"""testest"""

    # 加密
    encrypted_data = tn_encryption.encrypt(original_data)
    print(f"Encrypted (Base64): {encrypted_data}")

    # 解密
    decrypted_data = tn_encryption.decrypt(encrypted_data)
    print(f"Decrypted message: {decrypted_data.decode('utf-8')}")

添加了时间戳和HMAC签名基本上让黑客GG了,自此就完成了一版简单的对称加密算法

暂无评论

发表评论