diff --git a/keyserver/src/socket/tunnelbroker.js b/keyserver/src/socket/tunnelbroker.js index 3fd92394c..65f5969a6 100644 --- a/keyserver/src/socket/tunnelbroker.js +++ b/keyserver/src/socket/tunnelbroker.js @@ -1,56 +1,73 @@ // @flow import WebSocket from 'ws'; -import { type TBKeyserverConnectionInitializationMessage } from 'lib/types/tunnelbroker-messages.js'; +import { + type TBKeyserverConnectionInitializationMessage, + type MessageFromTunnelbroker, + tunnelbrokerMessageTypes, +} from 'lib/types/tunnelbroker-messages.js'; +import { ServerError } from 'lib/utils/errors.js'; import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; -import type { IdentityInfo } from '../user/identity.js'; +import { type IdentityInfo } from '../user/identity.js'; +import { uploadNewOneTimeKeys } from '../utils/olm-utils.js'; async function createAndMaintainTunnelbrokerWebsocket( identityInfo: IdentityInfo, ) { const accountInfo = await fetchOlmAccount('content'); const deviceID = JSON.parse(accountInfo.account.identity_keys()).ed25519; openTunnelbrokerConnection( deviceID, identityInfo.userId, identityInfo.accessToken, ); } +function handleTBMessageEvent(event: ArrayBuffer): Promise { + const message: MessageFromTunnelbroker = JSON.parse(event.toString()); + + if (message.type === tunnelbrokerMessageTypes.REFRESH_KEYS_REQUEST) { + return uploadNewOneTimeKeys(message.numberOfKeys); + } + throw new ServerError('unsupported_tunnelbroker_message'); +} + function openTunnelbrokerConnection( deviceID: string, userID: string, accessToken: string, ) { try { const tunnelbrokerSocket = new WebSocket('ws://127.0.0.1:51001'); tunnelbrokerSocket.on('open', () => { const message: TBKeyserverConnectionInitializationMessage = { type: 'sessionRequest', accessToken, deviceId: deviceID, deviceType: 'keyserver', userId: userID, }; tunnelbrokerSocket.send(JSON.stringify(message)); console.info('Connection to Tunnelbroker established'); }); tunnelbrokerSocket.on('close', async () => { console.warn('Connection to Tunnelbroker closed'); }); tunnelbrokerSocket.on('error', (error: Error) => { console.error('Tunnelbroker socket error', error.message); }); + + tunnelbrokerSocket.on('message', handleTBMessageEvent); } catch { console.log('Failed to open connection with Tunnelbroker'); } } export { createAndMaintainTunnelbrokerWebsocket }; diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js index 5727849d3..8fe9658b4 100644 --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -1,120 +1,165 @@ // @flow import olm from '@commapp/olm'; import type { Account as OlmAccount, Utility as OlmUtility, Session as OlmSession, } from '@commapp/olm'; +import { getRustAPI } from 'rust-node-addon'; import uuid from 'uuid'; import { olmEncryptedMessageTypes, type OLMOneTimeKeys, } from 'lib/types/crypto-types.js'; +import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; +import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; +import { fetchIdentityInfo } from '../user/identity.js'; + type PickledOlmAccount = { +picklingKey: string, +pickledAccount: string, }; const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; const maxOldPrekeyAge = 24 * 60 * 60 * 1000; async function createPickledOlmAccount(): Promise { await olm.init(); const account = new olm.Account(); account.create(); const picklingKey = uuid.v4(); const pickledAccount = account.pickle(picklingKey); return { picklingKey: picklingKey, pickledAccount: pickledAccount, }; } async function unpickleOlmAccount( pickledOlmAccount: PickledOlmAccount, ): Promise { await olm.init(); const account = new olm.Account(); account.unpickle( pickledOlmAccount.picklingKey, pickledOlmAccount.pickledAccount, ); return account; } async function createPickledOlmSession( account: OlmAccount, accountPicklingKey: string, initialEncryptedMessage: string, ): Promise { await olm.init(); const session = new olm.Session(); session.create_inbound(account, initialEncryptedMessage); account.remove_one_time_keys(session); session.decrypt(olmEncryptedMessageTypes.PREKEY, initialEncryptedMessage); return session.pickle(accountPicklingKey); } async function unpickleOlmSession( pickledSession: string, picklingKey: string, ): Promise { await olm.init(); const session = new olm.Session(); session.unpickle(picklingKey, pickledSession); return session; } let cachedOLMUtility: OlmUtility; function getOlmUtility(): OlmUtility { if (cachedOLMUtility) { return cachedOLMUtility; } cachedOLMUtility = new olm.Utility(); return cachedOLMUtility; } function validateAccountPrekey(account: OlmAccount) { const currentDate = new Date(); const lastPrekeyPublishDate = new Date(account.last_prekey_publish_time()); const prekeyPublished = !account.unpublished_prekey(); if ( prekeyPublished && currentDate - lastPrekeyPublishDate > maxPublishedPrekeyAge ) { // If there is no prekey or the current prekey is older than month // we need to generate new one. account.generate_prekey(); } if ( prekeyPublished && currentDate - lastPrekeyPublishDate >= maxOldPrekeyAge ) { account.forget_old_prekey(); } } function getOneTimeKeyValues(keyBlob: string): $ReadOnlyArray { const content: OLMOneTimeKeys = JSON.parse(keyBlob); const keys: $ReadOnlyArray = values(content.curve25519); return keys; } +async function uploadNewOneTimeKeys(numberOfKeys: number) { + const [rustAPI, identityInfo] = await Promise.all([ + getRustAPI(), + fetchIdentityInfo(), + ]); + + if (!identityInfo) { + throw new ServerError('missing_identity_info'); + } + + await fetchCallUpdateOlmAccount('content', (contentAccount: OlmAccount) => { + contentAccount.generate_one_time_keys(numberOfKeys); + const contentOneTimeKeys = getOneTimeKeyValues( + contentAccount.one_time_keys(), + ); + const deviceID = JSON.parse(contentAccount.identity_keys()).curve25519; + + return fetchCallUpdateOlmAccount( + 'notifications', + async (notifAccount: OlmAccount) => { + notifAccount.generate_one_time_keys(numberOfKeys); + const notifOneTimeKeys = getOneTimeKeyValues( + notifAccount.one_time_keys(), + ); + await rustAPI.uploadOneTimeKeys( + identityInfo.userId, + deviceID, + identityInfo.accessToken, + contentOneTimeKeys, + notifOneTimeKeys, + ); + + notifAccount.mark_keys_as_published(); + contentAccount.mark_keys_as_published(); + }, + ); + }); +} + export { createPickledOlmAccount, createPickledOlmSession, getOlmUtility, unpickleOlmAccount, unpickleOlmSession, validateAccountPrekey, getOneTimeKeyValues, + uploadNewOneTimeKeys, }; diff --git a/lib/types/tunnelbroker-messages.js b/lib/types/tunnelbroker-messages.js index d0e5994dc..1bf65208a 100644 --- a/lib/types/tunnelbroker-messages.js +++ b/lib/types/tunnelbroker-messages.js @@ -1,30 +1,44 @@ // @flow type TBSharedConnectionInitializationMessage = { +type: 'sessionRequest', +deviceId: string, +accessToken: string, +deviceAppVersion?: string, +userId: string, }; export type TBKeyserverConnectionInitializationMessage = { ...TBSharedConnectionInitializationMessage, +deviceType: 'keyserver', }; export type TBClientConnectionInitializationMessage = { ...TBSharedConnectionInitializationMessage, +deviceType: 'web' | 'mobile', }; export type TBNotifyClientConnectionInitializationMessage = { ...TBClientConnectionInitializationMessage, +notifyToken: string, +notifyPlatform: 'apns' | 'fcm' | 'web' | 'wns', }; -export type TBConnectionInitializationMessage = +export type MessageToTunnelbroker = | TBKeyserverConnectionInitializationMessage | TBClientConnectionInitializationMessage | TBNotifyClientConnectionInitializationMessage; + +export const tunnelbrokerMessageTypes = Object.freeze({ + REFRESH_KEYS_REQUEST: 'RefreshKeyRequest', +}); + +export type TBRefreshKeysRequest = { + +type: 'RefreshKeyRequest', + +deviceId: string, + +numberOfKeys: number, +}; + +// Disjoint enumeration of all messages received from Tunnelbroker +// Currently, only a single message +export type MessageFromTunnelbroker = TBRefreshKeysRequest;