13
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リンクアンドモチベーションAdvent Calendar 2024

Day 9

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

Last updated at Posted at 2024-12-09

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

最近、趣味で Electron を使ったアプリ開発を始めたのですが、何しろElectron初学者なもので、セキュリティリスクに怯えながら開発を進めております。

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

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

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

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

前提

サーバーは用いない

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

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

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

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

お知らせ

コードサンプルの公開

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

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

image.png

続編も考えています

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

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

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

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

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

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

  • サンプルコードの機密情報(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

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

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

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

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

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

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セキュアストレージ

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

良い実装例(OSセキュアストレージを利用)

Sample Code | os-secure-storage/good-example

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

13
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?