理系学生日記

おまえはいつまで学生気分なのか

忍者TOOLS

golang で AES/CBC/PKCS#7Padding の暗号化・復号化

golang では crypto/ciphercrypto/aes を使って、AES で暗号化を行うことができます。 しかし、ブロック暗号化であるにも関わらず、なぜかパディングが定義されていない。 これじゃ暗号化できないやんけ、ということで、これと戦った記録です。

ブロック暗号と Padding

AES はブロック暗号の一種です。 ではブロック暗号とは何かというとブロック単位で暗号化をしていく暗号方式で、詳細はwikipedia:ブロック暗号を見れば良い。 昔にエントリにも書きました。

ブロック暗号を使うには、暗号化を行う対象である平文が、そのブロック長の整数倍であることが前提になります。 たとえば AES におけるブロック長は 16 Byte (128 bit) なので、平文の長さは 16 Byte の倍数であることが求められます。

しかしこの世の中、そんな都合の良い平文の方が少ない。 ではそういう平文を暗号化するときにどうすれば良いか。暗号化対象が 16 Byte の倍数になるように、余計なバイトを詰めることになります。 いわゆる Padding ですね。

Go の標準モジュールと Padding

しかしこの世の中、都合よくできてはおりません。 以下は crypto/cipher に含まれる AES での暗号化の Example です。 len(plaintext)aes.BlockSize の整数倍でない場合に、エラーとなる実装になってしまっていることが分かります。

func main() {
    key, _ := hex.DecodeString("6368616e676520746869732070617373")
    plaintext := []byte("exampleplaintext")

    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(rand.Reader, iv); err != nil {
        panic(err)
    }

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

    fmt.Printf("%x\n", ciphertext)
}

このように、golang において Padding の実装はモジュールに含まれていないようです。 このため、ブロック暗号を使うためには、どうも Padding を自前で実装する他ない。

Padding 方式

では、実装する Padding はどのようなものがあるのかというと、wikipedia:en:Padding (cryptography) に色々な方式が記載されています。 ここで我々に馴染み深いのは、PKCS#5、PKCS#7 でしょう。 Java の JCA でも PKCS5Padding (実体は PKCS#7) が標準でサポートされていますし、openssl でも PKCS#7 が使われています。

この PKCS#7 の Padding は、RFC 5652 の 6.3 に定義があります。 Padding 方式としては非常に簡単で、

  • 1 バイト足りないときは 0x01 を末尾に 1 つ Padding として付与
  • 2 バイト足りないときは 0x02 を末尾に 2 つ Padding として付与
  • ...
  • n \mod \text{blocksize} バイト足りないときは n (16 進数) を末尾に n \mod \text{blocksize} つ Padding として付与

という内容になっています。 ただし暗号化対象の文字列長さがちょうどブロックサイズの倍数になっている場合は、末尾にブロックサイズ長だけ、ブロックサイズの16進表現 (AES の場合は 0x10) で埋めます。

長々しく書きましたが、実装としては非常に単純で、以下のようになります。

// pad は RFC 5652 6.3. Content-encryption Process に記述された通りに
// b にパディングとしてのバイトを追加する (PKCS#7 Padding)
func (c *AesCbcPkcs7Cipher) pad(b []byte) []byte {
    padSize := aes.BlockSize - (len(b) % aes.BlockSize)
    pad := bytes.Repeat([]byte{byte(padSize)}, padSize)
    return append(b, pad...)
}

// unpad は PKCS#7 Padding に従って付与されたパディングを削除する
func (c *AesCbcPkcs7Cipher) unpad(b []byte) []byte {
    padSize := int(b[len(b)-1])
    return b[:len(b)-padSize]
}

AES/CBC/PKCS#7Padding 実装

というわけで、AES/CBC/PKCS#7Padding での暗号化・復号化を実装してみました。

テストコードから簡単に抜粋しますが、以下のように使うイメージです。 (この場合、鍵長が 256 bit になるので、AES(CBC) の 256 bit での暗号化になります)

key, _ := hex.DecodeString("1234567890123456789012345678901234567890123456789012345678901234")
iv, _ := hex.DecodeString("1234567890ABCDEF1234567890ABCDEF")
sut, err := NewAesCbcPkcs7Cipher(key, iv)
if err != nil {
    t.Errorf("error must be nil, but [%s]", err)
}
encrypted, err := sut.Encrypt([]byte(tc.plain))
if err != nil {
    t.Errorf("error must be nil, but [%s]", err)
}
actual := base64.StdEncoding.EncodeToString(encrypted)

上記の実装で、openssl で以下のようにして行った暗号化と同じ結果が得られます。

$ echo -n "aaaaaaaaaaaaaaaa" \|
  openssl aes-256-cbc -e -base64 \
    -iv 1234567890ABCDEF1234567890ABCDEF \
    -K 1234567890123456789012345678901234567890123456789012345678901234