ゼロ知識ヘルシーアプリ向けクライアントサイド暗号化の完全ガイド
ゼロ知識ヘルシーアプリの構築に関する詳細ガイド。ReactとWeb Crypto APIを使用して、サーバーに触れる前にクライアント側でユーザーデータを暗号化する方法を学び、完全なユーザープライバシーを確保します。
このページの内容
/health techの世界では、ユーザーの信頼が最も重要です。1回のデータ漏洩は、あなたの評判を毀損するだけでなく、人的生活を一変させる可能性のある個人健康情報(PHI)を露出させる可能性があります。サーバー側の暗号化とHTTPSは標準的な実践ですが、それらには根本的な脆弱性があります:サーバー、およびそれを侵害したユーザーは、平文でユーザーデータを参照できてしまいます。ここでゼロ知識アーキテクチャの出番です。
ゼロ知識システムとは、サービス提供者(それはあなたです!)が保存するデータの内容を一切知らないセキュリティモデルです。これは、すべての暗号化・復号化操作をクライアントデバイスで実行することで実現されます。サーバーは暗号化されたデータのブロックのみを受け取り、保存します。データベース全体が侵害された場合でも、ユーザーの健康データは読み取り不能なまま保たれます。
この深掘りでは、React/React Nativeを使用したゼロ知識健康日記アプリのコア暗号化機能を構築します。ブラウザに組み込まれた安全で高性能なWeb Crypto APIを使用して、ユーザーのパスワードから鍵を導出し、日記エントリを暗号化し、表示のために復号化します。これにより、機微な健康情報にアクセスできるのはユーザーのみであることを保証します。
前提条件:
- React/React Nativeと современный JavaScript(async/await)の確かな理解
- Node.jsとパッケージマネージャー(npm/yarn)のインストール済み
- 対称暗号化のような基本的な暗号化概念の知識があると有利ですが、必須ではありません
これが開発者にとって重要な理由: ゼロ知識アプリを構築することは、強力な差別化要因となります。これは法的要件を超えたユーザーへのプライバシーへのコミットメントを示し、深いユーザー信頼を構築し、明確な競争優位性を提供します。
問題の理解:標準暗号化の限界
традиционно、ウェブアプリケーションのセキュリティモデルは次のようになります:
- ユーザーがReactアプリにデータを入力します。
- データはHTTPS接続経由でサーバーに送信されます。
- サーバーが平文データを受け取ります。
- サーバーはデータベースに保存する前にデータを暗号化する場合があります(保存時の暗号化)。
重要な欠陥はステップ3にあります。サーバーは平文データにアクセスできます。これは、サーバーにアクセス权を得た悪意のある行為者、不正な従業員、または政府からの令状が、潜在的にユーザーデータを露出させる可能性があることを意味します。
私たちのゼロ知識アプローチは、暗号化の境界をシフトさせます:
- ユーザーがReactアプリにデータを入力します。
- データはユーザーのみが保有する鍵を使用して、ブラウザ/デバイスで直接暗号化されます。
- 暗号化されたデータ(暗号文)は、HTTPS経由でサーバーに送信されます。
- サーバーは元の内容を一切知らないまま、暗号文を保存します。
この「ホストプルーフホスティング」モデルにより、サービス提供者であるあなたは、ユーザーの機微データのリスク要因となることがなくなります。
前提条件:暗号化ツールキットの設定
Web Crypto APIに依存します。これは сучаснихブラウザに直接組み込まれた低レベルの暗号化操作インターフェースです。これは、サードパーティの暗号化ライブラリをバンドルする必要がないため、攻撃対象領域を减小し、大きなセキュリティ上の利点となります。
重要な注意:Web Crypto APIはセキュアなコンテキスト(HTTPSまたはlocalhost)でのみ 利用可能です。これは、中間者攻撃を防ぐためのセキュリティ機能です。
暗号化関数を収容するユーティリティファイルを作成しましょう。
# ReactまたはReact Nativeプロジェクトで
touch src/cryptoUtils.js
このファイルをステップバイステップで構築していきます。
ステップ1:パスワードから暗号化鍵を導出する
核となる課題は、ユーザーのパスワードから強力な暗号化鍵を作成することです。ユーザーがしばしば弱くて予測可能なパスワードを選択するため、パスワードをそのまま鍵として使用することはできません。代わりに、**鍵導出関数(KDF)**を使用します。広くサポートされており堅牢なアルゴリズムであるPBKDF2(Password-Based Key Derivation Function 2)を使用します。
KDFは、パスワードとソルト(ユーザーごとに一意のランダム値)を受け取り、計算集約的なプロセスを経て、強く均一な暗号化鍵を生成します。
何をするか
ユーザーのパスワードとソルトを受け取り、Web Crypto APIで使用される特殊なオブジェクトであるCryptoKeyオブジェクトを生成する関数を作成します。
実装
// src/cryptoUtils.js
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
/**
* パスワードとソルトから暗号化鍵を導出します。
* @param {string} password - ユーザーのパスワード。
* @param {Uint8Array} salt - ランダムなソルト。
* @param {number} iterations - PBKDF2の反復回数。
* @returns {Promise<CryptoKey>} - 導出された暗号化鍵。
*/
export const deriveKeyFromPassword = async (password, salt, iterations = 100000) => {
const passwordBuffer = textEncoder.encode(password);
// パスワードをPBKDF2用の生鍵としてインポートします。
const baseKey = await window.crypto.subtle.importKey(
"raw",
passwordBuffer,
{ name: "PBKDF2" },
false,
["deriveKey"]
);
// AES-GCMパラメータを使用してPBKDF2で鍵を導出します。
const derivedKey = await window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: iterations,
hash: "SHA-256",
},
baseKey,
{ name: "AES-GCM", length: 256 },
true, // 必要に応じて鍵をエクスポートできます(例:バックアップ用)
["encrypt", "decrypt"]
);
return derivedKey;
};
/**
* 新しいランダムなソルトを生成します。
* @returns {Uint8Array} - 16バイトのランダムなソルト。
*/
export const generateSalt = () => {
return window.crypto.getRandomValues(new Uint8Array(16));
};
動作の仕組み
generateSalt:暗号学的に安全な16バイトのランダムなソルトを生成する関数を作成します。このソルトは、サーバー上のユーザーデータと一緒に保存する必要があります。シークレットではありませんが、レインボーテーブル攻撃を防ぐために、ユーザーごとに一意である必要があります。deriveKeyFromPassword:- ユーザーのパスワード文字列をバイトストリーム(
Uint8Array)にエンコードします。 importKeyが生パスワードを受け取り、PBKDF2アルゴリズムで使用できるように準備します。deriveKeyがプロセスのコアです。ベース鍵(パスワードから)、ソルト、多くの反復回数(攻撃者にとって遅く高価にするため)、ハッシュ関数(SHA-256)を受け取り、最終的なCryptoKeyを生成します。この鍵がAES-GCM(安全でモダンな暗号化アルゴリズム)に使用されることを指定します。
- ユーザーのパスワード文字列をバイトストリーム(
ステップ2:健康データの暗号化
鍵ができたので、ユーザーの健康日記エントリを暗号化できます。各暗号化操作には、一意の**初期化ベクトル(IV)**を生成する必要もあります。IVは、同じ鍵で同じテキストを複数回暗号化しても、毎回異なる暗号文を生成することを保証します。これはセキュリティにとって不可欠です。ソルトと同様に、IVはシークレットではなく、サーバーに保存する必要があります。
何をするか
ユーザーの日記エントリ(平文)と導出された鍵を受け取り、暗号化されたデータ(暗号文)と暗号化に使用されたIVを返す関数を作成します。
実装
// src/cryptoUtils.js
/**
* AES-GCMを使用してデータを暗号化します。
* @param {string} plaintext - 暗号化するデータ。
* @param {CryptoKey} key - 暗号化鍵。
* @returns {Promise<{ciphertext: ArrayBuffer, iv: Uint8Array}>}
*/
export const encryptData = async (plaintext, key) => {
const dataBuffer = textEncoder.encode(plaintext);
// IVは各暗号化操作で一意である必要があります。
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
dataBuffer
);
return { ciphertext, iv };
};
動作の仕組み
- この特定の暗号化操作に対して新しい12バイトのランダムIVを生成します。すべての暗号化に対して新しいIVを生成することが重要です。
- 平文の健康データをバイトストリームにエンコードします。
window.crypto.subtle.encryptは、指定されたアルゴリズム(AES-GCM)、導出された鍵、IV、およびデータバッファを使用して暗号化を実行します。- 結果は暗号文を含む
ArrayBufferであり、IVと一緒に返されます。
ステップ3:「ゼロ知識」サーバーへのデータの保存
React/React Nativeコンポーネントは、これらのユーティリティ関数を呼び出し、結果をサーバーに送信します。サーバーの仕事は、データを解釈しようとせず、暗号化されたデータを単に保存することです。
サーバーサイドのデータモデル
バックエンドAPIとデータベースは、これを処理できるように設計する必要があります。journal_entriesテーブルに対して、スキーマは次のようになります:
CREATE TABLE journal_entries (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
encrypted_entry TEXT NOT NULL, -- Base64エンコードされた暗号文
iv TEXT NOT NULL, -- Base64エンコードされたIV
created_at TIMESTAMPZ NOT NULL,
-- 平文データは保存しません!
);
-- ユーザーテーブルはソルトを保存する必要があります
CREATE TABLE users (
id UUID PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, -- 認証用、暗号化用ではありません
encryption_salt TEXT NOT NULL -- Base64エンコードされたソルト
);
Reactコンポーネントの実装
コンポーネントが新しい日記エントリを保存するために暗号化ユーティリティを使用する方法を示します。 전송と保存のために、ArrayBufferとUint8ArrayデータをBase64文字列に変換するのが最も簡単です。
// src/components/JournalEntryForm.jsx
import React, { useState } from 'react';
import { deriveKeyFromPassword, encryptData } from '../cryptoUtils';
// ArrayBufferをBase64に変換するヘルパー
const bufferToBase64 = (buffer) => {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
};
const JournalEntryForm = ({ userPassword, userSalt }) => {
const [entry, setEntry] = useState('');
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
if (!entry || !userPassword || !userSalt) {
alert("暗号化に必要なデータがありません。");
return;
}
setIsSaving(true);
try {
// 1. ユーザーのパスワードと一意のソルトから鍵を導出
const key = await deriveKeyFromPassword(userPassword, userSalt);
// 2. 日記エントリを暗号化
const { ciphertext, iv } = await encryptData(entry, key);
// 3. バイナリデータをJSON 전송用のBase64に変換
const encryptedEntryBase64 = bufferToBase64(ciphertext);
const ivBase64 = bufferToBase64(iv);
// 4. 暗号化されたデータをゼロ知識サーバーに送信
await fetch('/api/journal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
encrypted_entry: encryptedEntryBase64,
iv: ivBase64,
}),
});
setEntry('');
alert('エントリが安全に保存されました! ✨');
} catch (error) {
console.error("暗号化に失敗しました:", error);
alert("エントリを安全に保存できませんでした。");
} finally {
setIsSaving(false);
}
};
return (
<div>
<textarea
value={entry}
onChange={(e) => setEntry(e.target.value)}
placeholder="ここに個人的な健康日記を書いてください..."
/>
<button onClick={handleSave} disabled={isSaving}>
{isSaving ? '保存中...' : '安全に保存'}
</button>
</div>
);
};
export default JournalEntryForm;
ステップ4:データの復号化と表示
エントリを表示するには、プロセスを逆順にします。暗号化されたデータ、IV、ソルトをサーバーからフェッチし、ユーザーのパスワードを使用して鍵を再導出し、データを復号化します。
何をするか
復号化用の最終ユーティリティ関数を作成し、日記エントリを表示するコンポーネントで使用します。
実装
// src/cryptoUtils.js
/**
* AES-GCMを使用してデータを復号化します。
* @param {ArrayBuffer} ciphertext - 暗号化されたデータ。
* @param {CryptoKey} key - 暗号化鍵。
* @param {Uint8Array} iv - 暗号化に使用された初期化ベクトル。
* @returns {Promise<string>} - 復号化された平文。
*/
export const decryptData = async (ciphertext, key, iv) => {
const decryptedBuffer = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
ciphertext
);
return textDecoder.decode(decryptedBuffer);
};
復号化用Reactコンポーネント
// src/components/JournalViewer.jsx
import React, { useState, useEffect } from 'react';
import { deriveKeyFromPassword, decryptData } from '../cryptoUtils';
// Base64をUint8Arrayに変換するヘルパー
const base64ToUint8Array = (base64) => {
const binary_string = window.atob(base64);
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes;
};
const JournalViewer = ({ userPassword, userSalt }) => {
const [decryptedEntries, setDecryptedEntries] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchAndDecryptEntries = async () => {
try {
// 1. サーバーから暗号化されたエントリをフェッチ
const response = await fetch('/api/journal');
const entries = await response.json();
// 2. 鍵を導出(一度だけ実行すればOK)
const salt = base64ToUint8Array(userSalt);
const key = await deriveKeyFromPassword(userPassword, salt);
// 3. 各エントリを復号化
const decryptedPromises = entries.map(async (entry) => {
const ciphertext = base64ToUint8Array(entry.encrypted_entry);
const iv = base64ToUint8Array(entry.iv);
const plaintext = await decryptData(ciphertext, key, iv);
return { id: entry.id, text: plaintext, createdAt: entry.created_at };
});
const results = await Promise.all(decryptedPromises);
setDecryptedEntries(results);
} catch (error) {
console.error("復号化に失敗しました:", error);
alert("エントリを復号化できませんでした。パスワードを確認してください。");
} finally {
setIsLoading(false);
}
};
if(userPassword && userSalt) {
fetchAndDecryptEntries();
}
}, [userPassword, userSalt]);
if (isLoading) return <p>データを復号化中...</p>;
return (
<div>
{decryptedEntries.map(entry => (
<div key={entry.id}>
<p>{entry.text}</p>
<small>保存日: {new Date(entry.createdAt).toLocaleString()}</small>
</div>
))}
</div>
);
};
export default JournalViewer;
セキュリティのベストプラクティスとトレードオフ
- 鍵管理が全て:このシステム全体のセキュリティは、ユーザーのパスワードに依存しています。パスワードが弱ければ、導出された鍵も弱くなります。ユーザーに強く一意なパスワードを使用することを促しましょう。ゼロ知識システムにおける最も大きな課題はアカウントの回復です。ユーザーがパスワードを忘れた場合、データを回復することは不可能です。「パスワードを忘れた」フローで暗号化鍵を回復することはできません。一部のシステムは複雑なソーシャル回復メカニズムまたはダウンロード可能な回復鍵を実装していますが、これは значительная сложностьを追加します。
- パスワードや鍵を保存しない:ユーザーのパスワードまたは導出された
CryptoKeyをlocalStorage、Cookie、または長い間React stateに保存しないでください。鍵は、必要な直前にパスワードから導出し、メモリ内にのみ保持するのが理想です。 - 常にHTTPSを使用する:Web Crypto APIにはそれが必要な理由があります。HTTPSがなければ、攻撃者はあなたのJavaScriptコードを拦截し、ユーザーのパスワードを盗む恶意のあるバージョンに置き換えることができます。
- パフォーマンスの考慮事項:鍵導出(PBKDF2)は、ブルートフォース攻撃を思いとどまらせるために意図的に低速です。鍵を導出的时候会稍有延迟気づくでしょう。この操作は慎重に行ってください—セッションに1回が理想的です。ユーザーのセッション期间 동안、
CryptoKeyオブジェクトをメモリ内にキャッシュしてください(例:React Context内)。ただし、永続化しないように 주의してください。
代替アプローチ
- サードパーティライブラリ:
crypto-jsのようなライブラリは存在しますが、それらは一般的にはネイティブWeb Crypto APIよりも推奨されません。Web Crypto APIはより安全で、ブラウザベンダーによってメンテナンスされており、基礎となるハードウェアサポートを利用できるため、しばしば高速です。 - 非対称暗号化(公開鍵/秘密鍵):医師との健康データの共有のような機能には、非対称暗号化(例:RSA-OAEP)が必要です。このモデルでは、医師は公開鍵(暗号化用)と秘密鍵(復号化用)を持ちます。これは管理がより複雑ですが、安全なデータ共有を可能にします。
結論
ゼロ知識システムの基礎を正常に構築しました。クライアントデバイスと堅牢なWeb Crypto APIを活用することで、ユーザーのプライバシーが暗号学的に保証されるアーキテクチャを作成しました。サーバーは、保持する機微な健康情報にアクセスできない無機質なストレージロッカーとして機能します。
このアプローチは、ユーザーにデータの完全なコントロールを赋予し、機微な情報を扱う任意のアプリケーションにとって本質的な信頼の基盤を構築します。
次のステップ:
- ReduxやReact Contextのような状態管理ソリューションにこのロジックを統合し、
CryptoKeyを管理します。 - 暗号化ロジックにユーザーのパスワードを提供する安全な認証フローを構築します。
- ユーザーのための键のバックアップと回復戦略を探求します。
リソース
クイックリファレンス
| 側面 | 主要情報 |
|---|---|
| トピック | 開発 |
| 関連性 | 患者教育と認識 |
| アクション | 医療提供者にご相談ください |
| 頻度 | ケアチームのおすすめに応じて |
| 証拠 | 現在の臨床ガイドラインに基づく |
よくある質問
Q: これが自分に該当するかどうかはどのようにわかりますか? A: 個人的な病歴と家族歴について医療提供者と相談し、個人的なリスク要因を評価し、適切な次のステップを決定してください。
Q: 医師に何を聞くべきですか? A: 良い質問例:「私の結果の意味は何ですか?」「私のオプションは何ですか?」「どのような生活習慣の改善が役立ちますか?」「いつフォローアップすべきですか?」
Q: 帮助になる生活習慣の改善はありますか? A: ほとんどの健康状態は、基本的な対策から有益を得ます:バランスの 取れた栄養、定期的な運動(週150分以上)、質の良い睡眠(7〜9時間)、ストレス管理、タバコの回避。
医学的免責事項
この記事は教育的目的是のみを目的としており、医学的アドバイス构成しません。健康、治療、またはケアに関する决定する前に、常に资格のある医療提供者に相談してください。個人の状況は異なり、一个人に適切なものが別の人に適切であるとは限りません。
生命を脅かす緊急の医療状態にいている場合は、直ちに119番(または現地の緊急番号)に電話してください。