一、前言

最近项目中需要用到DES,3DES解密算法,所以了解一下。正好CSDN上有关于DES,3DES的资料。边看边写一下总结。

二、参考资料

密码学之DES,3DES详解与Python实现

三、总结

1.定义

DES(Data Encryption Standard-数据加密标准)属于对称加密,即使用相同的密钥来完成加密和解密。分组长度为8个字节64bit(密钥每个字节的最后一位都没有采用,所以有效位只有56位)。

2.算法原理

2.1密钥编排

第一步:
密钥置换(64bit -> 56bit) ,将64位的密钥K通过置换表PC-1置换得到56位的密钥K0
第二步:
将K0分为左右两半各28位,C0和D0
第三步:
根据’左移’时间表移动C0和D0上的位,得到C1,…,C16,D1,…,D16
第四步:
16对Cn和Dn分别合并得到16个56位的密钥。将每个56位的密钥通过置换表PC-2的到16个48位的密钥Kn。
这就通过’原始密钥’,'PC-1’表,'左移’时间表,'PC-2’表完成了密钥拓展。

2.2明文运算

第一步:明文填充与工作模式
des明文分组模式为64比特,当明文长度不够分组大小时,需要指定某种标准进行填充。
第二步:IP盒置换(初始置换)
64bit明文块M与初始置换IP表进行初始置换的到新的64bit的IP
第三步:分为左右各32bit
置换后的IP分为32bit的左半部分L0和32bit的右半部分R0
第四步:16轮运算
使用函数f对两个块(32位的数据块和48位的密钥Kn)进行操作,以生成一个32位的块。+表示XOR加法。从L0和R0开始,迭代16次,得到L16和R16。

Ln = Rn-1
Rn = Ln-1 + F(Rn-1, Kn) # +是XOR运算

一轮运算中的F函数:
Rn-1(32bit)->E(Rn-1)(48bit)->E(Rn-1)+Kn(48bit)->S盒压缩置换(32bit)->P盒置换(32bit)=F(Rn-1, Kn)
在f(Rn-1, Kn)中,首先将Rn-1从32位扩展为48位,这是通过重复Rn-1的一些位的选择表来完成的,这个选择表称为函数E,即E(Rn-1)实现了32位的输入块和48的输出块。
于是f(Rn-1, Kn)=E(Rn-1)+Kn,这样就的到了48位,也就是8组6位。
然后进行S盒压缩置换:根据这8组6位来寻找S盒子中的地址,位于该地址的是一个4位的数字。于是8组6位转化称为了8组4位共32位。
接着进行P盒置换,将刚刚的32位置换为32位,这样F函数就实现了。

然后执行一次异或操作得到Rn。

第五步:逆初始置换(末置换)
将第四步得到的R16和L16合并(R16在左)得到64位,再利用FP(Final Permutation)表置换,得到64位的加密结果。

2.3用到的置换表

中间涉及的置换表有IP(明文置换第一步)、PC_1(密钥置换第一步)、PC_2(密钥置换第四步)、E(明文置换第四步中的F函数中的E函数)、S_BOX(明文置换第四步中的F函数中的S盒)、P(明文置换第四步中的F函数最后的P盒置换)、FP(明文置换第五步)、SHIFT(密钥置换第三步)

四、3DES及DES与AES的区别

1.3DES

3DES是三重DES的意思,它表示明文经过DES密钥1加密,经过DES密钥2解密,再通过DES密钥3加密得到密文的过程。
这个设计出于两个原因:

引用:1.The second key is used to DES-decrypt the encrypted message. (Since the second key is not the right key, this decryption just scrambles the data further.)
第二个密钥用于对加密的消息进行DES解密。 (由于第二个密钥不是正确的密钥,因此该解密只会进一步扰乱数据。)

这个方案由IBM公司设计和提出,理由是这样的话,当三个密钥相同时,3重DES等于普通的DES,这样就可以实现向下的兼容,使用DES的,比如老旧的银行系统等等,兼容性对它们很重要,因此这个方案被采纳了

DES与AES的区别

参考资料:
加密标准中DES与AES到底是什么?两者有啥区别?

总的来说,AES是比DES更先进的加密算法。

五、项目需求及Python实现

1.需求

甲方提供了JS上的加密方式是基于Javascript的AES-192加密算法

const SECRET = "xxxxxxxxxxxx"; // 长为12个字节

function decrypt(encrypted) {
    const decipher = crypto.createDecipher("aes192", SECRET);
    let decrypted = decipher.update(encrypted, "hex", "utf8")
    decrypted += decipher.final("utf8");
    return decrypted;
}

然后后端实现了一个Java版本的AES-192加密算法,其中关键的一步是

keySizeBites = 24; // AES-192加密算法对应192位,即24字节
ivSize = cipher.getBlockSize(); // AES块的大小,固定为128位,即16字节
final byte[][] keyAndIV = EVP_BytesToKey(keySizeBites, ivSize, md5, salt: null, password.getBytes(), count:1);

现在要求用Python实现加密该算法。

2.分析

资料:python之js解密_如何在Python中解密来自JavaScript CryptoJS.AES.encrypt的密码中提到:在JS上,有两种加密密码的方法,它们分别是:

  • A. crypto.createCipher(algorithm, password)
  • B. crypto.createCipheriv(algorithm, key, iv)

而上面的代码块采用的是方法A(它支持通过EVP_BytesToKey函数从提供的密码派生密钥和IV向量),而方法B才相当于python中的AES.new()方法(pyCrypto只支持密钥向量类型加密),因此我们必须手动实现OpenSSL的EVP_BytesToKey函数。

3.Python实现

3.1导入Crypto模块

No module named “Crypto” 解决方案

参考:No module named “Crypto” 解决方案

某些脚本需要用到Crypto库,但当pip install Crypto后仍提示:
No module named ‘Crypto’,解决方案如下:

pip uninstall crypto pycryptodome
pip install pycryptodome

pycryptocrypto是同一个库,cryptopython 中又被称为pycrypto,它是一个第三方库,但是已经停止更新了,所以不建议大家安装。pycryptodomecrypto的延伸版本,用法和crypto是一模一样的,可以完全替代crypto

3.2Python实现EVP_BytesToKey函数

参考资料:在Python中实现OpenSSL AES加密给出了Python中的EVP_BytesToKey函数,它可以基于密码得到密钥和初始向量。

def EVP_BytesToKey(password, salt, key_len, iv_len):
    """
    Derive the key and the IV from the given password and salt.
    """
    from hashlib import md5
    dtot =  md5(password + salt).digest()
    d = [ dtot ]
    while len(dtot)<(iv_len+key_len):
        d.append( md5(d[-1] + password + salt).digest() )
        dtot += d[-1]
    return dtot[:key_len], dtot[key_len:key_len+iv_len]

3.3Python实现AES/CBC/PKCS5Padding 加解密

参考资料:[Python] AES/CBC/PKCS5Padding 加解密给出了Python基于密钥和初始向量实现CBC模式,PKCS5Padding填充的AES-192算法(输出bytes编码为base64)。因为我们的需求是要求hex编码,所以更改后的代码为

from Crypto.Cipher import AES
import base64

def EVP_BytesToKey(password, salt, key_len, iv_len):
    """
    Derive the key and the IV from the given password and salt.
    """
    from hashlib import md5
    dtot =  md5(password + salt).digest()
    d = [ dtot ]
    while len(dtot)<(iv_len+key_len):
        d.append( md5(d[-1] + password + salt).digest() )
        dtot += d[-1]
    return dtot[:key_len], dtot[key_len:key_len+iv_len]

def encrypt(text):
    """
    AES/CBC/PKCS5Padding 加密
    """
    BLOCK_SIZE = AES.block_size
    # 需要加密的文件,不足BLOCK_SIZE的补位(text可能是含中文,而中文字符utf-8编码占3个位置,gbk是2,所以需要以len(text.encode()),而不是len(text)计算补码)
    text = text + (BLOCK_SIZE - len(text.encode()) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(text.encode()) % BLOCK_SIZE)
    # 创建一个aes对象 key 秘钥 mdoe 定义模式 iv#偏移量--必须16字节
    cipher = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    # 利用AES对象进行加密
    encrypted_text = cipher.encrypt(text.encode())

    return base64.b16encode(encrypted_text).decode('utf-8').lower()

def decrypt(encrypted_text):
    """
    AES/CBC/PKCS5Padding 解密
    """
    # 需要解密的文件
    encrypted_text = base64.b16decode(encrypted_text.upper())
    # 创建一个aes对象 key 秘钥 mdoe 定义模式 iv#偏移量--必须16字节
    cipher = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
    # 利用AES对象进行解密
    decrypted_text = cipher.decrypt(encrypted_text)
    # 去除补位
    dec_res = decrypted_text[:-ord(decrypted_text[len(decrypted_text) - 1:])]

    return dec_res.decode()

password = 'xxxxxxxxxxxxx'.encode()           # bytes类型的密码
key_len, iv_len = 24, 16 # AES-192
AESkey = EVP_BytesToKey(password, b'', key_len, iv_len) # 通过密码的到密钥和向量
key, iv = AESkey[0], AESkey[1]
print('密钥: %s\n向量: %s' % (key, iv))
text = 'abcdefghijklmnhi' # 明文
print('明文:', text)
jiami = encrypt(text)
print('密文:', jiami)
jiemi = decrypt(jiami)
print('解密:', jiemi)