dongmou3615
2018-06-08 13:42
浏览 220

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

I've been having a bit of problems trying to figure out why my encryption is different in go compared to php and node. I was hoping someone could help me figure out the differences. Let's assume this is the data:

plaintext: hello big worldshello big worlds

key: jJr44P3WSM5F8AC573racFpzU5zj7Rg5

iv: 97iEhhtgVjoVwdUw

Here are the resulting encryptions in base64:

Node and PHP return :

OTdpRWhodGdWam9Wd2RVd0OgJ+Z7pSCVioYq41721jarxqLKXN3PcnnY6/AOrHeEfsTxXfCgm2uUi+vmCAdpvw==

Go returns:

OTdpRWhodGdWam9Wd2RVd0OgJ+Z7pSCVioYq41721jarxqLKXN3PcnnY6/AOrHeE

As you can see they're almost identical and its been driving me crazy. Could you guys take quick look at the encryption code below and give me hints on what the problem could be?

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

I've noticed that removing encryptor.final('binary') makes the two result in the same encryption but php does not have the .final() thing going for it. Php uses open_ssl_encrypt() which seems to have this built in. Is there a way to add an equivalent in go? Looking for advice. Thanks

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

1条回答 默认 最新

  • dongyan9950 2018-06-08 14:12
    已采纳

    Okay, I managed to get the go output to match your other outputs, but I'm not 100% clear on all of the details--in particular why the PHP and Node versions behave the way they do (your output from the Go version seems like the "correct" result in my mind, as I'll explain).

    My first observation was that your output from Node and PHP was longer than the Go version, by approximately one block length, and only the tail end is different. This told me that, somehow, those versions are being padded more than the go version.

    So, I tried padding the Go version according to the default padding scheme used by PHP and Node, PKCS#7. Basically, if you need to pad by 5 bytes, then each of the padding bytes should be equal to 0x05, 6 bytes are padded with 0x06, etc. Go's default aes.BlockSize is equal to 16, so I tried padding your input string with 16 0x10 bytes. This led to the correct answer!

    Honestly, not padding the input at all if it's already block-aligned is understandable behavior, but apparently Node and PHP follow RFC 5652 and always add padding (see the edit), even if they need to add another entire block just of padding.

    Here's the Go code to make your outputs match:

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

    Edit:

    Actually, it looks like Node and PHP are just following RFC 5652 correctly, which mandates that all input needs to be padded. The fact that padding will always be present, even if the input was block-aligned, disambiguates decryption. Go just leaves the padding step to the user.

    点赞 打赏 评论

相关推荐 更多相似问题