🔐

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

に公開

はじめに

最近暗号技術について興味があり、少しずつ学習をしています。
その中で、最もシンプルな暗号利用モードであるECBについて調べました。
分かったことを備忘録的に書いていきます。

この記事でわかること

  • ECBの特徴や注意点
  • Goでの実装方法

ECBの特徴

  • Electronic Codebookの略
  • 最もシンプルな暗号利用モード

暗号利用モード

ブロック暗号を使い、複数のデータブロックをどのように暗号化するかを定めたルールのこと
「1ブロックずつ独立して暗号化するのか」「前のブロックと関係させるのか」など複数の方式が存在


ブロック暗号

データを一定サイズ(例:16バイト)ごとのブロックに分割し、それぞれを別々に暗号化する方式
よく使われるブロック暗号方式の例:AES(Advanced Encryption Standard)


ECBモードのしくみ

  • 各ブロックは独立して暗号化される
  • 同じ平文ブロックは、常に同じ暗号文ブロックに変換される

例(AES:16バイトブロック)

  • 平文:Hello, ECB mode!
  • 暗号文:8b62ad2d2402446609086354e9e13b068e64ce873f174dbb2423fcd814580e15

ECBモードの注意点(非推奨になっている)

ECBモードは同じ平文ブロックが同じ暗号文ブロックに変換される
→データのパターンがそのまま暗号文にも現れてしまい、統計的に平文を推測できる

Goでの実装方法

暗号化・復号化の手順

◆ 暗号化の流れ

  1. データをPKCS#7でパディング
  • AESは16バイト単位(ブロック)でしか暗号化できない
  • 元のデータが16の倍数でない場合、末尾にパディングを足してサイズを調整
  1. 各ブロックをAESの Encrypt メソッドで暗号化
  • データを16バイトごとに分け、AESアルゴリズムを使って1ブロックずつ暗号化
  • ECBモードでは各ブロックを個別に、同じ鍵で暗号化する
  1. 暗号化された全ブロックを結合して、1つの暗号バイト列として返す

◆ 復号化の流れ

  1. 暗号文を16バイトごとに分け、Decrypt メソッドで復号化
  • 暗号文を同じ鍵で1ブロックずつ復号化する
  1. パディングを取り除く
  • 復号後のデータには、元の長さに合わせるために追加されていたパディングがある
  • PKCS#7のルールに従って、そのパディングを削除
  1. 最終的な平文(元の文字列)を返す

PKCS#7

  • 正式名称:Public-Key Cryptography Standards #7
  • 内容:公開鍵暗号方式などに関する一連の標準仕様のひとつ
  • 特にパディング(データ長の調整)方式としてよく使われる

PKCS#7パディング

  1. なぜパディングが必要か
  • AESなどのブロック暗号は、固定サイズ(例:16バイト)のデータしか扱えない
  • 例えば "Hello"(5バイト)では短すぎるため、残り11バイトを埋める必要がある
  1. どのように埋めるか
  • パディングでは、埋めた長さと同じ数値を使って埋める
  • 例えば「11バイト埋める」場合、バイト値 0x0B(11の16進数)を11個追加します。
元のデータ:        [H][e][l][l][o]
パディング後:      [H][e][l][l][o][0B][0B][0B][0B][0B][0B][0B][0B][0B][0B][0B]
(16バイトに調整)
  1. パディングの利点
  • 復号化時に、どれがパディングかが一目でわかるという特徴がある

実装

main.go
package main

import (
    "bytes"
    "crypto/aes"
    "fmt"
    "log"
)

// PKCS#7パディング
func pkcs7Padding(originalMessage []byte, blockSize int) []byte {
    padding := blockSize - len(originalMessage)%blockSize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(originalMessage, padtext...)
}

// PKCS#7アンパディング
func pkcs7UnPadding(data []byte) ([]byte, error) {
    length := len(data)
    if length == 0 {
        return nil, fmt.Errorf("data is empty")
    }
    padding := int(data[length-1])
    if padding > length {
        return nil, fmt.Errorf("invalid padding size")
    }
    return data[:length-padding], nil
}

// 暗号化
func encryptECB(originalMessage, key []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    blockSize := block.BlockSize()
    plaintext := pkcs7Padding(originalMessage, blockSize)
    cipherText := make([]byte, len(plaintext))
    for start := 0; start < len(plaintext); start += blockSize {
        block.Encrypt(cipherText[start:start+blockSize], plaintext[start:start+blockSize])
    }
    return cipherText, nil
}

// 復号化
func decryptECB(ciphertext, key []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    blockSize := block.BlockSize()
    if len(ciphertext)%blockSize != 0 {
        return nil, fmt.Errorf("ciphertext is not a multiple of the block size")
    }
    plaintext := make([]byte, len(ciphertext))
    for start := 0; start < len(ciphertext); start += blockSize {
        block.Decrypt(plaintext[start:start+blockSize], ciphertext[start:start+blockSize])
    }
    return pkcs7UnPadding(plaintext)
}

func main() {
    key := []byte("abcdefghijklmnop")
    originalMessage := []byte("Hello, ECB mode!")
    
    // 暗号化
    encrypted, err := encryptECB(originalMessage, key)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Encrypted: %x\n", encrypted)
    
    // 復号化
    decrypted, err := decryptECB(encrypted, key)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Decrypted: %s\n", decrypted)
}

まとめ

今回はECBモードについて書いていきました。
シンプルな暗号方式がゆえに、データが解読されやすいというセキュリティ的な問題があるということ、Goでの実装方法を知ることができました。
今度は、CBCモードについて書いていこうと思います。

参考

Discussion