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 deleted file mode 100644 --- a/keyserver/src/socket/tunnelbroker-socket.js +++ /dev/null @@ -1,306 +0,0 @@ -// @flow - -import _debounce from 'lodash/debounce.js'; -import { getRustAPI } from 'rust-node-addon'; -import uuid from 'uuid'; -import WebSocket from 'ws'; - -import { hexToUintArray } from 'lib/media/data-utils.js'; -import { tunnelbrokerHeartbeatTimeout } from 'lib/shared/timeouts.js'; -import type { TunnelbrokerClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; -import type { MessageReceiveConfirmation } from 'lib/types/tunnelbroker/message-receive-confirmation-types.js'; -import type { MessageSentStatus } from 'lib/types/tunnelbroker/message-to-device-request-status-types.js'; -import type { MessageToDeviceRequest } from 'lib/types/tunnelbroker/message-to-device-request-types.js'; -import { - type TunnelbrokerMessage, - tunnelbrokerMessageTypes, - tunnelbrokerMessageValidator, -} from 'lib/types/tunnelbroker/messages.js'; -import { - qrCodeAuthMessageValidator, - type RefreshKeyRequest, - refreshKeysRequestValidator, - type QRCodeAuthMessage, -} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; -import { - type QRCodeAuthMessagePayload, - qrCodeAuthMessagePayloadValidator, - qrCodeAuthMessageTypes, -} from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; -import type { - ConnectionInitializationMessage, - 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 { fetchOlmAccount } from '../updaters/olm-account-updater.js'; -import { decrypt } from '../utils/aes-crypto-utils.js'; -import { - uploadNewOneTimeKeys, - getNewDeviceKeyUpload, -} from '../utils/olm-utils.js'; - -type PromiseCallbacks = { - +resolve: () => void, - +reject: (error: string) => void, -}; -type Promises = { [clientMessageID: string]: PromiseCallbacks }; - -class TunnelbrokerSocket { - ws: WebSocket; - connected: boolean = false; - closed: boolean = false; - promises: Promises = {}; - heartbeatTimeoutID: ?TimeoutID; - oneTimeKeysPromise: ?Promise; - anonymous: boolean = false; - qrAuthEncryptionKey: ?string; - - constructor( - socketURL: string, - initMessage: - | ConnectionInitializationMessage - | AnonymousInitializationMessage, - onClose: () => mixed, - qrAuthEncryptionKey?: string, - ) { - const socket = new WebSocket(socketURL); - - socket.on('open', () => { - if (!this.closed) { - socket.send(JSON.stringify(initMessage)); - } - }); - - socket.on('close', async () => { - if (this.closed) { - return; - } - this.closed = true; - this.connected = false; - this.stopHeartbeatTimeout(); - console.error('Connection to Tunnelbroker closed'); - onClose(); - }); - - socket.on('error', (error: Error) => { - console.error('Tunnelbroker socket error:', error.message); - }); - - socket.on('message', this.onMessage); - - this.ws = socket; - this.anonymous = !initMessage.accessToken; - if (qrAuthEncryptionKey) { - this.qrAuthEncryptionKey = qrAuthEncryptionKey; - } - } - - onMessage: (event: ArrayBuffer) => Promise = async ( - event: ArrayBuffer, - ) => { - let rawMessage; - try { - rawMessage = JSON.parse(event.toString()); - } catch (e) { - console.error('error while parsing Tunnelbroker message:', e.message); - return; - } - - if (!tunnelbrokerMessageValidator.is(rawMessage)) { - console.error('invalid TunnelbrokerMessage: ', rawMessage.toString()); - return; - } - const message: TunnelbrokerMessage = rawMessage; - - this.resetHeartbeatTimeout(); - - if ( - message.type === - tunnelbrokerMessageTypes.CONNECTION_INITIALIZATION_RESPONSE - ) { - if (message.status.type === 'Success' && !this.connected) { - this.connected = true; - console.info( - this.anonymous - ? 'anonymous session with Tunnelbroker created' - : 'session with Tunnelbroker created', - ); - } else if (message.status.type === 'Success' && this.connected) { - console.info( - 'received ConnectionInitializationResponse with status: Success for already connected socket', - ); - } else { - this.connected = false; - console.error( - 'creating session with Tunnelbroker error:', - message.status.data, - ); - } - } else if (message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE) { - const confirmation: MessageReceiveConfirmation = { - type: tunnelbrokerMessageTypes.MESSAGE_RECEIVE_CONFIRMATION, - messageIDs: [message.messageID], - }; - this.ws.send(JSON.stringify(confirmation)); - - const { payload } = message; - try { - const messageToKeyserver = JSON.parse(payload); - if (qrCodeAuthMessageValidator.is(messageToKeyserver)) { - const request: QRCodeAuthMessage = messageToKeyserver; - const [qrCodeAuthMessage, rustAPI, accountInfo] = await Promise.all([ - this.parseQRCodeAuthMessage(request), - getRustAPI(), - fetchOlmAccount('content'), - ]); - if ( - !qrCodeAuthMessage || - qrCodeAuthMessage.type !== - qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS - ) { - return; - } - // eslint-disable-next-line no-unused-vars - const { primaryDeviceID, userID } = qrCodeAuthMessage; - const [nonce, deviceKeyUpload] = await Promise.all([ - rustAPI.generateNonce(), - getNewDeviceKeyUpload(), - ]); - const signedIdentityKeysBlob = { - payload: deviceKeyUpload.keyPayload, - signature: deviceKeyUpload.keyPayloadSignature, - }; - const nonceSignature = accountInfo.account.sign(nonce); - - await rustAPI.uploadSecondaryDeviceKeysAndLogIn( - userID, - nonce, - nonceSignature, - signedIdentityKeysBlob, - deviceKeyUpload.contentPrekey, - deviceKeyUpload.contentPrekeySignature, - deviceKeyUpload.notifPrekey, - deviceKeyUpload.notifPrekeySignature, - deviceKeyUpload.contentOneTimeKeys, - deviceKeyUpload.notifOneTimeKeys, - ); - } else if (refreshKeysRequestValidator.is(messageToKeyserver)) { - const request: RefreshKeyRequest = messageToKeyserver; - this.debouncedRefreshOneTimeKeys(request.numberOfKeys); - } - } catch (e) { - console.error( - 'error while processing message to keyserver:', - e.message, - ); - } - } else if ( - message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST_STATUS - ) { - for (const status: MessageSentStatus of message.clientMessageIDs) { - if (status.type === 'Success') { - if (this.promises[status.data]) { - this.promises[status.data].resolve(); - delete this.promises[status.data]; - } else { - console.log( - 'received successful response for a non-existent request', - ); - } - } else if (status.type === 'Error') { - if (this.promises[status.data.id]) { - this.promises[status.data.id].reject(status.data.error); - delete this.promises[status.data.id]; - } else { - console.log('received error response for a non-existent request'); - } - } else if (status.type === 'SerializationError') { - console.error('SerializationError for message: ', status.data); - } else if (status.type === 'InvalidRequest') { - console.log('Tunnelbroker recorded InvalidRequest'); - } - } - } else if (message.type === tunnelbrokerMessageTypes.HEARTBEAT) { - const heartbeat: Heartbeat = { - type: tunnelbrokerMessageTypes.HEARTBEAT, - }; - this.ws.send(JSON.stringify(heartbeat)); - } - }; - - refreshOneTimeKeys: (numberOfKeys: number) => void = numberOfKeys => { - const oldOneTimeKeysPromise = this.oneTimeKeysPromise; - this.oneTimeKeysPromise = (async () => { - await oldOneTimeKeysPromise; - await uploadNewOneTimeKeys(numberOfKeys); - })(); - }; - - debouncedRefreshOneTimeKeys: (numberOfKeys: number) => void = _debounce( - this.refreshOneTimeKeys, - 100, - { leading: true, trailing: true }, - ); - - sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise = ( - message: TunnelbrokerClientMessageToDevice, - ) => { - if (!this.connected) { - throw new Error('Tunnelbroker not connected'); - } - const clientMessageID = uuid.v4(); - const messageToDevice: MessageToDeviceRequest = { - type: tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST, - clientMessageID, - deviceID: message.deviceID, - payload: message.payload, - }; - - return new Promise((resolve, reject) => { - this.promises[clientMessageID] = { - resolve, - reject, - }; - this.ws.send(JSON.stringify(messageToDevice)); - }); - }; - - stopHeartbeatTimeout() { - if (this.heartbeatTimeoutID) { - clearTimeout(this.heartbeatTimeoutID); - this.heartbeatTimeoutID = null; - } - } - - resetHeartbeatTimeout() { - this.stopHeartbeatTimeout(); - this.heartbeatTimeoutID = setTimeout(() => { - this.ws.close(); - this.connected = false; - }, tunnelbrokerHeartbeatTimeout); - } - - parseQRCodeAuthMessage: ( - message: QRCodeAuthMessage, - ) => Promise = async message => { - const encryptionKey = this.qrAuthEncryptionKey; - if (!encryptionKey) { - return null; - } - const encryptedData = Buffer.from(message.encryptedContent, 'base64'); - const decryptedData = await decrypt( - hexToUintArray(encryptionKey), - new Uint8Array(encryptedData), - ); - const payload = convertBytesToObj(decryptedData); - if (!qrCodeAuthMessagePayloadValidator.is(payload)) { - return null; - } - - return payload; - }; -} - -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,16 +1,60 @@ // @flow -import { clientTunnelbrokerSocketReconnectDelay } from 'lib/shared/timeouts.js'; +import invariant from 'invariant'; +import _debounce from 'lodash/debounce.js'; +import { getRustAPI } from 'rust-node-addon'; +import uuid from 'uuid'; +import WebSocket from 'ws'; + +import { hexToUintArray } from 'lib/media/data-utils.js'; +import { + clientTunnelbrokerSocketReconnectDelay, + tunnelbrokerHeartbeatTimeout, +} from 'lib/shared/timeouts.js'; +import type { TunnelbrokerClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; +import type { MessageReceiveConfirmation } from 'lib/types/tunnelbroker/message-receive-confirmation-types.js'; +import type { MessageSentStatus } from 'lib/types/tunnelbroker/message-to-device-request-status-types.js'; +import type { MessageToDeviceRequest } from 'lib/types/tunnelbroker/message-to-device-request-types.js'; +import { + type TunnelbrokerMessage, + tunnelbrokerMessageTypes, + tunnelbrokerMessageValidator, +} from 'lib/types/tunnelbroker/messages.js'; +import { + qrCodeAuthMessageValidator, + type RefreshKeyRequest, + 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, + qrCodeAuthMessageTypes, +} from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; import type { ConnectionInitializationMessage, AnonymousInitializationMessage, } from 'lib/types/tunnelbroker/session-types.js'; +import type { Heartbeat } from 'lib/types/websocket/heartbeat-types.js'; import { getCommConfig } from 'lib/utils/comm-config.js'; +import { + convertBytesToObj, + convertObjToBytes, +} from 'lib/utils/conversion-utils.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import sleep from 'lib/utils/sleep.js'; -import TunnelbrokerSocket from './tunnelbroker-socket.js'; -import { type IdentityInfo } from '../user/identity.js'; -import { getContentSigningKey } from '../utils/olm-utils.js'; +import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; +import { fetchIdentityInfo, saveIdentityInfo } from '../user/identity.js'; +import type { IdentityInfo } from '../user/identity.js'; +import { encrypt, decrypt } from '../utils/aes-crypto-utils.js'; +import { + getContentSigningKey, + uploadNewOneTimeKeys, + getNewDeviceKeyUpload, + markPrekeysAsPublished, +} from '../utils/olm-utils.js'; type TBConnectionInfo = { +url: string, @@ -32,66 +76,396 @@ }; } -async function createAndMaintainTunnelbrokerWebsocket( - identityInfo: IdentityInfo, -) { +async function createAndMaintainTunnelbrokerWebsocket(encryptionKey: ?string) { const [deviceID, tbConnectionInfo] = await Promise.all([ getContentSigningKey(), getTBConnectionInfo(), ]); + const createNewTunnelbrokerSocket = async ( + shouldNotifyPrimaryAfterReopening: boolean, + primaryDeviceID: ?string, + ) => { + const identityInfo = await fetchIdentityInfo(); + new TunnelbrokerSocket({ + socketURL: tbConnectionInfo.url, + onClose: async (successfullyAuthed: boolean, primaryID: ?string) => { + await sleep(clientTunnelbrokerSocketReconnectDelay); + await createNewTunnelbrokerSocket(successfullyAuthed, primaryID); + }, + identityInfo, + deviceID, + qrAuthEncryptionKey: encryptionKey, + primaryDeviceID, + shouldNotifyPrimaryAfterReopening, + }); + }; + await createNewTunnelbrokerSocket(false, null); +} + +type TunnelbrokerSocketParams = { + +socketURL: string, + +onClose: (boolean, ?string) => mixed, + +identityInfo: ?IdentityInfo, + +deviceID: string, + +qrAuthEncryptionKey: ?string, + +primaryDeviceID: ?string, + +shouldNotifyPrimaryAfterReopening: boolean, +}; + +type PromiseCallbacks = { + +resolve: () => void, + +reject: (error: string) => void, +}; +type Promises = { [clientMessageID: string]: PromiseCallbacks }; + +class TunnelbrokerSocket { + ws: WebSocket; + connected: boolean = false; + closed: boolean = false; + promises: Promises = {}; + heartbeatTimeoutID: ?TimeoutID; + oneTimeKeysPromise: ?Promise; + identityInfo: ?IdentityInfo; + qrAuthEncryptionKey: ?string; + primaryDeviceID: ?string; + shouldNotifyPrimaryAfterReopening: boolean = false; + shouldNotifyPrimary: boolean = false; + + constructor(tunnelbrokerSocketParams: TunnelbrokerSocketParams) { + const { + socketURL, + onClose, + identityInfo, + deviceID, + qrAuthEncryptionKey, + primaryDeviceID, + shouldNotifyPrimaryAfterReopening, + } = tunnelbrokerSocketParams; + + this.identityInfo = identityInfo; + this.qrAuthEncryptionKey = qrAuthEncryptionKey; + this.primaryDeviceID = primaryDeviceID; + + if (shouldNotifyPrimaryAfterReopening) { + this.shouldNotifyPrimary = true; + } - const initMessage: ConnectionInitializationMessage = { - type: 'ConnectionInitializationMessage', - deviceID: deviceID, - accessToken: identityInfo.accessToken, - userID: identityInfo.userId, - deviceType: 'keyserver', + const socket = new WebSocket(socketURL); + + socket.on('open', this.onOpen(socket, deviceID)); + + socket.on('close', async () => { + if (this.closed) { + return; + } + this.closed = true; + this.connected = false; + this.stopHeartbeatTimeout(); + console.error('Connection to Tunnelbroker closed'); + onClose(this.shouldNotifyPrimaryAfterReopening, this.primaryDeviceID); + }); + + socket.on('error', (error: Error) => { + console.error('Tunnelbroker socket error:', error.message); + }); + + socket.on('message', this.onMessage); + + this.ws = socket; + } + + onOpen: (socket: WebSocket, deviceID: string) => void = ( + socket, + deviceID, + ) => { + if (this.closed) { + return; + } + + if (this.identityInfo) { + const initMessage: ConnectionInitializationMessage = { + type: 'ConnectionInitializationMessage', + deviceID, + accessToken: this.identityInfo.accessToken, + userID: this.identityInfo.userId, + deviceType: 'keyserver', + }; + socket.send(JSON.stringify(initMessage)); + } else { + const initMessage: AnonymousInitializationMessage = { + type: 'AnonymousInitializationMessage', + deviceID, + deviceType: 'keyserver', + }; + socket.send(JSON.stringify(initMessage)); + } }; - createAndMaintainTunnelbrokerWebsocketBase(tbConnectionInfo.url, initMessage); -} + onMessage: (event: ArrayBuffer) => Promise = async ( + event: ArrayBuffer, + ) => { + let rawMessage; + try { + rawMessage = JSON.parse(event.toString()); + } catch (e) { + console.error('error while parsing Tunnelbroker message:', e.message); + return; + } -async function createAndMaintainAnonymousTunnelbrokerWebsocket( - encryptionKey: string, -) { - const [deviceID, tbConnectionInfo] = await Promise.all([ - getContentSigningKey(), - getTBConnectionInfo(), - ]); + if (!tunnelbrokerMessageValidator.is(rawMessage)) { + console.error('invalid TunnelbrokerMessage: ', rawMessage.toString()); + return; + } + const message: TunnelbrokerMessage = rawMessage; + + this.resetHeartbeatTimeout(); + + if ( + message.type === + tunnelbrokerMessageTypes.CONNECTION_INITIALIZATION_RESPONSE + ) { + if (message.status.type === 'Success' && !this.connected) { + this.connected = true; + console.info( + this.identityInfo + ? 'session with Tunnelbroker created' + : 'anonymous session with Tunnelbroker created', + ); + if (!this.shouldNotifyPrimary) { + return; + } + const { primaryDeviceID } = this; + invariant( + primaryDeviceID, + 'Primary device ID is not set but should be', + ); + const payload = await this.encodeQRAuthMessage({ + type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, + requestBackupKeys: false, + }); + 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', + ); + } else { + this.connected = false; + console.error( + 'creating session with Tunnelbroker error:', + message.status.data, + ); + } + } else if (message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE) { + const confirmation: MessageReceiveConfirmation = { + type: tunnelbrokerMessageTypes.MESSAGE_RECEIVE_CONFIRMATION, + messageIDs: [message.messageID], + }; + this.ws.send(JSON.stringify(confirmation)); + + const { payload } = message; + try { + const messageToKeyserver = JSON.parse(payload); + if (qrCodeAuthMessageValidator.is(messageToKeyserver)) { + const request: QRCodeAuthMessage = messageToKeyserver; + const [qrCodeAuthMessage, rustAPI, accountInfo] = await Promise.all([ + this.parseQRCodeAuthMessage(request), + getRustAPI(), + fetchOlmAccount('content'), + ]); + if ( + !qrCodeAuthMessage || + qrCodeAuthMessage.type !== + qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS + ) { + return; + } + const { primaryDeviceID, userID } = qrCodeAuthMessage; + this.primaryDeviceID = primaryDeviceID; + + const [nonce, deviceKeyUpload] = await Promise.all([ + rustAPI.generateNonce(), + getNewDeviceKeyUpload(), + ]); + const signedIdentityKeysBlob = { + payload: deviceKeyUpload.keyPayload, + signature: deviceKeyUpload.keyPayloadSignature, + }; + const nonceSignature = accountInfo.account.sign(nonce); + + const identityInfo = await rustAPI.uploadSecondaryDeviceKeysAndLogIn( + userID, + nonce, + nonceSignature, + signedIdentityKeysBlob, + deviceKeyUpload.contentPrekey, + deviceKeyUpload.contentPrekeySignature, + deviceKeyUpload.notifPrekey, + deviceKeyUpload.notifPrekeySignature, + deviceKeyUpload.contentOneTimeKeys, + deviceKeyUpload.notifOneTimeKeys, + ); + await Promise.all([ + markPrekeysAsPublished(), + saveIdentityInfo(identityInfo), + ]); + this.shouldNotifyPrimaryAfterReopening = true; + this.closeConnection(); + } else if (refreshKeysRequestValidator.is(messageToKeyserver)) { + const request: RefreshKeyRequest = messageToKeyserver; + this.debouncedRefreshOneTimeKeys(request.numberOfKeys); + } + } catch (e) { + console.error( + 'error while processing message to keyserver:', + e.message, + ); + } + } else if ( + message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST_STATUS + ) { + for (const status: MessageSentStatus of message.clientMessageIDs) { + if (status.type === 'Success') { + if (this.promises[status.data]) { + this.promises[status.data].resolve(); + delete this.promises[status.data]; + } else { + console.log( + 'received successful response for a non-existent request', + ); + } + } else if (status.type === 'Error') { + if (this.promises[status.data.id]) { + this.promises[status.data.id].reject(status.data.error); + delete this.promises[status.data.id]; + } else { + console.log('received error response for a non-existent request'); + } + } else if (status.type === 'SerializationError') { + console.error('SerializationError for message: ', status.data); + } else if (status.type === 'InvalidRequest') { + console.log('Tunnelbroker recorded InvalidRequest'); + } + } + } else if (message.type === tunnelbrokerMessageTypes.HEARTBEAT) { + const heartbeat: Heartbeat = { + type: tunnelbrokerMessageTypes.HEARTBEAT, + }; + this.ws.send(JSON.stringify(heartbeat)); + } + }; - const initMessage: AnonymousInitializationMessage = { - type: 'AnonymousInitializationMessage', - deviceID: deviceID, - deviceType: 'keyserver', + refreshOneTimeKeys: (numberOfKeys: number) => void = numberOfKeys => { + const oldOneTimeKeysPromise = this.oneTimeKeysPromise; + this.oneTimeKeysPromise = (async () => { + await oldOneTimeKeysPromise; + await uploadNewOneTimeKeys(numberOfKeys); + })(); }; - createAndMaintainTunnelbrokerWebsocketBase( - tbConnectionInfo.url, - initMessage, - encryptionKey, + debouncedRefreshOneTimeKeys: (numberOfKeys: number) => void = _debounce( + this.refreshOneTimeKeys, + 100, + { leading: true, trailing: true }, ); -} -function createAndMaintainTunnelbrokerWebsocketBase( - url: string, - initMessage: ConnectionInitializationMessage | AnonymousInitializationMessage, - encryptionKey?: string, -) { - const createNewTunnelbrokerSocket = () => { - new TunnelbrokerSocket( - url, - initMessage, - async () => { - await sleep(clientTunnelbrokerSocketReconnectDelay); - createNewTunnelbrokerSocket(); - }, - encryptionKey, + sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise = ( + message: TunnelbrokerClientMessageToDevice, + ) => { + if (!this.connected) { + throw new Error('Tunnelbroker not connected'); + } + const clientMessageID = uuid.v4(); + const messageToDevice: MessageToDeviceRequest = { + type: tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST, + clientMessageID, + deviceID: message.deviceID, + payload: message.payload, + }; + + return new Promise((resolve, reject) => { + this.promises[clientMessageID] = { + resolve, + reject, + }; + this.ws.send(JSON.stringify(messageToDevice)); + }); + }; + + stopHeartbeatTimeout() { + if (this.heartbeatTimeoutID) { + clearTimeout(this.heartbeatTimeoutID); + this.heartbeatTimeoutID = null; + } + } + + resetHeartbeatTimeout() { + this.stopHeartbeatTimeout(); + this.heartbeatTimeoutID = setTimeout(() => { + this.ws.close(); + this.connected = false; + }, tunnelbrokerHeartbeatTimeout); + } + + closeConnection() { + this.ws.close(); + this.connected = false; + } + + parseQRCodeAuthMessage: ( + message: QRCodeAuthMessage, + ) => Promise = async message => { + const encryptionKey = this.qrAuthEncryptionKey; + if (!encryptionKey) { + return null; + } + const encryptedData = Buffer.from(message.encryptedContent, 'base64'); + const decryptedData = await decrypt( + hexToUintArray(encryptionKey), + new Uint8Array(encryptedData), ); + const payload = convertBytesToObj(decryptedData); + if (!qrCodeAuthMessagePayloadValidator.is(payload)) { + return null; + } + + 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; + } + + let encryptedContent; + try { + const payloadBytes = convertObjToBytes(payload); + const keyBytes = hexToUintArray(encryptionKey); + const encryptedBytes = await encrypt(keyBytes, payloadBytes); + encryptedContent = Buffer.from(encryptedBytes).toString('base64'); + } catch (e) { + console.error( + 'Error encoding QRCodeAuthMessagePayload:', + getMessageForException(e), + ); + return null; + } + + return { + type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE, + encryptedContent, + }; }; - createNewTunnelbrokerSocket(); } -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; @@ -324,4 +335,5 @@ validateAndUploadAccountPrekeys, publishPrekeysToIdentity, getNewDeviceKeyUpload, + markPrekeysAsPublished, };