diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -35,10 +35,7 @@ } from './responders/website-responders.js'; import { webWorkerResponder } from './responders/webworker-responders.js'; import { onConnection } from './socket/socket.js'; -import { - createAndMaintainTunnelbrokerWebsocket, - createAndMaintainAnonymousTunnelbrokerWebsocket, -} from './socket/tunnelbroker.js'; +import { createAndMaintainTunnelbrokerWebsocket } from './socket/tunnelbroker.js'; import { multerProcessor, multimediaUploadResponder, @@ -105,7 +102,7 @@ const aes256Key = crypto.randomBytes(32).toString('hex'); const ed25519Key = await getContentSigningKey(); - await createAndMaintainAnonymousTunnelbrokerWebsocket(aes256Key); + await createAndMaintainTunnelbrokerWebsocket(aes256Key); console.log( '\nOpen the Comm app on your phone and scan the QR code below\n', @@ -130,9 +127,7 @@ // We don't await here, as Tunnelbroker communication is not needed for // normal keyserver behavior yet. In addition, this doesn't return // information useful for other keyserver functions. - ignorePromiseRejections( - createAndMaintainTunnelbrokerWebsocket(identityInfo), - ); + ignorePromiseRejections(createAndMaintainTunnelbrokerWebsocket(null)); if (process.env.NODE_ENV === 'development') { await createAuthoritativeKeyserverConfigFiles(identityInfo.userId); } diff --git a/keyserver/src/socket/tunnelbroker-socket.js b/keyserver/src/socket/tunnelbroker-socket.js --- a/keyserver/src/socket/tunnelbroker-socket.js +++ b/keyserver/src/socket/tunnelbroker-socket.js @@ -1,5 +1,6 @@ // @flow +import invariant from 'invariant'; import _debounce from 'lodash/debounce.js'; import { getRustAPI } from 'rust-node-addon'; import uuid from 'uuid'; @@ -22,6 +23,7 @@ refreshKeysRequestValidator, type QRCodeAuthMessage, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; +import { peerToPeerMessageTypes } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import { type QRCodeAuthMessagePayload, qrCodeAuthMessagePayloadValidator, @@ -32,13 +34,18 @@ AnonymousInitializationMessage, } from 'lib/types/tunnelbroker/session-types.js'; import type { Heartbeat } from 'lib/types/websocket/heartbeat-types.js'; -import { convertBytesToObj } from 'lib/utils/conversion-utils.js'; +import { + convertBytesToObj, + convertObjToBytes, +} from 'lib/utils/conversion-utils.js'; import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; -import { decrypt } from '../utils/aes-crypto-utils.js'; +import { saveIdentityInfo } from '../user/identity.js'; +import { encrypt, decrypt } from '../utils/aes-crypto-utils.js'; import { uploadNewOneTimeKeys, getNewDeviceKeyUpload, + markPrekeysAsPublished, } from '../utils/olm-utils.js'; type PromiseCallbacks = { @@ -54,22 +61,59 @@ promises: Promises = {}; heartbeatTimeoutID: ?TimeoutID; oneTimeKeysPromise: ?Promise; - anonymous: boolean = false; + userID: ?string; + accessToken: ?string; qrAuthEncryptionKey: ?string; + primaryDeviceID: ?string; + justSuccessfullyAuthenticated: boolean = false; + shouldNotifyPrimary: boolean = false; constructor( socketURL: string, - initMessage: - | ConnectionInitializationMessage - | AnonymousInitializationMessage, - onClose: () => mixed, - qrAuthEncryptionKey?: string, + onClose: (boolean, ?string) => mixed, + userID: ?string, + deviceID: string, + accessToken: ?string, + qrAuthEncryptionKey: ?string, + primaryDeviceID: ?string, + justSuccessfullyAuthenticated: boolean, ) { + this.userID = userID; + this.accessToken = accessToken; + this.qrAuthEncryptionKey = qrAuthEncryptionKey; + this.primaryDeviceID = primaryDeviceID; + + if (justSuccessfullyAuthenticated) { + this.shouldNotifyPrimary = true; + } + const socket = new WebSocket(socketURL); socket.on('open', () => { if (!this.closed) { - socket.send(JSON.stringify(initMessage)); + let initMessageString; + + if (userID && accessToken) { + console.log('Creating authenticated tunnelbroker connection'); + const initMessage: ConnectionInitializationMessage = { + type: 'ConnectionInitializationMessage', + deviceID, + accessToken: accessToken, + userID: userID, + deviceType: 'keyserver', + }; + initMessageString = JSON.stringify(initMessage); + } else { + console.log('Creating anonymous tunnelbroker connection'); + const initMessage: AnonymousInitializationMessage = { + type: 'AnonymousInitializationMessage', + deviceID, + deviceType: 'keyserver', + }; + initMessageString = JSON.stringify(initMessage); + } + + socket.send(initMessageString); } }); @@ -81,7 +125,7 @@ this.connected = false; this.stopHeartbeatTimeout(); console.error('Connection to Tunnelbroker closed'); - onClose(); + onClose(this.justSuccessfullyAuthenticated, this.primaryDeviceID); }); socket.on('error', (error: Error) => { @@ -91,10 +135,6 @@ socket.on('message', this.onMessage); this.ws = socket; - this.anonymous = !initMessage.accessToken; - if (qrAuthEncryptionKey) { - this.qrAuthEncryptionKey = qrAuthEncryptionKey; - } } onMessage: (event: ArrayBuffer) => Promise = async ( @@ -123,10 +163,29 @@ if (message.status.type === 'Success' && !this.connected) { this.connected = true; console.info( - this.anonymous - ? 'anonymous session with Tunnelbroker created' - : 'session with Tunnelbroker created', + this.userID && this.accessToken + ? 'session with Tunnelbroker created' + : 'anonymous session with Tunnelbroker created', + ); + if (!this.shouldNotifyPrimary) { + return; + } + const primaryDeviceID = this.primaryDeviceID; + invariant( + primaryDeviceID, + 'Primary device ID is not set but should be', ); + const payload = await this.encodeQRAuthMessage({ + type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, + }); + if (!payload) { + this.closeConnection(); + return; + } + await this.sendMessage({ + deviceID: primaryDeviceID, + payload: JSON.stringify(payload), + }); } else if (message.status.type === 'Success' && this.connected) { console.info( 'received ConnectionInitializationResponse with status: Success for already connected socket', @@ -162,8 +221,9 @@ ) { return; } - // eslint-disable-next-line no-unused-vars const { primaryDeviceID, userID } = qrCodeAuthMessage; + this.primaryDeviceID = primaryDeviceID; + const [nonce, deviceKeyUpload] = await Promise.all([ rustAPI.generateNonce(), getNewDeviceKeyUpload(), @@ -174,7 +234,7 @@ }; const nonceSignature = accountInfo.account.sign(nonce); - await rustAPI.uploadSecondaryDeviceKeysAndLogIn( + const identityInfo = await rustAPI.uploadSecondaryDeviceKeysAndLogIn( userID, nonce, nonceSignature, @@ -186,6 +246,12 @@ deviceKeyUpload.contentOneTimeKeys, deviceKeyUpload.notifOneTimeKeys, ); + await Promise.all([ + markPrekeysAsPublished(), + saveIdentityInfo(identityInfo), + ]); + this.justSuccessfullyAuthenticated = true; + this.closeConnection(); } else if (refreshKeysRequestValidator.is(messageToKeyserver)) { const request: RefreshKeyRequest = messageToKeyserver; this.debouncedRefreshOneTimeKeys(request.numberOfKeys); @@ -282,6 +348,11 @@ }, tunnelbrokerHeartbeatTimeout); } + closeConnection() { + this.ws.close(); + this.connected = false; + } + parseQRCodeAuthMessage: ( message: QRCodeAuthMessage, ) => Promise = async message => { @@ -301,6 +372,24 @@ return payload; }; + + encodeQRAuthMessage: ( + payload: QRCodeAuthMessagePayload, + ) => Promise = async payload => { + const encryptionKey = this.qrAuthEncryptionKey; + if (!encryptionKey) { + console.error('Encryption key missing - cannot send QR auth message.'); + return null; + } + const payloadBytes = convertObjToBytes(payload); + const keyBytes = hexToUintArray(encryptionKey); + const encryptedBytes = await encrypt(keyBytes, payloadBytes); + const encryptedContent = Buffer.from(encryptedBytes).toString('base64'); + return Promise.resolve({ + type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE, + encryptedContent, + }); + }; } export default TunnelbrokerSocket; diff --git a/keyserver/src/socket/tunnelbroker.js b/keyserver/src/socket/tunnelbroker.js --- a/keyserver/src/socket/tunnelbroker.js +++ b/keyserver/src/socket/tunnelbroker.js @@ -1,15 +1,11 @@ // @flow import { clientTunnelbrokerSocketReconnectDelay } from 'lib/shared/timeouts.js'; -import type { - ConnectionInitializationMessage, - AnonymousInitializationMessage, -} from 'lib/types/tunnelbroker/session-types.js'; import { getCommConfig } from 'lib/utils/comm-config.js'; import sleep from 'lib/utils/sleep.js'; import TunnelbrokerSocket from './tunnelbroker-socket.js'; -import { type IdentityInfo } from '../user/identity.js'; +import { fetchIdentityInfo } from '../user/identity.js'; import { getContentSigningKey } from '../utils/olm-utils.js'; type TBConnectionInfo = { @@ -32,66 +28,31 @@ }; } -async function createAndMaintainTunnelbrokerWebsocket( - identityInfo: IdentityInfo, -) { +async function createAndMaintainTunnelbrokerWebsocket(encryptionKey: ?string) { const [deviceID, tbConnectionInfo] = await Promise.all([ getContentSigningKey(), getTBConnectionInfo(), ]); - - const initMessage: ConnectionInitializationMessage = { - type: 'ConnectionInitializationMessage', - deviceID: deviceID, - accessToken: identityInfo.accessToken, - userID: identityInfo.userId, - deviceType: 'keyserver', - }; - - createAndMaintainTunnelbrokerWebsocketBase(tbConnectionInfo.url, initMessage); -} - -async function createAndMaintainAnonymousTunnelbrokerWebsocket( - encryptionKey: string, -) { - const [deviceID, tbConnectionInfo] = await Promise.all([ - getContentSigningKey(), - getTBConnectionInfo(), - ]); - - const initMessage: AnonymousInitializationMessage = { - type: 'AnonymousInitializationMessage', - deviceID: deviceID, - deviceType: 'keyserver', - }; - - createAndMaintainTunnelbrokerWebsocketBase( - tbConnectionInfo.url, - initMessage, - encryptionKey, - ); -} - -function createAndMaintainTunnelbrokerWebsocketBase( - url: string, - initMessage: ConnectionInitializationMessage | AnonymousInitializationMessage, - encryptionKey?: string, -) { - const createNewTunnelbrokerSocket = () => { + const createNewTunnelbrokerSocket = async ( + justSuccessfullyAuthenticated: boolean, + primaryDeviceID: ?string, + ) => { + const identityInfo = await fetchIdentityInfo(); new TunnelbrokerSocket( - url, - initMessage, - async () => { + tbConnectionInfo.url, + async (successfullyAuthed: boolean, primaryID: ?string) => { await sleep(clientTunnelbrokerSocketReconnectDelay); - createNewTunnelbrokerSocket(); + await createNewTunnelbrokerSocket(successfullyAuthed, primaryID); }, + identityInfo?.userId, + deviceID, + identityInfo?.accessToken, encryptionKey, + primaryDeviceID, + justSuccessfullyAuthenticated, ); }; - createNewTunnelbrokerSocket(); + await createNewTunnelbrokerSocket(false, null); } -export { - createAndMaintainTunnelbrokerWebsocket, - createAndMaintainAnonymousTunnelbrokerWebsocket, -}; +export { createAndMaintainTunnelbrokerWebsocket }; diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -103,6 +103,17 @@ return cachedOLMUtility; } +async function markPrekeysAsPublished(): Promise { + await Promise.all([ + fetchCallUpdateOlmAccount('content', (contentAccount: OlmAccount) => { + contentAccount.mark_prekey_as_published(); + }), + fetchCallUpdateOlmAccount('notifications', (notifAccount: OlmAccount) => { + notifAccount.mark_prekey_as_published(); + }), + ]); +} + async function getNewDeviceKeyUpload(): Promise { let contentIdentityKeys: string; let contentOneTimeKeys: $ReadOnlyArray; @@ -319,4 +330,5 @@ validateAndUploadAccountPrekeys, publishPrekeysToIdentity, getNewDeviceKeyUpload, + markPrekeysAsPublished, };