HashiCorp Vault の Data Encryption (Transit Secret Backend) を試す

# はじめに
HashiCorpのVaultが提供している Data Encryption (Transit Secret Backend)機能について公式ドキュメントを参考に動作を確認しましたので備忘録として残しておきます。

# やったこと
Vaultの Data Encryption 機能を利用してにテキストの暗号化・復号化を行う。

# HashiCorp Vaultとは？
HashiCorpが提供している機密情報の管理ツールです。

# Data Encryption (Transit Secret Backend)とは？
Vaultにデータの暗号化、復号化を行わせる機能です。

この機能でVaultは暗号化復号化に利用するKeyの管理のみ行い、暗号化したデータは保持しません。
暗号化されたデータはVaultの呼び出し元で管理(DBなどに格納)する必要があります。

HashiCorpからRailsアプリ向けに提供されている[vault\-rails](https://github.com/hashicorp/vault-rails)ではこの機能が利用されてます。

# 参考

## 公式サイト
[Vault by HashiCorp](https://www.vaultproject.io/)

## ドキュメント
[Transit Secret Backend](https://www.vaultproject.io/docs/secrets/transit/index.html)
[Decrypt Data](https://www.vaultproject.io/api/secret/transit/index.html#type)

----

# 事前準備

Transit Secret Backendを利用できるようにしておきます。

## VaultをDevモードで起動

DevモードではVaultを停止させるとデータが消えてしまいますが、今回は動作確認のためDevモードで起動します。

```shell-session
$ vault server -dev

...
    export VAULT_ADDR='http://127.0.0.1:8200'
...
Root Token: e301d832-a587-0092-7dec-eaf71369c2b3
...
```

## 環境変数の設定

Vaultがリクエストを受け付けるアドレスとTokenを環境変数に設定しておきます。

```shell-session
$ export VAULT_ADDR='http://127.0.0.1:8200'
$ export VAULT_TOKEN='e301d832-a587-0092-7dec-eaf71369c2b3'
```

## transitをマウント

transit機能を利用できるようにマウントしておきます。

```shell-session
$ vault mount transit
```

----

# Keyを生成・削除

データの暗号化・復号化に利用するKeyを生成します。
Keyの名称は`my-key`とします。

## Create Key (aes256-gcm96)

暗号化方式のデフォルトは`aes256-gcm96`です。
typeオプションを指定しないでKeyを生成すると`aes256-gcm96`でKeyが生成されます。
typeオプションを指定することで `ecdsa-p256`, `ed25519`も指定可能です。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    $VAULT_ADDR/v1/transit/keys/my-key
```

or 

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "type": "aes256-gcm96" }' \
    $VAULT_ADDR/v1/transit/keys/my-key
```

### Create Key (ecdsa-p256)

typeオプション(暗号化方式)に`ecdsa-p256`を指定してKeyを生成。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "type": "ecdsa-p256" }' \
    $VAULT_ADDR/v1/transit/keys/my-key
```

### Create Key (ed25519)

typeオプション(暗号化方式)に`ed25519`を指定してKeyを生成。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "type": "ed25519" }' \
    $VAULT_ADDR/v1/transit/keys/my-key
```

## Read Key

Keyの情報を確認できます。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    $VAULT_ADDR/v1/transit/keys/my-key
    
{
  "request_id": "5d0e4a83-da8f-80b8-0ffb-d9cd9158cc0b",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "deletion_allowed": false,
    "derived": false,
    "exportable": false,
    "keys": {
      "1": 1506730923
    },
    "latest_version": 1,
    "min_decryption_version": 1,
    "min_encryption_version": 0,
    "name": "my-key",
    "supports_decryption": true,
    "supports_derivation": true,
    "supports_encryption": true,
    "supports_signing": false,
    "type": "aes256-gcm96"
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}
```

## List Keys

Keyの一覧を取得できます。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request LIST \
    $VAULT_ADDR/v1/transit/keys
    
{
  "request_id": "bf844792-a14c-4e6e-20f6-b10a0f1af141",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "keys": [
      "my-key"
    ]
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}
```

## Delete Key

Keyを削除します。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request DELETE \
    $VAULT_ADDR/v1/transit/keys/my-key    
```

Keyの削除が許可されてない場合、400エラーと以下のメッセージが返却されます。
削除するためにはKeyの`deletion_allowed`オプションを`true`に変更する必要があります。

```shell-session
< HTTP/1.1 400 Bad Request
...
{
  "errors": [
    "error deleting policy my-key: deletion is not allowed for this policy"
  ]
}
```

## Update Key Configuration

Keyの設定を変更します。
Keyの削除を許可する場合は`deletion_allowed`オプションを`true`にして実行します。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "deletion_allowed": true }' \
    $VAULT_ADDR/v1/transit/keys/my-key/config
```

----

# データの暗号化・復号化

生成したKeyを利用してデータの暗号化、復号化を実施します。

## plaintext

暗号化したいデータ(plaintext)を指定して暗号化します。
plaintextはbase64でエンコードされてる必要があります。

### base64 encode

vaultには暗号化対象のデータをbase64で渡す必要があるのでbase64でエンコードしておきます。

```shell-session
$ echo "テスト" | base64
44OG44K544OICg==
```

### Encrypt Data

base64化したテキスト(`plaintext`)をVaultに暗号化させます。
暗号化されたデータはレスポンスの`ciphertext`部分で返却されます。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "plaintext": "44OG44K544OICg==" }' \
    $VAULT_ADDR/v1/transit/encrypt/my-key
    
{
  "request_id": "3b15a62d-e5d5-dc9e-e60d-1763969dccf4",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "ciphertext": "vault:v1:iwDQyBSyXgo+MutoHXPT76MpVek78fxMQW1LXI3C3C++EVIJyFY="
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}
```

### Decrypt Data

暗号化したデータ(`ciphertext`)をVaultに復号化させます。
復号化されたデータはレスポンスの`plaintext`部分で返却されます。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "ciphertext": "vault:v1:iwDQyBSyXgo+MutoHXPT76MpVek78fxMQW1LXI3C3C++EVIJyFY=" }' \
    $VAULT_ADDR/v1/transit/decrypt/my-key

{
  "request_id": "7857e0c1-1f68-73e3-c54d-09fdfafd684f",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "plaintext": "44OG44K544OICg=="
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}
```

### base64 decode

Vaultから返却されたデータ(`plaintext`)をbase64でデコードすると元のテキストに戻ります。

```shell-session
$ echo "44OG44K544OICg==" | base64 -D
テスト
```

## 補足

### 暗号化

`plaintext`がbase64でエンコードされてないと暗号化できません。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "plaintext": "テスト" }' \
    $VAULT_ADDR/v1/transit/encrypt/my-key

< HTTP/1.1 400 Bad Request
...
{
  "errors": [
    "failed to base64-decode plaintext"
  ]
}
```

### 復号化

`ciphertext`の値が誤っていると復号化できなくなります。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "ciphertext": "vault:v1:iwDQyBSyXgo+MutoHXPT76MpVek78fxMQW1LXI3C3D++EVIJyFY=" }' \
    $VAULT_ADDR/v1/transit/decrypt/my-key

< HTTP/1.1 400 Bad Request
...
{
  "errors": [
    "invalid ciphertext: unable to decrypt"
  ]
}
```

## plaintext & context

Keyを生成する際に`derived`オプションに`true`を指定した場合、`plaintext`に加えて`context`が付与されてないと暗号化・復号化が出来ないようになります。

## Keyの生成

`derived`オプションに`true`を指定してKeyを生成します。
Keyの名称は`my-key2`とします。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "derived": true }' \
    $VAULT_ADDR/v1/transit/keys/my-key2
```

## base64 encode

`context`と`plaintext`をbase64でエンコードしておきます。

```shell-session
$ echo "キー" | base64
44Kt44O8Cg==
$ echo "テスト" | base64
44OG44K544OICg==
```

## Encrypt Data

`context`と`plaintext`を渡してVaultに暗号化させます。
暗号化したデータは`ciphertext`として返却されます。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "context":"44Kt44O8Cg==" , "plaintext": "44OG44K544OICg==" }' \
    $VAULT_ADDR/v1/transit/encrypt/my-key2
    
{
  "request_id": "29c41d1b-ec94-3800-af4b-f57b2aa58063",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "ciphertext": "vault:v1:WAJUPIOn65NV7dI+Zlbcr0z8XJvz1AqYQYJN6+yzGT+CVoKuK3s="
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}
```

## Decrypt Data


`context`と`plaintext`を渡してVaultに復号化させます。
復号化したデータは`plaintext`として返却されます。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "context":"44Kt44O8Cg==", "ciphertext": "vault:v1:WAJUPIOn65NV7dI+Zlbcr0z8XJvz1AqYQYJN6+yzGT+CVoKuK3s=" }' \
    $VAULT_ADDR/v1/transit/decrypt/my-key2
    
{
  "request_id": "15ea30d1-9c0c-e711-b947-3ace1a55c622",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "plaintext": "44OG44K544OICg=="
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}
```

### base64 decode

Vaultから返却されたデータをbase64でデコードすると元のテキストに戻ります。

```shell-session
$ echo "44OG44K544OICg==" | base64 -D
テスト
```

## 補足

### 暗号化

`derived`オプションが有効なKeyでは`context`を指定しないと暗号化できません。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "plaintext": "44OG44K544OICg==" }' \
    $VAULT_ADDR/v1/transit/encrypt/my-key2

< HTTP/1.1 400 Bad Request
...
{
  "errors": [
    "missing 'context' for key derivation; the key was created using a derived key, which means additional, per-request information must be included in order to perform operations with the key"
  ]
}
```

### 復号化

`derived`オプションが有効なKeyでは`context`を指定しないと復号化できません。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "ciphertext": "vault:v1:WAJUPIOn65NV7dI+Zlbcr0z8XJvz1AqYQYJN6+yzGT+CVoKuK3s=" }' \
    $VAULT_ADDR/v1/transit/decrypt/my-key2
 
 < HTTP/1.1 400 Bad Request
 ...
 {
  "errors": [
    "missing 'context' for key derivation; the key was created using a derived key, which means additional, per-request information must be included in order to perform operations with the key"
  ]
}
```

`context`が暗号化した時と不一致でも復号化できません。

```shell-session
$ curl \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request POST \
    --data '{ "context":"44Kr44O8Cg==", "ciphertext": "vault:v1:WAJUPIOn65NV7dI+Zlbcr0z8XJvz1AqYQYJN6+yzGT+CVoKuK3s=" }' \
    $VAULT_ADDR/v1/transit/decrypt/my-key2
    
< HTTP/1.1 400 Bad Request
...
{
  "errors": [
    "invalid ciphertext: unable to decrypt"
  ]
}
```