我使用密码脚本加密的安全性如何? (Golang, AES256, pbkdf2, hmac)
How secure is my encryption with password script? (Golang, AES256, pbkdf2, hmac)
首先,我想说这只是一个学习练习,我不打算在生产中使用它。
我用 Golang 写了一个小应用程序,有两个函数:encrypt(plaintext string, password string)
和 decrypt(encrypted string, password string)
加密步骤为:
- 生成随机的 256 位用作盐
- 生成 128 位用作初始化向量
- 使用 PDKDF2 从密码和 salt 生成 32 位密钥
- 用密钥和明文生成一个32位的HMAC,并将其附加到明文的开头
- 在CFB模式下用AES加密hmac+明文
返回的字节数组如下所示:
[256 bit salt] [128 bit iv] encrypted([256 bit hmac] [plaintext])
解密时:
- 提取盐并将其与提供的密码一起使用来计算密钥
- 提取 IV 并解密密文的加密部分
- 从解密值中提取mac
- 用明文
验证 mac
我还没有疯到在任何生产项目中使用我自己的加密脚本,所以请指出任何为我执行此操作的库(相对安全的简单密码/消息加密)
下面是两个函数的源代码:
package main
import (
"io"
"crypto/rand"
"crypto/cipher"
"crypto/aes"
"crypto/sha256"
"crypto/hmac"
"golang.org/x/crypto/pbkdf2"
)
const saltlen = 32
const keylen = 32
const iterations = 100002
// returns ciphertext of the following format:
// [32 bit salt][128 bit iv][encrypted plaintext]
func encrypt(plaintext string, password string) string {
// allocate memory to hold the header of the ciphertext
header := make([]byte, saltlen + aes.BlockSize)
// generate salt
salt := header[:saltlen]
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
panic(err)
}
// generate initialization vector
iv := header[saltlen:aes.BlockSize+saltlen]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
// generate a 32 bit key with the provided password
key := pbkdf2.Key([]byte(password), salt, iterations, keylen, sha256.New)
// generate a hmac for the message with the key
mac := hmac.New(sha256.New, key)
mac.Write([]byte(plaintext))
hmac := mac.Sum(nil)
// append this hmac to the plaintext
plaintext = string(hmac) + plaintext
//create the cipher
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
// allocate space for the ciphertext and write the header to it
ciphertext := make([]byte, len(header) + len(plaintext))
copy(ciphertext, header)
// encrypt
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize+saltlen:], []byte(plaintext))
return string(ciphertext)
}
func decrypt(encrypted string, password string) string {
ciphertext := []byte(encrypted)
// get the salt from the ciphertext
salt := ciphertext[:saltlen]
// get the IV from the ciphertext
iv := ciphertext[saltlen:aes.BlockSize+saltlen]
// generate the key with the KDF
key := pbkdf2.Key([]byte(password), salt, iterations, keylen, sha256.New)
block, err := aes.NewCipher(key)
if (err != nil) {
panic(err)
}
if len(ciphertext) < aes.BlockSize {
return ""
}
decrypted := ciphertext[saltlen+aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(decrypted, decrypted)
// extract hmac from plaintext
extractedMac := decrypted[:32]
plaintext := decrypted[32:]
// validate the hmac
mac := hmac.New(sha256.New, key)
mac.Write(plaintext)
expectedMac := mac.Sum(nil)
if !hmac.Equal(extractedMac, expectedMac) {
return ""
}
return string(plaintext)
}
注意,因为问题是关于加密消息而不是密码:如果你加密小消息而不是散列密码,Go 的 secretbox 包——作为一部分它的 NaCl 实现——是要走的路。如果你打算自己动手——我强烈建议不要这样做,除非它保留在你自己的开发环境中——那么 AES-GCM 就是选择这里的方法。
否则,以下大部分内容仍然适用:
- 对称加密对密码没有用。您应该没有理由需要返回明文——您应该只关心比较哈希(或者更准确地说,派生密钥)。
- PBKDF2,与 scrypt 或 bcrypt 相比,并不理想(10002 轮,2015 年,可能也有点低)。 scrypt 是 memory-hard 并且更难在 GPU 上并行化,并且在 2015 年,它的寿命足够长,使其比 bcrypt 更安全(如果您的语言的 scrypt 库不可用,您仍然会使用 bcrypt太棒了)。
- MAC-then-encrypt has issues - 你应该 encrypt-then-MAC.
- 鉴于#3,您应该使用 AES-GCM(Galois 计数器模式)而不是 AES-CBC + HMAC。
Go 有一个很棒的 bcrypt 包,带有 easy-to-use API(为您生成盐;安全地比较)。
我还写了一个 scrypt 包来镜像那个包,因为底层的 scrypt 包需要你验证你自己的参数并生成你自己的盐。
首先,我想说这只是一个学习练习,我不打算在生产中使用它。
我用 Golang 写了一个小应用程序,有两个函数:encrypt(plaintext string, password string)
和 decrypt(encrypted string, password string)
加密步骤为:
- 生成随机的 256 位用作盐
- 生成 128 位用作初始化向量
- 使用 PDKDF2 从密码和 salt 生成 32 位密钥
- 用密钥和明文生成一个32位的HMAC,并将其附加到明文的开头
- 在CFB模式下用AES加密hmac+明文
返回的字节数组如下所示:
[256 bit salt] [128 bit iv] encrypted([256 bit hmac] [plaintext])
解密时:
- 提取盐并将其与提供的密码一起使用来计算密钥
- 提取 IV 并解密密文的加密部分
- 从解密值中提取mac
- 用明文 验证 mac
我还没有疯到在任何生产项目中使用我自己的加密脚本,所以请指出任何为我执行此操作的库(相对安全的简单密码/消息加密)
下面是两个函数的源代码:
package main
import (
"io"
"crypto/rand"
"crypto/cipher"
"crypto/aes"
"crypto/sha256"
"crypto/hmac"
"golang.org/x/crypto/pbkdf2"
)
const saltlen = 32
const keylen = 32
const iterations = 100002
// returns ciphertext of the following format:
// [32 bit salt][128 bit iv][encrypted plaintext]
func encrypt(plaintext string, password string) string {
// allocate memory to hold the header of the ciphertext
header := make([]byte, saltlen + aes.BlockSize)
// generate salt
salt := header[:saltlen]
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
panic(err)
}
// generate initialization vector
iv := header[saltlen:aes.BlockSize+saltlen]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
// generate a 32 bit key with the provided password
key := pbkdf2.Key([]byte(password), salt, iterations, keylen, sha256.New)
// generate a hmac for the message with the key
mac := hmac.New(sha256.New, key)
mac.Write([]byte(plaintext))
hmac := mac.Sum(nil)
// append this hmac to the plaintext
plaintext = string(hmac) + plaintext
//create the cipher
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
// allocate space for the ciphertext and write the header to it
ciphertext := make([]byte, len(header) + len(plaintext))
copy(ciphertext, header)
// encrypt
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize+saltlen:], []byte(plaintext))
return string(ciphertext)
}
func decrypt(encrypted string, password string) string {
ciphertext := []byte(encrypted)
// get the salt from the ciphertext
salt := ciphertext[:saltlen]
// get the IV from the ciphertext
iv := ciphertext[saltlen:aes.BlockSize+saltlen]
// generate the key with the KDF
key := pbkdf2.Key([]byte(password), salt, iterations, keylen, sha256.New)
block, err := aes.NewCipher(key)
if (err != nil) {
panic(err)
}
if len(ciphertext) < aes.BlockSize {
return ""
}
decrypted := ciphertext[saltlen+aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(decrypted, decrypted)
// extract hmac from plaintext
extractedMac := decrypted[:32]
plaintext := decrypted[32:]
// validate the hmac
mac := hmac.New(sha256.New, key)
mac.Write(plaintext)
expectedMac := mac.Sum(nil)
if !hmac.Equal(extractedMac, expectedMac) {
return ""
}
return string(plaintext)
}
注意,因为问题是关于加密消息而不是密码:如果你加密小消息而不是散列密码,Go 的 secretbox 包——作为一部分它的 NaCl 实现——是要走的路。如果你打算自己动手——我强烈建议不要这样做,除非它保留在你自己的开发环境中——那么 AES-GCM 就是选择这里的方法。
否则,以下大部分内容仍然适用:
- 对称加密对密码没有用。您应该没有理由需要返回明文——您应该只关心比较哈希(或者更准确地说,派生密钥)。
- PBKDF2,与 scrypt 或 bcrypt 相比,并不理想(10002 轮,2015 年,可能也有点低)。 scrypt 是 memory-hard 并且更难在 GPU 上并行化,并且在 2015 年,它的寿命足够长,使其比 bcrypt 更安全(如果您的语言的 scrypt 库不可用,您仍然会使用 bcrypt太棒了)。
- MAC-then-encrypt has issues - 你应该 encrypt-then-MAC.
- 鉴于#3,您应该使用 AES-GCM(Galois 计数器模式)而不是 AES-CBC + HMAC。
Go 有一个很棒的 bcrypt 包,带有 easy-to-use API(为您生成盐;安全地比较)。
我还写了一个 scrypt 包来镜像那个包,因为底层的 scrypt 包需要你验证你自己的参数并生成你自己的盐。