ssh-agentを復号パスワードのキャッシュのように利用する簡易的なデータ暗号化/復号化するPythonモジュールを作ってみた。

# 動機

`cron`等で定期的に認証のあるサーバーにhttp(s)アクセスするpythonスクリプトを書こうと思ったのだが、認証情報(`http_user`,`http_password`)を安全に保管する方法がないだろうか？と考えた。

  - 一般的な方法は、スクリプトとは別のファイルに保存して、ファイルのアクセス権限をファイルオーナーのみにする方法(`chmod 0600 ....`)である。 この場合、root権限のある人には見えてしまいそう。あと、ファイルバックアップとかでファイルが複製されたりする可能性とかも考えると、情報管理的に心配。
  - これを避けるためには、暗号化してファイルに書き込む必要がある。Pythonのスクリプトだと、ソースコードが丸見えで暗号化／復号化の方法を隠すことができないので、暗号化されたデータとともに復号のための情報(暗号化の際に用いたキーとか)が揃ってしまうと数行のスクリプトを書けば復号できてしまうので、これらの情報をファイルにセーブすると前項と同じ問題が生じる。
  - 上記の問題を避けるには、暗号化キーなどをファイル等にセーブするのではなく、人間が記憶して(コマンドラインオプション等で)スクリプトを実行する際に与える、といったことが必要。ただ、スクリプトを`cron`等で定期的に自動起動したい場合には別の手を考える必要がある。なにか別の方法が必要。
  - たとえば, `SSH`経由の`rsync`などで定期的にファイルを同期したい場合、`ssh-agent`に鍵をキャッシュさせたりする。最初に一度`ssh-add`をする必要があるが、`ssh-agent`に鍵が登録されている間は,`ssh-agent`のクライアントは鍵の利用が可能である。これを利用できないか？
  - `ssh-agent`から得られるのは秘密鍵なのでデータを直接暗号化することには適さないが、あらかじめ決めたデータを秘密鍵で署名することで、データを暗号化するためのキーの生成にはつかえるのでは？

と思ったところで、pythonに`ssh-agent`のクライアントとしてつかえるモジュール[`paramiko`](https://www.paramiko.org/)を見つけたので、`ssh-agent`にある鍵を使って暗号／復号キーを生成して、データを暗号/復号するライブラリを作成することにした。 **自分は暗号の専門家ではないので本当に安全な方法かわからないので、簡易的なデータのモザイク加工程度だと思って利用することにする。**

# 暗号／復号の具体的な方法の概要

上記で思いついた方法を（絵で）まとめると下記。（図中の`EncDSUtil....()`は、後述の自作モジュールの関数）

```mermaid
---
title: "暗号化の手順"
---
flowchart TD
   raw_data[/"**Raw Data**"/]

   master_key_phrase[/"**Master Key Phrase**"/]
   master_key_signed{{Master Key Signature}}
   ssh_private_key_enc[/"SSH Private Key (Encripted)"/]
   ssh_private_key_raw{{SSH Private Key}}
   ssh_public_key[\SSH Public Key\]
   ssh_passphrase[/"**SSH Key pass phrase**"/]

   ssh_key_gen(SSH Key Generation)
   sha_hash_0(SHA2 Hash)
   sha_hash_1(SHA2 Hash)

   gen_salt("EncDSUtil.GenSalt()")
   gen_iv("EncDSUtil.GenIV()")

   ssh_agent[SSH agent]
   key_sign[Signature]
   ssh_kdf["PBKDF2-HMAC"]
   enc_aes["AES-GCM"]

   enc_salt[\Salt\]
   enc_iv[\IV\]
   enc_data[\Encrypted Data\]
   
   enc_dataset@{ shape: docs, label: "{Encrypted_data, Salt, IV}" }

   merger@{ shape: f-circ, label: " " }

   ssh_passphrase -.-> ssh_key_gen

   subgraph "ssh-keygen"
   ssh_key_gen    --> ssh_private_key_enc
   ssh_key_gen    --> ssh_public_key
   end

   ssh_private_key_enc --> ssh_agent
   ssh_passphrase -.-> ssh_agent
   ssh_agent --> ssh_private_key_raw

   master_key_phrase   --> sha_hash_0

   raw_data --> enc_aes

   subgraph "encryption"
   ssh_private_key_raw --> key_sign
   sha_hash_0 --> key_sign
   key_sign --> master_key_signed
   master_key_signed --> sha_hash_1

   sha_hash_1 --> ssh_kdf
   gen_salt --> enc_salt
   enc_salt --> ssh_kdf
   ssh_kdf --> enc_aes
   gen_iv --> enc_iv
   enc_iv --> enc_aes
   enc_aes --> enc_data

   enc_data --> merger
   enc_salt --> merger
   enc_iv --> merger
   end
   
   merger --> enc_dataset
```

復号化の手順をまとめると、下記。 **暗号化されたデータ**と、暗号化の際に用いた**Master Key Phrase**は平文で保存しておいたとしても、`ssh-agent`から秘密鍵をもってこないと復号できないはずで、そのためには **`ssh-agent`に秘密鍵をデコードして渡すためのパスフレーズが必要** である。(実用的には、このほかに`ssh-agent`に複数の登録されていることがありうるので、どの鍵を使うかという情報も必要。) そのため **`ssh-agent`の信頼性と同程度には**信頼できる気がする。(あくまでも無保証)

ちなみにRSA暗号ではうまくいくが、`ed25519`では、署名が必ずしも一定値でないのでこの目的には適さない模様。

```mermaid
---
title: "復号化の手順"
---
flowchart TD

   enc_dataset[/"**{Encrypted_data, Salt, IV}**"/]

   master_key_phrase[/"**Master Key Phrase**"/]
   ssh_passphrase[/"**SSH Key pass phrase**"/]

   master_key_signed{{Master Key Signature}}
   ssh_private_key_raw{{SSH Private Key}}

   sha_hash_0(SHA2 Hash)
   sha_hash_1(SHA2 Hash)

   ssh_agent[SSH agent]
   key_sign[Signature]
   ssh_kdf["PBKDF2-HMAC"]
   dec_aes["AES-GCM"]

   dec_salt{{Salt}}
   dec_iv{{IV}}
   enc_data{{Encrypted Data}}

   divider@{ shape: f-circ, label: " " }
   
   raw_data@{ shape: docs, label: "**Decrypted_data**" }



   ssh_passphrase -.-> ssh_agent

   ssh_agent --> ssh_private_key_raw

   master_key_phrase   --> sha_hash_0

   enc_dataset --> divider

   subgraph "decryption"

   divider --> enc_data
   divider --> dec_iv
   divider --> dec_salt

   ssh_private_key_raw --> key_sign
   sha_hash_0 --> key_sign
   key_sign --> master_key_signed
   master_key_signed --> sha_hash_1

   sha_hash_1 --> ssh_kdf
   dec_salt --> ssh_kdf
   ssh_kdf --> dec_aes
   dec_iv --> dec_aes
   enc_data --> dec_aes
   end

   dec_aes --> raw_data

```

## 実装(1)


この目的で使うRSAの鍵ペアとしては、普段`ssh`コマンドで使う鍵ペアとは別途用意するのが無難に思われる。
[`paramiko`](https://www.paramiko.org/)は、`ssh-agent`にある鍵のリストを作る機能はあるが、
この目的ではいくつかの鍵を使い分けることにもなると思うので、まず、`python`で`SSH`の鍵を管理するモジュール[`sshkeyring`](https://pypi.org/project/sshkeyring/)を作成した。

- ファイル置き場
    - [github](https://github.com/nanigashi-uji/sshkeyring)
    - [gitlab](https://gitlab.com/nanigashi_uji/sshkeyring)
- インストール方法
    ```bash:sample
    % pip install sshkeyring
    ```
- 機能
    - 必要な場合にSSH鍵ペアの自動生成
    - 必要な場合にSSH鍵を`ssh-agent`に登録
    - 必要な場合に`ssh-agent`を起動
    - `ssh-agent`から特定のIDの秘密鍵を取得
    - (`ssh-agent`を使わず、ローカルの鍵ファイルの利用することにも対応)

`paramiko`を拡張してみた件については、[別記事](https://qiita.com/Nanigashi_Uji/items/901fb07e7b439b094c6b)にあります。


## 実装(2)

実際にデータを暗号化/復号化する機能を持ったクラスを提供するモジュール[`enc-ds`](https://pypi.org/project/enc-ds/)を作成した。

- ファイル置き場
    - [github](https://github.com/nanigashi-uji/enc-ds)
    - [gitlab](https://gitlab.com/nanigashi_uji/enc-ds)
- インストール方法
    ```bash:sample
    % pip install enc-ds
    ```
- 機能:  
    - `sshkeyring`を使って、SSH鍵を使えるようにセットアップ
    - 多層の`dict`型、`list`型による木構造のデータ構造(`enc_ds.DataTree` クラス)のバッファをもつ。
    - バッファにあるデータの暗号化、復号化
    - 暗号化されたデータをファイルから読み込んで復号
    - データを暗号化してファイルに保存
    - ファイル形式としては、`json`形式,`yaml`形式,`toml`形式,`ini`形式および、これらの圧縮ファイル(`bzip2`,`xz`,`gzip`)に対応。

詳しい[使い方の説明](https://github.com/nanigashi-uji/enc-ds/blob/main/README.md)はこれから整備したいとおもいますが、サンプルスクリプト`enciphered_datastorage`(`.py`)も同梱しています。このサンプルスクリプトは、コマンドラインツールとして、データやファイルの暗号化・復号が可能です。

初回起動時には、
```bash:sample
% ./bin/enciphered_datastorage -p ...somewhere... -v -i id_rsa_test 'Sample Plain Text' 'Second Data'

...
[Enc_ds.enc_ds.setup_sshkeyinfo:744] Info :  Key ID in use : id_rsa_test, Key Type in use : rsa
[SSHKeyInfo.set_passphrase:108] private key passphrase: 
[SSHKeyRing.ssh_add_keyinfo:308] Info ssh-add : ssh-agent ( key id: id_rsa_test, type: rsa, sock :  *******)
...
```
とSSH鍵をつくるためのパスフレーズのプロンプトが出て、入力するとRSA鍵ペアができて、`ssh-agent`が登録されます。２回目以降は`ssh-agent`に登録されている鍵が使われます。上記の例では、
```
---------------------------------------------------------------
(0) Input              : Sample Plain Text
---------------------------------------------------------------
(0) Enciphered         : b'\x00\x00\x00\x10\xab\x85\x18\x9d\xa3Op\xa9$\x1eD\x1fuEY\x9e\xa4~\xf5G/\xd2\xb9\xbf\xa3\xbd\xd2\x10&\xb6XS\xbb\xf1Y\x94\xcd\xcdwnrX\x1a\x94Can\x10\xe7+#YO\xde\xa6\xe8\xcc*\xefdx\x8e\x9d\xc8\x1d'
(0) Enciphered data    : {'data': b'\x00\x00\x00\x10\xab\x85\x18\x9d\xa3Op\xa9$\x1eD\x1fuEY\x9e\xa4~\xf5G/\xd2\xb9\xbf\xa3\xbd\xd2\x10&\xb6XS\xbb\xf1Y\x94\xcd\xcdwnrX\x1a\x94Can\x10\xe7+#YO\xde\xa6\xe8\xcc*\xefdx\x8e\x9d\xc8\x1d', 'iv': b'\xe7\xff\x89+J\xae}\xa7S\x91\xa3\xd0', 'salt': b'\xb8\xcf\xa9+}\xb3\xf8Lw%\x02M\xac\x16\x9f:\xa5W4V\x07p\x8eMP\t+K\xcfI#`', 'mkey': 'A0/WJqvYO6VtnpmXH/cUGj5s2a5ACw8minvHvhemLnw=', 'pkey': '8e04UCkhFk+eW7nM8ewH9g=='}
(0) Deciphered (Check) : Sample Plain Text
---------------------------------------------------------------
[EncDSUtil.CheckBytesLength:52] New 256-bytes data is created recreated
---------------------------------------------------------------
(1) Input              : Second Data
---------------------------------------------------------------
(1) Enciphered         : b'\x00\x00\x00\x10\x9dL\x10\x07\x15\x17\xe6\x081\xa8\xbd\x19I@\xdaR\xf7a\x81K\x97\xb6c\xe8 G\xb2\xeav\xb7\x1a\xe0\xf6\xcf\x1f\xfe\x82\x03X\n[\xdbT(}\xc4\x9c\x8d\xfa\x05\xba\x0b\x96\xf3z\xbc/\xda\xe4'
(1) Enciphered data    : {'data': b'\x00\x00\x00\x10\x9dL\x10\x07\x15\x17\xe6\x081\xa8\xbd\x19I@\xdaR\xf7a\x81K\x97\xb6c\xe8 G\xb2\xeav\xb7\x1a\xe0\xf6\xcf\x1f\xfe\x82\x03X\n[\xdbT(}\xc4\x9c\x8d\xfa\x05\xba\x0b\x96\xf3z\xbc/\xda\xe4', 'iv': b'\xbb\xdd\xe4yH+\xea\xb1\xd5T)f', 'salt': b'a\x95j\xa6~\x03\xb4\xfd\xe0F\xa3\x92\x0cq&\x1a\x00\x9a\x96"\x1aXD7X\xf1}d\x82[\x83\xbf', 'mkey': 'A0/WJqvYO6VtnpmXH/cUGj5s2a5ACw8minvHvhemLnw=', 'pkey': '8e04UCkhFk+eW7nM8ewH9g=='}
(1) Deciphered (Check) : Second Data
---------------------------------------------------------------
```
といった感じで、引数の文字列を暗号化し、また復号できることをチェックします。また、
` --encode-mode`か` --decode-mode`というオプション引数をつけると、ファイルIOに対応します。

## まとめ

汎用性の高いクラスになった気もしますが、説明が足りないですね。おいおい使用例を作っていこうと思います。