前回の記事にて、Box APIに於けるOAuth2.0認証を行ってAccess Token取得までのフローをElectronにて構築しました。今回はこれらのTokenの暗号化保存、対象のフォルダのZIP圧縮、Refresh Tokenの処理、実際にBoxへアップロード、パッケージの作成までを今回実装したいと思います。

Token暗号化ではユーザにパスワードを登録してもらい、keytarで資格情報マネージャに格納、以降Tokenの暗号化や復号化で利用します。

難易度:

今回追加で利用するモジュール等

  • keytar – npm様々な機密情報を資格情報マネージャ等に格納するモジュール
  • Crypto – Node.js – ファイルやデータを暗号化・復号化する為のモジュール
  • archiver – npm – ファイルやフォルダを圧縮する為のモジュール
  • request – npm – Box APIを叩く為に必要なHTTPリクエストモジュール
  • fs-extra – npm – fsモジュールに足りない便利な機能を追加してくれるモジュール
  • date-utils – npm – new Date()を拡張するモジュール
  • electron-packager – 各種OS用に実行ファイル形式のパッケージを作ってくれるモジュール

今回のプログラムでは、予めユーザが指定したパスワードをkeytarで保管。そのパスワードを持ってAccess TokenやRefresh Tokenを暗号化保存、対象のフォルダをZIPで圧縮して、Access Tokenを用いてBoxへREST APIを使ってアップロードの手順になります。

今回のプロジェクトファイルのテンプレートを作りました。

今回のファイルでは以下の手直し等が必要です(PASS:monster-energy)

  1. プロキシのURLやPACファイルのURLなどいくつか手動で入力しなければならない
  2. npm installにてモジュールはインストールされますが、keytarについてはリビルドが必須です
  3. バージョン情報などは修正が必要です(package.json含む)

事前準備

各種モジュールをインストールしておきましょう。requestは既にdepreateになっていますがまだ使えます。いずれ他のモジュールに置き換える必要があるでしょう。今回はrequestモジュールで続行します。

CryptoモジュールはNode.js標準装備なので、追加作業は必要ありません。Keytarについてはこちらを参照してください。

パスワードの保存

トークンを暗号化する為のパスワードを資格情報マネージャに登録する為の一連の機能を実装します。設定にパスワード入力欄を設けて、メインプロセス側にはそれを保存するコードを用意します。

setting.html

HTML部分にはパスワード入力欄を追加します。

savesetting関数には、入力欄の値を配列に加えるコードを加えます。

また、起動時に保存済みパスワードを呼び出す為のipcrender.onにもコードの追加が必要です。

index.js

HTML側から受け取ったパスワードをまずはkeytarで保存。servicenameは「box_auth」としました。

また、設定ダイアログを起動時にパスワードを呼び出す項目の追加も必要です。

図:無事に保存されました

トークン類の暗号化

Access TokenやRefresh Tokenは文字が長すぎる為、keytarなどで資格情報マネージャに格納ができません。そこで、取得したこれらのデータはJSON化し、設定したパスワードを持って、AES256bitで暗号化して保存しておきます。暗号化には、Cryptoモジュールを利用します(標準装備なので別途インストールは不要)。以前別のエントリーでも復号化だけは実際に作っています。

また、生成時に復号化してTokenが切れているかチェックしやすいように、取得時の日付時間および期限の日付時間もJSONに含めて置こうと思います。

暗号化用関数

  • 暗号化の為のencryptAes関数を用意。取得したTokenデータ(JSON形式)を暗号化して、user.jsonというファイルで保存します。
  • この関数はapp.js側で利用するので、app.js側に記述しています。
  • 今回復号化のルーチンは、index.js側に用意してるので、こちらには記載していません。
  • 暗号化は、資格情報マネージャに登録してあるパスワードを使って暗号化しています(そのためにkeytarを利用)。
  • 暗号化する場合には、encryptAes関数には、JSON.stringifyでJSON文字列に変換してから渡す必要があります。
  • 以前使用していたcrypto.createCipherはセキュリティ上の理由でDeprecatedになってしまったので、こちらのサイトを参考にメソッドを置き換えています。
  • ivの値は16byteで設定し、keytarから取得したパスワードをHASH化しBase64でエンコードした後、32byteで切り出したものを暗号化のkeyとして利用します。
  • 復号化する時の為にランダム生成iv値はkeytarを使って、資格情報マネージャのbox_ivに値を格納しておく

図:暗号化されたTokenデータファイル

復号化用関数

Boxの各種APIを叩く為には、Access Tokenが必要です。しかし、前項でTokenデータは暗号化してありますので、利用時には復号化してあげなければTokenデータを取り出せません。decryptAes関数にてuser.jsonのデータを復号化してコンソールに表示するというものを作ってみました。

  • fs.existsSyncにてまず、user.jsonの有無を確認。ファイルが無い場合は認証がされていないので、ダイアログで認証を促します。
  • index.js側にも、cryptoモジュールを読み込ませておきます。
  • 復号化の為に、decryptAes関数を用意。user.jsonを暗号化されたまま、まずは取り込みます。
  • 暗号化時に生成したiv値をkeytarで資格情報マネージャから取り出し復号化で利用します。
  • 復号化したデータをそのまま返しても、返された側で取り出せないので、Stringで文字列に変換します。
  • 復号化は、資格情報マネージャに登録してあるパスワードを使って復号化しています(そのためにkeytarを利用)。
  • decryptAes関数をcallbackにしておかないと、呼び出し元で呼び出されても返り値を受け取れません。
  • 呼び出し元では以下のようなコードで、呼び出してcallbackで受け取ってコンソール表示しています。

図:無事に復号化して、Access Tokenだけ取り出せた

取得したAccess Tokenを暗号化

app.js側に取得したAccess Tokenを実際に作成した関数で暗号化してみます。Box APIのTokenの仕様は

  • Access Tokenは1時間で失効する
  • refresh_tokenは60日で失効する

そのため、60日後には再認証が必要になります。また、日付時刻で失効してるかどうかを確認する必要があるので、取得日時などをJSONファイルに含めてあげるようにします。

user.jsonの保管場所

Tokenを暗号化したファイルであるuser.jsonですが、autolaunchモジュールを利用した自動起動時にファイルを見失う事があるので、保管場所をドキュメントフォルダ直下にし、user.jsonを参照する場合にはフルパスで指定するようにすると、きちんとファイルを特定してくれます。

app.jsおよびindex.js双方でuser.jsonを参照してる箇所があるので、これを以下のコードを冒頭に追加し、jpath変数を参照するように変更します。

Refresh Tokenを使ったTokenの再取得

取得したAccess Tokenはおよそ1時間で失効します。其のため、継続的に使うには毎回ログインし直さないといけない。これではあまりにも不便です。そこで用意されているのがRefresh Token。これを使って新しいAccess Tokenを自動的に取得して、継続してアプリを使えるようにする仕組みが、この手のアプリケーションでは必須です。

requestモジュールをインストールする

今回使用するのは、requestというモジュール。passport自体にリフレッシュ機能が備わっていないので、これを使う必要があります。インストール自体はとても簡単。

これだけ。simple-oauth2passport-oauth2-refreshなどのモジュールもあるのですが、どちらも自分の環境では使えなかったので、自力でrequestモジュールで更新するコードを構築しました。

リフレッシュするコード

特定のモジュールがなくとも、requestモジュールで組み立ててあげれば、Refresh Tokenにてトークンの取得が可能です。Refresh Tokenでリフレッシュすると、新たにAccess TokenとRefresh Tokenが取得出来ます。60日間Refresh Tokenを使ってリフレッシュしないと、再認証が必要になります。

ログアウト時にTokenを廃棄する

app.js側に於いて、ログアウトを実行した際に、user.jsonを廃棄するコードを追加しておきます。

Access Tokenをリフレッシュする

今回は、プログラム側で自動でToken期限切れを検知して、自動でRefresh Tokenを使ってAccess Tokenを取得し直すようにする為、index.js側にリフレッシュ用のコードを用意します。そのため、index.js側

  • encryptAES関数を追加で記述する(app.js側の関数を参照できない為)
  • encryptAES関数で利用する変数も追記する
  • requestモジュールの呼び出しと社内の場合プロキシ越えの設定を追記

作業が必要です。また、リフレッシュさせる為に、index.js側にrefreshToken関数とrenewToken関数の2つを用意します。

  • index.js側でrequestモジュールを社内プロキシの後ろで使う場合には、request.defaultsにてプロキシを直接指定し、proxy.postでHTTPリクエストを投げるようにします。
  • descriptAESでuser.jsonを復号化し、そのままrenewTokenへわたしてあげます
  • Box APIをrequestモジュールでrefresh要求する場合、公式ドキュメント通りのやり方を行うと400エラーになります。
  • リクエストヘッダーはapplication/x-www-form-urlencodedではなく、application/jsonを指定しましょう。
  • optionsを組み立てたら、proxy.postでHTTPリクエストを投げます。
  • ステータスコード200が帰ってきたら、bodyの中身はJSONなので、取り出しencryptAES関数で再びuser.jsonを再生成し、ivの値をkeytarで格納してあげます。
  • Token失効の条件判定を行ってこの関数を呼び出せば自動で、失効時にはrenewTokenが実行されてBox APIが実行される仕組みです。

フォルダをZIPで圧縮

archiverモジュールを利用して圧縮を行いますが、fsモジュールも併用する必要があるので、事前に読み込みが必要です。

  • date-utilsにてnew Date()を拡張し、日付の処理をしやすくしています。
  • 今回はデスクトップにbackupという名前に日付でzipのファイル名をつけています。
  • あらかじめelectron-storeにて設定しておいた圧縮対象フォルダをarchive.directoryにて圧縮を指定しています。
  • output.onにて圧縮完了を待って、呼び出し元へcallbackでZIPファイルのパスを返しています。
  • archive.finalizeにて圧縮を実行します。

ただし、この状態の場合、「誰かが指定のフォルダ内のファイルを開いてる場合(特にExcel)」には、圧縮時にエラーが発生します。Excelなどが作成する一時ファイルが圧縮時のエラーの原因です。これを回避するには、対象のフォルダを一旦、デスクトップに作った一時フォルダ内にまるごとコピーし、それを圧縮。完了後に一時フォルダごと削除する処理が必要です。フォルダまるごとコピーや削除では、fs-extraを利用します。

  • fs.existsSyncにて、デスクトップの一時フォルダの有無を確認後に、一時フォルダを作成します。
  • fsExtra.copySyncにて、指定のフォルダをまるごと、一時フォルダにコピーします
  • fsExtra.removeにて、指定のフォルダをまるごと削除します(ZIP圧縮完了後)。この処理はファイル数によっては時間が掛かるので、非同期で処理をします

ZIPファイルをBoxへアップロード

いよいよ最後の関門である、Boxへのファイルのアップロードですが、こちらもリファレンス通りに行うと400エラーでアップロードが出来ません。Invalid API request pathというエラーがでます。

過去に作ったVBAでBox APIを叩くコードを参考にrequestモジュールだけでアップロードができるように、またアップロード時にAccess Tokenが期限切れかどうかをチェックする関数を用意し、継続的にアップロードができるようにします。

Access Tokenが期限切れかチェックする関数

  • 60日のRefresh Token切れのチェックおよび1時間のAccess Token切れをチェックし、前者の場合1日以下の場合にはリフレッシュを実行し、後者の場合10分を下回ってる場合にはリフレッシュを実行します。
  • それぞれ、renewToken関数を利用してるので、access_tokenが取得できたらcallbackし、そうでない場合falseを返すようにしています。
  • プログラム側から自動で更新作業を行うので、前項のrenewToken関数内のダイアログを表示するコードは削除しています。

ファイルをアップロードする

ファイルのアップロードは、嵌りポイントがいくつかありました。それらをクリアしつつ、Access Tokenの期限をチェックしつつAPIへZIPファイルを送りつけます。これに前回のコードのCronで自動で処理を行う部分で呼び出してあげれば、起動している間は自動で対象のフォルダを無限にBoxへバックアップし続けてくれます。

※Box APIのアップロードは50MBの制限があります。50MB以上のファイルの場合には分割アップロードのAPIを利用する必要があります。

  • upload用のエンドポイントは、公式サイトにあるURLだと404になります。https://upload.box.com/api/2.0/files/contentですのでご注意
  • renewToken関数にあったエラーダイアログの表示は、こちらのboxupload関数の側に配置しました。
  • chkexpireToken関数でTokenの期限切れチェックを行い、利用可能なAccess Tokenを取得してproxy.post(request)で投げています。
  • 無事に成功すると、statuscodeに201が返ってくるので、関数呼び出し元へtrueをcallbackします。
  • エラー時にはresponseの内容をerror.jsonに書き出します。
  • requestのコードを組み立てたら、それをもってform.appendでパラメータを送りリクエストを実行しています。
  • VBAの時のような面倒なコードはなく、スッキリとしたコードでバイナリデータを送り込むことが可能です。
  • ファイルのフルパスからファイル名path.basenameで切り出して、アップロード時のファイル名としています。

図:無事にZIPをアップロード出来た

Cronで自動アップと後処理

前項までのコードだと、どんどんデスクトップに圧縮されたZIPファイルが溜まり続けてしまいます。また、Cronでこのboxuploadを呼び出すコードがまだ装備されていません。この部分を完成させて完了となります。まずは、ZIP作成とアップロードをまとめて行う関数を用意してまとめておきます。

  • fs.unlinkSyncにてアップロード処理後にZIPファイルを自動削除させています。
  • zipman関数の結果をboxupload関数に渡しています。boxupload内ではrenewTokenや実際にファイルをアップロードする処理がなされています。
  • callbackの引数で呼び出し元でダイアログなどを表示すると良いでしょう

そして、Cronjobの設定部分は以下のように書き直します。

  • CronJob.scheduleにて、uploadfolderを呼び出しています。callbackの値であるretで処理を追加すると尚良いでしょう
  • ただし、自動処理の時にはエラー時以外メッセージダイアログなどを出すのは適切ではないので、ログ出力程度に留めておくと良いでしょう。

パッケージを作る

現在はelectron-builderを使うのが主流になっているのですが、今回はelectron-packagerを使うことにします。まずはインストールします。

今回プロジェクトはboxmanというフォルダ内に構築しています。このプロジェクトからexeを作成するのがこのパッケージャの役割ですが、色々とオプションがついており、コマンドラインから指定してパッケージングします。boxmanフォルダがあるフォルダまで移動し(boxmanフォルダには入らず)、以下のコマンドを実行すると、electron 5.0で64bit用のWindows向けパッケージが作成されます。exeのアイコンも指定しています。

実行すると、electronが別途ダウンロードされ、ビルドが始まります(この時プロキシに阻まれることがあるので、前回の記事を参照しプロキシ越えできるようにしておく必要がある)。完了すると、boxman-win32-x64というフォルダに色々パッケージとexeが入っています。

resources\app\の中に自分の作ったファイル群がいます。しかし今回問題が。。

node_modulesの中身の一部が取り込まれていない・・・よってexeを実行しても動かない。という事で、プロジェクト側にあるnode_modulesフォルダまるごと上書きしてみたところ無事に起動。electron-builderを使えというお告げなのかもしれません。配布用のインストーラはEXEPressInno Setupで別途作ると良いでしょう。

毎回ビルドをするたびにコマンドライン打つのは面倒なので、BATファイル作って置くと捗ります。

図:boxman.exeが本体です

関連リンク

共有してみる: