初心者でもElectronを安全に作りたい - 安全なデータ管理方法編 -

:::note info
本記事は [リンクアンドモチベーション Advent Calendar 2024](https://qiita.com/advent-calendar/2024/lmi) の9日目です🎄
:::

こんにちは！  
リンクアンドモチベーションでアプリケーションエンジニアをしております鵜木と申します。

https://x.com/nokki_y

最近、趣味で [Electron](https://www.electronjs.org/ja/) を使ったアプリ開発を始めたのですが、何しろElectron初学者なもので、セキュリティリスクに怯えながら開発を進めております。

そこで今回は、**「初心者が知っておきたい、Electronでの安全なデータ保存方法」** について、僕が調べたことをまとめたいと思います。

本記事では以下を中心に解説します：

1. Electronで使えるデータ保存方法とその選択基準
2. 初心者でも実践できる、安全性を考慮したデータ管理の具体例  

安全なアプリを作るための第一歩として、ぜひ参考にしてください！


## 前提

### サーバーは用いない  

本記事では、サーバーなしのElectronアプリを想定しています。（サーバーを使用する場合、データ管理はサーバー側で行うのが一般的です）
そのため、ローカルでのデータ保存に焦点を当てて説明します。

:::note warn
本記事の方法をすべて実践しても、完全な安全性が保証されるわけではありません。

Electronアプリはクライアントサイドで動作するため、コードの解析やリバースエンジニアリングが可能です。暗号化しても、その方法自体が攻撃者に解析されるリスクがあります。

そのため、重要なデータを保存する際はサーバーを利用し、クライアント側での保管は避けてください。
:::


## お知らせ

### コードサンプルの公開  

本記事で紹介するコードは以下のGitHubリポジトリにて公開しています。

https://github.com/nokki-y/electron-secure-data-examples

起動するとElectronアプリが起動しみなさんのローカル環境で、本記事で紹介した内容を試すことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640515/c8dcdf43-6dd3-0b7c-f099-95a132ff17a5.png)


### 続編も考えています

安全にデータを管理するためには、**「攻撃の手口を知る」** ことも重要です。例えば、不正アクセスや改ざん、リバースエンジニアリングなどのリスクを理解して初めて、適切な対策を講じることができます。

本記事では、「安全なデータ管理方法」に焦点を当てましたが、機会があれば [リンクアンドモチベーション Advent Calendar 2024](https://qiita.com/advent-calendar/2024/lmi) の後続記事で **「初心者でもElectronを安全に作りたい - 攻撃方法と自衛編 -」** の執筆をしたいと考えています。

#### 続編で取り上げたい内容（予定）

1. ローカルファイルへの不正アクセスのリスクと防止策
2. リバースエンジニアリングの手法とその対策
3. XSS攻撃を含む、アプリ内での潜在的な攻撃シナリオ
4. これらの攻撃に対抗するためのセキュリティ設計の考え方


## 1. Electronアプリにおけるデータ保存の概要

### 1.1 Electronアプリの特性

Electronは、HTML、CSS、JavaScriptを使ってデスクトップアプリを構築できるクロスプラットフォームのフレームワークです。Webアプリの技術スタックを利用してWindows、macOS、Linux向けのアプリケーションを作れるため、多くの開発者にとって魅力的です。

しかし、今回のようにサーバーを用いない構成においては、データ保存がクライアントサイドで完結することになります。つまり、データはユーザーのローカル環境に保存されます。


### 1.2 データ保存に伴う一般的な課題

今回の前提に立った時、以下の検討に悩むことが多いです。

#### 保存先の選択  

データをどこに保存するかは、アプリの性質やユーザー環境に依存します。例えば、以下のような観点で保存先を選ぶ必要があります。

- **性能**: 高速にアクセスできる保存先か  
- **セキュリティ**: データが安全に保管されるか  
- **可用性**: OSや環境の違いに影響されず動作するか

特に本記事では、セキュリティリスクに関するリスクを取り上げます。以下に代表的なリスクを挙げます。

1. **不正アクセス**: 他のアプリやユーザーが保存ファイルにアクセスし、データを読み取る可能性があります

2. **データ改ざん**: 保存されたファイルが書き換えられ、アプリケーションの挙動に悪影響を及ぼす可能性があります

3. **リバースエンジニアリング**: アプリのコードやデータ構造が解析され、悪用されるリスクがあります

これらのリスクを理解し、適切な対策を講じることが、Electronアプリ開発の安全性を確保する上で重要です。


## **2. Electronで使えるデータ保存方法**

1. Electron Store
1. ローカルファイル保存
1. ローカルストレージ / SessionStorage
1. SQLiteやNeDB
1. IndexedDB
1. OSセキュアストレージ
1. メモリ内保存

| 保存方法                      | 用途                                  | メリット                                        | デメリット                                      |
|-------------------------------|---------------------------------------|------------------------------------------------|------------------------------------------------|
| **Electron Store**            | 設定情報や軽量なデータの保存          | 簡単なAPIでデータ保存が可能                    | 平文保存のため、機密情報には不向き              |
| **ローカルファイル保存**      | 設定ファイルやユーザーデータのエクスポート | 実装が簡単で、ファイル操作に慣れていれば使いやすい | 平文保存のため、機密データには不向き            |
| **ローカルストレージ / SessionStorage** | 軽量なデータや一時的な状態の保存    | Web標準APIで簡単に保存・取得が可能             | 開発者ツールから容易にアクセス可能              |
| **SQLiteやNeDB**              | 履歴データやログ、複雑なデータ構造の保存 | 構造化されたデータを効率的に管理可能           | データベースファイルへの不正アクセスリスク      |
| **IndexedDB**                 | 構造化されたデータやキャッシュの保存  | 大量のデータを保存可能でオフライン管理に適している | 開発者ツールからアクセスできるためセキュリティ対策が必要 |
| **OSセキュアストレージ**      | パスワードやAPIトークンの保存          | OSが提供するセキュリティ機能を活用可能          | 利用するAPIの知識が必要                        |
| **メモリ内保存**              | セッション中のみ必要な一時データの保存 | データがディスクに書き込まれず永続化されない    | アプリ終了でデータが消えるため長期保存には不向き|


本記事では、『初心者が理解しやすく、セキュリティ対策の基礎を学べる』というテーマから、以下の実装例と安全に利用するためのポイントを紹介します。

- Electron Store
- ローカルファイル保存
- SQLiteやNeDB
- OSセキュアストレージ


## 3. 各保存方法の良い実装例とだめな実装例

初心者でも安全なElectronアプリを作るために、各保存方法における **だめな実装例**（リスクがある実装）と **良い実装例**（セキュリティを考慮した実装）を解説します。

:::note warn

- サンプルコードの機密情報（APIキー、パスワード）は例示用です
- 実際の実装では、ユーザー入力や安全な方法で取得したデータを使用してください
- 環境変数（ENCRYPTION_KEY）の設定方法は以下の通りです：
  1. プロジェクトルートに`.env`ファイルを作成
  2. `ENCRYPTION_KEY=your-secure-random-key`を設定
  3. `.gitignore`に`.env`を追加
:::

### 3.1 Electron Store

#### だめな実装例（平文保存）

[Sample Code | electron-store/bad-example](https://github.com/nokki-y/electron-secure-data-examples/blob/main/examples/electron-store/bad-example.js)

```javascript
const { app } = require('electron');
const Store = require("electron-store");

exports.runBadExample = async () => {
   const result = {
       apiKey: null,
       storePath: null,
       message: ''
   };

   try {
       // Store初期化（暗号化なし）
       const store = new Store({ 
           name: "bad-example",
           cwd: app.getPath('userData')
       });

       // 機密情報を保存（暗号化なし）
       store.set("apiKey", "my-secret-api-key");

       // 結果を設定
       result.apiKey = store.get("apiKey");
       result.storePath = store.path;
       result.message = '⚠️ APIキーが平文で保存されました。これは安全ではありません！';

       return result;

   } catch (error) {
       throw new Error(`Bad Example Error: ${error.message}`);
   }
};
```

**問題点**:  

- 機密情報（例: APIキー）を暗号化せずに平文で保存しているため、ファイルを直接読み取られると漏洩するリスクがあります
- ファイルはJSONフォーマットで保存されるため、一般的なテキストエディタで簡単に内容を確認できてしまいます


#### 良い実装例（暗号化を利用）

[Sample Code | electron-store/good-example](https://github.com/nokki-y/electron-secure-data-examples/blob/main/examples/electron-store/good-example.js)

```javascript
const { app } = require('electron');
const Store = require("electron-store");

exports.runGoodExample = async () => {
    const result = {
        apiKey: null,
        storePath: null,
        message: ''
    };

    try {
        // Store初期化（暗号化あり）
        const store = new Store({ 
            name: "good-example",
            cwd: app.getPath('userData'),
            encryptionKey: process.env.ENCRYPTION_KEY,
            clearInvalidConfig: true // 無効な設定ファイルをクリア
        });

        // 既存のデータをクリア
        store.clear();

        // 機密情報を保存（暗号化あり）
        store.set("apiKey", "my-secret-api-key");

        // 結果を設定
        result.apiKey = store.get("apiKey");
        result.storePath = store.path;
        result.message = '✅ APIキーが暗号化されて保存されました。これは安全な実装例です！';

        return result;

    } catch (error) {
        throw new Error(`Good Example Error: ${error.message}`);
    }
};
```

**ポイント**:

- `encryptionKey` を設定することで、保存されるデータが暗号化されます
- 暗号化キーは環境変数などで安全に管理し、ソースコード内に直接記述することは避けましょう
- 暗号化により、ファイルが直接読み取られても機密情報は保護されます


### 3.2 ローカルファイル保存

#### だめな実装例（平文で機密データ保存）

[Sample Code | local-file/bad-example](https://github.com/nokki-y/electron-secure-data-examples/blob/main/examples/local-file/bad-example.js)

```javascript
const { app } = require('electron');
const fs = require("fs");
const path = require("path");

exports.runBadExample = async () => {
    const result = {
        userData: null,
        storePath: null,
        message: ''
    };

    try {
        // 保存ファイルパス
        const filePath = path.join(app.getPath('userData'), "userData.json");
        
        const data = { username: "user_a", password: "mypassword" };

        // 平文で保存
        fs.writeFileSync(filePath, JSON.stringify(data, null, 2));

        // 結果を設定
        result.userData = data;
        result.storePath = filePath;
        result.message = '⚠️ ユーザーデータが平文で保存されました。これは安全ではありません！';

        return result;

    } catch (error) {
        throw new Error(`Bad Example Error: ${error.message}`);
    }
};
```

**問題点**:  

- 機密データ（例: パスワード）を暗号化せずに保存しているため、不正アクセスやデータ漏洩のリスクがあります
- JSONファイルとして平文で保存されるため、一般的なテキストエディタで簡単に内容を確認できてしまいます

#### 良い実装例（暗号化して保存）

[Sample Code | local-file/good-example](https://github.com/nokki-y/electron-secure-data-examples/blob/main/examples/local-file/good-example.js)

```javascript
const { app } = require('electron');
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
require('dotenv').config();

exports.runGoodExample = async () => {
    const result = {
        userData: null,
        storePath: null,
        message: ''
    };

    try {
        // 保存ファイルパス
        const filePath = path.join(app.getPath('userData'), "userData.enc");

        // 暗号化設定
        const algorithm = "aes-256-gcm";
        const salt = crypto.randomBytes(16);
        const key = crypto.scryptSync(process.env.ENCRYPTION_KEY, salt, 32);
        const iv = crypto.randomBytes(16);

        const data = { username: "user_a", password: "mypassword" };

        // データを暗号化して保存
        const cipher = crypto.createCipheriv(algorithm, key, iv);
        let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
        encrypted += cipher.final('hex');
        const authTag = cipher.getAuthTag();

        // 暗号化データ、IV、認証タグ、ソルトをファイルに保存
        const saveData = {
            iv: iv.toString('hex'),
            salt: salt.toString('hex'),
            authTag: authTag.toString('hex'),
            encryptedData: encrypted
        };

        fs.writeFileSync(filePath, JSON.stringify(saveData));

        // 結果を設定
        result.userData = data;
        result.storePath = filePath;
        result.message = '✅ ユーザーデータが暗号化されて保存されました。これは安全な実装例です！';

        return result;

    } catch (error) {
        throw new Error(`Good Example Error: ${error.message}`);
    }
};
```

**ポイント**:  

- AES-GCM暗号化を使用し、データを安全に保護します
- 環境変数から暗号化キーを取得し、ソースコード内にシークレット情報を保存しません
- ソルト、IV、認証タグを使用することで、より強固なセキュリティを実現します
- 暗号化されたデータはJSONとして構造化して保存され、復号に必要な情報を安全に管理します


### 3.3 SQLiteやNeDB

#### だめな実装例（暗号化せずにデータベース保存）

[Sample Code | sqlite-nedb/bad-example](https://github.com/nokki-y/electron-secure-data-examples/blob/main/examples/sqlite-nedb/bad-example.js)

```javascript
const { app } = require('electron');
const Datastore = require("nedb");
const path = require("path");

exports.runBadExample = async () => {
 const result = {
  userData: null,
  storePath: null,
  message: ''
 };

 try {
  // 保存ファイルパス
  const dbFilePath = path.join(app.getPath('userData'), "users.db");
  const db = new Datastore({ filename: dbFilePath, autoload: true });

  const data = { username: "user_a", password: "mypassword" };

  const newDoc = await new Promise((resolve, reject) => {
   db.insert(data, (err, doc) => {
    if (err) reject(err);
    else resolve(doc);
   });
  });

  // 結果を設定
  result.userData = newDoc;
  result.storePath = dbFilePath;
  result.message = '⚠️ パスワードが平文で保存されました。これは安全ではありません！';

  return result;

 } catch (error) {
  throw new Error(`Bad Example Error: ${error.message}`);
 }
};
```

**問題点**:  

- パスワードを暗号化せずに保存しているため、データベースファイルが読み取られると漏洩するリスクがあります
- データベースファイルのパーミッションが適切に設定されていないため、他のプロセスやユーザーからアクセス可能です
- パスワードなどの機密情報が平文のままレスポンスデータに含まれています

#### 良い実装例（パスワードをハッシュ化して保存）

[Sample Code | sqlite-nedb/good-example](https://github.com/nokki-y/electron-secure-data-examples/blob/main/examples/sqlite-nedb/good-example.js)

```javascript
const { app } = require('electron');
const Datastore = require("nedb");
const bcrypt = require("bcrypt");
const path = require("path");

exports.runGoodExample = async () => {
 const result = {
  userData: null,
  storePath: null,
  message: ''
 };

 try {
  // 保存ファイルパス
  const dbFilePath = path.join(app.getPath('userData'), "users.db");
  const db = new Datastore({ 
   filename: dbFilePath, 
   autoload: true,
   fileMode: 0o600
  });

  const data = { username: "user_a", password: "mypassword" };
  const saltRounds = 10;

  // パスワードのハッシュ化
  const hash = await bcrypt.hash(data.password, saltRounds);

  // ハッシュ化したパスワードで置き換え
  const secureData = {
   username: data.username,
   password: hash
  };

  const newDoc = await new Promise((resolve, reject) => {
   db.insert(secureData, (err, doc) => {
    if (err) reject(err);
    else resolve(doc);
   });
  });

  // 結果を設定（パスワードは非表示に）
  result.userData = { 
   ...newDoc, 
   password: '[HASHED]' 
  };
  result.storePath = dbFilePath;
  result.message = '✅ パスワードがハッシュ化されて保存されました。これは安全な実装例です！';

  return result;

 } catch (error) {
  throw new Error(`Good Example Error: ${error.message}`);
 }
};
```

**ポイント**:  

- `bcrypt`を使用してパスワードを安全にハッシュ化し、ソルトと組み合わせて保存
- データベースファイルのパーミッションを`0o600`に設定し、所有者のみがアクセス可能に
- レスポンスデータからパスワードハッシュを隠蔽し、`[HASHED]`という表示に置き換え
- `Promise`を使用して非同期処理を適切に扱い、エラーハンドリングを実装


### 3.4 OSセキュアストレージ

:::note info
『だめな実装例』は、ローカルファイルに平文で保存することになり、『ローカルファイル保存の悪い例と重複するため』割愛します。
:::

#### 良い実装例（OSセキュアストレージを利用）

[Sample Code | os-secure-storage/good-example](https://github.com/nokki-y/electron-secure-data-examples/blob/main/examples/os-secure-storage/good-example.js)

```javascript
const { app } = require('electron');
const keytar = require("keytar");
const os = require("os");

exports.runGoodExample = async () => {
    const result = {
        apiKey: null,
        storePath: null,
        message: ''
    };

    try {
        const service = "myApp";
        const account = "user_a";
        const apiKey = "my-secret-api-key";

        // セキュアストレージに保存
        await keytar.setPassword(service, account, apiKey);

        // 保存したAPIキーを取得
        const retrievedKey = await keytar.getPassword(service, account);

        // 結果を設定
        result.apiKey = retrievedKey;
        result.storePath = os.platform() === "darwin" ? 
            "Keychain Access" : 
            "OSのセキュアストレージ";
        result.message = '✅ APIキーがOSのセキュアストレージに保存されました。これは安全な実装例です！';

        return result;

    } catch (error) {
        throw new Error(`Good Example Error: ${error.message}`);
    }
};
```

**ポイント**:  

- OSのセキュアストレージ（macOSのKeychain、WindowsのCredential Vault）を使用し、機密データを安全に保存
- 非同期処理を適切に扱い、エラーハンドリングを実装
- 保存場所をOS別に分かりやすく表示（macOSの場合は「Keychain Access」、その他は「OSのセキュアストレージ」）
- APIキーなどの機密情報を安全に管理


## 4. まとめ

本記事では、初心者でも安全にElectronアプリを作るためのデータ保存方法について、**良い実装例**と**だめな実装例**を比較しながら解説しました。

### 安全なデータ管理のポイント

- **暗号化を忘れずに**: 機密情報は暗号化して保存し、暗号化キーは安全に管理しましょう
- **ハッシュ化を活用**: パスワードなどの認証情報は必ずハッシュ化して保存し、平文保存は避けましょう
- **セキュアストレージの利用**: OSが提供するセキュアストレージを活用して、機密情報の漏洩を防ぎましょう


### Electronアプリにおける限界

サーバーを用いないElectronアプリでは、**クライアントサイドのみでデータを管理するため、完璧に攻撃を防ぐことは不可能**です。前述したように以下の理由から、完全なセキュリティを実現するのは難しいです。

1. **コードの解析可能性**: アプリケーションコードがクライアントサイドに存在するため、リバースエンジニアリングが可能
2. **暗号化キーの保管**: 暗号化に使用するキーもクライアントサイドで管理する必要があり、完全な秘匿が困難
3. **実行環境の制御**: ユーザーの環境で動作するため、改変や解析を防ぐことができない

それでも、少しでもリスクを低減するために、本記事で紹介した対策を実践することをお勧めします😊✨

**最後に**、本記事のコードサンプルは以下のGitHubリポジトリに公開しています。実際に動かしながら学んでみてください！

📦 **[GitHubリポジトリ: electron-secure-data-examples](https://github.com/nokki-y/electron-secure-data-examples)**