GUIなのにCLIなパスワードマネージャを作った

#概要
ソースと資料→[Keasy - GitHub](https://github.com/sh141/Keasy)

パスワードマネージャーといえば普通は[KeePass](https://keepass.info/)．

こんな便利なものが既にあるのに，わざわざ新たに作った理由は以下．


* パスワードマネージャーをマウスで操作するのが面倒
* ショートカットキー(キー3つ)すら面倒
* タスクトレイに入ったり出たりする機能が動作しなかった


開発したKeasyができることは以下．

* 動作は全てコマンド入力
* マウスにできるのはウィンドウの移動かサイズ変更
* Ctrlを0.3秒位内に2回押せばタスクトレイに入ったり出たりする
* Firefoxにフォーカスしてる時にトレイから出すとアカウントを自動予測する
* Shiftを0.3秒位内に2回押せば，ユーザIDとパスワード自動入力する

#主な機能
##アカウント自動予測
↓デモでやっているのは，

* [ctrl->ctrl]でKeasyをタスクトレイに格納
* GitHubのログインページを開く
* [ctrl->ctrl]でKeasyをタスクトレイから展開
* このときwebページのURLからアカウントを予測して一時記憶
* [ctrl->ctrl]でKeasyをしまう
* 一時記憶したユーザID/Mailとパスワードを[shift->shift]で自動入力
* ログイン

実際の入力は全てKeasyがやってくれる．

![auto_memorize.gif](https://qiita-image-store.s3.amazonaws.com/0/201962/256991c5-3506-5d30-aa27-8977935436c2.gif)


##アカウント追加
```console
$ add
```
Keasyにアカウントを追加する．

![add.gif](https://qiita-image-store.s3.amazonaws.com/0/201962/f179d273-6c2c-450d-cfd6-d684266cd216.gif)

##アカウント検索
```console
$ find [検索したい文字]
```

Keasyに登録されているアカウントを検索して表示する．

上のアカウント追加のデモで最後にやっているコマンド．

##ユーザID/mailとパスワードの一時記憶
```console
$ memorize [サービス名] [ユーザID/Mail]
```

Keasyに登録されているアカウントを選び，そのユーザID/Mailとパスワードを一時記憶する．

一時記憶したユーザID/Mailとパスワードは，Shift->Shiftで自動入力できる．

また，Ctrl+[Shift->Shift]（要するにCtrl押しながらShiftを2回）で片方だけ自動入力する．

自動入力の対象はCtrl+Spaceで切替可能．押す度に，ユーザID/Mail <--> パスワード　を切り替える．


※片方だけの自動入力機能はGmailのために作った(メール欄しか出してくれないから)．
Googleアカウントへのログインはこうやる↓

* Ctrl+[Shift->Shift] -> 認証 -> Ctrl+Space -> Ctrl+[Shift->Shift] -> 認証
* 要約：メールアドレスだけ自動入力 -> 入力対象をパスワードに切り替え -> パスワードだけ自動入力

##アカウント編集
```console
$ edit [サービス名] [ユーザID/Mail] [編集したい情報]
```

Keasyに登録されているアカウントを編集する．
[編集したい情報]は次のいずれかを入力できる．Tab補完可能．

* ServiceName
* SearchWord
* LoginURL
* ID_or_Mail
* PassWord
* Remarks

![edit.gif](https://qiita-image-store.s3.amazonaws.com/0/201962/bd7eb5ac-7994-8de5-04bf-8c981b7c6c36.gif)


##パスワードの表示/非表示
パスワードを表示する

```console
$ show
```

パスワードを伏せる

```console
$ hide
```

![show_hide.gif](https://qiita-image-store.s3.amazonaws.com/0/201962/db5e5e06-bb0b-15ce-6702-59c73148469a.gif)


##アカウント削除
```console
$ delete [サービス名] (ユーザID/Mail)
```

Keasyに登録されているサービスまたはアカウントを削除する．

* サービスを削除すると，紐付けられたアカウントも削除する
* サービスに紐付けられたアカウントが1つだけの場合，そのアカウントを削除するとサービスも削除する

![delete.gif](https://qiita-image-store.s3.amazonaws.com/0/201962/5fbe2e31-d118-abdc-8139-aa15fa85e9e7.gif)


#開発コスト
## 期間
およそ10ヶ月．しかし実際には開発していない日も多い．

日数で見ると47日だった．
1日8時間とすれば376時間だが，そんなに詰めていたとは考えられないので半分の188時間ほどではないかと思う．

##規模

* Keasy.py:10行
* WindowGUI.py:545行
* CommandEvents.py:1802行
* Database.py:644行
* TimerThread.py:18行
* AES_Cipher_SQLite.py:48行

計：3067行

もマ無．

#セキュリティ
Keasy自体はネットワークに接続しないので，PCに直接アクセスされなければ特に問題がない．

とはいえ，DBファイルを外部に持ち出して漏洩するのはテンプレなので，予めDBファイルごとAES暗号化するようにしている．
※Python3のcryptodomexを使った
※cryptodomexはcryptodomeの旧式だが，domexでないとビルドしたexeを起動できない

最初にDB作成する時，マスターパスワード文字列によってKeasy.dbをファイルごと暗号化する（-> encrypted.keasy）．

以降はKeasyを起動する度に，以下のようにデータ操作している．

* マスターパスワードを使ってencrypted.keasyをKeasy.dbに復号
* 復号したKeasy.dbを操作
* Keasy.dbを変更する度に暗号化し，encrypted.keasyを上書き保存
* Keasy終了時にKeasy.dbを削除

つまりencrypted.keasyを入れ替えれば，別のPCでもDBを使える．

※強力なパスワードをかけていても，encrypted.keasyが漏洩した時点でもはやリスクしかないことに注意

※パスワード暗号化で足止めしている間に全アカウントを交換するしかないかもしれない

#exe化に成功するまでの軌跡
PyInstallerを使ったexeビルドがとてもつらかったので記録だけ残す．

## 何も考えずビルド
```console
pyinstaller --onefile .\Keasy.py
```

ビルド中エラー

```console
WARNING: lib not found: api-ms-win-crt-stdio-l1-1-0.dll dependency of c:\users\[ユーザ名]\appdata\local\programs\python\python36\python.exe
```

GitHub上のissueなどを眺めている限りでは，

* Windows10特有の問題らしい？
* api-ms-win-crt-stdio-l1-1-0.dllはVisualStudio2015以降の再頒布可能パッケージに含まれ，見つからないはずはないらしい

## dllへのパスを含めてビルド

```console
pyinstaller --onefile --path C:\Windows\WinSxS\amd64_avg.vc140.crt_f92d94485545da78_14.0.24210.0_none_69fa0197d9b096ae\ .\Keasy.py
```

ビルド中エラー．しかしこれは無視しておｋ

```console
WARNING: library coredll required via ctypes not found
```

実行時にコンソールが吐き出すエラー

```console
OSError: Cannot load native module 'Crypto.Cipher._raw_ecb'
```

## ソースコードを変えてビルド
ソースコードでのインポート先を変更したら消えた．

```python
from Crypto.Cipher import AES  # <-エラー出る
from Cryptodome.Cipher import AES  # <-エラー出ない
```

元々CryptoをインストールできなかったからCryptodomeを使っていたが，実行時にも絡んでくるとは…

しかし次の実行時エラー

```console
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\[ユーザ名]\\AppData\\Local\\Temp\\_MEI187602\\comtypes\\gen\\_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py'
```

## インポートを隠してビルド
該当する.pyファイルをhidden-importの対象にすることで解決

```console
pyinstaller --onefile --path C:\Windows\WinSxS\amd64_avg.vc140.crt_f92d94485545da78_14.0.24210.0_none_69fa0197d9b096ae\ --hidden-import comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 .\Keasy.py
```

実行時エラー

```console
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\GNMACH~1\\AppData\\Local\\Temp\\_MEI165362\\comtypes\\gen\\UIAutomationClient.py'
```

何も考えず該当ファイルをhidden-importの対象にして解決

```console
pyinstaller --onefile --path C:\Windows\WinSxS\amd64_avg.vc140.crt_f92d94485545da78_14.0.24210.0_none_69fa0197d9b096ae\ --hidden-import comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 --hidden-import comtypes.gen.UIAutomationClient .\Keasy.py
```

同様の議論が交わされていた．
>[PyInstaller 3.3.1 does not work with Pywinauto lib import](https://github.com/pyinstaller/pyinstaller/issues/3177)
falconws commented on 21 Feb  
@luziling many thanks!!  
Note:
This is work for me
pyinstaller --hidden-import comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 --hidden-import comtypes.gen.UIAutomationClient test.py --onefile --clean  
But this is not work for me
pyinstaller --onefile --clean --hidden-import comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 --hidden-import comtypes.gen.UIAutomationClient test.py  
Different is --onefile and --clean argument place.
Maybe, first place of argument --clean is remove required files.

アプリ実行に成功．しかしタスクトレイから展開する際に真っ白になってフリーズする．

## コンソールを排除してビルド
コンソールを排除したらフリーズしなくなった．なぜ？？？

```console
pyinstaller --onefile --noconsole --path C:\Windows\WinSxS\amd64_avg.vc140.crt_f92d94485545da78_14.0.24210.0_none_69fa0197d9b096ae\ --hidden-import comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 --hidden-import comtypes.gen.UIAutomationClient .\Keasy.py
```

しかし，exeファイルのアイコンが無いのはつらい．

## アイコンを指定してビルド
ビルド時にアイコン画像を指定することでいけた．

```console
pyinstaller --onefile --noconsole --icon icon.ico --hidden-import comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 --hidden-import comtypes.gen.UIAutomationClient --path C:\Windows\WinSxS\amd64_avg.vc140.crt_f92d94485545da78_14.0.24210.0_none_69fa0197d9b096ae\ .\Keasy.py
```

しかし，pythonのデバッグモードでは表示されていたアプリ内・タスクバー・タスクトレイのアイコン画像がビルド後には見えなくなっている．

## アイコンのパスを.specで指定してビルド
クッソややこしいが，以下の手順で解決．ソース元：[How to include GUI images with Pyinstaller?](https://www.reddit.com/r/learnpython/comments/4kjie3/how_to_include_gui_images_with_pyinstaller/)

pyinstallerでexe化した時に出てくるKeasy.specにこれを追記

```console
a.datas += [('icon.png', '[ディレクトリの絶対パス]\icon.png', 'DATA')]
```

※((画像ファイル名),(画像ファイルの絶対パス),'DATA')

↓最初からspecファイルを丸ごと作ってもおｋ

```python:Keasy.spec
# -*- mode: python -*-

block_cipher = None


a = Analysis(['Keasy.py'],
             pathex=['C:\\Windows\\WinSxS\\amd64_avg.vc140.crt_f92d94485545da78_14.0.24210.0_none_69fa0197d9b096ae\\', '[作業ディレクトリの絶対パス]'],
             binaries=[],
             datas=[],
             hiddenimports=['comtypes.gen._944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0', 'comtypes.gen.UIAutomationClient'],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
a.datas += [('icon.ico', '[ディレクトリの絶対パス]\icon.ico', 'DATA')]
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='Keasy',
          debug=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=False , icon='icon.ico')
```

画像ファイルを扱うソースコード(ここではWindowGUI.py)に以下の関数を追加

```python
def resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)
```

ソース内で画像を扱う場合，ファイルパスをresource_path()で加工して使う

アイコンを表示するための行も書き換える

```python
# exe化後はアイコンが表示されない
self.setWindowIcon(QIcon(self.icon_path))

# exe化後もアイコンが表示される
self.setWindowIcon(QIcon(QPixmap(self.resource_path(self.icon_path)))) 
```

pyinstallerで普段はKeasy.pyを指定していたが，ここではKeasy.specを指定してexe化．
これまで追加してきたオプションは.specに記述済みなのでコマンドもスッキリ．

```console
pyinstaller .\Keasy.spec
```

**完　全　な　ビ　ル　ド　成　功**

## 注意
[pipenv使ってる時はその中でインストールしたpyinstallerによってビルドしないといけない](https://qiita.com/TakamiChie/items/8dba8459343db898b335#pipenv%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%81%9F%E5%A0%B4%E5%90%88)。

機能追加して `よ～しビルドするルン！`と思ったら思いっきりハマったorz

PyCharm先輩に任せきりでpipenv何もわからなかったのでプロジェクト内に `コマンド叩く用のpyファイル`を作ってPyCharmから実行した…

```python:runpyinstaller.py
# -*- coding: utf-8 -*-

import subprocess

cmd = "pyinstaller Keasy_win.spec"  # Keasy_win.specを使う場合
subprocess.call(cmd.split())
```

# まとめ

* 従来のパスワードマネージャーも使いにくさがある
* いっそのことぶっ飛んだマネージャー作りたい
* 作った
* Windowsで使うのにexeにできないクソ
* ビルドできないビルドできないビルドできないビルドできないビルドできない
* で　き　た
* どうせWindowsで使うんだからVBAとかでいいと思った
* 一応Macでの使用も予想してマルチプラットフォームを考えていたのでまあ良しとする