ElectronでcontextBridgeによる安全なIPC通信

## はじめに
Electronの情報って、検索すると沢山出てくるのに、ところどころみな違っていて見極めが難しいですよね。まだまだ私自身よくわかっていないですが、調べた情報を共有します。

現時点での結論として、**セキュアなIPC通信にはcontextBridgeを使おう**、ということらしいです。

とはいえ、Electronの状況はversionによってかなり変わるようなので、以下の際内容には注意してください。こちらで検証した時点でのElectronのversionは7.1.9です。

## Electronにおけるセキュアな設計とは

前提として、Electronでは、**メインプロセス**と、webページ画面として動く**レンダラープロセス**が立ち上がります。最初にelectronコマンドの引数として指定したjsファイル(今回はmain.jsとします)がmainプロセス上で実行され、

```
$ electron ./main.js
```

その中で`BrowserWindow.loadURL()`関数などで読み込まれたhtmlがレンダラープロセス上で起動します(今回はindex.htmlとします)。また、index.html上で読み込まれたjsファイルもレンダラープロセス上で実行されます。

たたき台として、以下のようなコードが最小コードとしましょう。

```javascript
/* main.js, case 0 (initial) **************************/
const {electron,BrowserWindow,app} = require('electron');
let mainWindow = null;
const CreateWindow = () => {
    mainWindow = new BrowserWindow({width: 800, height: 600});
    mainWindow.loadURL('file://' + __dirname + '/index.html');
    mainWindow.webContents.openDevTools(); 
    mainWindow.on('closed', function() {
        mainWindow = null;
    });
}
app.on('ready', CreateWindow);
```

```html
<!--index.html, case 0 (initial) -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Test</title>
  </head>
  <body>
    <button id="button1">test1</button>
  </body>
  <script type = "text/javascript">
      //適当なプログラム
      const electron = require('electron');//これがエラーになる
      const {ipcRenderer} = require('electron');//これもエラー
 </script>
</html>
```

ここで、昔のversionのElectronではレンダラープロセス上でもファイル読み書きなどのnodeの便利なメソッドが使えたわけですが、最近はdefaultでは使えなくなっているそうです。ですので、上記の様にレンダラープロセス上の「適当なプログラム」の部分で`require('electron')`と書いて実行しようとすると、"Uncaught ReferenceError: require is not defined at index.html"のようなエラーメッセージが出ます。

じゃあ、ファイル読み書きなどのnodeの機能はメインプロセス上だけでやろう、という方針を取るにしても、レンダラープロセスからの信号や情報をメインプロセスへ伝える手段がいるわけです。プロセス間の通信は`IPC通信`としてElectronのAPIが用意されているものの、最低限レンダラープロセス上での通信処理を司る`ipcRenderer`が欲しくなります[(公式docs)](https://electronjs.org/docs/api/ipc-renderer)。しかし、requireが使えないのでそれすら取得できません。

どうしましょう。

## 巷の情報

検索して出てくる情報は以下のようなものが多いです。

1. `nodeIntegration: true`にすればよい。

2. セキュアにするには`nodeIntegration: false`のままにすべし。

3. その代わりpreloadを使おう。

4. preload内で準備したオブジェクトや関数をレンダラープロセスのjsで使うためには、(`global`や)`window`の変数に追加することでインスタンスを渡そう。

5. あるversion以降、プロセス間で`window`が同一のオブジェクトではなくなった。よって受け渡しできない。同一オブジェクトにするには`contextIsolation: false`としよう。
6. いやいや、セキュアにするには`contextIsolation: true`のままにしよう。
7. `contextBridge`を使えば、`nodeIntegration: false,contextIsolation: true`でもIPC通信できる [^1] [^2]。

巡り巡って、どうやら、7番の方法で解決みたいですが、それ以前の手立ても含めて以下にまとめていきます。

## 方法1(情報1):  nodeIntegration: true

nodeIntegrationというのは、メインプロセスでウィンドウを生成するとき位のオプションで指定します。先のmain.jsにおいて、`BrowserWindow`の生成部分のコードを以下の様に書き替えます。

```javascript
/* main.js, case 1 */
// ~略~ //
const CreateWindow = () => {
  mainWindow = new BrowserWindow({width: 800, height: 600, 
                 webPreferences: { 
                   nodeIntegration: true,
                 } 
               });
// ~略~ //
```

これだけで、レンダラープロセスで`require`関数が使えるようになります。しかし、デバッグコンソールには"**Electron Security Warning (Insecure Content-Security-Policy)**"というwarningメッセージがでてきて、なにやら危ないようです。XSSの危険が大きいということで、あまりお勧めできないようです。



## 方法2(情報2-6)：preloadを使う

では、`nodeintegration: false`としながら、レンダラープロセスでせめてIPC通信だけでもするにはどうするのか。そこで出てくるのがpreloadで追加jsを先行して読ませる方法です。読ませるjsをpreload.jsとします。このpreload.jsにおいてはnode.jsの機能、つまり`require`関数が使えるので、これをグローバルなオブジェクト変数として記録します。それをレンダラープロセスから使うということになります。コードで書くと、次のようになります。

```javascript
/* main.js, case 2 */
//ipcMainの追加
const {electron,BrowserWindow,app,ipcMain} = require('electron');
let mainWindow = null;
const CreateWindow = () => {
    mainWindow = new BrowserWindow({width: 800, height: 600,
        webPreferences: { 
            nodeIntegration: false, //ここはfalseのまま
            contextIsolation: false,  //これをfalseに
            preload: __dirname + '/preload.js' //preloadするjs指定
        } });
    mainWindow.loadURL('file://' + __dirname + '/index.html');
    mainWindow.webContents.openDevTools(); 
    mainWindow.on('closed', function() {
        mainWindow = null;
    });
}
app.on('ready', CreateWindow);
//IPCメッセージの受信部(レンダラープロセスから送られる)//
ipcMain.on("msg_render_to_main", (event, arg) => {
    console.log(arg); //printing "good job"
});
```

```javascript
/* preload.js, case 2*/
const {ipcRenderer} = require('electron');
window.MyIPCSend = (msg)=>{         
    ipcRenderer.send("msg_render_to_main", msg);
}
```

```html
<!-- index.html, case 2 -->
<!DOCTYPE html>
<html>
~~略~~
<script type = "text/javascript">
  //適当なプログラム
  const button1 = document.getElementById("button1");
  button1.addEventListener("click", (e)=>{
      window.MyIPCSend("good job");});      
 </script>
</html>
```

まず、main.jsでは、`BrowserWindow`の生成のoptionに`preload`と`contextIsolation`の項目を追加しています。またIPCメッセージの受信部として`ipcMain.on`を設定しています。

preload.jsでは`require`が利用できるので、グローバル変数として`window.MyIPCSend(msg)`関数を追加し、その中で`ipcRenderer`を使ったメッセージ送信の機能を持たせます。ここからメインプロセスの`ipcMain.on`へメッセージを送ります。

index.htmlではボタンを押したときに`window.MyIPCSend(msg)`関数を呼び出します。これはpreload.jsで定義したものですが、グローバルなwindowオブジェクトに保持されているので使えるようです。

このような形でIPCメッセージだけでもやり取りできれば、それで必要な情報を送り、node関連の機能を使った処理は全てメインプロセスへ押し付けてしまうこともできるでしょう。

ところがこの方法でも、`contextIsolation: false`が必要です。あるversionからデフォルトでは`contextIsolation: true`となったようです。そしてセキュアにするには、ここもtrueがよいと。しかし、trueとすると、preload.jsから呼び出したwindowと、index.htmlで呼び出すwindowのインスタンスが別物になってしまいます。よって、`window.MyIPCSend(msg)`関数をindex.htmlから呼び出しても、定義されていない旨のエラーメッセージが出ます。



## 方法3(情報7)：contextBridgeを利用する

さて、`nodeIntegration: false`かつ`contextIsolation: true`のままでIPC通信する手段として、`contextBridge`というElectron APIがあるそうです[^1]。これはElectronで公式に提案されたセキュアなプロセス間通信の実現のためのAPIだそうです（これを見つけた時は、嬉しくて叫んじゃいました）。

コードは次のようになります。

```javascript
/* main.js, case 3 (final) */
// ~~略~~ ここまでcase2と同じ//
    mainWindow = new BrowserWindow({width: 800, height: 600,
        webPreferences: { 
            nodeIntegration: false, //ここはfalseのまま
            contextIsolation: true,  //trueのまま(case2と違う)
            preload: __dirname + '/preload.js' //preloadするjs指定
        } });
// ~~略~~ 以後もcase2と同じ//
```

```javascript
/* preload.js, case 3 (final)*/
const { contextBridge, ipcRenderer} = require("electron");
contextBridge.exposeInMainWorld(
    "api", {
        send: (data) => {
            ipcRenderer.send("msg_render_to_main", data);
        }
    }
);
```

```html
<!-- index.html, case 3 (final) -->
<!DOCTYPE html>
<html>
~~略~~
<script type = "text/javascript">
  //適当なプログラム
  const button1 = document.getElementById("button1");
  button1.addEventListener("click", (e)=>{
      window.api.send("god job");});      
 </script>
</html>
```

さて、main.jsは方法2と比べて`contextIsolation: true`に変えただけです。

大きく変わったのはpreload.jsです。electronからオブジェクト`contextBridge`を取り出し、`exposeInMainWorld()`によってグローバルな関数`send()`を登録しています。ここで登録した関数は、レンダラープロセスのindex.htmlの中からも`window.api.send()`として呼び出すことができます。

めでたし、めでたし。

## 注意点

contextBridgeはとっても良さそうなAPIですが、Electronのドキュメント[^3]には次のように書かれています。

"The `contextBridge` API has been published to Electron's master branch, but has not yet been included in an Electron release."

一応、私の環境のversion7.1.9では使えていますが、いつから使えるようになったのかはちょっと不明なので、気を付けてください。

## 感想

HTML+Javascriptでブラウザ上だけでほぼ動くものを作ってしまえば、パッケージングはElectronですぐにできると思っていた時期が僕にもありました。。。

この記事がだれかの参考になれば幸いです。とはいえ、なにぶんJavascriptはライト勢なので、間違いもたくさんありそう。ご指摘いただければ大変嬉しいです。

## 追記 2020/2/13: [レンダラープロセスでの受信編](https://qiita.com/pochman/items/62de713a014dcacbad68)も投稿しました。


## 追記 2020/9/23

contextBridgeのより良い使い方として、もう少し注意すべき点があります。
上記のサンプルコードは、Electronのversion upの歴史に沿ってコードをどのように改変していくかを示すために残しておきますが、使用する前に下記の記事も是非読んでみてください。

[Electronのセキュリティについて大きく誤認していたこと](https://qiita.com/sprout2000/items/2b65f7d02e825549804b)

[Electron（v10.1.2現在）の IPC 通信入門 - よりセキュアな方法への変遷](https://qiita.com/hibara/items/c59fb6924610fc22a9db)



[^1]: [https://electronjs.org/docs/api/context-bridge](https://electronjs.org/docs/api/context-bridge)

[^2]: [StackOverFlow:With contextIsolation = true, is it possible to use ipcRenderer?](https://stackoverflow.com/questions/55164360/with-contextisolation-true-is-it-possible-to-use-ipcrenderer)

[^3]: [https://electronjs.org/docs/api/context-bridge/history](https://electronjs.org/docs/api/context-bridge/history)