🔐

Goで対称暗号化(AES-256-GCM)を実装する

に公開

はじめに

最近、Go言語でデータの暗号化・復号化を実装する機会がありました。
「どの暗号化方式が安全なのか」「実装時の注意点は何か」を調べる中で、OWASPでは対称暗号化の場合、AES方式の256ビット鍵をGCMで利用することが推奨されていると分かりました。
実際に実装してみたので、同じような実装を検討している方の参考になれば幸いです。

OWASP Cryptographic Storage Cheat Sheet

対称暗号化と非対称暗号化の違い

暗号化方式には大きく分けて2つの方式があるようです。

  • 対称暗号化: 同じ鍵を使って暗号化と復号化を行う方式
  • 非対称暗号化: 「公開鍵」と「秘密鍵」の2つの異なる鍵を使用する方式

本記事では、アプリケーション内でのデータ保存を想定しているため、処理速度に優れた対称暗号化を選択しました。

実装例

以下のコードを encryption.go などのファイル名で保存して go run encryption.go を実行してください。

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"fmt"
	"io"
)

const secretKey = "sk_0123456789abcdefghijklmnopqrstuvwxyz"

// AES-256-GCMで暗号化(バイト配列を返す)
func encryptAES256GCM(plaintext string, key []byte) ([]byte, error) {
	plainbytes := []byte(plaintext)

	// AESブロック暗号を作成
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("failed to create new AES cipher: %w", err)
	}

	// GCMを作成
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("failed to create new GCM: %w", err)
	}

	// nonceを生成
	nonce := make([]byte, gcm.NonceSize())
	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, fmt.Errorf("failed to generate nonce: %w", err)
	}

	// 暗号化
	// Sealはnonce + 暗号化データ + 認証タグを含む結果を返す
	ciphertext := gcm.Seal(nonce, nonce, plainbytes, nil)

	return ciphertext, nil
}

// AES-256-GCMで復号化(バイト配列から復号化)
func decryptAES256GCM(cipherbytes []byte, key []byte) (string, error) {
	// AESブロック暗号を作成
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", fmt.Errorf("failed to create new AES cipher: %w", err)
	}

	// GCMを作成
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", fmt.Errorf("failed to create new GCM: %w", err)
	}

	// nonceサイズを取得
	nonceSize := gcm.NonceSize()

	// データサイズの検証
	if len(cipherbytes) < nonceSize {
		return "", fmt.Errorf("cipherbytes too short")
	}

	// nonceと暗号化データを分離
	nonce, ciphertextData := cipherbytes[:nonceSize], cipherbytes[nonceSize:]

	// 復号化実行
	plaintext, err := gcm.Open(nil, nonce, ciphertextData, nil)
	if err != nil {
		return "", fmt.Errorf("failed to decrypt (data may have been tampered with): %w", err)
	}

	return string(plaintext), nil
}

// 256-bit (32-byte) の鍵を生成
func generateKey() ([]byte, error) {
	key := make([]byte, 32) // 256-bit
	_, err := rand.Read(key)
	if err != nil {
		return nil, fmt.Errorf("failed to generate key: %w", err)
	}
	return key, nil
}

func main() {
	// 鍵を生成
	key, err := generateKey()
	if err != nil {
		fmt.Printf("Key generation error: %v\n", err)
		return
	}

	// 暗号化対象のデータ
	plaintext := secretKey
	fmt.Println("Original data   :", plaintext)

	// 暗号化
	encrypted, err := encryptAES256GCM(plaintext, key)
	if err != nil {
		fmt.Printf("Encryption error: %v\n", err)
		return
	}

	// 復号化
	decrypted, err := decryptAES256GCM(encrypted, key)
	if err != nil {
		fmt.Printf("Decryption error: %v\n", err)
		return
	}
	fmt.Println("Decrypted result:", decrypted)

	// 検証
	if plaintext == decrypted {
		fmt.Println("✅ Encryption and decryption successful")
	} else {
		fmt.Println("❌ Failed")
	}
}

参考資料

Discussion