macOS向けのElectron で "can't be opened because Apple cannot check it for malicious software." と出るようになった時の対処

# 経緯

以前作ってCI上でビルド・署名・リリースを行うようにしておいたElectron製のアプリケーションで、
いつのまにか
`can't be opened because Apple cannot check it for malicious software.`
こういうメッセージが出るようになっていた。

「署名の期限が切れた覚えもないし（←あとで確認したら切れてたけど、これは直接の原因ではなかった）なんだろうなぁ」と思い、
理由を調べて直したので、その記録。

## 原因

MacOS 10.14.5以降、Appleの基準が厳しくなって、notarize（※公証の意味）という処理を通じて、
アプリに対して認める権限の範囲をより詳細に規定しなければいけなくなったらしい。

## 対処

※ ここから先は `electron-builder` を使っている場合について書きます
※ 前提として、AppleのDeveloper IDを取得して、ビルド・署名・パッケージングする部分は出来ていることとします

### 1. entitlements.mac.plistの追加

以下のような感じで用意する。

```xml:entitlements.mac.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
  </dict>
</plist>
```

### 2. electron-builder の設定の追加

configの `mac` 配下に下記を追加

- `hardenedRuntime`を `true` に設定
- `entitlements` と `entitlementsInherit` に 1で作成したplistへのパスを設定する
- `gatekeeperAssess` を `false` に設定
    - [これをしないと動かないmacのバージョンがあるらしい](https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/)

参考:
https://www.electron.build/configuration/mac

### 3. app-specific passwordの取得

[Apple ID Account Page](https://appleid.apple.com/) から app-specific passwordを生成します。
これは、自由に再生成・破棄できるAPIキーのようなものです。

秘匿情報なので、gitにコミットなどはしないように気をつけましょう。

参考:
https://support.apple.com/en-us/HT204397

### 4. notarizeスクリプトの用意

[electron-notarize](https://github.com/electron/electron-notarize)を使うので、
まず `npm install -D electron-notarize` します

そして、下記のようなスクリプトでnotarize処理を書きます。

```javascript:notarize.js
const { notarize } = require('electron-notarize');
const appBundleId = 'electron-builderのconfigでappIdに設定している値と同じもの'

// 下記例は環境変数から読み取っていますが、適宜読み替えてください
const appleId = process.env['APPLEID'] // apple developer IDのemailアドレス
const appleIdPassword = process.env['APPLEID_PASSWORD'] // 3で取得した、app-specific password

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context;
  if(!appleId) throw new Error('appleId is not set in the env')
  if(!appleIdPassword) throw new Error('appleIdPassword is not set in the env')
  if (electronPlatformName !== 'darwin') {
    throw new Error(`Notarization failed. Expected platform is 'darwin' but you are on ${electronPlatformName}`)
  }

  const appName = context.packager.appInfo.productFilename;
  const appPath = `${appOutDir}/${appName}.app`

  console.log(`Notarizing ${appBundleId} found at ${appPath}`);

  return await notarize({
    appBundleId,
    appPath,
    appleId,
    appleIdPassword
  });
};
```

### 5. electron-builderのフックにnotarizeを設定

electron-builderではcodesign後に任意のスクリプトを実行できるフックが提供されているので、
4で作成したnotarizeを実行する。

```json
"afterSign": "node /path/to/notarize.js"
```

## 対応完了

諸々対応後のelectron-builderの設定:

```json
"build": {
  "appId": "com.なんらかの.ID",
  "productName": "アプリ名",
  "afterSign": "./scripts/notarize.js",
  "mac": {
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.plist"
  }
}
```

この状態で、`electron-builder` を走らせると、パッケージングなどの処理の一環で無事にnotarize処理が行われ、
配布したアプリを開けるようになります。

ただし、アプリによっては追加の権限が必要になる場合もあるかもしれないので、その場合は、plistに必要な権限を追記してください。

ちなみに、notarize部分単体でも **5分ぐらいかかる** ので、トータルのコマンド実行時間は結構長くなります。

## 特に参考にしたもの

https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/

https://twitter-archive-eraser.medium.com/notarize-electron-apps-7a5f988406db