AES-256-CBC加密在golang和node / php之间不匹配

时间:2018-06-08 13:42:48

标签: php node.js go encryption aes

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

明文:hello big worldshello big worlds

键:jJr44P3WSM5F8AC573racFpzU5zj7Rg5

iv:97iEhhtgVjoVwdUw

以下是base64中的结果加密:

节点和PHP返回:

OTdpRWhodGdWam9Wd2RVd0OgJ+Z7pSCVioYq41721jarxqLKXN3PcnnY6/AOrHeEfsTxXfCgm2uUi+vmCAdpvw==

Go返回:

OTdpRWhodGdWam9Wd2RVd0OgJ+Z7pSCVioYq41721jarxqLKXN3PcnnY6/AOrHeE

正如你所看到的那样,它们几乎完全一样,它让我疯狂。你们可以快速查看下面的加密代码,并给我一些关于问题可能的提示吗?

GO:

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)
}

NODE:

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中添加等价物?寻求建议。感谢

1 个答案:

答案 0 :(得分:2)

好吧,我设法让go输出与你的其他输出相匹配,但我并不是100%清楚所有细节 - 特别是为什么PHP和Node版本的行为与他们的行为相同(你的Go版本的输出看起来像是"正确的"结果在我脑海中,正如我将要解释的那样。

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

所以,我尝试根据PHP和Node PKCS#7使用的默认填充方案填充Go版本。基本上,如果你需要填充5个字节,那么每个填充字节应该等于0x05,6个字节用0x06填充,等等。默认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,这要求所有输入都需要填充。即使输入是块对齐的,填充将始终存在的事实消除了解密的歧义。只需将填充步骤留给用户。