Carpe Diem

備忘録

データ暗号化で考慮すること(鍵編)

概要

前回の続きです。

christina04.hatenablog.com

今回は暗号化における鍵(共通鍵)について説明します。

前提知識

鍵とは

鍵とはとても大きな数です。
鍵のビット長が大きい=鍵空間が大きいことを意味します。つまり可能な鍵の総数が大きいため総当り攻撃が困難になります。

AESでは128bits, 192bits, 256bitsのビット長の鍵を選択できますが、長いほど鍵空間が大きくて良いということになります。

鍵導出関数(KDF: Key Derivation Function)

鍵導出関数は与えられたパスワードから共通鍵を生成する関数です。同一パスワードからでも異なる鍵が生成できるよう、saltや繰り返し数cといったパラメータを使います。

Goでは以下のpackageが使えます。

CEK(Contents Encrypting Key)とKEK(Key Encrypting Key)

一般的に暗号化は

  • 乱数から鍵を生成するのが良い
  • コンテンツ(暗号化したいデータ)ごとに異なる鍵を用いるのが良い

ですが、一方で乱数の鍵は覚えることができないのでどこかしらに保存する必要があります。
しかしそうなると今度は鍵を保存する場所の安全面が課題になりますし、コンテンツごとに鍵が別々だと管理が非常に大変です。

そこでエンベロープ暗号化のようにコンテンツを暗号化する鍵(CEK)と、その鍵を暗号化する鍵(KEK)を分ける方法があります。

こうしておくことで仮に暗号化したCEKが漏洩してもKEKがある限り解読は困難になり、コンテンツの解読もできなくなります。

また、鍵のローテーション面でもCEK・KEKの仕組みはメリットがあります。 なぜなら古いKEKでCEKを復号して新しいKEKでCEKを暗号化するだけ良く、暗号化したデータを新しい鍵で暗号化する必要がありません

CEKとKEKの生成方法

CEKを乱数から生成し、KEKをsaltとパスワードから鍵導出関数を使って生成することでセキュアな鍵管理を実現できます。

具体的な実装

Goでの具体的な実装を紹介します。

CEKの生成

擬似乱数生成器はcrypto/randを使います。

import (
    "crypto/rand"
)

const (
    keySize  = 32
)

func GenerateCEK() ([]byte, error) {
        key := make([]byte, keySize)  // keySize is 32 bytes
        if _, err := rand.Read(key); err != nil {
                return nil, err
        }
        return key, nil
}

KEKの生成

KEKを生成する際は擬似乱数生成器からsaltを生成します。
復号する際は暗号化に使用したsaltから同じKEKを生成できるようにします。

今回鍵導出関数にはargon2を使っています。

import (
    "crypto/rand"
    "golang.org/x/crypto/argon2"
)

func generateKEK(password string, salt []byte) ([]byte, []byte, error) {
        if salt == nil {
                salt = make([]byte, saltSize)  // 16 bytes are recommended
                if _, err := rand.Read(salt); err != nil {
                        return nil, nil, err
                }
        }
        key := argon2.Key([]byte(password), salt, 3, 32*1024, 4, keySize)

        return key, salt, nil
}

CEKの暗号化

平文を暗号化したらCEKをKEKで暗号化します。

func encryptCEK(password string, cek []byte) ([]byte, []byte, error) {
        kek, salt, err := generateKEK(password, nil)
        if err != nil {
                return nil, nil, err
        }

        encrypted, err := encrypt(kek, cek)
        if err != nil {
                return nil, nil, err
        }
        return salt, encrypted, nil
}

saltと暗号化済みCEKはどこか安全な場所に保存しておきます。

CEKの復号

パスワード、保存されたsaltからKEKを再び生成して、暗号化済みCEKを元のCEKに復号します。

func decryptCEK(password string, salt, ecek []byte) ([]byte, error) {
        kek, salt, err := generateKEK(password, salt)
        if err != nil {
                return nil, err
        }

        cek, err := decrypt(kek, ecek)
        if err != nil {
                return nil, err
        }
        return cek, nil
}

復号されたCEKにより、暗号文を復号することが可能です。

実行

実行するたびにKEK、salt、CEK、暗号文が変わっていくことを確認できます。

crypto $ go run main.go
cek: a84420357a8d7b3822bba3ae312ec8bfd33eb507b925cacb6aeba7cc61bd677f
ciphertext: 60a550657175930682ecd90459ae7a94d7d073736e293d217033949ed508cfa0456a7b000d7664
kek: 6c3e997e5fe873e532cc7a4feb111d11bbb59e9a6c559da35e58bcda44657f61
salt: eaa9a11d68c42d5b7be553543cde2b2f
encrypted CEK: 69b53ddc9746797c33f7c278c2b1f8626a4e7f83b32f364bd8aaf8f0c3b15670cdf09d4108fa71186e91926159d4d746eeca6c7cdd2580a159e2f7fb
plaintext: secret text

crypto $ go run main.go
cek: 20cd808eeeea9929f94173afc926abd8c9d7913cc91402f79db00e4085ca393f
ciphertext: 5eeb2784deeb002820df772a57d8f76660ef952c74f89a0e48a08a9c50d34081118ec9c26b28ae
kek: 7e195f5d7525b4bbe0cf6fe4be5fe3ac9f5f1fdfca21f4fb257b39b85e21cbd6
salt: 126fd63d443b78abed4b735f2560c2b2
encrypted CEK: 516df8d4872255f0e1e2d99a81ea328054d07c8f3617b28f5428814e3f1261d2fac48255c7068fcf7783aabfe916cdf643bbd613310797f61a199860
plaintext: secret text

サンプルコード

今回の全体のコードは以下です。

github.com

まとめ

データの暗号化においてセキュアな鍵の生成・利用方法を紹介しました。

ソース