@Lenciel

Cryptography and Python

这周我们的一个项目里面需要调用第三方接口。对方的安全机制是调用时要进行加密和校验,但它们的文档没有描述具体是怎么加密的,而是给了一段代码:

    private static BASE64Encoder base64 = new BASE64Encoder();
//  private static byte[] myIV = { 50, 51, 52, 53, 54, 55, 56, 57 };
//  private static byte[] myIV = null;
//  private static String strkey = "W9qPIzjaVGKUp7CKRk/qpCkg/SCMkQRu"; // 字节数必须是8的倍数
    //密钥
    private static String strkey = "NDg5MDY2NjczMxxxxXXXXXyNzUzNTg2";
    private static String removeBR(String str) {
        StringBuffer sf = new StringBuffer(str);

        for (int i = 0; i < sf.length(); ++i)
        {
          if (sf.charAt(i) == '\n')
          {
            sf = sf.deleteCharAt(i);
          }
        }
        for (int i = 0; i < sf.length(); ++i)
        {
          if (sf.charAt(i) == '\r')
          {
            sf = sf.deleteCharAt(i);
          }
        }
        return sf.toString();
    }

    private static String desEncrypt(String input) throws Exception
    {

        BASE64Decoder base64d = new BASE64Decoder();
        DESedeKeySpec p8ksp = null;
        p8ksp = new DESedeKeySpec(base64d.decodeBuffer(strkey));
        Key key = null;
        key = SecretKeyFactory.getInstance("DESede").generateSecret(p8ksp);

        byte[] plainBytes = (byte[])null;
        Cipher cipher = null;
        byte[] cipherText = (byte[])null;
        //「算法/模式/填充」
        plainBytes = input.getBytes("UTF8");
        cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
        SecretKeySpec myKey = new SecretKeySpec(key.getEncoded(), "DESede");
 //       IvParameterSpec ivspec = new IvParameterSpec(myIV);
        cipher.init(1, myKey);
        cipherText = cipher.doFinal(plainBytes);
        return removeBR(base64.encode(cipherText));
    }

    private static String desDecrypt(String cipherText) throws Exception
    {

        BASE64Decoder base64d = new BASE64Decoder();
        DESedeKeySpec p8ksp = null;
        p8ksp = new DESedeKeySpec(base64d.decodeBuffer(strkey));
        Key key = null;
        key = SecretKeyFactory.getInstance("DESede").generateSecret(p8ksp);

        Cipher cipher = null;
        byte[] inPut = base64d.decodeBuffer(cipherText);
        //「算法/模式/填充」
        cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
        SecretKeySpec myKey = new SecretKeySpec(key.getEncoded(), "DESede");
 //       IvParameterSpec ivspec = new IvParameterSpec(myIV);
        cipher.init(2, myKey);
        byte[] output = cipher.doFinal(inPut);
        return new String(output, "UTF8");
    }

很明显,在提供文档的同学看来大家都是 JEE 程序员。仔细看了半天这两个函数desEncryptdesDecrypt外加 Google,才明白是DES3算法。

接着这份文档的后面还有另一个对加密的描述也是代码,不过这次比较明显是 MD5:

public static final String MD5(String s)
{
  char hexDigits[] = {
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    'a', 'b', 'c', 'd', 'e', 'f'
    };
  char str[] = null;
  byte strTemp[] = s.getBytes();
  MessageDigest mdTemp;
  try {
    mdTemp = MessageDigest.getInstance("MD5");
    mdTemp.update(strTemp);
    byte md[] = mdTemp.digest();
    int j = md.length;
    str = new char[j * 2];
    int k = 0;
    for (int i = 0; i < j; i++)
    {
      byte b = md[i];
      str[k++] = hexDigits[b >> 4 & 0xf];
      str[k++] = hexDigits[b & 0xf];
    }
  } catch (NoSuchAlgorithmException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
  }

  return new String(str);
}

这些算法用 python 来实现当然就简单多了:MD5 就是一句话,看起来非常复杂的 DES3 也不过几句话:

from pyDes import triple_des, ECB, PAD_PKCS5
import base64
import datetime

input_str = "test string"

key_base64_str = base64.b64decode("NDg5MDY2NjczMxxxxXXXXXyNzUzNTg2", "utf-8")
key_bytes = key_base64_str.encode('utf-8')
k = triple_des(key_bytes, ECB, pad=None, padmode=PAD_PKCS5)
d = base64.b64encode(k.encrypt(input_str))
print d

当然,要明白这些算法究竟怎么回事才叫认真负责的态度:下面这些内容主要来自这篇文章

Hash

hash 就是给输入的字符串生成一个固定长度的字符串(被称为 hash 值)。理想的 hash 要满足:

  • 根据生成的字符串非常难猜到输入的字符串
  • 任意两个不同的字符串不会生成相同的 hash 值
  • 如果输入字符串没有变生成的 hash 值应该不会变

Don't touch me

hash 函数可以被用来计算 checksum,也可以用来进行数字签名和认证。

MD5

1991 年面世的一种 hash 算法,生成的字符串长度为 128bit。

它的算法详情可以看这里,简单说如下:

  • 首先需要对字符串进行扩展,使其位长对 512 求余的结果等于 448。因此,位长(Bits Length)将被扩展至 N*512+448,N 为一个非负整数,N 可以是零。填充的方法一般是在信息的后面填充一个 1 和无数个 0,直到满足上面的条件时才停止用 0 对信息的填充。
  • 然后,在这个结果后面附加一个以 64 位二进制表示的填充前信息长度。经过这两步的处理,现在的位长是 N*512+448+64 = (N+1)*512,即长度恰好是 512 的整数倍。
  • 最后以 512 位分组来处理输入的信息,且每一分组又被划分为 16 个 32 位子分组,经过了一系列的处理后,算法的输出由四个 32 位分组组成,将这四个 32 位分组级联后将生成一个 128 位散列值。

MD5 对碰撞攻击(不同的输入生成相同的 hash)等攻击的抵抗力比较差。

在 python 中使用 MD5:

import os
from Crypto.Hash import MD5

def get_file_checksum(filename):
    h = MD5.new()
    chunk_size = 8192
    with open(filename, 'rb') as f:
        while True:
            chunk = f.read(chunk_size)
            if len(chunk) == 0:
                break
            h.update(chunk)
    return h.hexdigest()

加密算法

加密算法使用 key 把输入的文本变成加密的文本。有两种加密的方式:分块和流。分块处理的单位是固定大小比如 8 或者 16 个 bytes,流则是一个一个 byte。只有知道了解密的key才能对加密的文本进行解密。

分块

DES 是分块加密的一种,其处理对象的大小是 8 个 bytes。DES 最简单的模式是所谓的ECB( electronic code book)模式,也就是每个 block 都是独立加密,最后组成整个加密后的文本。

Don't touch me

使用 pycrpto 对文本使用DES/ECB加密很简单。假设 key 是10234567,而我们要加密的文本是abcdefgh,那么:

>>> from Crypto.Cipher import DES
>>> des = DES.new('01234567', DES.MODE_ECB)
>>> text = 'abcdefgh'
>>> cipher_text = des.encrypt(text)
>>> cipher_text
'\xec\xc2\x9e\xd9] a\xd0'
>>> des.decrypt(cipher_text)
'abcdefgh'

ECB更健壮的是CFB (Cipher feedback)模式,也就是先组合前面加密的文本和待加密的文本,然后进行加密。

Don't touch me

下面的例子说明了算法的工作流程:待加密的是abcdefghijklmnop,两倍 8bytes。首先生成一个随机的字符串作为初始的iv来生成两个DES对象,一个用来加密一个用来解密。之所以需要这两个对象,是因为feedback值会随着 block 被加密后变化。

>>> from Crypto.Cipher import DES
>>> from Crypto import Random
>>> iv = Random.get_random_bytes(8)
>>> des1 = DES.new('01234567', DES.MODE_CFB, iv)
>>> des2 = DES.new('01234567', DES.MODE_CFB, iv)
>>> text = 'abcdefghijklmnop'
>>> cipher_text = des1.encrypt(text)
>>> cipher_text
"?\\\x8e\x86\xeb\xab\x8b\x97'\xa1W\xde\x89!\xc3d"
>>> des2.decrypt(cipher_text)
'abcdefghijklmnop'

这些算法基于一个个 bytes,所以 block 的大小总是 1 byte。pycrypto 提供了两个这样的算法:ARC4XOR。这种基于流的算法只有一种模式:ECB

下面是一个ARC4的算法,使用了 key 01234567:

>>> from Crypto.Cipher import ARC4
>>> obj1 = ARC4.new('01234567')
>>> obj2 = ARC4.new('01234567')
>>> text = 'abcdefghijklmnop'
>>> cipher_text = obj1.encrypt(text)
>>> cipher_text
'\xf0\xb7\x90{#ABXY9\xd06\x9f\xc0\x8c '
>>> obj2.decrypt(cipher_text)
'abcdefghijklmnop'

应用程序

在程序里面我们常常使用 DES3 对文件进行加密解密操作。一般来说操作对象是文件时,总是分成一个个 chunck 来处理以免占用太多内存。如果读入的 chunck 少于 16bytes,就需要扩展它才能进行加密。

import os
from Crypto.Cipher import DES3

def encrypt_file(in_filename, out_filename, chunk_size, key, iv):
    des3 = DES3.new(key, DES3.MODE_CFB, iv)

    with open(in_filename, 'r') as in_file:
        with open(out_filename, 'w') as out_file:
            while True:
                chunk = in_file.read(chunk_size)
                if len(chunk) == 0:
                    break
                elif len(chunk) % 16 != 0:
                    chunk += ' ' * (16 - len(chunk) % 16)
                out_file.write(des3.encrypt(chunk))

def decrypt_file(in_filename, out_filename, chunk_size, key, iv):
    des3 = DES3.new(key, DES3.MODE_CFB, iv)

    with open(in_filename, 'r') as in_file:
        with open(out_filename, 'w') as out_file:
            while True:
                chunk = in_file.read(chunk_size)
                if len(chunk) == 0:
                    break
                out_file.write(des3.decrypt(chunk))

有了上面定义的这两个函数我们可以这样使用它们:

from Crypto import Random
iv = Random.get_random_bytes(8)
with open('to_enc.txt', 'r') as f:
    print 'to_enc.txt: %s' % f.read()
encrypt_file('to_enc.txt', 'to_enc.enc', 8192, key, iv)
with open('to_enc.enc', 'r') as f:
    print 'to_enc.enc: %s' % f.read()
decrypt_file('to_enc.enc', 'to_enc.dec', 8192, key, iv)
with open('to_enc.dec', 'r') as f:
    print 'to_enc.dec: %s' % f.read()

程序的输出如下:

to_enc.txt: this content needs to be encrypted.

to_enc.enc: ??~?E??.??]!=)??"t?
                                JpDw???R?UN0?=??R?UN0?}0r?FV9
to_enc.dec: this content needs to be encrypted.

Public-key algorithms

上面提到的加密算法的一大问题是双方都需要知道 key。而public-key算法提供了两个 key,一个用来加密,一个用来解密。

Don't touch me

public/private key

使用 pycrpto 很容易就可以生成一对private/public key,生成 key 的时候必须规定 key 的长度,越长越安全。除开长度,还需要设定生成 key 的方法。下面是一个使用 RSA 生成 1024bit 长度 key 的过程:

>>> from Crypto.PublicKey import RSA
>>> from Crypto import Random
>>> random_generator = Random.new().read
>>> key = RSA.generate(1024, random_generator)
>>> key
<_RSAobj @0x7f60cf1b57e8 n(1024),e,d,p,q,u,private>

key 对象有一系列的方法:

  • can_encrypt() 返回是否能用 key 来加密数据
  • can_sign() 返回是否能用 key 来进行签名
  • has_private() 返回是否有 private key
>>> key.can_encrypt()
True
>>> key.can_sign()
True
>>> key.has_private()
True

加密

现在我们有了一对 key,我们就可以加密一些数据了。加密的时候使用的是公钥: public key

>>> public_key = key.publickey()
>>> enc_data = public_key.encrypt('abcdefgh', 32)
>>> enc_data
('\x11\x86\x8b\xfa\x82\xdf\xe3sN ~@\xdbP\x85
\x93\xe6\xb9\xe9\x95I\xa7\xadQ\x08\xe5\xc8$9\x81K\xa0\xb5\xee\x1e\xb5r
\x9bH)\xd8\xeb\x03\xf3\x86\xb5\x03\xfd\x97\xe6%\x9e\xf7\x11=\xa1Y<\xdc
\x94\xf0\x7f7@\x9c\x02suc\xcc\xc2j\x0c\xce\x92\x8d\xdc\x00uL\xd6.
\x84~/\xed\xd7\xc5\xbe\xd2\x98\xec\xe4\xda\xd1L\rM`\x88\x13V\xe1M\n X
\xce\x13 \xaf\x10|\x80\x0e\x14\xbc\x14\x1ec\xf6Rs\xbb\x93\x06\xbe',)

解密

只要有用于解密的私钥(private key)解密是很简单的:

>>> key.decrypt(enc_data)
'abcdefgh'

签名

对信息进行签名,可以用来验证信息的作者,让我们相信它的来源。下面这个例子展示了如何先算出信息的 hash 值,然后送给 RSA key 的sign()方法。使用其他算法如DSA或者是ElGamal也类似。

>>> from Crypto.Hash import MD5
>>> from Crypto.PublicKey import RSA
>>> from Crypto import Random
>>> key = RSA.generate(1024, random_generator)
>>> text = 'abcdefgh'
>>> hash = MD5.new(text).digest()
>>> hash
'\xe8\xdc@\x81\xb144\xb4Q\x89\xa7 \xb7{h\x18'
>>> signature = key.sign(hash, '')
>>> signature
(1549358700992033008647390368952919655009213441715588267926189797
14352832388210003027089995136141364041133696073722879839526120115
25996986614087200336035744524518268136542404418603981729787438986
50177007820700181992412437228386361134849096112920177007759309019
6400328917297225219942913552938646767912958849053L,)

验证

只要有公钥,验证信息就很简单了。未加密的文本和签名一起被发送给接收方。接收方计算 hash 值然后调用公钥的verify()方法来进行验证:

>>> text = 'abcdefgh'
>>> hash = MD5.new(text).digest()
>>> public_key.verify(hash, signature)
True

欢迎留言