こんにちは👋
今日は、 Pubnubと sealed SDK を使用してエンドツーエンドの暗号化チャットを構築する方法を見てみましょう。
👉 PubNub と Seald の使用方法の完全に機能する例は、ここにあります。
パブナブって何?
PubNub は、ほとんどのアプリケーションに統合できるリアルタイム通信サービスです。信頼性と拡張性に優れており、最も一般的なフレームワークと簡単に統合できます。
シールって何?
Seald.io は、事前の暗号化知識がなくても、高度な管理機能を備えたエンドツーエンド暗号化を実行できる SDK を提供します。この SDK は、Web、バックエンド、モバイル、またはデスクトップ アプリケーションに統合できます。
エンドツーエンドの暗号化により、最高レベルのプライバシーとセキュリティが提供されます。これにより、機密データが収集されるとすぐに暗号化できます。早期に暗号化を行うと、アプリの攻撃対象領域が減少します。もう 1 つの利点は、データにアクセスできるユーザーを正確に管理できることです。これにより、サードパーティのサービスなど、自分の範囲外の場合も保護されます。
エンドツーエンドの暗号化により、データが転送中、保管中、さらにはデータが手元にないときでも、常に制御を維持できます。したがって、他の暗号化テクノロジ (TLS、フルディスク暗号化など) よりもはるかに幅広い保護を提供します。
コンプライアンスが重要なケース (GDPR、HIPAA、SOC-2 など) や機密データ(医療、防衛など) が存在するケースに直面した場合、エンドツーエンドの暗号化は必須です。ただし、より一般的なデータであっても、これを用意することをお勧めします。データ侵害は、ますます頻繁になっている壊滅的なイベントです。
PubNub SDK は、SDK をインスタンス化するときにcipherKey
引数を使用して、単純な暗号化フックを提供します。そうすることで、アップロードされたすべてのメッセージが送信前に確実に暗号化されます。ただし、鍵管理は自分で行う必要があり、これがエンドツーエンド暗号化システムの最も難しい部分です。
脆弱性が暗号化自体から発生することはほとんどありませんが、ほとんどの場合、セキュリティ モデルの欠陥を通じてキーが漏洩することが原因です。
セキュリティのためにサードパーティのサービスを使用すると、単一障害点をなくすことができます。 Seald.io は、リアルタイムのアクセス管理制御、ユーザーの取り消しと回復、2FA などを備えた、 ANSSI によって認定された堅牢なセキュリティ モデルを提案しています。
この記事では、エンドツーエンドの暗号化でチャットを保護するために、Seald を PubNub と統合する方法を段階的に説明します。次の機能を備えたサンプル メッセージング アプリを構築します。
1対1およびグループチャットルーム。
各メンバーには、他のすべてのユーザーと専用のチャット ルームがあります。
誰でも他の複数のユーザーとのグループ チャット ルームを作成できます。
送信されるすべてのメッセージとファイルにエンドツーエンドの暗号化を使用します。
チャットへのリアルタイムのアクセス管理を許可します。
始めるには、 PubNub アカウントが必要です。サインアップできます
デモ キーセットを選択し、設定タブまでスクロールします。このデモでは、 Files
とObjects
アクセス許可を有効にする必要があります。 Object
権限には、 User Metadata Events
、 Channel Metadata Events
、およびMembership Events
のイベントを使用します。
キーセットを作成して構成したら、それをフロントエンドにコピーする必要があります。
src/
フォルダーにsettings.json
という JSON ファイルを作成しましょう。このファイルは、必要となるすべての API キーに使用します。 PubNub キーセットから始めます。
{ "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" }
ほぼすべてのバックエンド タスクに PubNub を使用します。バックエンドはユーザーのサインアップ/サインインのみを処理し、ID、名前、電子メールのみを持つ最小限のユーザー モデルを使用します。
フロント側には、小さな認証インターフェイスが必要です。
ユーザーがアカウントを取得したら、最初に必要となるのは PubNub SDK のインスタンスです。
Pubnub でユーザーを識別するには、 UUID を提供する必要があります。
物事を簡単にするために、バックエンドと同じ ID を使用します。
/* frontend/src/App.js */ import settings from './settings.json' // our settings file for API keys /* ... */ const pubnub = new PubNub({ publishKey: settings.PUBNUB_PUB_KEY, subscribeKey: settings.PUBNUB_SUB_KEY, uuid: currentUser.id })
バックエンドをできるだけシンプルにするために、PubNub のユーザー メタデータを使用してユーザー情報を交換します。
SDK のインスタンス化の直後に、 PubNub のsetUUIDMetadata
関数を呼び出すだけです。
/* frontend/src/App.js */ await pubnub.objects.setUUIDMetadata({ uuid: currentUser.id, data: { email: currentUser.emailAddress, name: currentUser.name } })
PubNub で最初に行うことは、既存のメンバーをすべて取得し、ローカル データ ストアに保存することです。
/* frontend/src/App.js */ const existingMembers = await pubnub.objects.getAllUUIDMetadata() dispatch({ type: SET_USERS, payload: { users: existingMembers.data.map(u => new User({ id: u.id, name: u.name, emailAddress: u.email })) } })
各チャット ルームは PubNub チャネルに対応します。また、各チャネルにいくつかのメタデータを追加します。
ownerId
: ルームを作成したユーザーの ID。
one2one
: ダイレクト メッセージング ルームとグループ ルームを区別するためのブール値。
archived
: 削除されたグループ ルームを非表示にするブール値。
ownerId
メタデータは、後で Seald SDK を追加するときに使用されます。 PubNub には所有権の概念がありませんが、Seald には所有権の概念があります。チャネルに対してユーザーを追加または削除できるユーザーを定義します。基本的にはグループ管理者を定義します。
まず、既存のチャット ルームを取得します。部屋のメタデータも必要になるため、カスタム フィールドを含める必要があります。次に、アーカイブされたルームをフィルタリングして除外し、すべてをデータ ストアに送信する必要があります。
最後に、ルームに関連付けられた PubNub チャネルにsubscribe
、新しいメッセージを受信できるようになります。
/* frontend/src/App.js */ // Retrieve rooms of which we are members const memberships = await pubnub.objects.getMemberships({ include: { customChannelFields: true } }) const knownRooms = [] // For each room, retrieve room members for (const room of memberships.data.filter(r => !r.channel.custom.archived)) { const roomMembers = await pubnub.objects.getChannelMembers({ channel: room.channel.id }) knownRooms.push(new Room({ id: room.channel.id, name: room.channel.name, users: roomMembers.data.map(u => u.uuid.id), ownerId: room.channel.custom.ownerId, one2one: room.channel.custom.one2one })) } // Store rooms in our data store dispatch({ type: SET_ROOMS, payload: { rooms: knownRooms } }) // Subscribe to channels to get new messages pubnub.subscribe({ channels: knownRooms.map(r => r.id) })
これで、現在いるすべてのルームを取得しました。アプリの初期化を完了するには、最後に 1 つ必要があります。それは、新しく登録したメンバーを含む他のすべてのメンバーとone2one
ルームを確保することです。
新しく見つかったユーザーごとに、新しいルームを作成し、Hello メッセージを送信します。
次に、ルームのメタデータを設定し、サブスクライブします。
/* frontend/src/App.js */ // Ensure that we have a one2one room with everyone const one2oneRooms = knownRooms.filter(r => r.one2one) for (const m of existingMembers.data.filter(u => u.id!== currentUser.id)) { if (!one2oneRooms.find(r => r.users.includes(m.id))) { // New user found: generating a new one2one room const newRoomId = PubNub.generateUUID() const newRoom = new Room({ id: newRoomId, users: [currentUser.id, m.id], one2one: true, name: m.name, ownerId: currentUser.id }) // Add the new room to our local list dispatch({ type: EDIT_OR_ADD_ROOM, payload: { room: new Room({ id: newRoomId, users: [currentUser.id, m.id], one2one: true, name: m.name, ownerId: currentUser.id }) } }) // Publish a "Hello" message in the room await pubnub.publish({ channel: newRoomId, message: { type: 'message', data: (await sealdSession.encryptMessage('Hello 👋')) } }) // Subscribe to the new room pubnub.subscribe({ channels: [newRoomId] }) await pubnub.objects.setChannelMetadata({ channel: newRoomId, data: { name: 'one2one', custom: { one2one: true, ownerId: currentUser.id, }, } }) await pubnub.objects.setChannelMembers({ channel: newRoomId, uuids: [currentUser.id, m.id] }) } }
すべての作業が完了すると、アプリの初期状態が完全に定義されます。ただし、常に最新の状態に保つ必要があります。
これは、 membership
イベントのイベント リスナーを追加するだけで実行できます。
/* frontend/src/App.js */ pubnub.addListener({ objects: async function(objectEvent) { if (objectEvent.message.type === 'membership') { if (objectEvent.message.event === 'delete') { // User is removed from a room /* Removing the room from store... */ } if (objectEvent.message.event === 'set') { // User is added to a room const metadata = await pubnub.objects.getChannelMetadata({ channel: objectEvent.message.data.channel.id }) const roomMembers = (await pubnub.objects.getChannelMembers({ channel: objectEvent.message.data.channel.id })).data.map(u => u.uuid.id) /* Adding new room to store + subscribing to new room channel... */ } } } }) pubnub.subscribe({ channels: [currentUser.id] }) // channel on which events concerning the current user are published
one2one
チャット ルーム自体を見てみましょう。続いてグループルームの対応をさせていただきます。
chat.js
ファイルには、チャット ルームのメッセージを表示するためのすべてのロジックが含まれます。
このルームを初期化するには、既存のメッセージをすべてフェッチするだけです。
これは、ルーム ID を知るだけで実行できます。
/* frontend/src/components/Chat.jsx */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId]
チャンネルに登録して新しいメッセージを取得し、リスナーを追加してリアルタイムで表示できます。
/* frontend/src/components/Chat.jsx */ pubnub.addListener({ message: handleReceiveMessage }) pubnub.subscribe({ channels: [currentRoomId] })
メッセージを送信するには、それをチャネルで公開するだけです。
/* frontend/src/components/Chat.jsx */ const handleSubmitMessage = async e => { /* Some checks that the room is in a correct state... */ await pubnub.publish({ channel: state.room.id, message: { type: 'message', data: state.message } }) }
ファイルを送信するには、まずファイルを PubNub にアップロードします。次に、アップロードされたファイル URI を取得し、チャット ルームでメッセージとして公開します。
/* frontend/src/components/UploadButton.jsx */ // Upload Encrypted file const uploadData = await pubnub.sendFile({ channel: room.id, file: myFile, storeInHistory: false }) const fileURL = await pubnub.getFileUrl({ id: uploadData.id, name: uploadData.name, channel: room.id }) await pubnub.publish({ channel: state.room.id, message: { type: 'file', url: fileURL, fileName: await sealdSession.encryptMessage(selectedFiles[0].name) } })
グループを作成および管理するには、ユーザーを選択するためのインターフェイスが必要です。
グループ メンバーを選択したら、ルームの PubNub チャネルを作成し、チャネルのメタデータとメンバーシップを設定できます。コードは one2one ルームで実行されるものと非常に似ているため、ここでは繰り返しません。
完全なチャット アプリが完成しました。すべてのメッセージにエンドツーエンドの暗号化を追加しましょう。
Seald を始めるには、無料トライアルアカウントを作成してください
Seald ダッシュボードにアクセスすると、いくつかの URL と API トークンが表示されます。
次の要素を取得します。
これらのキーをsettings.json
に追加します。
{ "PUBNUB_PUB_KEY": "pub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "PUBNUB_SUB_KEY": "sub-c-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_APP_ID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_API_URL": "https://api.staging-0.seald.io", "SEALD_KEYSTORAGE_URL": "https://ssks.staging-0.seald.io" }
Seald SDK を使用できるようにするには、すべてのユーザーがサインアップするときにライセンス JWT が必要です。
これらの JWT は、シークレットとシークレット ID を使用してバックエンドで生成する必要があります。
ダッシュボードのランディング ページから、 JWT シークレットとそれに関連付けられた ID をbackend/settings.json
にコピーします。
{ "SEALD_JWT_SECRET_ID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "SEALD_JWT_SECRET": "XXXXXXXXXXXXXXXXXXXXXXXX" }
サインアップ API 呼び出し中に、Seald ライセンス JWT を生成し、それを返します。
/* backend/routes/account.js */ const token = new SignJWT({ iss: settings.SEALD_JWT_SECRET_ID, jti: uuidv4(), /// Random string with enough entropy to never repeat. iat: Math.floor(Date.now() / 1000), // JWT valid only for 10 minutes. `Date.now()` returns the in milliseconds, this needs it in seconds. scopes: [3], // PERMISSION_JOIN_TEAM join_team: true }) .setProtectedHeader({ alg: 'HS256' }) const signupJWT = await token.sign(Buffer.from(settings.SEALD_JWT_SECRET, 'ascii'))
これについて詳しくは、次を参照してください。
次に、 Seald SDK をインストールする必要があります。
また、Seald サーバー上のユーザーを識別するためのプラグインをインストールする必要があります。これを行うには、 sdk-plugin-ssks-password
パッケージを使用します。
このプラグインにより、ユーザーの簡単なパスワード認証が可能になります。
npm i -S @seald-io/sdk @seald-io/sdk-plugin-ssks-password
次に、 seald.js
ファイルを作成します。このファイルでは、まず Seald SDK をインスタンス化する関数を作成します。
/* frontend/src/services/seald.js */ import SealdSDK from '@seald-io/sdk-web' import SealdSDKPluginSSKSPassword from '@seald-io/sdk-plugin-ssks-password' import settings from './settings.json' let sealdSDKInstance = null const instantiateSealdSDK = async () => { sealdSDKInstance = SealdSDK({ appId: settings.SEALD_APP_ID, apiURL: settings.SEALD_API_URL, plugins: [SealdSDKPluginSSKSPassword(settings.SEALD_KEYSTORAGE_URL)] }) }
seald.js
には、ID を作成する関数と ID を取得する関数の 2 つの関数も追加します。 ID を作成するには、アカウント作成時に返されたライセンス JWT も必要です。
/* frontend/src/services/seald.js */ export const createIdentity = async ({ userId, password, signupJWT }) => { await instantiateSealdSDK() await sealdSDKInstance.initiateIdentity({ signupJWT }) await sealdSDKInstance.ssksPassword.saveIdentity({ userId, password }) } export const retrieveIdentity = async ({ userId, password }) => { await instantiateSealdSDK() await sealdSDKInstance.ssksPassword.retrieveIdentity({ userId, password }) }
サインアップおよびサインインのフローでは、ユーザーがログインした後にこれらの関数を呼び出すだけで済みます。
この時点で、ユーザーが接続するたびに、Seald SDK が動作し、暗号化と復号化が可能になります。
警告:適切なセキュリティを確保するには、パスワードを認証サーバーに送信する前に事前ハッシュする必要があります。詳細については、を参照してください。
各チャット ルームは、Seald SDK 上のencryptionSession
に関連付けられます。
チャット ルームを作成するたびに、 1 行を追加して暗号化セッションを作成し、それを使用するだけです。
/* frontend/src/App.js */ // Create a Seald session const sealdSession = await getSealdSDKInstance().createEncryptionSession( { userIds: [currentUser.id, m.id] }, { metadata: newRoomId } ) // Publish a "Hello" message in the room await pubnub.publish({ channel: newRoomId, message: { type: 'message', data: (await sealdSession.encryptMessage('Hello 👋')) } })
ユーザーはデフォルトでencryptionSession.
ルームにアクセスするときは、対応するencryptionSession
を取得する必要があります。これは、暗号化されたメッセージまたはファイルから取得できます。取得したら、コンポーネント参照に保管します。
次に、 session.encryptMessage
、 session.encryptFile
、 session.decryptMessage
、およびsession.decryptFile
関数を単純に使用できます。
まずはメッセージから始めましょう。メッセージを送信するには:
/* frontend/src/components/Chat.js */ const handleSubmitMessage = async m => { /* Some validation that we are in a valid room... */ // if there is no encryption session set in cache yet, create one // (should never happen, as a "Hello" is sent on room creation) if (!sealdSessionRef.current) { sealdSessionRef.current = await getSealdSDKInstance().createEncryptionSession( { userIds: state.room.users }, { metadata: state.room.id } ) } // use the session to encrypt the message we are trying to send const encryptedMessage = await sealdSessionRef.current.encryptMessage(state.message) // publish the encrypted message to pubnub await pubnub.publish({ channel: state.room.id, message: { type: 'message', data: encryptedMessage } }) /* Some cleanup... */ }
そしてメッセージを受け取ったとき:
/* frontend/src/components/Chat.js */ const decryptMessage = async m => { /* Filter out files... */ let encryptedData = m.message.data if (!sealdSessionRef.current) { // no encryption session set in cache yet // we try to get it by parsing the current message sealdSessionRef.current = await getSealdSDKInstance().retrieveEncryptionSession({ encryptedMessage: encryptedData }) // now that we have a session loaded, let's decrypt } const decryptedData = await sealdSessionRef.current.decryptMessage(encryptedData) // we have successfully decrypted the message return { ...m, uuid: m.uuid || m.publisher, value: decryptedData } /* Some error handling... */ } /* Other stuff... */ const handleReceiveMessage = async m => { const decryptedMessage = await decryptMessage(m) setState(draft => { draft.messages = [...draft.messages, decryptedMessage] }) }
また、このdecryptMessage
関数を使用して、ルームを開くときにセッション内の既存のすべてのメッセージを復号化します。
/* frontend/src/components/Chat.js */ const fetchedMessages = (await pubnub.fetchMessages({ channels: [currentRoomId] })).channels[currentRoomId] const clearMessages = fetchedMessages ? await Promise.all(fetchedMessages.map(decryptMessage)) : []
次にファイルについてです。ファイルをアップロードするには:
/* frontend/src/components/UploadButton.js */ // Encrypt file const encryptedBlob = await sealdSession.encryptFile( selectedFiles[0], selectedFiles[0].name, { fileSize: selectedFiles[0].size } ) const encryptedFile = new File([encryptedBlob], selectedFiles[0].name) // Upload Encrypted file const uploadData = await pubnub.sendFile({ channel: room.id, file: encryptedFile, storeInHistory: false })
ファイルを復号化するには:
/* frontend/src/components/Message.js */ const onClick = async () => { if (state.data.type === 'file') { const response = await fetch(state.data.url) const encryptedBlob = await response.blob() const { data: clearBlob, filename } = await sealdSession.decryptFile(encryptedBlob) const href = window.URL.createObjectURL(clearBlob) /* Create an <a> element and simulate a click on it to download the created objectURL */ } }
グループ チャットにもencryptionSession
があります。グループが作成されるたびに、グループを作成する必要があります。
/* frontend/src/components/ManageDialogRoom.js.js */ // To create the encryptionSession const sealdSession = await getSealdSDKInstance().createEncryptionSession( { userIds: dialogRoom.selectedUsersId }, { metadata: newRoomId } )
その後、グループ メンバーを変更するたびに、グループメンバーを追加または削除する必要があります。
/* frontend/src/components/ManageDialogRoom.js.js */ // we compare old and new members to figure out which ones were just added or removed const usersToRemove = dialogRoom.room.users.filter(id => !dialogRoom.selectedUsersId.includes(id)) const usersToAdd = dialogRoom.selectedUsersId.filter(id => !dialogRoom.room.users.includes(id)) if (usersToAdd.length > 0) { // for every added user, add them to the Seald session await dialogRoom.sealdSession.addRecipients({ userIds: usersToAdd }) // then add them to the pubnub channel await pubnub.objects.setChannelMembers({ channel: dialogRoom.room.id, uuids: usersToAdd }) } if (usersToRemove.length > 0) { // for every removed user, revoke them from the Seald session await dialogRoom.sealdSession.revokeRecipients({ userIds: usersToRemove }) // then remove them from the pubnub channel for (const u of usersToRemove) { await pubnub.objects.removeMemberships({ channels: [dialogRoom.room.id], uuid: u }) } }
それが完了したら、完了です。
わずか数行のコードでSeald を PubNub に統合することができました。
チャットがエンドツーエンドで暗号化されるようになったので、データ侵害が発生した場合でもデータの機密性が保たれることをユーザーに保証できます。
いつものように、遠慮せずに
あなたが構築したものを見るのが待ちきれません 🥳。