🔐
【Go】Goで学ぶ暗号技術 ~CBCモード~
はじめに
最近暗号技術について興味があり、少しずつ学習をしています。
前回はECBモードについてまとめたのですが、今回はその続きとしてCBCモードを取り上げます。
前回もよろしければご覧ください!
✅この記事でわかること
- CBCモードの仕組み(暗号化・復号化の流れ)
- 初期化ベクトル(IV)の役割
- CBCモードを使う上での注意点
📝CBCモードについて
- Cipher Block Chainingの略
- ECBモードの欠点を補うために考案された暗号利用モード
- 前のブロックとのXOR(排他的論理和)を取って暗号化を行う
- 最初は前のブロックが存在しないので、初期化ベクトル(IV, initialization vector)を生成する
📊CBCモード図解
暗号化
- 最初のブロックは「IV」とXOR
- それ以降は「前の暗号ブロック」とXOR
- すべてAESなどの共通鍵暗号で暗号化
復号化
- 復号後に、前の暗号ブロック(またはIV)とXORすることで平文を取り出す
⚠️CBCモードの注意点
IVは毎回生成する必要がある
→同じIVだと暗号ブロックも毎回同じになってしまうので、セキュリティ的に問題が生じる
🛠Goでの実装方法
暗号化の手順
- データをPKCS#7でパディング
- AESは16バイト単位(ブロック)でしか暗号化できない
- 元のデータが16の倍数でない場合、末尾にパディングを足してサイズを調整
- IVを生成
- 毎回ランダムなIVを生成する
- 疑似乱数
math/rand
は予測されやすいので、暗号に強い乱数crypto/rand
を出力するようにする
- CBCモードで暗号化
-
crypto/cipher
パッケージのNewCBCEncrypter
メソッドでCBCモードを指定 -
CryptBlocks
で暗号化を行う
復号化の手順
- CBCモードで復号化
-
crypto/cipher
パッケージのNewCBCDecrypter
メソッドでCBCモードを指定 -
CryptBlocks
で復号化を行う
- パディングを取り除く
- 復号後のデータには、元の長さに合わせるために追加されていたパディングがある
- PKCS#7のルールに従って、そのパディングを削除
- 最終的な平文(元の文字列)を返す
実装
main.go
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"log"
)
// PKCS#7パディング
func pkcs7Pad(data []byte, blockSize int) []byte {
padLen := blockSize - (len(data) % blockSize)
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
return append(data, padding...)
}
// PKCS#7アンパディング
func pkcs7Unpad(data []byte) ([]byte, error) {
len := len(data)
if len == 0 {
return nil, fmt.Errorf("unpad error: input too short")
}
padLen := int(data[len-1])
if padLen > len {
return nil, fmt.Errorf("unpad error: invalid padding")
}
return data[:(len - padLen)], nil
}
// CBCモード暗号化
func encryptCBC(key, plainText []byte) ([]byte, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
blockSize := block.BlockSize()
paddedText := pkcs7Pad(plainText, blockSize)
cipherText := make([]byte, len(paddedText))
iv := make([]byte, blockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, nil, err
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText, paddedText)
return cipherText, iv, nil
}
// CBCモード復号化
func decryptCBC(key, cipherText, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(cipherText)%block.BlockSize() != 0 {
return nil, fmt.Errorf("ciphertext is not a multiple of the block size")
}
plainText := make([]byte, len(cipherText))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(plainText, cipherText)
return pkcs7Unpad(plainText)
}
func main() {
key := []byte("example-cbc-key!")
plainText := []byte("Hello, CBC!")
cipherText, iv, err := encryptCBC(key, plainText)
if err != nil {
log.Fatal(err)
}
fmt.Println("Encrypted:", hex.EncodeToString(cipherText))
fmt.Println("IV:", hex.EncodeToString(iv))
decryptedText, err := decryptCBC(key, cipherText, iv)
if err != nil {
log.Fatal(err)
}
fmt.Println("Decrypted:", string(decryptedText))
}
出力結果例
ターミナル
Encrypted: abe0eee739b4a10c902a2e2fb17c0610
IV: e28f7e91956b19ebdbf21f2af0751344
Decrypted: Hello, CBC!
💡まとめ
今回はCBCモードについて書いていきました。
ECBモードに実装方法が近いなと感じつつ、ECBにはなかったIVの存在を知ることができました。
今度は、CFBモードについて書いていこうと思います。
Discussion