英文:
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位作为初始化向量(IV)
- 使用PDKDF2从密码和盐生成32位密钥
- 使用密钥和明文生成32位HMAC,并将其附加到明文的开头
- 使用CFB模式的AES加密HMAC+明文
返回的字节数组如下所示:
[256位盐] [128位IV] 加密后的([256位HMAC] [明文])
解密时:
- 提取盐并使用提供的密码计算密钥
- 提取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
// 返回以下格式的密文:
// [32位盐][128位IV][加密后的明文]
func encrypt(plaintext string, password string) string {
// 分配内存以容纳密文的头部
header := make([]byte, saltlen + aes.BlockSize)
// 生成盐
salt := header[:saltlen]
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
panic(err)
}
// 生成初始化向量
iv := header[saltlen:aes.BlockSize+saltlen]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
// 使用提供的密码生成32位密钥
key := pbkdf2.Key([]byte(password), salt, iterations, keylen, sha256.New)
// 使用密钥为消息生成HMAC
mac := hmac.New(sha256.New, key)
mac.Write([]byte(plaintext))
hmac := mac.Sum(nil)
// 将此HMAC附加到明文上
plaintext = string(hmac) + plaintext
// 创建密码器
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
// 为密文分配空间并将头部写入其中
ciphertext := make([]byte, len(header) + len(plaintext))
copy(ciphertext, header)
// 加密
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)
// 从密文中获取盐
salt := ciphertext[:saltlen]
// 从密文中获取IV
iv := ciphertext[saltlen:aes.BlockSize+saltlen]
// 使用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)
// 从明文中提取HMAC
extractedMac := decrypted[:32]
plaintext := decrypted[32:]
// 验证HMAC
mac := hmac.New(sha256.New, key)
mac.Write(plaintext)
expectedMac := mac.Sum(nil)
if !hmac.Equal(extractedMac, expectedMac) {
return ""
}
return string(plaintext)
}
以上是源代码中的两个函数。
英文:
First, I want to say that this is just an learning exercise and I do not intend to use this in production.
I wrote a small application in Golang with two functions: encrypt(plaintext string, password string)
and decrypt(encrypted string, password string)
The encryption steps are:
- Generate random 256 bits to use as salt
- Generate 128 bits to use as an Initialization Vector
- Use PDKDF2 to generate a 32 bit key from the password and salt
- Generate an 32 bit HMAC with the key and plaintext, and append it to the beginning of the plaintext
- Encrypt the hmac+plaintext with AES in CFB mode
The returned byte array looks like this:
[256 bit salt] [128 bit iv] encrypted([256 bit hmac] [plaintext])
When decrypting:
- Extract the salt and use it with the provided password to compute the key
- Extract the IV and decrypt the encrypted portion of the ciphertext
- Extract the mac from the decrypted value
- Validate the mac with the plaintext
I'm not crazy enough to use my own encryption script in any production projects, so please point me to any libraries that do this for me (simple password / message encryption that is relatively secure)
Here is the source code to the two functions:
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)
}
答案1
得分: 8
注意:由于问题是关于加密消息而不是密码,如果你要加密小型消息而不是哈希密码,Go的secretbox包——作为其NaCl实现的一部分——是一个不错的选择。如果你坚持自己编写代码——我强烈建议不要这样做,除非它仅限于你自己的开发环境——那么AES-GCM是一个不错的选择。
否则,下面的大部分仍然适用:
- 对于密码来说,对称加密并不实用。你不应该需要明文密码,你只需要比较哈希值(或者更准确地说,派生密钥)。
- 相比于scrypt或bcrypt,PBKDF2并不理想(2015年的10002轮可能有点低)。scrypt是内存密集型的,很难在GPU上并行化,而且在2015年,它已经有足够长的生命周期,使其比bcrypt更安全(在你的语言中的scrypt库不太好的情况下,你仍然可以使用bcrypt)。
- 先进行MAC再进行加密存在问题——你应该先加密再进行MAC。
- 根据第3点,你应该使用AES-GCM(Galois Counter Mode)而不是AES-CBC + HMAC。
Go语言有一个很棒的bcrypt包,具有易于使用的API(为你生成盐;安全比较)。
我还编写了一个scrypt包,它与该包相似,因为底层的scrypt包要求你验证自己的参数并生成自己的盐。
英文:
Note, since the question was about encrypting messages rather than passwords: If you're encrypting small messages rather than hashing passwords, Go's secretbox package—as part of its NaCl implementation—is the way to go. If you're intent on rolling your own—and I strongly recommend against it, unless it stays within your own dev environment—then AES-GCM is the way to go here.
Otherwise, most of the below still applies:
- Symmetric encryption isn't useful for passwords. There should be no reason why you need the plaintext back—you should only care about comparing hashes (or, more precisely, derivative keys).
- PBKDF2, compared to scrypt or bcrypt, is not ideal (10002 rounds, in 2015, is probably a bit low too). scrypt is memory-hard and much harder to parallelize on a GPU, and in 2015, has had a sufficiently long life as to make it safer than bcrypt (you would still use bcrypt in cases where the scrypt library for your language wasn't great).
- MAC-then-encrypt has issues - you should encrypt-then-MAC.
- Given #3, you should use AES-GCM (Galois Counter Mode) over AES-CBC + HMAC.
Go has a great bcrypt package with an easy-to-use API (generates salts for you; securely compares).
I also wrote an scrypt package that mirrors that package, as the underlying scrypt package requires you to validate your own params and generate your own salts.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论