GoでAESアルゴリズム(CBCモード)+PKCS7パディングを使った実装をする


GoでAESアルゴリズム(CBCモード)を使った実装をするの続き

AESはブロック暗号であるため、16の倍数バイトの平文にしか適用できない。そうでない平文をAESで暗号化するには追加のデータ(パディング)を付与して16の倍数バイトにしてやる必要がある。AESを含めた共通鍵方式のパディングにはいくつか種類があって、Wikiによると以下の通りである。
* Bit Padding, Byte Padding, ANSI X.923, ISO 10126, PKCS#7, ISO/IEC 7816-4, Zero padding

白状すると、各々が何に適しているかはわからないし、調べる気力もなかった。ただPKCS #7は、RSA公開鍵方式で使われていたはずなので、信頼と実績でそれを使ってみようと思う。ただ、Goの場合は、Padding関係のパッケージが容易されてないので、自前実装することになる。メンドクサイ

PKCS7による実装

PKCS7のパディングは、RFC5652にある通り、追加したいパディング長を値にしたバイトを追加する形になる。数式にすると、以下のとおり。

  • k-(lth mod k)

kはブロックサイズであり、AESの場合は16になる。lthは平文(バイト)の長さになる。上記で導きだしたものが、パディングの値になる。例えば(lth mod k)=1だとしたら、01がパディングになり、(lth mod k)=2だとしたら、02 02がパディングになる。ただ、(lth mod k)=0の場合、16 16.....16がパディングになる。これをGoで実装すると以下の通り。

func PadByPkcs7(data []byte) []byte {
    padSize := aes.BlockSize
    if len(data) % aes.BlockSize != 0 {
        padSize = aes.BlockSize - (len(data)) % aes.BlockSize
    }

    pad := bytes.Repeat([]byte{byte(padSize)}, padSize)
    return append(data, pad...) // Dots represent it unpack Slice(pad) into individual bytes
}

func UnPadByPkcs7(data []byte) []byte {
    padSize := int(data[len(data) - 1])
    return data[:len(data) - padSize]
}

func EncryptByCBCMode(key []byte, plainText string) ([]byte, error) {
    //if len(plainText) % aes.BlockSize != 0 { <-いらなくなった
    //  panic("Plain text must be multiple of 128bit")
    //}

    block, err := aes.NewCipher(key); if err != nil {
        return nil, err
    }

    paddedPlaintext := PadByPkcs7([]byte(plainText))
    fmt.Printf("Padded Plain Text in byte format: %v\n", paddedPlaintext)
    cipherText := make([]byte, aes.BlockSize + len(paddedPlaintext)) // cipher text must be larger than plaintext
    iv := cipherText[:aes.BlockSize] // Unique iv is required
    _, err = rand.Read(iv); if err != nil {
        return nil, err
    }

    cbc := cipher.NewCBCEncrypter(block, iv)
    cbc.CryptBlocks(cipherText[aes.BlockSize:], paddedPlaintext)
    cipherTextBase64 := base64.StdEncoding.EncodeToString(cipherText)
    return []byte(cipherTextBase64), nil
}

func DecryptByCBCMode(key []byte, cipherTextBase64 []byte) (string, error) {
    block, err := aes.NewCipher(key); if err != nil {
        return "", err
    }

    cipherText, _ := base64.StdEncoding.DecodeString(string(cipherTextBase64))

    if len(cipherText) < aes.BlockSize {
        panic("cipher text must be longer than blocksize")
    } else if len(cipherText) % aes.BlockSize != 0 {
        panic("cipher text must be multiple of blocksize(128bit)")
    }
    iv := cipherText[:aes.BlockSize] // assuming iv is stored in the first block of ciphertext
    cipherText = cipherText[aes.BlockSize:]
    plainText := make([]byte, len(cipherText))

    cbc := cipher.NewCBCDecrypter(block, iv)
    cbc.CryptBlocks(plainText, cipherText)
    return string(UnPadByPkcs7(plainText)), nil
}
func main() {
    plainText = "12345" // Paddingしないとエラーになる平文
    cipherText, _ = EncryptByCBCMode(key, plainText)
    fmt.Printf("Plaintext %v is encrypted into %v:\n", plainText, cipherText)
    decryptedText,_ = DecryptByCBCMode(key, cipherText)
    fmt.Printf("Decrypted Text: %v\n ", decryptedText)
}
>> 
                                                  |-> こっからパディング
Padded Plain Text in byte format: [49 50 51 52 53 11 11 11 11 11 11 11 11 11 11 11]
Plaintext 12345 is encrypted into [116 51 109 107 87 53 103 43 86 65 114 73 104 99 75 97 85 76 121 71 109 90 122 70 83 122 90 120 98 102 112 72 43 74 111 69 48 76 79 97 98 69 56 61]:
Decrypted Text: 12345

以上。

おまけ - パディングに対する攻撃

暗号文+パディングを繰り返し送ることで平文を一部推測できてしまう攻撃手法(パディングオラクル攻撃)がある。SSL3.0における脆弱性(POODLE)はこれを利用したものになる。この攻撃を防ぐためには、復号しようとしている暗号文が正しいサブジェクトによって生成されたものかを認証ればよい(MAC)。MACは又今後書くつもり。