Goで暗号化する備忘録(AES-128-CBC)

※ この記事は、2021年3月にQiitaに投稿した記事を移植したものです。

はじめに

Goで暗号化・復号の処理を書いたときに(主にパディングで)手こずったのでメモ。

暗号化

流れ

AES-128-CBCによる暗号化は以下のような流れで行う。

  • 秘密鍵 (128 bit) の用意
  • IV (128 bit) の生成
  • パディング
  • 暗号化

秘密鍵 (128 bit) の用意

省略。

IV (128 bit) の生成

IVは長さ16のランダムな byte 配列になる。この長さはAESにおけるブロックサイズなので、定数 aes.BlockSize が使える。

import (
    "crypto/aes"
    "crypto/rand"
)

func GenerateIV() ([]byte, error) {
    iv := make([]byte, aes.BlockSize)
    if _, err := rand.Read(iv); err != nil {
        return nil, err
    }
    return iv, nil
}

パディング

平文 []byte の長さが16の倍数ではない可能性がある場合、16の倍数にするためにパディングする必要がある(パディングする場合は16の倍数でも16 byte足す必要がある)。 PKCS#7は l bytes のパディングを行うときに l 個の l を追加する方式。

import (
    "bytes"
    "crypto/aes"
)

func Pkcs7Pad(data []byte) []byte {
    length := aes.BlockSize - (len(data) % aes.BlockSize)
    trailing := bytes.Repeat([]byte{byte(length)}, length)
    return append(data, trailing...)
}

暗号化

ここまでで用意した GenerateIV()Pkcs7Pad() を使って実際に暗号化処理を行う。

import (
    "crypto/aes"
    "crypto/cipher"
)

func Encrypt(data []byte, key []byte) (iv []byte, encrypted []byte, err error) {
    iv, err = GenerateIV()
    if err != nil {
        return nil, nil, err
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, nil, err
    }
    padded := Pkcs7Pad(data)
    encrypted = make([]byte, len(padded))
    cbcEncrypter := cipher.NewCBCEncrypter(block, iv)
    cbcEncrypter.CryptBlocks(encrypted, padded)
    return iv, encrypted, nil
}

復号

復号するときも BlockMode 構造体を作って CryptBlocks() を呼ぶのは暗号化時と同じ。 パディングの削除は、最後の要素をintキャストした個数分だけ削ればよい。

import (
    "crypto/aes"
    "crypto/cipher"
)

func Pkcs7Unpad(data []byte) []byte {
    dataLength := len(data)
    padLength := int(data[dataLength-1])
    return data[:dataLength-padLength]
}

func Decrypt(data []byte, key []byte, iv []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    decrypted := make([]byte, len(data))
    cbcDecrypter := cipher.NewCBCDecrypter(block, iv)
    cbcDecrypter.CryptBlocks(decrypted, data)
    return Pkcs7Unpad(decrypted), nil
}

動作確認

opensslコマンドと比較する。

import (
    "encoding/base64"
    "encoding/hex"
    "fmt"
)

func main() {
    text := "this is test message"
    fmt.Println("Input:", text)
    plain := []byte(text)
    keyString := "645E739A7F9F162725C1533DC2C5E827"
    key, _ := hex.DecodeString(keyString)
    fmt.Println("Key:", keyString)
    iv, encrypted, _ := Encrypt(plain, key)
    fmt.Println("IV:", hex.EncodeToString(iv))
    fmt.Println("Encrypted:", base64.StdEncoding.EncodeToString(encrypted))
    decrypted, _ := Decrypt(encrypted, key, iv)
    fmt.Printf("Decrypted: %s", decrypted)
}
$ go run main.go
Input: this is test message
Key: 645E739A7F9F162725C1533DC2C5E827
IV: 71fbf00383b6e214dc08b8b94183cf30
Encrypted: z41UoV93PIS0OYElzUd7nwA9TO6XxSDlf9N+P4nFuJw=
Decrypted: this is test message

opensslコマンド。echo-n をつけないと改行で結果がずれる。

$ echo -n 'this is test message' | openssl aes-128-cbc -e -base64 -nosalt -K 645E739A7F9F162725C1533DC2C5E827 -iv 71fbf00383b6e214dc08b8b94183cf30
z41UoV93PIS0OYElzUd7nwA9TO6XxSDlf9N+P4nFuJw=

同じ結果が得られた。

参考文献