首先,我想说这只是一个学习练习,我不打算在生产中使用它。

我在Golang中编写了一个小应用程序,它具有两个功能:encrypt(plaintext string, password string)decrypt(encrypted string, password string)

加密步骤为:

  • 生成随机的256位用作盐
  • 生成128位用作初始化向量
  • 使用PDKDF2从密码和salt生成一个32位密钥
  • 用密钥和明文生成一个32位的HMAC,并将其附加到明文的开头
  • 在CFB模式下使用AES加密hmac + plaintext
  • 返回的字节数组如下所示:

    1
    [256 bit salt] [128 bit iv] encrypted([256 bit hmac] [plaintext])

    解密时:

  • 提取盐并与提供的密码一起使用以计算密钥
  • 提取IV并解密密文的加密部分
  • 从解密值中提取Mac
  • 使用明文验证Mac
  • 我还不够疯狂,无法在任何生产项目中使用自己的加密脚本,因此请指向为我执行此操作的任何库(相对安全的简单密码/消息加密)

    这是两个函数的源代码:

    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
    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)
    }
    • 注意codereview.stackexchange.com是一回事
    • codahale.com/how-to-safe-store-a-password
    • 您实际上还在加密密码吗?因为如果是这样,那么,如果您需要的只是验证,那就是一个相对糟糕的想法。如果没有,那么1Password会对它们的执行方式有相当广泛的规范。
    • 通常,密码不使用加密-不需要能够检索明文-实际上,那是负面的。通常,您执行:V = Hash(SALT, PASSWORD)并保存V&SALT。然后,当需要进行验证时,您将重新计算V2 = Hash(SALT, MIGHTBEPASSWORD)。然后,如果V2 == V,则密码正确。棘手的一点是确保Hash()具有您希望它具有的所有加密属性。
    • 我没有加密或存储密码。我在密文中存储了未加密的随机盐和IV,并在解密时使用盐和提供的密码重新计算了密钥。
    • 我将HMAC存储在密文的加密部分中的想法是对纯文本进行身份验证并验证其完整性。
    • @pvg另外,使用bcrypt而不是pbkdf2有什么好处?似乎大多数资料来源都建议pdkdf2> bcrypt
    • 哪些来源? PBKDF2仅应在规格要求较高的地方使用(例如,满足FIPS / NIST要求)。 bcrypt很难并行化,而且(按照我的回答)scrypt再次很难。 RE:对明文进行身份验证:您应该对密文进行身份验证,在这种情况下,应使用AES-GCM(golang.org/pkg/crypto/cipher/#NewGCM)一次完成。
    • @DesmondLee bcrypt在其典型实现中具有更简单,更简单的API。在大多数情况下,关于一个人相对于另一个人的优势的争论是理论上的,如果有疑问,请使用bcrypt。如果可以导出密码的明文,则可以,您正在存储密码。除了编写密码管理器之外,您不应该这样做,因为它倾向于具有以明文形式存储密码的安全性。
    • @pvg我想我误会了。我无法得出密码的明文。用户正在提供它来解密消息。
    • 攻击者可以通过测试密码转储中的一堆密码而无需运行任何bcrypt / scrypt攻击。您要存储[32 bit salt] [128 bit iv] encrypted([32 bit hmac] [plaintext])-现代CPU上的AES解密操作非常快(非常快)。
    • 好的,这个问题真的是关于使用从用户提供的机密派生的密钥对消息进行加密/解密吗?因为那是一个稍微不同的问题,并且您通过说密码加密来唤醒了人们。
    • pvg:是的,那就是这个问题。我意识到标题中的措辞是造成误解的原因。谢谢
    • 对此的答案可能是使用NaCl。如果它涉及实时消息,请使用OTR。
    • 32位盐,32 IV和32位HMAC似乎都太小,无法提供任何有意义的安全性。您可能指的是字节,对不对?否则,您确实应该增加这些数字。
    • 是的,我的问题混为一谈。我纠正了问题中的错误

    请注意,由于问题是关于加密消息而不是密码:如果要加密小消息而不是对口令进行哈希处理,那么Go的密钥箱程序包(作为其NaCl实现的一部分)就是解决之道。如果您打算自己动手,并且强烈建议您这样做,除非它不属于您自己的开发环境,那么AES-GCM是解决之道。

    否则,以下大多数情况仍然适用:

  • 对称加密对密码没有用。没有理由需要纯文本,只需要比较散列(或更确切地说是派生键)就可以了。
  • 与scrypt或bcrypt相比,PBKDF2并不理想(2015年进行了10002发,可能也有点低)。 scrypt难以记忆,因此很难在GPU上并行化.2015年,scrypt具有足够长的使用寿命,使其比bcrypt更安全(如果您的语言的scrypt库不太出色,您仍然可以使用bcrypt )。
  • MAC-then-encrypt有问题-您应该先加密-然后-MAC。
  • 给定#3,您应该在AES-CBC + HMAC上使用AES-GCM(Galois计数器模式)。
  • Go有一个很棒的bcrypt软件包,带有易于使用的API(可以为您生成盐;可以安全地进行比较)。

    我还写了一个scrypt包来镜像该包,因为基础scrypt包要求您验证自己的参数并生成自己的盐。

    • 我对#1感到困惑,因为我不相信自己正在加密密码(除非我对我对HMAC部分所做的理解不正确),我存储生成的盐,并在解密阶段使用PBKDF2来获取密钥。我进行MAC加密的原因是为了防止对手提供无效的密文。为了更好地理解,我先研究一下MAC,然后加密,然后再加密。另外,我对关于不需要明文的句子感到困惑。那不是解密的重点吗?
    • 根据您的帖子,您将存储[32 bit salt] [128 bit iv] encrypted([32 bit hmac] [plaintext])-这表明您正在加密密码。此外,如果对手提供了无效的密文,也没关系:HMAC可以帮助您验证它(通过在密文上运行HMAC并比较两个HMAC的恒定时间)。
    • 解密确实会给您带来纯文本,但请问自己:您为什么需要它?为什么其他所有人都建议使用bcrypt / scrypt而不是某种奇怪的PDKDF2 + HMAC-SHA-256 + AES-CBC结构?
    • 我原来的问题标题可能引起了我脚本目的的困惑。它不是用于加密密码,而是用于使用用户提供的密码来加密消息。提供的密码与保存的盐一起使用,以通过PBKDF2生成对称密钥。但是从您的答案看来,PBKDF2似乎无法抵抗暴力攻击,因此我应该使用bcrypt / script。似乎我也应该使用AES-GCM而不是AES-CBC / HMAC-SHA-256。那是对的吗?
    • 在这种情况下,您应该(仅)或(更好)使用AES-GCM Gos NaCl实现,该实现使用Salsa20和Poly1305来使用共享密钥来加密消息:godoc.org/golang.org/x/crypto/nacl/secretbox
    • 感谢您的详细回答
    • @DesmondLee没问题。 Ive还为后代添加了对secretbox和AES-GCM的提及。
    • @elithrar可能值得删除有关密码的内容(或交换顺序),因为事实证明,这实际上与密钥派生和加密有关,而不是密码验证或存储,这在最初看起来是令人困惑的。
    • @elithar我将使用什么来加密大量数据? (例如期刊,文章等)
    • 我们在聊多大?秘密箱仍然可以,否则为AES-GCM。