Electron（v.15.0.0 現在）の IPC 通信入門 - よりセキュアな方法への変遷

> 2021/09/22にリリースされた v.15.0.0 中でIPC通信周りに大きな変更はありませんでした。
> ただ、その前のバージョン（14.0.0）で、```contextBridge.exposeInMainWorld(apiKey, api)```から```Experimental（実験的）```が取れ、正式運用となっていたようです（[#30011](https://github.com/electron/electron/pull/30011)）。

## Electron における IPC 通信

Electron で Desktop アプリケーションを作るにあたって理解しなければならないのは、根幹を成す「IPC通信」かと思います。IPC は Inter-Process Communication、プロセス間通信の略です。

IPC 通信の方法については、Electron において、いくつかの段階を経て、進化を遂げています。今回の記事では、その歴史を追いながら、仕組みとセキュアな方法を書いていきたいと思います。

## なぜ IPC 通信を使うのか？　Electron のベースは、Chromium のため

まずは、なぜ IPC 通信を使うのか？

Electron は、Chromium を使用しているため、Chromium のマルチプロセスアーキテクチャも使用されます。そのため Electron の各 Webページはそれぞれのプロセスで実行され、`process` というオブジェクトを持って処理を行っています。

**Electron アプリケーションアーキテクチャ - Electron 公式ドキュメント**
<https://www.electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes>

Electron のフレームワークの一部である、Node.js も、スレッドで処理するには、どうしても重い処理などはデッドロックを回避できません。そこで Node.js は、ネットワークアプリケーションにおいてすべての処理を「プロセス」として**非同期**で処理するというノンブロッキング I/O とイベントループという仕様が採用されました。

そのため、Node.js ベースの、Electron は、非同期の「プロセス間通信」の仕様がそのまま採用されています。

## メインプロセスと、レンダラープロセス

Electron には「メインプロセス」と「レンダラープロセス」という二種類のプロセスを持っています。それぞれをざっとおさらいしておきましょう。

### メインプロセス

アプリケーションが開始されてから終了されるまでを制御します。１アプリケーションにつき、１プロセスしかなく、メインプロセスが複数のレンダラープロセスと通信を行うことでアプリケーションを制御します。また、各OSのネイティブ要素（Node.jsモジュール）の管理も担当します。

逆を言えば、レンダラープロセスから、アプリケーションの終了はできないし、OSのネイティブ要素に直接アクセスすることはできません。

**メインプロセス (main process) - Electron 公式ドキュメント**
<https://www.electronjs.org/docs/glossary#%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9-main-process>

### レンダラープロセス

レンダラープロセスは、メインプロセスとちがって複数作ることができます。

主に「表示」や「GUI などのインターフェース」を担当します。レンダラープロセス間同士での通信は行えず、常にメインプロセスを通じて通信することになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/f31e940b-ae78-1c65-aa13-5e88e9688387.png" width="512px">

## 最初期： 今までやってきたレンダラープロセスからメインプロセスへの IPC 通信

Electron 初期の頃に行われていた通信方法です。

まず、前提として、いまこの方法でのレンダラープロセスからメインプロセスへの通信を行うには、レンダラープロセスで Electron や、他の Node.js モジュールを動作させるための「設定」をわざわざ行わないといけません。

メインプロセスで、レンダラープロセス（表示側のウェブページ）を読み込むところで `nodeIntegration=true` としてあげる必要があります。

```javascript:main.js
mainWindow = new BrowserWindow({
  width: 1024,
  height: 640,
  webPreferences: {
    nodeIntegration: true /** ココ **/
  },
});
// HTMLファイルをロードする
mainWindow.loadURL(`file://${__dirname}/index.html`)
```

`nodeIntegration` は、Electron v.5 からデフォルトが `false` となってしまったので、プログラマー側で有効 `true` にしてやる必要があります。なぜ、デフォルトが、`false` となってしまったのかは、セキュリティの問題からですが、これについては別の IPC 通信方法を提示しつつ後述します。

### ipcRendererから、ipcMainへのIPC通信

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/2c868970-7700-da2b-34a7-c4bc30c29812.png" width="640px" />

非同期で通信する方法で、メインプロセスのコード例は以下の通りです。

```javascript:main.js
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {  // channel名は「asynchronous-message」
  console.log(arg)  // "ping"を表示
  event.reply('asynchronous-reply', 'pong')
})
```

そして、通信先であるレンダラープロセス側のコード例は以下の通りです。

```javascript:index.js
// レンダラープロセス（ウェブページ）
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // "pong"を表示
})
ipcRenderer.send('asynchronous-message', 'ping')  //  channel名は「asynchronous-message」
```

上の例は、**非同期通信**の例ですが、もし**同期**通信を行いたい場合は、`send()` の部分を `sendSync()` に書き換えれば実現できます。

ただし、同期通信にした場合、メインプロセスが呼ばれている間は レンダラープロセス上の処理は完全にブロックされます。メインプロセスからの応答があるまでは、レンダラープロセス側の操作画面はいわゆるフリーズしたような状態になります。描画処理も止まるので、ローディング画面のような CSS アニメーションも容赦なく固まります。

ということから、よっぽどの理由がない限りは、非同期通信を使うことをオススメします。

### ipcMain から、ipcRendererへのIPC送信

当然ですが、ipcMain から、ipcRenderer へデータ送信することもできます。

**webContents.send(channel, ...args)**
<https://www.electronjs.org/docs/api/web-contents#contentssendchannel-args>

イメージとしては、以下の図の通りです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/069e44ff-7977-d46f-2f94-1fb43f820958.png" width="640px" />

メインプロセス側のコードは、

```javascript:main.js
// メインプロセス
const { app, BrowserWindow } = require('electron')
let win = null

app.whenReady().then(() => {
  win = new BrowserWindow({ width: 800, height: 600 })
  win.loadURL(`file://${__dirname}/index.html`)
  win.webContents.on('did-finish-load', () => {
    win.webContents.send('ping', 'whoooooooh!') // レンダラープロセスへsendしています
  })
})
```

受けるレンダラープロセス側はこう書きます。

```html:index.html
<html>
<body>
  <script>
    require('electron').ipcRenderer.on('ping', (event, message) => {
      console.log(message) // 'whoooooooh!' と出力される
    })
  </script>
</body>
</html>
```

## 二段階目： ipcRenderer.invoke() を使う

前段、メインプロセスとレンダラープロセスを直接する通信方法から、よりセキュアな方法として考えられたのが、`ipcRenderer.invoke()` を使う方法です。

Electron v.7 から追加になったメソッドです。もともとは、`remote` モジュールを使うことでレンダラープロセス内であたかもメインプロセス内のオブジェクトを直接触れるように見せかけることができた仕組みを安全に置換したものです。

ちなみに、`remote` モジュールは、Electron v.12 では非推奨になり、[@electron/remote](https://github.com/electron/remote) に置き換わりました。ソースコードの書き換えを要求されているので、ほとんど廃止に近い措置です。

参考：***[Electron] IPC には新しい ipcRenderer.invoke() メソッドを使ったほうが便利 (v7+) - Qiita***
<https://qiita.com/jrsyo/items/abe19dff2d950132d9cd>

そこで、`ipcRenderer.invoke()` メソッドを使った例です。

```javascript:index.js
// レンダラープロセス
ipcRenderer.invoke('some-name', someArgument).then((result) => {
  // ...
})
```

```javascript:main.js
// メインプロセス
ipcMain.handle('some-name', async (event, someArgument) => {
  const result = await doSomeWork(someArgument)
  return result
})
```

こうすることで、`ipcRenderer.on()` といった受け側を作る必要がなく、メインプロセスから `Promise`  のデータで受け取れるようになるので便利です。受けるメインプロセスが、`ipcMain.on()` ではなく、`ipcMain.handle()` になっていることに注意してください。

なお、参考先の例にあるように、

```javascript:index.js
// renderer から Main プロセスを呼び出す
const data = await ipcRenderer.invoke('invoke-test', 'ping')
console.log(data)
```

と、ワンライナーで書くこともできます。

## 最新の方法： セキュアなIPC通信 contextBridge を使う

ただ、これでもセキュアな IPC 通信には足りません。

レンダラープロセスでも Electron API や Node.js モジュールを使う前提のコード例を書いてきましたが、実際には、Electron v.5 からデフォルトが、`nodeIntegration=false` となったので、将来的にはレンダラープロセス側で `ipcRenderer.on()` といった処理が行えなくなるかもしれません。

そこで、よりセキュアな IPC 通信を行うために追加されたのが、`contextBridge` です。

**contextBridge - Electron公式ドキュメント**
<https://www.electronjs.org/docs/api/context-bridge#contextbridgeexposeinmainworldapikey-api>

公式ドキュメントでは、

>分離されたコンテキスト間に、安全、双方向で同期されたブリッジを作成します

と、分かりにくい説明がされていますが、分かりやすく図にすると以下の通りです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/cb250104-50dc-b23e-a829-4d51e427c811.png" width="640px" />

ようは、メインプロセスにある Electron API や、Node.js モジュールをレンダラープロセスで扱わせずに、メインプロセスの外部にAPIとして別途定義して、それを関数のように呼ぶという方法です。

なお、`contextBridge.exposeInMainWorld(apiKey, api)` については、v.14.0.0から、公式ドキュメントにおいて、**Experimental（実験段階）** が取れ、正式採用されるようになりました（[#30011](https://github.com/electron/electron/pull/30011)）。

実際、ｖ12において、
>Allowed ContextBridge exposeInMainWorld method to expose non-object APIs. [#26834](https://github.com/electron/electron/pull/27638)

と、オブジェクト型でないAPIも許可するようになりました。

これにはドキュメントにあるように、以下の考えから来ています。

**メインプロセスとレンダラープロセスの違い - Electron公式ドキュメント**
<https://www.electronjs.org/docs/tutorial/application-architecture#%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%81%A8%E3%83%AC%E3%83%B3%E3%83%80%E3%83%A9%E3%83%BC%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%81%AE%E9%81%95%E3%81%84>

>ウェブページでは、ネイティブ GUI 関連の API を呼び出すことは許可されていません。これは、ウェブページがネイティブ GUI リソースを管理することは非常に危険であり、リソースをリークさせるのは容易いからです。 ウェブページで GUI 操作を実行する場合、ウェブページのレンダラープロセスはメインプロセスと通信して、メインプロセスがそれらの操作を実行するよう要求する必要があります。

## さらにもう一歩！： contextBridge を使ったからといって必ずしも安全は担保できない

今や `contextBridge` も安全な方法とは言えないようです。つまり、`contextBridge` を使えば、すべて安心というわけでもないのです。

参考：***ElectronでcontextBridgeによる安全なIPC通信（受信編） - Qiita***
<https://qiita.com/pochman/items/62de713a014dcacbad68>

にあるように、汎用的な書き方をするとダメなようです。

```javascript
const { contextBridge, ipcRenderer} = require("electron");
contextBridge.exposeInMainWorld(
  "api", {
    send: (channel, data) => { // レンダラーからの送信用
      ipcRenderer.send(channel, data);
    },
    on: (channel, func) => { // メインプロセスからの受信用
      ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
  }
);
```

参考：***Electronのセキュリティについて大きく誤認していたこと - Qiita***
<https://qiita.com/sprout2000/items/2b65f7d02e825549804b>

ここに書いてあるとおり、上記のコードの書き方は、Electron 公式ドキュメントによれば、
<https://www.electronjs.org/docs/api/context-bridge#contextbridgeexposeinmainworldapikey-api>

```javascript
// ❌ Bad code
contextBridge.exposeInMainWorld('myAPI', {
  send: ipcRenderer.send
})
```

という書き方は、**unsafe** だそうです。

曰く、
>いかなる種類の引数フィルタリングなしで強力なAPIを直接公開しています。これにより、どんなウェブサイトでも、可能にしてほしくない任意のIPCメッセージを送信することができるようになります。IPCベースのAPIを公開する正しい方法は、代わりにIPCメッセージごとに1つのメソッドを提供することでしょう

とのこと。

```javascript
// ✅ Good code
contextBridge.exposeInMainWorld('myAPI', {
  loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
```

つまり、１つの IPC 通信につき、１処理とした方が良いでしょう。

前の図にも描きましたが、レンダラープロセスからは `preload.js` に登録されている**関数**を呼び、`preload.js` の中で、IPC 通信を行うというのが安全ということになります。

## v12から、worldSafeExecuteJavaScript: ture になった

ちなみに Electron の v12から、`BrowserWindow` の `options` にある、`webPreferences: { worldSafeExecuteJavaScript: true }` がデフォルト値になりました。基本的に `false` は推奨されません。

ドキュメントによると、`webFrame.executeJavaScript` で受け渡されるJavaScriptコードの値が安全に引き渡されるようにサニタイズされると書いてありますが、具体的にどう行われるかは書いてありません。そもそも、`webFrame.executeJavaScript` 自体がイレギュラーっぽい使い方なので、避けておいた方が良いかもしれません。

`BrowserWindow` を作るときは、以下のような `webPreferences`のオプション設定にすると良いでしょう。

```javascript:main.js
  let mainWindow = new BrowserWindow({
    title: config.name,
    width: 1024,
    height: 640,
    minWidth: 1024,
    minHeight: 640,
    webPreferences: {
      // ｖ12 からデフォルト=true になった
      worldSafeExecuteJavaScript: true,
      // XSS対策としてnodeモジュールをレンダラープロセスで使えなくする
      nodeIntegration: false,
      // レンダラープロセスに公開するAPIのファイル
      // v12 からデフォルト=true になった 
      contextIsolation: true,
      preload: __dirname + '/preload.js'
    }
  });
```

## けっきょく contextIsolation: ture にしておくべきか

v12 から、デフォルトで `true` となりました。そうするべきでしょう。

## コンテンツセキュリティポリシー（CSP）警告を回避する

いつからか、以下のような警告が出るようになりました。

![CSP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/bbbbbdc6-6f62-ae03-0b90-48da1c2101e1.png)

どうやら、コンテンツセキュリティポリシーに則っていないというブラウザ側からの警告のようです。

コンテンツセキュリティポリシー (CSP)
<https://developer.mozilla.org/ja/docs/Web/HTTP/CSP>

これは、ページ内で JavaScript を書いたときに、クロスサイト・スクリプティング攻撃(XSS) やデータインジェクションの脆弱性を入れてしまう可能性があるということで警告が出ているようです。

特にそういったアプリケーションを作らないのであれば、無視しても問題なさそうですが、気になるようであれば、以下のように該当のページの `<meta>` タグに、

```html:index.html
<meta http-equiv="Content-Security-Policy" content="default-src 'self';" />
```

を追加してあげると良いでしょう。

ただ、これをしてしまうと、

```html:index.html
<script type="text/javascript">
  // なんらかのスクリプト
</script>
```

というふうに、HTML ファイルに直接、JavaScript を書くことができなくなります（今度は明確なエラーを吐きます）。

ですので、書いていたスクリプトを `index.js` とか適当なファイルを作って、外へ避けてあげれば良いでしょう。

```html:index.html
<script src="index.js"></script>
```

これで、コンテンツセキュリティポリシーに関する警告は消えてくれます。

## サンプルプログラム

これら、セキュアな IPC 通信のサンプルプログラムを作成しました。「３分間タイマー」という、いかにも単純なアプリですが、`nodeIntegration=false` かつ、`contextBridge` を使って実装されています。

***hibara/SimpleTimer - GitHub***
<https://github.com/hibara/SimpleTimer>

```javascript:preload.js
const { contextBridge, ipcRenderer} = require("electron");

contextBridge.exposeInMainWorld(
  "api", {
    // タイマーの開始
    TimerStart: () =>
        ipcRenderer.invoke("ipc-timer-start")
            .then(result => result)
            .catch(err => console.log(err)),
    // タイマーの停止
    TimerStop: () => ipcRenderer.send("ipc-timer-stop"),
    // タイマーのリセット（ミリ秒を最大の初期値へ戻す）
    TimerReset: () => ipcRenderer.send("ipc-timer-reset"),
    // 現在のタイマーの値（ミリ秒）をレンダラープロセスへ投げて表示させる
    DisplayTimer: (listener) => {
      ipcRenderer.on("ipc-display-timer", (event, arg) => listener(arg));
    }
    // send: (channel, data) => { // レンダラーからの送信用
    //   ipcRenderer.send(channel, data);
    // },
    // on: (channel, func) => { // メインプロセスからの受信用
    //   ipcRenderer.on(channel, (event, ...args) => func(...args));
    // }
  }
);
```

この上記コードでは、"`api`"を `key` として設定しています。ですので、呼び出す側（レンダラープロセス）の `index.html` では以下のように呼び出します。

また、コメント欄からもご指摘いただいたように、各関数にチャネル名を含める必要はありません（すべて `preload.js` で書かれているため）。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8">
  <title></title>
  <link rel="stylesheet" href="style.css">
  <script type="text/javascript">
    window.onload = () => {
      // 表示の初期化
      window.api.TimerReset();  // contextBridge
      // 「開始」ボタンをクリック
      document.getElementById('button-start').addEventListener('click', async () => {
        const result = await window.api.TimerStart(); // contextBridge
        if (result === true) {
          document.getElementById('button-reset').textContent = "停止";
        }
        else {
          document.getElementById('button-reset').textContent = "リセット";
        }
      });
      // 「リセット」or「停止」ボタンをクリック
      document.getElementById('button-reset').addEventListener('click', async () => {
        if ( document.getElementById('button-reset').textContent === "停止" ) {
          document.getElementById('button-reset').textContent = "リセット";
          window.api.TimerStop(); // contextBridge
        }
        else {
          window.api.TimerReset();  // contextBridge
        }
      });
    }
    // タイマー（ミリ秒）の受け取り
    window.api.DisplayTimer((milliseconds) => { // contextBridge
      // console.log("ipc-display-timer: " + arg);
      if (milliseconds <= 0){
        document.getElementById('button-reset').textContent = "リセット";
      }
      let min = parseInt((milliseconds / 1000) / 60);
      let sec = parseInt(milliseconds / 1000) % 60;
      document.getElementById('timer-number').textContent =
          ('00' + min).slice(-2) + ':' + ('00' + sec).slice(-2);
    });
  </script>
</head>
<body>

<div>
  <div id="timer-number"><!-- 3:00 --></div>
</div>

<div class="button-wrapper">
  <button id="button-start" class="button button-color-blue">開始</button>
  <button id="button-reset" class="button button-color-red">リセット</button>
</div>

</body>
</html>
```

`window.api.TimerStart(channel)` や、`window.api.TimerStop(channel)` のように、設定した "`api`" キーを通して関数を呼んでいます。

ソースコードではなく、サンプルアプリケーションの動作だけ見たいという方は以下から、実行ファイルのみをダウンロードできます。

***macOS：***
<https://github.com/hibara/SimpleTimer/releases/download/v1.0.8/SimpleTimer-darwin-x64.zip>

***Windows：***
<https://github.com/hibara/SimpleTimer/releases/download/v1.0.8/SimpleTimer-win32-ia32.zip>

## 参考

ipcRenderer.invoke()
<https://qiita.com/jrsyo/items/abe19dff2d950132d9cd>

ElectronでcontextBridgeによる安全なIPC通信（受信編）
<https://qiita.com/pochman/items/62de713a014dcacbad68>