对称加密算法简介和编程实践

众所周知,加密算法是用来加密数据的,可以分为对称加密和非对称加密两种。很多人可能不知道非对称加密,但一定用过至少听过对称加密,因为对称加密就是大众眼中的加密。

本文简单介绍下对称加密算法的基本原理,并提供一些编程例子,讲解如何在程序中对数据进行加密。开始之前,我们先来了解几个概念:

  • 加密前的原始数据,叫做 原文original text ),或者 明文plain text );
  • 加密后的不规则( scrambled )数据,通常叫做 密文cipher text );
  • 加密所用的密码,通常叫做 密钥secret key );

对称加密算法最大的特点是,它只有一把密钥,加密和解密过程用的都是同一把密钥:

这也符合大众对加密算法的认知,用密码对数据进行加密之后,必须用同一个密码才能将数据解密出来。那么,对称加密算法都有哪些呢?哪些算法安全性比较高呢?

  • AES ,高级加密标准,新一代加密算法标准,速度快,安全级别高;
  • DES ,数据加密标准,速度较快,适用于加密大量数据,但安全性较弱;
  • Blowfish ,使用变长密钥,运行速度很快,非专利算法,没有使用限制;
  • etc
安全级别 ( Security Level ) 工作因素 ( Work Factor ) 算法 ( Algorithm )
薄弱 ( Weak ) O(240) DES
传统 ( Legacy ) O(264) RC4
基准 ( Baseline ) O(280) 3DES
标准 ( Standard ) O(2128) AES-128
较高 ( High ) O(2192) AES-192
超高 ( Ultra ) O(2256) AES-256

根据安全性,对称加密算法应该优先选择 AES ,位数尽可能大,最好是 AES-256

AES

AES 算法对明文加密时,是以块为单位的进行的。一块的长度是 128 位,折合 16 字节。明文拆成一个个等长块,经过加密器复杂处理后,得到一个个密文块,最后再拼接起来。

由于明文不一定是块大小的整数倍,因此最后一块很可能不足 128 位,这时就必须对数据进行填充补齐。而补齐方式又分为好几种,比如 PKCS5PKCS7 等等。当然了,也有加密模式不需要填充。

说到加密模式,AES 共支持 5 种加密模式,分别是:

  • ECB ,电码本模式( electronic cookbook );
  • CBC ,密码分组链接模式( cipher block chaining ),需要填充;
  • CTR ,计算器模式( couter );
  • CFB ,密码反馈模式( cipher feedback );
  • OFB ,输出反馈模式( output feedback );

AES 密钥长度也有 3 种不同选择(为确保安全,建议尽量选最长的密钥):

  • 16 字节,即 128 位;
  • 24 字节,即 192 位;
  • 32 字节,即 256 位;

AES 加密时还需要一个初始矩阵 IV ,它的作用相当于计算哈希值时用的盐。如果没有加盐,那么相同的明文加密后得到的密文都是一样的,那么密钥就有可能通过彩虹表破解出来。IV 长度跟密钥一样,通常是随机生成的。因解密需要到,IV 通常跟密文拼在一起,解密时直接提取出来用。

IV 只是为了让相同的明文加密出来的密文没有规律可寻,因此公开了也没有关系,跟哈希盐一样。但密钥就不一样了,必须妥善保管,一旦泄露别人就可以轻而易举地解密数据了。

代码实现

这段代码以 Go 语言为例,来编码 AES 加解密步骤:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"math/rand"
	"time"
)

type AesCipher struct {
	block      cipher.Block
	blockBytes int
}

func NewAesCipher(key []byte, bits int) (*AesCipher, error) {
	// 对密钥算哈希,这样密钥不必是固定长度
	hash := sha256.Sum256(key)
	key = hash[:bits>>3]

	// 以哈希值为密钥,初始化加密器
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	return &AesCipher{
		block:      block,
		blockBytes: block.BlockSize(),
	}, nil
}

func (a *AesCipher) Encrypt(plaintext []byte, b64 bool) ([]byte, error) {
	// 对明文进行填充
	plaintext = Pkcs5Padding(plaintext, a.blockBytes)

	// 随机生成初始向量
	iv := make([]byte, a.blockBytes)
	if _, err := rand.Read(iv); err != nil {
		return nil, err
	}

	// 为密文分配空间(为拼接IV预留空间),并完成加密
	ciphertext := make([]byte, len(plaintext), len(plaintext)+len(iv))
	cipher.NewCBCEncrypter(a.block, iv).CryptBlocks(ciphertext, plaintext)

	// 将IV拼接在密文后面,两者合在一起作为加密结果
	result := append(ciphertext, iv...)

	// 由于结果是无序的二进制字节序列,可以做BASE64编码(可选)
	if b64 {
		result = Base64Encode(base64.StdEncoding, result)
	}

	return result, nil
}

func (a *AesCipher) Decrypt(cipherdata []byte, b64 bool) ([]byte, error) {
	// 如果做了BASE64编码,需要先解密
	if b64 {
		var err error
		cipherdata, err = Base64Decode(base64.StdEncoding, cipherdata)
		if err != nil {
			return nil, err
		}
	}

	// 计算密文长度
	textBytes := len(cipherdata) - a.blockBytes

	// 切出密文和IV
	iv := cipherdata[textBytes:]
	ciphertext := cipherdata[:textBytes]

	// 为明文分配空间并解密
	plaintext := make([]byte, len(ciphertext))
	cipher.NewCBCDecrypter(a.block, iv).CryptBlocks(plaintext, ciphertext)

	// 取出填充字符
	return Pkcs5Unpadding(plaintext), nil
}

func Pkcs5Padding(data []byte, blockSize int) []byte {
	padding := blockSize - len(data)%blockSize
	paddings := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(data, paddings...)
}

func Pkcs5Unpadding(data []byte) []byte {
	bytes := len(data)
	padding := int(data[bytes-1])
	return data[:bytes-padding]
}

func Base64Encode(enc *base64.Encoding, data []byte) []byte {
	result := make([]byte, enc.EncodedLen(len(data)))
	enc.Encode(result, data)
	return result
}

func Base64Decode(enc *base64.Encoding, data []byte) ([]byte, error) {
	result := make([]byte, enc.DecodedLen(len(data)))
	if n, err := enc.Decode(result, data); err != nil {
		return nil, err
	} else {
		return result[:n], nil
	}
}

func main() {
  // 设置随机数种子(随机生成IV)
	rand.Seed(time.Now().UnixMicro())

	cipher, err := NewAesCipher([]byte("abc"), 256)
	if err != nil {
		panic(err)
	}

	for _, text := range [][]byte{
		[]byte("fasionchan"),
		[]byte("小菜学编程"),
		[]byte("fasionchan.com"),
		[]byte("coding-fans"),
		[]byte("https://fasionchan.com"),
		[]byte("Python源码剖析"),
	} {
		ciphertext, err := cipher.Encrypt(text, true)
		if err != nil {
			panic(err)
		}

		plaintext, err := cipher.Decrypt(ciphertext, true)
		if err != nil {
			panic(err)
		}

		if bytes.Compare(plaintext, text) != 0 {
			panic(fmt.Sprintf("case failed: %s", text))
		}
	}
}

代码不再赘述,结合注释和前面的示意图,应该很好理解。

小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: