🔐

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

に公開

はじめに

最近暗号技術について興味があり、少しずつ学習をしています。
第1回はECBモード、第2回はCBCモード、第3回はCFBモード、第4回はOFBモード、第5回はCTRモードを取り上げました。
今回はGCMモードを紹介します。

第1回
https://zenn.dev/tmyhrn/articles/65ad6aa32964a5
第2回
https://zenn.dev/tmyhrn/articles/19542a7057f1c6
第3回
https://zenn.dev/tmyhrn/articles/b7cee9a8951a5e
第4回
https://zenn.dev/tmyhrn/articles/430c893f2016e7
第5回
https://zenn.dev/tmyhrn/articles/716d50b2c09da8

✅この記事でわかること

  • GCMモードの特徴
  • GCMモードの仕組み(暗号化・復号化の流れ)
  • GCMモードを使う上での注意点

📝GCMモードの特徴

  • Galois/Counter Modeの略(認証付きCTRモード)
  • 「Nonce + Counter」でCTRモードとして暗号化を行う
  • 暗号化に加えて認証(改ざん検知)機能がある
    • Galois(ガロア)演算によって認証タグ(MAC)を生成
  • 暗号文と一緒に 認証タグ(例:16バイト) も出力される
  • 復号時には認証タグを用いて 整合性チェック(改ざん検出) を行う
  • Nonceは一意である必要がある(同じ鍵で再利用すると危険)

📊GCMモード解説

暗号化

  1. 暗号化した「Nonce + Counter」と平文をXOR
  2. Counterをインクリメントし、その値を暗号化し平文とXOR

復号化

  1. 暗号化した「Nonce + Counter」と暗号ブロックをXOR
  2. Counterをインクリメントし、その値を暗号化し暗号ブロックとXOR

⚠️GCMモードの注意点

  • Nonceの再利用はNG
    crypto/randなどで毎回異なるNonceを生成
  • 認証タグの検証を必ず行う
    gcm.Open()メソッドは、復号と認証タグの検証を同時に行う

🛠Goでの実装方法

暗号化の手順

  1. Nonce(IV)とカウンタ(CTR)をAESで暗号化
  2. 出力と平文をXORして暗号文を取得
  3. 同時にGalois認証処理で認証タグ(MAC)を生成

復号化

  1. 暗号文からNonceを取得
  2. AESで暗号化し、暗号文とXORして平文を取得
  3. 認証タグと復号化された内容を検証
  4. 認証がOKなら平文を出力、NGなら改ざんが検出されたということで復号化に失敗

実装

main.go
package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "io"
    "log"
)

// GCM暗号化
func encryptGCM(key, plainText []byte) ([]byte, []byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, nil, err
    }
    aesgcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, nil, err
    }
    nonce := make([]byte, aesgcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, nil, err
    }
    encryptedText := aesgcm.Seal(nil, nonce, plainText, nil)
    encryptedText = append(nonce, encryptedText...)
    
    return encryptedText, nonce, nil
}

// GCM復号化
func decryptGCM(key, encryptedText []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    aesgcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }
    if len(encryptedText) < aesgcm.NonceSize() {
        return nil, fmt.Errorf("too short encrypted text")
    }
    nonce := encryptedText[:aesgcm.NonceSize()]
    encryptedText = encryptedText[aesgcm.NonceSize():]
    decryptedText, err := aesgcm.Open(nil, nonce, encryptedText, nil)
    if err != nil {
        return nil, err
    }
    
    return decryptedText, nil
}

func main() {
    key := []byte("gcm-example-key!")
    plainText := []byte("Hello, GCM mode!")
    fmt.Println("PlainText:", string(plainText))
    
    encryptedText, nonce, err := encryptGCM(key, plainText)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("EncryptedText:", hex.EncodeToString(encryptedText))
    fmt.Println("Nonce:", hex.EncodeToString(nonce))
    
    decryptedText, err := decryptGCM(key, encryptedText)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("DecryptedText:", string(decryptedText))
}

出力例

ターミナル
PlainText: Hello, GCM mode!
EncryptedText: c5f281e4876139ad372c9cd757814d23e4236fff303ae498400d5c4b1fb6ff27bc585733e894a517e8fff046
Nonce: c5f281e4876139ad372c9cd7
DecryptedText: Hello, GCM mode!

まとめ

今回はGCMモードについて書いていきました。
認証付き暗号という新しい要素(改ざん検知)が加わるため、他のモードと比べて仕組みが複雑に感じられました。
この辺りをもっと学習していきたいですね!

参考

https://pkg.go.dev/crypto/aes
https://pkg.go.dev/crypto/cipher
https://qiita.com/ken5scal/items/da387d6db8b8308474e6
https://www.ios-net.co.jp/blog/20230823-1313/
https://qiita.com/yonaka15/items/d4535ab905cca6335f50

Discussion