メインコンテンツまでスキップ

暗号化詳細

このガイドでは、secretctl で使用される暗号アルゴリズム、パラメータ、実装について詳細な情報を提供します。

暗号仕様

サマリー

コンポーネントアルゴリズムパラメータ
対称暗号AES-256-GCM256ビットキー、96ビット nonce
鍵導出Argon2id64MB メモリ、3イテレーション、4スレッド
Saltランダム128ビット(16バイト)
Nonceランダム暗号化ごとに96ビット(12バイト)
HMAC(監査ログ)HMAC-SHA256256ビットキー

標準準拠

仕様リファレンス
AES-GCMNIST SP 800-38D
Argon2RFC 9106
OWASPパスワードストレージチートシート
鍵導出HKDF (RFC 5869)

AES-256-GCM

なぜ AES-256-GCM か?

プロパティメリット
認証付き暗号化改ざんを自動検出
NIST 推奨政府および業界標準
ハードウェアアクセラレーション現代の CPU で高速(AES-NI)
十分な検証数十年の暗号解析

実装

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

func encrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

// ランダム nonce を生成
nonce := make([]byte, gcm.NonceSize()) // 12 バイト
if _, err := rand.Read(nonce); err != nil {
return nil, err
}

// 暗号化と認証
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}

func decrypt(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}

nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}

Blob フォーマット

すべての暗号化データは一貫したフォーマットを使用:

┌──────────────┬─────────────────────┬────────────────┐
│ Nonce (12B) │ 暗号文 │ GCM Tag (16B) │
└──────────────┴─────────────────────┴────────────────┘

適用対象:

  • encrypted_dek (vault_keys テーブル)
  • encrypted_key (secrets テーブル)
  • encrypted_value (secrets テーブル)
  • encrypted_metadata (secrets テーブル)

このフォーマットの利点

利点説明
自己完結型各 blob は独立して復号可能
別の nonce カラム不要シンプルなデータベーススキーマ
業界標準libsodium sealed box と同じ

Argon2id 鍵導出

なぜ Argon2id か?

プロパティメリット
メモリハードGPU/ASIC に高コスト
時間ハード複数イテレーションが必要
並列性複数コアを活用
RFC 標準RFC 9106 (2021)

Argon2id は Argon2i(サイドチャネル耐性)と Argon2d(GPU 耐性)を組み合わせています。

パラメータ

import "golang.org/x/crypto/argon2"

const (
memory = 64 * 1024 // 64 MB
iterations = 3
parallelism = 4
keyLength = 32 // 256 ビット
saltLength = 16 // 128 ビット
)

func deriveKey(password string, salt []byte) []byte {
return argon2.IDKey(
[]byte(password),
salt,
iterations,
memory,
parallelism,
keyLength,
)
}

OWASP 準拠

これらのパラメータは OWASP パスワードストレージチートシート に準拠:

パラメータOWASP 推奨
メモリ64 MB64 MB 最小
時間33 イテレーション
スレッド44(現代のマルチコア)

環境別パフォーマンス

環境メモリアンロック時間ステータス
現代の PC/Mac8GB+0.5-1秒最適
低スペックラップトップ4GB1-2秒良好
Raspberry Pi 42-8GB1-3秒許容
CI 環境2-4GB1-2秒良好
Docker(制限あり)可変可変64MB+ 必要
Docker メモリ

64MB 未満のメモリを持つコンテナは鍵導出中に失敗します。コンテナに十分なメモリが割り当てられていることを確認してください。

Nonce 管理

一意性の保証

AES-GCM は一意の nonce が必要です。同じキーで nonce を再利用するとセキュリティが損なわれます(Forbidden Attack)。

戦略: ランダム Nonce

func generateNonce() ([]byte, error) {
nonce := make([]byte, 12) // 96 ビット
if _, err := rand.Read(nonce); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
return nonce, nil
}

衝突確率

Nonce 長衝突発生確率
96ビット2^32 暗号化後2^-33 ≈ 86億分の1

個人使用では、40億回の暗号化に達することは実質的に不可能です。

ランダム性ソース

import "crypto/rand"

Go の crypto/rand は以下を使用:

  • Linux: /dev/urandom (CSPRNG)
  • macOS: getentropy() (カーネルエントロピー)
  • Windows: CryptGenRandom() (CryptoAPI)

すべて暗号学的に安全な疑似乱数生成器です。

鍵階層詳細

3層構造

┌─────────────────────────────────────────────────────────────────────┐
│ 鍵階層 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 第1層: マスターパスワード(ユーザー入力) │
│ │ │
│ │ 保存されない、導出にのみ使用 │
│ │ │
│ ▼ │
│ 第2層: マスターキー(導出) │
│ │ │
│ │ = Argon2id(password, salt) │
│ │ セッション中のみメモリに存在 │
│ │ │
│ ▼ │
│ 第3層: データ暗号化キー (DEK) │
│ │ │
│ │ 保存形式: AES-GCM(DEK, MasterKey) │
│ │ すべてのシークレットの暗号化に使用 │
│ │ │
│ ▼ │
│ シークレット: AES-GCM(secret, DEK) │
│ │
└─────────────────────────────────────────────────────────────────────┘

なぜこの設計か?

機能メリット
パスワードローテーションすべてのシークレットを再暗号化せずにパスワード変更
防御を深める1つの層の侵害ですべてが露出しない
セッション分離ロック後にマスターキーをクリア

パスワードローテーションフロー

1. Vault をアンロック(旧マスターキーを導出)
2. 旧マスターキーで DEK を復号
3. 新しい salt を生成
4. 新パスワードから新マスターキーを導出
5. 新マスターキーで DEK を再暗号化
6. 新しい暗号化された DEK と salt を保存
7. シークレットは変更なし(同じ DEK で暗号化されたまま)

データベーススキーマ

テーブル

-- 暗号化された DEK ストレージ
CREATE TABLE vault_keys (
id INTEGER PRIMARY KEY,
encrypted_dek BLOB NOT NULL, -- nonce || AES-GCM 暗号文
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- シークレットストレージ
CREATE TABLE secrets (
id INTEGER PRIMARY KEY,
key_hash TEXT UNIQUE NOT NULL, -- SHA-256(key) ルックアップ用
encrypted_key BLOB NOT NULL, -- nonce || 暗号化されたキー名
encrypted_value BLOB NOT NULL, -- nonce || 暗号化された値
encrypted_metadata BLOB, -- nonce || 暗号化された JSON(オプション)
tags TEXT, -- カンマ区切り、平文
expires_at TIMESTAMP, -- クエリ用平文
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- HMAC チェーン付き監査ログ
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY,
action TEXT NOT NULL, -- get, set, delete, list
key_hash TEXT, -- アクセスしたキーの SHA-256
source TEXT NOT NULL, -- cli, mcp, ui
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
prev_hash TEXT NOT NULL, -- 前のレコードハッシュ
record_hash TEXT NOT NULL -- このレコードの HMAC
);

なぜ key_hash か?

平文のキー名の代わりに SHA-256(key) を保存:

  • 復号せずにルックアップが可能
  • 保存時のキー名を保護
  • 一方向(ハッシュからキー名を導出できない)

メタデータ暗号化

構造

type SecretMetadata struct {
Version int `json:"v"` // スキーマバージョン (1)
Notes string `json:"notes,omitempty"` // 最大 10KB
URL string `json:"url,omitempty"` // 最大 2048 文字
}

ストレージルール

条件encrypted_metadata
notes="" AND url=""NULL(暗号化なし)
notes OR url に値ありnonce + AES-GCM(JSON)

MCP アクセス制限

AI安全設計はメタデータにも拡張:

データMCP アクセス
key (名前)あり(secret_list 経由)
tagsあり(平文、検索用)
expires_atあり(平文、クエリ用)
has_notesあり(ブールフラグのみ)
has_urlあり(ブールフラグのみ)
notes 内容なし
url 内容なし
valueなし

ルール: 暗号化されたカラム = MCP アクセス禁止。

監査ログ整合性

HMAC チェーン

// マスターキーから監査キーを導出
auditKey := hkdf.Expand(sha256.New, masterKey, []byte("audit-log-v1"), 32)

// レコード HMAC を計算
func computeRecordHMAC(record AuditRecord, prevHash string, key []byte) string {
data := fmt.Sprintf("%d|%s|%s|%s|%s|%s",
record.ID,
record.Action,
record.KeyHash,
record.Source,
record.Timestamp.Format(time.RFC3339Nano),
prevHash,
)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}

検証

func verifyChain(records []AuditRecord, key []byte) error {
for i, r := range records {
// HMAC を検証
var prevHash string
if i > 0 {
prevHash = records[i-1].RecordHash
}
expected := computeRecordHMAC(r, prevHash, key)
if r.RecordHash != expected {
return fmt.Errorf("record %d: HMAC mismatch", r.ID)
}

// チェーンを検証
if i > 0 && r.PrevHash != records[i-1].RecordHash {
return fmt.Errorf("record %d: chain broken", r.ID)
}
}
return nil
}

使用ライブラリ

Go 標準ライブラリ

import (
"crypto/aes" // AES 暗号化
"crypto/cipher" // GCM モード
"crypto/hmac" // HMAC
"crypto/rand" // セキュアランダム
"crypto/sha256" // SHA-256 ハッシュ
)

golang.org/x/crypto

import (
"golang.org/x/crypto/argon2" // 鍵導出
"golang.org/x/crypto/hkdf" // 鍵拡張
)

なぜこれらのライブラリか?

ライブラリ理由
Go 標準ライブラリ実績あり、監査済み、メンテナンス済み
golang.org/x/crypto公式 Go 暗号拡張

サードパーティの暗号ライブラリは使用せず、サプライチェーンリスクを最小化。

セキュリティ考慮事項

保護されるもの

資産保護
シークレット値AES-256-GCM 暗号化
キー名AES-256-GCM 暗号化
メタデータAES-256-GCM 暗号化
マスターパスワード保存されない
監査整合性HMAC チェーン

保護されないもの

資産理由
タグ検索用に平文で保存
有効期限クエリ用に平文で保存
レコード数DB から観察可能
アクセスパターン監査ログで保護

バックアップ考慮事項

# バックアップ安全(暗号化済み)
~/.secretctl/vault.db
~/.secretctl/vault.salt
~/.secretctl/vault.meta

# リストアに必要
# - 上記3ファイルすべて
# - マスターパスワード(保存されていない)
パスワード紛失

マスターパスワードを忘れた場合、シークレットは復元できません。これは設計によるものです - バックドアはありません。

次のステップ