【问题标题】:AES-256-CBC encryption not matching between golang and node/phpgolang 和 node/php 之间的 AES-256-CBC 加密不匹配
【发布时间】:2018-11-18 14:17:34
【问题描述】:

我一直有一些问题试图弄清楚为什么我的加密与 php 和 node 相比在 go 中有所不同。我希望有人可以帮助我找出差异。假设这是数据:

明文:hello big worldshello big worlds

密钥:jJr44P3WSM5F8AC573racFpzU5zj7Rg5

iv:97iEhhtgVjoVwdUw

以下是 base64 中的加密结果:

节点和 PHP 返回:

OTdpRWhodGdWam9Wd2RVd0OgJ+Z7pSCVioYq41721jarxqLKXN3PcnnY6/AOrHeEfsTxXfCgm2uUi+vmCAdpvw==

Go 返回:

OTdpRWhodGdWam9Wd2RVd0OgJ+Z7pSCVioYq41721jarxqLKXN3PcnnY6/AOrHeE

如您所见,它们几乎完全相同,这让我发疯。你们能否快速查看下面的加密代码并提示我可能出现的问题?

去:

func EncryptString(plainstring string, keystring string, encFormat int, ivOverride bool) (string) {
    // Load your secret key from a safe place and reuse it across multiple
    // NewCipher calls. (Obviously don't use this example key for anything
    // real.) If you want to convert a passphrase to a key, use a suitable
    // package like bcrypt or scrypt.
    key := []byte(keystring)
    plaintext := []byte(plainstring)

    // CBC mode works on blocks so plaintexts may need to be padded to the
    // next whole block. For an example of such padding, see
    // https://tools.ietf.org/html/rfc5246#section-6.2.3.2. Here we'll
    // assume that the plaintext is already of the correct length.
    if len(plaintext)%aes.BlockSize != 0 {
        panic("plaintext is not a multiple of the block size")
    }

    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }

    // The IV needs to be unique, but not secure. Therefore it's common to
    // include it at the beginning of the ciphertext.
    ciphertext := make([]byte, aes.BlockSize+len(plaintext))
    iv := ciphertext[:aes.BlockSize]
    if _, err := io.ReadFull(bytes.NewReader([]byte("97iEhhtgVjoVwdUw")), iv); err != nil {
       panic(err)
    }

    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)

    // It's important to remember that ciphertexts must be authenticated
    // (i.e. by using crypto/hmac) as well as being encrypted in order to
    // be secure.

    return base64.StdEncoding.EncodeToString(ciphertext)
}

节点:

encryptString: function(string, key, fmt = null, ivOverride = false) {
    // Build an initialisation vector
    let iv;
    if(!ivOverride) {
        iv = crypto.randomBytes(IV_NUM_BYTES).toString('hex').slice(0,16);
    } else {
        iv = IV_OVERRIDE_VALUE; //97iEhhtgVjoVwdUw
    }
    // and encrypt
    let encryptor = crypto.createCipheriv('aes-256-cbc', key, iv);
    let encryptedData = encryptor.update(string, 'utf8', 'binary') + encryptor.final('binary');
    encryptedData = iv+''+encryptedData;
    encryptedData = Buffer.from(encryptedData, 'binary').toString('base64');

    return encryptedData;
}

我注意到删除encryptor.final('binary') 会使两者得到相同的加密,但 php 没有 .final() 功能。 Php uses open_ssl_encrypt() 似乎内置了这个。有没有办法在 go 中添加一个等价物?寻求建议。谢谢

【问题讨论】:

  • echo $output = openssl_encrypt("hello big worldshello big worlds", "AES-256-CBC", "jJr44P3WSM5F8AC573racFpzU5zj7Rg5", 0, "97iEhhtgVjoVwdUw");在 PHP Q6An5nulIJWKhirjXvbWNqvGospc3c9yedjr8A6sd4R+xPFd8KCba5SL6+YIB2m/ 中获得不同的输出

标签: php node.js go encryption aes


【解决方案1】:

好的,我设法让 go 输出与您的其他输出相匹配,但我并不是 100% 清楚所有细节——特别是为什么 PHP 和 Node 版本的行为方式如此(您的输出来自在我看来,Go 版本似乎是“正确”的结果,我会解释)。

我的第一个观察是 Node 和 PHP 的输出比 Go 版本长,大约一个块长度,只有尾部不同。这告诉我,不知何故,这些版本比 go 版本被填充更多

所以,我尝试根据 PHP 和 Node 使用的默认填充方案 PKCS#7 填充 Go 版本。基本上,如果你需要填充 5 个字节,那么每个填充字节应该等于 0x05,6 个字节用 0x06 填充,等等。Go 的默认 aes.BlockSize 等于 16,所以我尝试用16 个 0x10 字节。这导致了正确的答案!

老实说,如果输入已经是块对齐的,则根本不填充输入是可以理解的行为,但显然 Node 和 PHP 遵循 RFC 5652 并且总是添加填充(参见编辑),即使他们需要添加另一个完整的块填充。

这是使输出匹配的 Go 代码:

package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "encoding/base64"
    "fmt"
    "io"
)

// Based on Wikipedia: https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7
func PadToBlockSize(input string) string {
    paddingNeeded := aes.BlockSize - (len(input) % aes.BlockSize)
    if paddingNeeded >= 256 {
        panic("I'm too lazy to handle this case for the sake of an example :)")
    }
    
    if paddingNeeded == 0 {
        paddingNeeded = aes.BlockSize
    }

    // Inefficient, once again, this is an example only!
    for i := 0; i < paddingNeeded; i++ {
        input += string(byte(paddingNeeded))
    }
    return input
}

// (Identical to your code, I just deleted comments to save space)
func EncryptString(plainstring string, keystring string, encFormat int, ivOverride bool) string {
    key := []byte(keystring)
    plaintext := []byte(plainstring)
    if len(plaintext)%aes.BlockSize != 0 {
        panic("plaintext is not a multiple of the block size")
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }
    ciphertext := make([]byte, aes.BlockSize+len(plaintext))
    iv := ciphertext[:aes.BlockSize]
    if _, err := io.ReadFull(bytes.NewReader([]byte("97iEhhtgVjoVwdUw")), iv); err != nil {
        panic(err)
    }
    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
    return base64.StdEncoding.EncodeToString(ciphertext)
}

func main() {
    plaintext := "hello big worldshello big worlds"
    key := "jJr44P3WSM5F8AC573racFpzU5zj7Rg5"
    phpText := "OTdpRWhodGdWam9Wd2RVd0OgJ+Z7pSCVioYq41721jarxqLKXN3PcnnY6/AOrHeEfsTxXfCgm2uUi+vmCAdpvw=="

    fmt.Println("Go : " + EncryptString(PadToBlockSize(plaintext), key, 0, false))
    fmt.Println("PHP: " + phpText)
}

编辑:

实际上,看起来 Node 和 PHP 只是正确地遵循 RFC 5652,这要求所有输入都需要填充。即使输入是块对齐的,填充也将始终存在,这一事实消除了解密的歧义。 Go 只是将填充步骤留给用户。

【讨论】:

  • 请注意,您的 EncryptString 函数只是从密码文档示例中剪切+粘贴,故意没有填充,因为它只是 crypto/cipher 的示例。
猜你喜欢
  • 2021-06-07
  • 2019-11-17
  • 2022-11-16
  • 1970-01-01
  • 1970-01-01
  • 2014-02-06
  • 1970-01-01
  • 2012-08-05
相关资源
最近更新 更多