Golang与密码学
Thu, Sep 6, 2018
0x0 概述
这篇文档主要聊聊三种加密方式与Golang实现
- 哈希加密
- 对称加密
- 非对称加密
0x1 哈希加密
哈希算法
我们知道,查找中,有「哈希查找」,是一种比「顺序查找」更快的查找方法。
「哈希查找」的关键点,就是实现一种「哈希算法」,使得每个任意key,经过哈希算法计算后,可以获得一个定长的散列值。
- 相同key经过相同哈希算法散列之后,获得相同的散列值
- 不同key经过相同哈西算法散列之后,获得不同的散列值
特点:
- 可以把任意长度的「明文」,散列成固定长度的「指纹」
- 正向计算简单快速,逆向推算困难,基本不可能逆推出明文
- 明文有一点点变化,密文就会改变
- 优秀的散列算法需要尽量避免两个不同的明文,加密出来是相同的指纹
发展:
取模操作 → 异或运算→ 位移操作
密码学里面,一般都通过「位移操作」「取模操作」「异或操作」来实现加密 - 无论对称加密非对称加密、哈希散列加密
成熟Hash算法
- MD5
- SHA
- RIPEMD
Golang相关hash代码片段
MD5
MD5加密结果为16字节串
data := []byte("test string")
s := fmt.Sprintf("%x", md5.Sum(data)) |
m := md5.New() m.Write(content) s := hex.EncodeToString(m.Sum(nil)) |
SHA256
sha256加密方式,通常用在公链中,散列结果为32字节
s := fmt.Sprintf("%x", sha256.Sum256(content)) |
m := sha256.New() m.Write(content) fmt.Println(hex.EncodeToString(m.Sum(nil))) |
文件内容加密
f,_ := os.Open("filename")
h := sha256.New()
io.Copy(h,f)
s := h.Sum(nil)
fmt.Println(hex.EncodeToString(s)) |
RIPEMD160
ripemd160目前只在数字货币中用到了 - 以太坊
三方包:golang.org/x/crypto/ripemd160
可以使用gopm安装
gopm get -v -u golang.org/x/crypto/ripemd160
hasher := ripemd160.New()
hasher.Write([]byte("test string"))
fmt.Println(hex.EncodeToString(hasher.Sum(nil))) |
0x2 对称加密
对称加密,加密完之后,是可以通过密钥解密的 - 和hash加密不一样
常见对称加密算法:
- DES
- AES
补码、去码 & 分组加密
补码:给15个字符做「分组加密」,无法平均分成两组,所以需要补一个码凑成16个字符,这种操作叫做「补码」
分组之后,给两个组分别加密,之后把两部分整合起来,获得最终密文
去码:解密的时候把补码去掉
三种补码方式
- NoPadding:「要补几个码」、「补入的码的内容是什么」API或算法本身不进行定义,而是由加密双方进行约定后,进行填补
- PKCS5Padding:「补足8个字符」、「补入的码内容为:8 - n」
- 如一个分组只有3个字符,差5个字符到8位,所以补5个「数字5」到串中
- 去码的时候,通过最后一位的数字值,就知道补了几个码,拿到需要「去几个码」,然后做切片就好
- PKCS7Padding:可自定义分组长度
补码代码片段
func PKCS5Padding(cipherTxt []byte, blockSize int) []byte {
padding := blockSize - len(cipherTxt)%blockSize
padTxt := bytes.Repeat([]byte{byte(padding)}, padding)
byteTxt := append(cipherTxt, padTxt...)
return byteTxt
} |
去码代码片段
func PKCS5UnPadding(cipherTxt []byte) []byte {
l := len(cipherTxt)
txt := int(cipherTxt[l-1])
return cipherTxt[:l-txt]
} |
DES加密
DES加密中,密钥的长度为:
- DES:8
- 3DES:24
代码片段
加密
func DESEncrypt(origData []byte, key []byte) []byte {
//校验秘钥
block, _ := des.NewCipher(key)
//补码
origData = PKCS5Padding(origData, block.BlockSize())
//设置分组加密模式CBC
blockMode := cipher.NewCBCEncrypter(block, key)
//加密明文
crypted := make([]byte, len(origData))
blockMode.CryptBlocks(crypted, origData)
return crypted
} |
解密
func DESDecrypt(cryted []byte, key []byte) []byte {
//校验key的有效性
block, _ := des.NewCipher(key)
//使用CBC模式解密
blockMode := cipher.NewCBCDecrypter(block, key)
//实现解密
origData := make([]byte, len(cryted))
blockMode.CryptBlocks(origData, cryted)
//去码
origData = PKCS5UnPadding(origData)
return origData
} |
AES加密
Advanced Encryption Standard
AES是DES的替代品,算法更复杂且更安全。明文分组为128位(16字节),密钥长度可为(16、24、32字节)
AES加解密原理

- 左边是加密的过程,右边是解密的过程
- 前9轮用相同的步骤,最后一轮只有三个步骤(少一个步骤)
- AES和DES都经过了「多轮加密」,AES的轮数根据密钥的长度而定
- 16字节:10轮
- 24字节:12轮
- 32字节:14轮
- 密钥角度 - 会做扩展密钥,如图生成了11个子密钥。每一轮加密使用不同的密钥
AES原理动画演示(from: Howard Straubing)
<embed type="application/x-shockwave-flash" width="680" height="400" src="https://coolshell.cn/wp-content/uploads/2010/10/rijndael_ingles2004.swf" quality="high" align="middle" /> |
分组加密
两种密码算法:
- 分组密码(block cipher):每次只处理特定长度的一块数据。分组的比特数就是「分组长度」
- 流密码(stream cipher):对数据流进行连续处理的一类密码算法
- 缺点:需要记录加密到哪了...不便于并行处理
ECB模式
Electronic Code Book mode
将「明文分组」加密后的结果直接作为「密文分组」。最后一组的长度小于分组长度时,做「补码」。


特点:明文分组和密文分组一一对应,观察密文分组能知道明文分组代表什么 - 以前比较好做外挂的网游封包常采用这种加密方式...(参考:手游封包辅助教程)
攻击思路
交换分组顺序攻击法 - 交换密文分组的顺序,解密后明文的顺序就自然发生了改变
举例:分组一、二、三分别代表了付款人、收款人、转账金额,攻击者交换分组一和分组二的顺序...不是操纵算法,而是操纵密文分组的顺序
CBC模式
Cipher Block Chaining
密文分组链接模式

特点:加密的内容,是上一组的「密文分组」和下一组的「明文分组」的异或
- 希望通过这种方式,保证分组之间的顺序不可被篡改
- 一旦交换密文分组的顺序,就不能解出正确的明文
逆运算:异或的逆运算还是异或 - 按位与和按位或没有逆运算
补充:由于加密第一个明文分组的时候,木有和它异或的密文分组,所以需要一个随机数和它做异或。这个随机数叫做「初始化向量IV」
解密:本组密文和上一组得出的明文(或初始化向量)做异或,可以得到本组明文(加密使用的是异或,异或的逆运算还是异或)

- IV和第一组密文,获得第一组明文
- 第一组明文和第二组密文,获得第二组明文
- ...
好处:
- 由于明文和密文不是一一对应的,明文一和明文二内容即使相同,密文也不同。攻击者不能通过观察明文和密文的对应关系,发现密文内容和明文内容的对应关系。
- 顺序相关:要加密分组2的明文,必须知道分组1才行。
- 密文分组丢失任何一个比特,后面的所有密文分组,都无法正确解密出来
攻击思路
如果初始化向量被反转,通过被反转的初始化向量解密出来的明文的分组内容也会被反转

代码片段
//PKCS5Padding 要求分组长度只能为8
//PKCS7Padding 要求分组的长度可以[1-255]
func PKCS7Padding(org []byte, blockSize int) []byte {
pad := blockSize - len(org)%blockSize
padArr := bytes.Repeat([]byte{byte(pad)}, pad)
return append(org, padArr...)
}
//去掉补码
func PKCS7Unpadding(org []byte) []byte {
l := len(org)
//获得数组中最后一个元素值
pad := org[l-1]
return org[:l-int(pad)]
}
//通过CBC分组模式,完成AES的密码过程
//AES 也是对称加密,AES 是DES 的替代品
//AES 秘钥长度,要么16,或者 24, 或者32
func AesCBCEncrypt(org []byte, key []byte) []byte {
//校验秘钥
block, _ := aes.NewCipher(key)
//按照公钥的长度进行分组补码
org = PKCS7Padding(org, block.BlockSize())
//设置AES的加密模式
blockMode := cipher.NewCBCEncrypter(block, key)
//加密处理
cryted := make([]byte, len(org))
blockMode.CryptBlocks(cryted, org)
return cryted
}
//AES解密
func AesCBCDecrypt(cipherTxt []byte, key []byte) []byte {
//校验key
block, _ := aes.NewCipher(key)
//设置解密模式CDC
blockMode := cipher.NewCBCDecrypter(block, key)
//开始解密
org := make([]byte, len(cipherTxt))
blockMode.CryptBlocks(org, cipherTxt)
//去除补码
org = PKCS7Unpadding(org)
return org
}
func main() {
ciphertxt := AesCBCEncrypt([]byte("hello 123"), []byte("1234567890123456"))
fmt.Println("解密后的结果", string(AesCBCDecrypt(ciphertxt, []byte("1234567890123456"))))
} |
CFB模式
Cipher FeedBack
对CBC的安全升级 - 保护初始化向量

对初始化向量做加密处理,然后再和明文做异或,形成密文 - 防止初始化向量被反转
攻击思路
重放攻击(replay attack)
假设第一次发送了3个密文分组abc(对应明文ABC),攻击者截获后两个密文分组bc。
第二次又发送了3个密文分组xyz(对应明文XYZ),攻击者截获并修改密文分组为xbc。
第二次解密时:
- vi + x 获得第一组明文X
- x + b 无法获得有效明文?
- b + c 获得了第一组明文C
解密结果为X?C,攻击者成功把Z篡改成了C

代码片段
//通过CFB分组模式加密
func AesCFBEncrypt(plainTxt []byte, key []byte) []byte {
//key是否合法
block, _ := aes.NewCipher(key)
cipherTxt := make([]byte, aes.BlockSize+len(plainTxt))
iv := cipherTxt[:aes.BlockSize]
//向iv切片数组初始化rand.Reader(随机内存流)
io.ReadFull(rand.Reader, iv)
//设置加密模式为CFB
stream := cipher.NewCFBEncrypter(block, iv)
//加密
stream.XORKeyStream(cipherTxt[aes.BlockSize:], plainTxt)
//cipherTxt 包含了key和明问的两部分加密的内容
return cipherTxt
}
//通过AES算法,利用CFB分组模式解密
func AesCFBDecrypt(cipherTxt []byte, key []byte) []byte {
block, _ := aes.NewCipher(key)
//拆分iv和密文
iv := cipherTxt[:aes.BlockSize]
cipherTxt = cipherTxt[aes.BlockSize:]
//设置解密模式
stream := cipher.NewCFBDecrypter(block, iv)
var des = make([]byte, len(cipherTxt))
//解密
stream.XORKeyStream(des, cipherTxt)
return des
}
func main() {
//对称加密DES,key为8
//对称加密3DES,key为24
//对称加密AES,可以16,24,32
var cipher = AesCFBEncrypt([]byte("hello 123"), []byte("1234567890123456"))
//通过编码,编译用户可以看到的密文
fmt.Println(hex.EncodeToString(cipher))
fmt.Println(base64.StdEncoding.EncodeToString(cipher))
//解密
var des = AesCFBDecrypt(cipher, []byte("1234567890123456"))
fmt.Println(string(des))
} |
OFB模式
每组明文分组生成「密文分组」时,所用的密钥都不同 - 保证了CFB的攻击模式不能遂
- 分组2用的密钥由分组1用的密钥二次加密而成
特点:流密码


代码片段
// 加密
func AesEncrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
// NewOFB返回一个在输出反馈模式下使用分组密码b进行加密或解密的Stream。初始化矢量iv的长度必须等于b的块大小
stream := cipher.NewOFB(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return ciphertext, nil
}
// 解密
func AesDecrypt(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
iv := ciphertext[:aes.BlockSize]
if len(ciphertext) < aes.BlockSize {
panic("ciphertext too short")
}
plaintext2 := make([]byte, len(ciphertext))
stream := cipher.NewOFB(block, iv)
stream.XORKeyStream(plaintext2, ciphertext[aes.BlockSize:])
return plaintext2, nil
} |
CTR模式
引入「计数器」的加密方式,通过对计数器加密,获得本明文分组所需要的密钥 - 也可以保证CFB的攻击模式不能遂

(加解密步骤一致)
特点:
- 使用了流密码
- 加解密使用了完全相同的结构,容易实现
- 可以以任意顺序解密分组 - 只要知道分组序号就好 - 所以,可以「并行解密」
代码片段
// 加密
func AesEncrypt(plaintext, key []byte) ([]byte, error) {
// 申明初始化获取一个新的密钥块。关键参数应该是AES密钥,16,24或32个字节来选择AES-128,AES-192或AES-256。
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// 切片处理申明初始化一个较大长度的新字符串变量
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
// 申明初始化,同时调用加密函数得到流接口
stream := cipher.NewCTR(block, iv)
// 流处理
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return ciphertext, nil
}
// 解密
func AesDecrypt(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
iv := ciphertext[:aes.BlockSize]
if len(ciphertext) < aes.BlockSize {
panic("ciphertext too short")
}
plaintext2 := make([]byte, len(ciphertext))
// 申明初始化,同时调用加密函数得到流接口
stream := cipher.NewCTR(block, iv)
stream.XORKeyStream(plaintext2, ciphertext[aes.BlockSize:])
return plaintext2, nil
} |
0x3 非对称加密 & 验签
非对称加密
概述
有一对儿密钥,其中一个是公开的(公钥),另一个是保密的(私钥)
通过「公钥」不能得到「私钥」
对称加密的问题
密钥配送问题
- 传密文的时候需要一并传输密钥,会导致中间人把密钥也拿到
解决方案:
- 加/解密双方事先共享密钥,不传输
- 密钥分配中心分配密钥
- 通过Diffie-Hellman密钥交换来解决
- 通过非对称加密解决
常用非对称加密算法
- RSA
- 椭圆加密(比特币用到)
RSA算法解析
RSA - 三位开发者的首字母组成的名字
加密原理

明文的E次幂,对N取余,即可得到密文。 - 「E和N的组合」就是「公钥」
解密原理

密文的D次幂,对N取余,即可得到明文 - 「D和N的组合」就是「私钥」
生成密钥对
我们知道,所谓的密钥对,其实就是计算生成「N、E、D」三个数字
为了生成这三个数字,我们需要引入一个临时数:L
求N
N = p x q (p与q为质数)
p与q如果过小,容易被破译
p与q如果过大,计算时间会变长
求L
L使p - 1和q - 1的最小公倍数,表示为:L = lcm(p - 1, q - 1)
求E
1 < E < L
gcd(E, L) = 1 (E和L的最大公约数为1)
满足条件的E可能有很多,随机选一个就好
求D
1 < D < L
(E x D) % L = 1
只要E确定了,D的值就是唯一的14
图示

计算密钥对 & 加解密模拟
生成公钥 & 私钥
求N:N = q * p = 17 * 19 = 323
求L:L = lcm(p - 1, p - 1) = lcm(16, 18) = 144
求E:gcd(E, L) = 1 即gcd(E, 144) = 1, 任取E = 5
求D:E * D % L = 1 即5 * D % 144 = 1, 得D = 29
由上:
- 公钥:E = 5 N = 323
- 私钥:D = 29 N = 323
加密
待加密信息为:123,公钥E=5 N=323
由
,密文 = 255
解密
由
,明文 = 123
代码片段
使用外部传入的公/私钥加解密
// 加密
func RSAEncrypt(origData []byte, pubKey []byte) []byte {
//公钥加密
block, _ := pem.Decode(pubKey)
//解析公钥
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
//加载公钥
pub := pubInterface.(*rsa.PublicKey)
//加密明文
bits, _ := rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
//bits为加密的密文
return bits
}
// 解密
func RSADecrypt(origData []byte, priKey []byte) []byte {
block, _ := pem.Decode(priKey)
//解析私钥
priv, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
//解密
bts, _ := rsa.DecryptPKCS1v15(rand.Reader, priv, origData)
//返回明文
return bts
} |
使用Golang生成密钥对加解密
func main() {
//创建私钥
priv, _ := rsa.GenerateKey(rand.Reader, 1024)
fmt.Println("私钥为:", priv)
//通过私钥创建公钥
pub := priv.PublicKey
//加密
org := []byte("hello China")
//通过oaep函数实现公钥加密
//EncryptOAEP的第一参数的作用为,将不同长度的明文,通过hash散列实现相同长度的散列值,此过程就是生成密文摘要过程
cipherTxt, _ := rsa.EncryptOAEP(md5.New(), rand.Reader, &pub, org, nil)
//打印密文
fmt.Println(cipherTxt)
fmt.Println(base64.StdEncoding.EncodeToString(cipherTxt))
//解密
plaintext, _ := rsa.DecryptOAEP(md5.New(), rand.Reader, priv, cipherTxt, nil)
//打印明文
fmt.Println(plaintext)
fmt.Println(string(plaintext))
} |
攻击:中间人
中间人拦截公钥,窃听私钥发过来的加密消息,用公钥伪造ack内容...
数字签名
概述
只有信息发送者才能产生的,别人无法伪造的一段数字串,可以作为发送者发送信息真实性的证明
生成签名:用私钥给消息加密生成的指纹
验证签名:用公钥解密指纹得到明文,把明文和消息做比较,如果一致,说明消息木有被篡改

签名方案
- 直接对消息签名
- 对消息的散列值签名(计算更便捷)
签名算法
- RSA
- DSA
- ECC - 利用椭圆曲线密码来实现的数字签名算法
代码片段
RSA签名 & 验签
func main() {
//生成私钥
priv, _ := rsa.GenerateKey(rand.Reader, 1024)
//通过私钥生成公钥
pub := &priv.PublicKey
//通过hash散列对准备签名的名为做hash散列
plaitxt := []byte("hello world")
//实现散列过程
h := md5.New()
h.Write(plaitxt)
hashed := h.Sum(nil)
//通过pss函数,实现对明文hello world的签名
//pss函数可以加盐,能够使得签名过程更安全
opts := rsa.PSSOptions{rsa.PSSSaltLengthAuto, crypto.MD5}
//实现签名
sig, _ := rsa.SignPSS(rand.Reader, priv, crypto.MD5, hashed, &opts)
//sig就是RSA对“hello world”签名结果
fmt.Println(sig)
//通过公钥实现验签
err := rsa.VerifyPSS(pub, crypto.MD5, hashed, sig, &opts)
if err == nil {
fmt.Println("验签成功")
}
} |
DSA签名 & 验签
DSA - 专业做数字签名的技术方案 - 不能用于加密和解密
func main() {
//设置私钥使用的参数
var param dsa.Parameters
dsa.GenerateParameters(¶m, rand.Reader, dsa.L1024N160)
//创建私钥
var pri dsa.PrivateKey
pri.Parameters = param
//生成私钥
dsa.GenerateKey(&pri, rand.Reader)
//创建公钥
pub := pri.PublicKey
message := []byte("hello world")
//签名
r, s, _ := dsa.Sign(rand.Reader, &pri, message)
//公钥验签
if dsa.Verify(&pub, message, r, s) {
fmt.Println("验签成功")
}
} |
ECC(椭圆加密)签名 & 验签
ECC椭圆加密
- 由DSA改进而成
- 密钥可以比RSA更短,安全性可以比RSA更高
- 比特币就是用ecc作签名的
func main() {
message := []byte("hello world")
//生成私钥
//elliptic.P256()设置生成私钥为256
privatekey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
//创建公钥
publickey := privatekey.PublicKey
//hash散列明文
digest := sha256.Sum256(message)
//用私钥签名
r, s, _ := ecdsa.Sign(rand.Reader, privatekey, digest[:])
//设置私钥的参数类型
param := privatekey.Curve.Params()
//获取私钥的长度(字节)
curveOrderBytes := param.P.BitLen() / 8
//获得签名返回的字节
rByte, sByte := r.Bytes(), s.Bytes()
//创建数组
signature := make([]byte, curveOrderBytes*2)
copy(signature[:len(rByte)], rByte)
copy(signature[len(sByte):], sByte)
//现在signature中就存放了完整的签名的结果
//验签
digest = sha256.Sum256(message)
//获得公钥的字节长度
curveOrderBytes = publickey.Curve.Params().P.BitLen() / 8
//创建大整数类型保存rbyte,sbyte
r, s = new(big.Int), new(big.Int)
r.SetBytes(signature[:curveOrderBytes])
s.SetBytes(signature[curveOrderBytes:])
//开始认证
e := ecdsa.Verify(&publickey, digest[:], r, s)
if e == true {
fmt.Println("验签成功")
}
} |