工作中经常涉及和不同编程语言使用3DES加解密的对接问题,很多语言对于同一种加密算法的细节处理还是有些出入的,典型的如加密模式、填充方法。
3DES加解密简介
DES使用分块加密,加密密钥为56位,块大小为64位。由于56位版本的DES加密非常容易破解(一般机器一天以内),就有了和DES兼容性较好的3DES算法。
原理
3DES加密算法表示为C=EncryptK3(DecryptK2(EncryptK1(message)
,如果K1、K2、K3为密钥,如果各不相同,则相当于加密密钥长度为112(由于中途相遇攻击);解密算法表示为message=DecryptK1((EncryptK2(DecryptK3(C)))
以加密算法举例:
- 使用了3次DES算法,有3个密钥
- 加密时第一个密钥K1用来加密消息(P),输出C1密文
- 第二个密钥K2用来解密C1,输出C2密文
- 第三个密钥K3用来加密C2,输出C3密文
分组密码工作模式
分组密码的工作模式允许使用同一个分组密码密钥对多于一块的数据进行加密,并保证其安全性。分组密码自身只能加密长度等于密码分组长度的单块数据,若要加密变长数据,则数据必须先被划分为一些单独的密码块。通常而言,最后一块数据也需要使用合适填充方式将数据扩展到匹配密码块大小的长度。一种工作模式描述了加密每一数据块的过程,并常常使用基于一个通常称为初始化向量的附加输入值以进行随机化,以保证安全。
对于分组密码工作模式的介绍可参考wiki
常见的工作模式包括ECB,CBC,OFB和CFB等。
分组密码初始化向量(IV)
用于将加密随机化的一个位块,由此即使同样的明文被多次加密也会产生不同的密文,避免了较慢的重新产生密钥的过程。
分组密码填充
块密码只能对确定长度的数据块进行处理,而消息的长度通常是可变的。因此部分模式(即ECB和CBC)需要最后一块在加密前进行填充。
加密后字节转文本
对消息加密后通常会得到字节数组,在传输时一般会使用BASE64或HEX等进行编码。
多语言举例
统一起见,工作模式选择ECB,填充模式选择PKCS5Padding或使用8-byte块大小的PKCS7Padding(两种填充模式差别可参考StackExchange),使用base64将加密后字节转换为文本密文。
大家在使用时,一定记得要明确工作模式(mode of operation)和填充模式(padding),因为不同语言(甚至同一种语言不同版本)中对于工作模式和填充模式的实现是可以自由选择的。
Java
代码
public static String encrypt(String text, String key){
try {
DESedeKeySpec spec = new DESedeKeySpec(key.getBytes());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("desede");
Key destKey = keyFactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("desede" + "/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, destKey);
byte[] textBs = text.getBytes("UTF-8");
byte[] cipherText = cipher.doFinal(textBs);
BASE64Encoder b64encoder = new BASE64Encoder();
return b64encoder.encode(cipherText);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
return null;
}
public static String decrypt(String decryptedText, String key){
try {
BASE64Decoder b64decoder = new BASE64Decoder();
byte[] textBs = b64decoder.decodeBuffer(decryptedText);
DESedeKeySpec spec = new DESedeKeySpec(key.getBytes());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("desede");
Key destKey = keyFactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("desede" + "/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, destKey);
byte[] cipherText = cipher.doFinal(textBs);
return new String(cipherText,"UTF-8");
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
注意
注意在使用Cipher.getInstance
选择加密解密算法时,如果选择desede,也即忽略工作模式和填充模式后,将使用默认的"/ECB/PKCS5Padding"。由于其他语言的默认方式可能不同,将导致不同语言在加解密时异常。
使用到生产环境时,需要处理异常
Python
代码
Python2
# encoding:utf-8
from Crypto.Cipher import DES3
import base64
# 加密模式 ECB , 填充模式 PKCS5Padding
BS = DES3.block_size
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
'''
DES3加密
text 待加密字符串
key 密钥,使用appsecret
'''
def encrypt(text, key):
text = pad(text)
cipher = DES3.new(key,DES3.MODE_ECB)
m = cipher.encrypt(text)
m = base64.b64encode(m)
return m.decode('utf-8')
'''
DES3解密
decrypted_text 解密字符串
key 密钥,使用appsecret
'''
def decrypt(decrypted_text, key):
text = base64.b64decode(decrypted_text)
cipher = DES3.new(key, DES3.MODE_ECB)
s = cipher.decrypt(text)
s = unpad(s)
return s.decode('utf-8')
if __name__ == '__main__':
print encrypt('Hello','1234567887654321')
print decrypt('qO8nDeYzqTs=','1234567887654321')
Python3
# encoding:utf-8
from Crypto.Cipher import DES3
import base64
# 加密模式 ECB , 填充模式 PKCS5Padding
BS = DES3.block_size
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-s[-1]]
'''
DES3加密
参数:
text str 待加密字符串
key str 密钥,使用appsecret
返回:
str 加密后的字符串
'''
def encrypt(text, key):
text = pad(text)
cipher = DES3.new(key,DES3.MODE_ECB)
m = cipher.encrypt(text) # m is bytes
#m = base64.encodestring(m) # 会追加\n
m = base64.b64encode(m)
decrypted_text = m.decode('utf-8')
return decrypted_text
'''
DES3解密
参数:
decrypted_text str 解密字符串
key str 密钥,使用appsecret
返回:
str 解密后的原始字符串
'''
def decrypt(decrypted_text, key):
decrypted_bytes = bytes(decrypted_text, encoding='utf-8')
#text = base64.decodestring(decrypted_bytes) #
text = base64.b64decode(decrypted_bytes) #
cipher = DES3.new(key, DES3.MODE_ECB)
s = cipher.decrypt(text)
s = unpad(s)
s = s.decode('utf-8') # unpad and decode bytes to str
return s
if __name__ == '__main__':
print(encrypt('Hello','1234567887654321'))
print(decrypt('qO8nDeYzqTs=','1234567887654321'))
注意
注意base64的encodestring已不推荐使用,且会在末尾加入"\n",使用b64encode代替。
python2中cipher.decrypt
和cipher.encrypt
以及base64.b64encode
和base64.b64decode
返回的是str
,而python3中返回的是bytes
。所以python3中unpad时无需再使用ord获取字符的对应ascii数值。
NodeJS
代码
var crypto = require('crypto');
var key = '123456781234567812345678'; //加密的秘钥 app secret
var demo_encryped = 'oVmfzWxhH88=';
var demo_decryped = 'Hello';
encrypt = function (str) {
var cipher = crypto.createCipheriv('des-ede3', key, new Buffer(0));
var crypted = cipher.update(str, 'utf8', 'base64'); // data[, input_encoding][, output_encoding]
crypted += cipher.final('base64');
return crypted;
}
decrypt = function(str) {
var decipher = crypto.createDecipheriv('des-ede3', key, new Buffer(0));
var dec = decipher.update(str, 'base64', 'utf8');
dec += decipher.final('utf8');
return dec;
}
console.log(encrypt(demo_decryped))
console.log(decrypt(demo_encryped))
注意
nodejs中的crypto
依赖openssl,使用>openssl list-cipher-algorithms
获取支持的算法列表。 如果需要选择其他的算法,可参考openssl。
desede其实是3des的别称(反过来说也可以)。
注意使用nodejs时,要使用createDecipheriv
,IV可传入new Buffer(0)
CSharp
代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Security.Cryptography;
namespace ConsoleApplication4
{
class Program
{
public static string Encrypt3DES(string a_strString, string a_strKey)
{
TripleDESCryptoServiceProvider DES = new TripleDESCryptoServiceProvider();
DES.Key = ASCIIEncoding.ASCII.GetBytes(a_strKey);
DES.Mode = CipherMode.ECB;
ICryptoTransform DESEncrypt = DES.CreateEncryptor();
byte[] Buffer = UTF8Encoding.UTF8.GetBytes(a_strString);
return Convert.ToBase64String(DESEncrypt.TransformFinalBlock(Buffer, 0, Buffer.Length));
}
public static string Decrypt3DES(string a_strString, string a_strKey)
{
TripleDESCryptoServiceProvider DES = new TripleDESCryptoServiceProvider();
DES.Key = ASCIIEncoding.ASCII.GetBytes(a_strKey);
DES.Mode = CipherMode.ECB;
DES.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
ICryptoTransform DESDecrypt = DES.CreateDecryptor();
string result = "";
try
{
byte[] Buffer = Convert.FromBase64String(a_strString);
byte[] temp = DESDecrypt.TransformFinalBlock(Buffer, 0, Buffer.Length);
result = UTF8Encoding.UTF8.GetString(temp);
}
catch (Exception e)
{
Console.WriteLine("A Cryptographic error occurred: {0}", e.Message);
}
return result;
}
static void Main()
{
// Console.WriteLine("hello world");
// Console.ReadLine();
System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
string sourceTxt = "Hello";
string appSecert = "123456781234567812345678";
string result = Encrypt3DES(sourceTxt, appSecert);
Console.WriteLine(result);
Console.ReadLine();
string result2 = Decrypt3DES(result, appSecert);
Console.WriteLine(result2);
Console.ReadLine();
}
}
}
PHP
代码
<?php
//定义3DES加密key
define('CRYPT_KEYS', '123456781234567812345678');
class DES {
private $key = CRYPT_KEYS;
//只有CBC模式下需要iv,其他模式下iv会被忽略
private $iv = '';
/**
* 加密
*/
public function encrypt($value) {
//先确定加密模式,此处以ECB为例
$td = mcrypt_module_open ( MCRYPT_3DES,'', MCRYPT_MODE_ECB,'');
//$iv = pack ( 'H16', $this->iv );
$value = $this->PaddingPKCS7 ( $value ); //填充
//$key = pack ( 'H48', $this->key );
mcrypt_generic_init ( $td, $this->key, $this->iv);
$ret = base64_encode ( mcrypt_generic ( $td, $value ) );
mcrypt_generic_deinit ( $td );
mcrypt_module_close ( $td );
return $ret;
}
/**
* 解密
*/
public function decrypt($value) {
$td = mcrypt_module_open ( MCRYPT_3DES, '', MCRYPT_MODE_ECB, '' );
//$iv = pack ( 'H16', $this->iv );
//$key = pack ( 'H48', $this->key );
mcrypt_generic_init ( $td, $this->key,$this->iv );
$ret = trim ( mdecrypt_generic ( $td, base64_decode ( $value ) ) );
$ret = $this->UnPaddingPKCS7 ( $ret );
mcrypt_generic_deinit ( $td );
mcrypt_module_close ( $td );
return $ret;
}
private function PaddingPKCS7($data) {
$padlen = 8 - strlen( $data ) % 8 ;
for($i = 0; $i < $padlen; $i ++)
$data .= chr( $padlen );
return $data;
}
private function UnPaddingPKCS7($data) {
$padlen = ord (substr($data, (strlen( $data )-1), 1 ) );
if ($padlen > 8 )
return false;
for($i = -1*($padlen-strlen($data)); $i < strlen ( $data ); $i ++) {
if (ord ( substr ( $data, $i, 1 ) ) != $padlen)return false;
}
return substr ( $data, 0, -1*($padlen-strlen ( $data ) ) );
}
}
CPP
代码
#include <string>
#include <openssl/des.h>
#include <openssl/evp.h>
#pragma comment(lib, "libcrypto.lib")
#pragma comment(lib, "libssl.lib")
class Des {
private:
static const int CFBMODE = 64;
public:
static std::string encrypt(std::string tmp_key, std::string in) {
unsigned char key[24];
memset(key, 0, sizeof(key));
memcpy(key, tmp_key.c_str(), sizeof(key));
DES_key_schedule schedule;
DES_key_schedule schedule2;
DES_key_schedule schedule3;
DES_cblock desKey = { 0 };
memcpy(desKey, key, 8);
DES_set_key_unchecked(&desKey, &schedule);
memcpy(desKey, key + 8, 8);
DES_set_key_unchecked(&desKey, &schedule2);
memcpy(desKey, key + 16, 8);
DES_set_key_unchecked(&desKey, &schedule3);
int i = 0;
const size_t paddingLength = (8 - in.length() % 8);
unsigned char padding[8];
memset(padding, 8 - (in.length() % 8), 8);
if (paddingLength)
{
in.append((char*)padding, paddingLength);
}
size_t dataLength = paddingLength + in.length();
char tmp_buf[8];
memset(tmp_buf, 0, 8);
std::string data;
//data.reserve(dataLength);
unsigned char input[8];
for (size_t i = 0; i < (dataLength / 8); i++)
{
/*
DES_ede3_cfb_encrypt((const unsigned char*)in.c_str() + (i * 8), (unsigned char*)tmp_buf, CFBMODE, 8, &schedule,
&schedule2, &schedule3, &iv,
DES_ENCRYPT);
*/
//memset(input, 0, 8);
memcpy(input, in.c_str() + (i * 8), 8);
DES_ecb3_encrypt((const_DES_cblock*)input, (DES_cblock*)tmp_buf,&schedule, &schedule2, &schedule3, DES_ENCRYPT);
data.append(tmp_buf, sizeof(tmp_buf));
}
return data;
}
static std::string decrypt(std::string tmp_key, std::string in) {
//偏移向量
//24位加密key,3des下秘钥必须为24位
unsigned char key[24];
memset(key, 0, sizeof(key));
memcpy(key, tmp_key.c_str(), sizeof(key));
DES_key_schedule schedule;
DES_key_schedule schedule2;
DES_key_schedule schedule3;
DES_cblock desKey = { 0 };
DES_cblock iv = { 0 };
memcpy(desKey, key, 8);
DES_set_key_unchecked(&desKey, &schedule);
memcpy(desKey, key + 8, 8);
DES_set_key_unchecked(&desKey, &schedule2);
memcpy(desKey, key + 16, 8);
DES_set_key_unchecked(&desKey, &schedule3);
int i = 0;
std::string padding;
unsigned char input[8];
unsigned char tmp_buf[8];
//memset(tmp_buf, 0, 8);
for (size_t i = 0; i < (in.length() / 8); i++)
{
/*
DES_ede3_cfb_encrypt((const unsigned char*)in.c_str() + (i * 8), (unsigned char*)tmp_buf, CFBMODE, 8, &schedule,
&schedule2, &schedule3, &iv,
DES_DECRYPT);
*/
memcpy(input, in.c_str() + (i * 8), 8);
DES_ecb3_encrypt((const_DES_cblock*)input, (DES_cblock*)tmp_buf, &schedule,
&schedule2, &schedule3, DES_DECRYPT);
if (i == ((in.length() / 8) - 1) && tmp_buf[7] >= 0x01 && tmp_buf[7] <= 0x08)
{
padding.append((char *)tmp_buf, 8 - tmp_buf[7]);
}
else
{
padding.append((char *)tmp_buf, 8);
}
}
return padding;
}
};
C++不懂,同事帮忙写的。
以上代码可以在我的Github上找到。
参考:
1 A Security Site
2 使用 PyCrypto 进行 AES/ECB/PKCS#5(7) 加密
3 https://github.com/eXcellme/tripledes-demo
4 PKCS5Padding和PKCS7Padding的区别
5 StackOverflow Difference between DESede and TripleDES for cipher.getInstance()
6 Python2.7 base64 doc , Python3.6 base64 doc
7 GrepCode com.sun.crypto.provider.DESedeCipher OpenJDK8实现
8 YouTube - AES DES 3DES简介
9 YouTube - ECB vs CBC
10 Nodejs crypto api
11 中途相遇攻击Wiki
12 分组密码工作模式Wiki