🔐

【Go】Goで学ぶ暗号技術 ~CBCモード~

に公開

はじめに

最近暗号技術について興味があり、少しずつ学習をしています。
前回はECBモードについてまとめたのですが、今回はその続きとしてCBCモードを取り上げます。
前回もよろしければご覧ください!
https://zenn.dev/tmyhrn/articles/65ad6aa32964a5

✅この記事でわかること

  • CBCモードの仕組み(暗号化・復号化の流れ)
  • 初期化ベクトル(IV)の役割
  • CBCモードを使う上での注意点

📝CBCモードについて

  • Cipher Block Chainingの略
  • ECBモードの欠点を補うために考案された暗号利用モード
  • 前のブロックとのXOR(排他的論理和)を取って暗号化を行う
  • 最初は前のブロックが存在しないので、初期化ベクトル(IV, initialization vector)を生成する

📊CBCモード図解

暗号化

  • 最初のブロックは「IV」とXOR
  • それ以降は「前の暗号ブロック」とXOR
  • すべてAESなどの共通鍵暗号で暗号化

復号化

  • 復号後に、前の暗号ブロック(またはIV)とXORすることで平文を取り出す

⚠️CBCモードの注意点

IVは毎回生成する必要がある
→同じIVだと暗号ブロックも毎回同じになってしまうので、セキュリティ的に問題が生じる

🛠Goでの実装方法

暗号化の手順

  1. データをPKCS#7でパディング
  • AESは16バイト単位(ブロック)でしか暗号化できない
  • 元のデータが16の倍数でない場合、末尾にパディングを足してサイズを調整
  1. IVを生成
  • 毎回ランダムなIVを生成する
  • 疑似乱数math/randは予測されやすいので、暗号に強い乱数crypto/randを出力するようにする
  1. CBCモードで暗号化
  • crypto/cipherパッケージのNewCBCEncrypterメソッドでCBCモードを指定
  • CryptBlocksで暗号化を行う

復号化の手順

  1. CBCモードで復号化
  • crypto/cipherパッケージのNewCBCDecrypterメソッドでCBCモードを指定
  • CryptBlocksで復号化を行う
  1. パディングを取り除く
  • 復号後のデータには、元の長さに合わせるために追加されていたパディングがある
  • PKCS#7のルールに従って、そのパディングを削除
  1. 最終的な平文(元の文字列)を返す

実装

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