Goで暗号化する備忘録（AES-128-CBC）

# はてなブログに移行しました

https://p5750-tech.hateblo.jp/entry/golang-aes-cipher

# はじめに

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

# 暗号化

## 流れ

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

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

## 秘密鍵 (128 bit) の用意

省略。

## IV (128 bit) の生成

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

```go:iv
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` を追加する方式。

```go:padding
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()` を使って実際に暗号化処理を行う。

```go:encrypt
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キャストした個数分だけ削ればよい。

```go:decrypt
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コマンドと比較する。

```go:main.go
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=
```

同じ結果が得られた。

# 参考文献

- [bytes](https://golang.org/pkg/bytes/)
- [crypto/aes](https://golang.org/pkg/crypto/aes/)
- [crypto/cipher](https://golang.org/pkg/crypto/cipher/)
- [crypto/rand](https://golang.org/pkg/crypto/rand/)
- [RFC 2315 - PKCS #7: Cryptographic Message Syntax](https://tools.ietf.org/html/rfc2315)
- [RFC 3602 - The AES-CBC Cipher Algorithm and Its Use with IPsec](https://tools.ietf.org/html/rfc3602)