本記事は リンクアンドモチベーション Advent Calendar 2024 の9日目です🎄
こんにちは!
リンクアンドモチベーションでアプリケーションエンジニアをしております鵜木と申します。
最近、趣味で Electron を使ったアプリ開発を始めたのですが、何しろElectron初学者なもので、セキュリティリスクに怯えながら開発を進めております。
そこで今回は、「初心者が知っておきたい、Electronでの安全なデータ保存方法」 について、僕が調べたことをまとめたいと思います。
本記事では以下を中心に解説します:
- Electronで使えるデータ保存方法とその選択基準
- 初心者でも実践できる、安全性を考慮したデータ管理の具体例
安全なアプリを作るための第一歩として、ぜひ参考にしてください!
前提
サーバーは用いない
本記事では、サーバーなしのElectronアプリを想定しています。(サーバーを使用する場合、データ管理はサーバー側で行うのが一般的です)
そのため、ローカルでのデータ保存に焦点を当てて説明します。
本記事の方法をすべて実践しても、完全な安全性が保証されるわけではありません。
Electronアプリはクライアントサイドで動作するため、コードの解析やリバースエンジニアリングが可能です。暗号化しても、その方法自体が攻撃者に解析されるリスクがあります。
そのため、重要なデータを保存する際はサーバーを利用し、クライアント側での保管は避けてください。
お知らせ
コードサンプルの公開
本記事で紹介するコードは以下のGitHubリポジトリにて公開しています。
起動するとElectronアプリが起動しみなさんのローカル環境で、本記事で紹介した内容を試すことができます。
続編も考えています
安全にデータを管理するためには、「攻撃の手口を知る」 ことも重要です。例えば、不正アクセスや改ざん、リバースエンジニアリングなどのリスクを理解して初めて、適切な対策を講じることができます。
本記事では、「安全なデータ管理方法」に焦点を当てましたが、機会があれば リンクアンドモチベーション Advent Calendar 2024 の後続記事で 「初心者でもElectronを安全に作りたい - 攻撃方法と自衛編 -」 の執筆をしたいと考えています。
続編で取り上げたい内容(予定)
- ローカルファイルへの不正アクセスのリスクと防止策
- リバースエンジニアリングの手法とその対策
- XSS攻撃を含む、アプリ内での潜在的な攻撃シナリオ
- これらの攻撃に対抗するためのセキュリティ設計の考え方
1. Electronアプリにおけるデータ保存の概要
1.1 Electronアプリの特性
Electronは、HTML、CSS、JavaScriptを使ってデスクトップアプリを構築できるクロスプラットフォームのフレームワークです。Webアプリの技術スタックを利用してWindows、macOS、Linux向けのアプリケーションを作れるため、多くの開発者にとって魅力的です。
しかし、今回のようにサーバーを用いない構成においては、データ保存がクライアントサイドで完結することになります。つまり、データはユーザーのローカル環境に保存されます。
1.2 データ保存に伴う一般的な課題
今回の前提に立った時、以下の検討に悩むことが多いです。
保存先の選択
データをどこに保存するかは、アプリの性質やユーザー環境に依存します。例えば、以下のような観点で保存先を選ぶ必要があります。
- 性能: 高速にアクセスできる保存先か
- セキュリティ: データが安全に保管されるか
- 可用性: OSや環境の違いに影響されず動作するか
特に本記事では、セキュリティリスクに関するリスクを取り上げます。以下に代表的なリスクを挙げます。
-
不正アクセス: 他のアプリやユーザーが保存ファイルにアクセスし、データを読み取る可能性があります
-
データ改ざん: 保存されたファイルが書き換えられ、アプリケーションの挙動に悪影響を及ぼす可能性があります
-
リバースエンジニアリング: アプリのコードやデータ構造が解析され、悪用されるリスクがあります
これらのリスクを理解し、適切な対策を講じることが、Electronアプリ開発の安全性を確保する上で重要です。
2. Electronで使えるデータ保存方法
- Electron Store
- ローカルファイル保存
- ローカルストレージ / SessionStorage
- SQLiteやNeDB
- IndexedDB
- OSセキュアストレージ
- メモリ内保存
保存方法 | 用途 | メリット | デメリット |
---|---|---|---|
Electron Store | 設定情報や軽量なデータの保存 | 簡単なAPIでデータ保存が可能 | 平文保存のため、機密情報には不向き |
ローカルファイル保存 | 設定ファイルやユーザーデータのエクスポート | 実装が簡単で、ファイル操作に慣れていれば使いやすい | 平文保存のため、機密データには不向き |
ローカルストレージ / SessionStorage | 軽量なデータや一時的な状態の保存 | Web標準APIで簡単に保存・取得が可能 | 開発者ツールから容易にアクセス可能 |
SQLiteやNeDB | 履歴データやログ、複雑なデータ構造の保存 | 構造化されたデータを効率的に管理可能 | データベースファイルへの不正アクセスリスク |
IndexedDB | 構造化されたデータやキャッシュの保存 | 大量のデータを保存可能でオフライン管理に適している | 開発者ツールからアクセスできるためセキュリティ対策が必要 |
OSセキュアストレージ | パスワードやAPIトークンの保存 | OSが提供するセキュリティ機能を活用可能 | 利用するAPIの知識が必要 |
メモリ内保存 | セッション中のみ必要な一時データの保存 | データがディスクに書き込まれず永続化されない | アプリ終了でデータが消えるため長期保存には不向き |
本記事では、『初心者が理解しやすく、セキュリティ対策の基礎を学べる』というテーマから、以下の実装例と安全に利用するためのポイントを紹介します。
- Electron Store
- ローカルファイル保存
- SQLiteやNeDB
- OSセキュアストレージ
3. 各保存方法の良い実装例とだめな実装例
初心者でも安全なElectronアプリを作るために、各保存方法における だめな実装例(リスクがある実装)と 良い実装例(セキュリティを考慮した実装)を解説します。
- サンプルコードの機密情報(APIキー、パスワード)は例示用です
- 実際の実装では、ユーザー入力や安全な方法で取得したデータを使用してください
- 環境変数(ENCRYPTION_KEY)の設定方法は以下の通りです:
- プロジェクトルートに
.env
ファイルを作成 -
ENCRYPTION_KEY=your-secure-random-key
を設定 -
.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アプリでは、クライアントサイドのみでデータを管理するため、完璧に攻撃を防ぐことは不可能です。前述したように以下の理由から、完全なセキュリティを実現するのは難しいです。
- コードの解析可能性: アプリケーションコードがクライアントサイドに存在するため、リバースエンジニアリングが可能
- 暗号化キーの保管: 暗号化に使用するキーもクライアントサイドで管理する必要があり、完全な秘匿が困難
- 実行環境の制御: ユーザーの環境で動作するため、改変や解析を防ぐことができない
それでも、少しでもリスクを低減するために、本記事で紹介した対策を実践することをお勧めします😊✨
最後に、本記事のコードサンプルは以下のGitHubリポジトリに公開しています。実際に動かしながら学んでみてください!