diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -35,7 +35,10 @@ } from './responders/website-responders.js'; import { webWorkerResponder } from './responders/webworker-responders.js'; import { onConnection } from './socket/socket.js'; -import { createAndMaintainTunnelbrokerWebsocket } from './socket/tunnelbroker.js'; +import { + createAndMaintainTunnelbrokerWebsocket, + createAndMaintainAnonymousTunnelbrokerWebsocket, +} from './socket/tunnelbroker.js'; import { multerProcessor, multimediaUploadResponder, @@ -97,32 +100,13 @@ process.exit(2); } - // Allow login to be optional until staging environment is available - try { - // We await here to ensure that the keyserver has been provisioned a - // commServicesAccessToken. In the future, this will be necessary for - // many keyserver operations. - const identityInfo = await verifyUserLoggedIn(); - // 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), - ); - if (process.env.NODE_ENV === 'development') { - await createAuthoritativeKeyserverConfigFiles(identityInfo.userId); - } - } catch (e) { - console.warn( - 'Failed identity login. Login optional until staging environment is available', - ); - } - if (shouldDisplayQRCodeInTerminal) { try { const aes256Key = crypto.randomBytes(32).toString('hex'); const ed25519Key = await getContentSigningKey(); + await createAndMaintainAnonymousTunnelbrokerWebsocket(aes256Key); + console.log( '\nOpen the Comm app on your phone and scan the QR code below\n', ); @@ -136,6 +120,27 @@ } catch (e) { console.log('Error generating QR code', e); } + } else { + // Allow login to be optional until staging environment is available + try { + // We await here to ensure that the keyserver has been provisioned a + // commServicesAccessToken. In the future, this will be necessary for + // many keyserver operations. + const identityInfo = await verifyUserLoggedIn(); + // 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), + ); + if (process.env.NODE_ENV === 'development') { + await createAuthoritativeKeyserverConfigFiles(identityInfo.userId); + } + } catch (e) { + console.warn( + 'Failed identity login. Login optional until staging environment is available', + ); + } } if (!isCPUProfilingEnabled) { 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 @@ -4,6 +4,7 @@ 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'; @@ -15,12 +16,24 @@ 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 { ConnectionInitializationMessage } from 'lib/types/tunnelbroker/session-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 { decrypt } from '../utils/aes-crypto-utils.js'; import { uploadNewOneTimeKeys } from '../utils/olm-utils.js'; type PromiseCallbacks = { @@ -36,11 +49,16 @@ promises: Promises = {}; heartbeatTimeoutID: ?TimeoutID; oneTimeKeysPromise: ?Promise; + anonymous: boolean = false; + qrAuthEncryptionKey: ?string; constructor( socketURL: string, - initMessage: ConnectionInitializationMessage, + initMessage: + | ConnectionInitializationMessage + | AnonymousInitializationMessage, onClose: () => mixed, + qrAuthEncryptionKey?: string, ) { const socket = new WebSocket(socketURL); @@ -68,6 +86,10 @@ socket.on('message', this.onMessage); this.ws = socket; + this.anonymous = !initMessage.accessToken; + if (qrAuthEncryptionKey) { + this.qrAuthEncryptionKey = qrAuthEncryptionKey; + } } onMessage: (event: ArrayBuffer) => Promise = async ( @@ -95,7 +117,11 @@ ) { if (message.status.type === 'Success' && !this.connected) { this.connected = true; - console.info('session with Tunnelbroker created'); + 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', @@ -117,7 +143,17 @@ const { payload } = message; try { const messageToKeyserver = JSON.parse(payload); - if (refreshKeysRequestValidator.is(messageToKeyserver)) { + if (qrCodeAuthMessageValidator.is(messageToKeyserver)) { + const request: QRCodeAuthMessage = messageToKeyserver; + const qrCodeAuthMessage = await this.parseQRCodeAuthMessage(request); + if ( + !qrCodeAuthMessage || + qrCodeAuthMessage.type !== + qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS + ) { + return; + } + } else if (refreshKeysRequestValidator.is(messageToKeyserver)) { const request: RefreshKeyRequest = messageToKeyserver; this.debouncedRefreshOneTimeKeys(request.numberOfKeys); } @@ -212,6 +248,26 @@ 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,7 +1,10 @@ // @flow import { clientTunnelbrokerSocketReconnectDelay } from 'lib/shared/timeouts.js'; -import type { ConnectionInitializationMessage } from 'lib/types/tunnelbroker/session-types.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'; @@ -45,13 +48,50 @@ 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 = () => { - new TunnelbrokerSocket(tbConnectionInfo.url, initMessage, async () => { - await sleep(clientTunnelbrokerSocketReconnectDelay); - createNewTunnelbrokerSocket(); - }); + new TunnelbrokerSocket( + url, + initMessage, + async () => { + await sleep(clientTunnelbrokerSocketReconnectDelay); + createNewTunnelbrokerSocket(); + }, + encryptionKey, + ); }; createNewTunnelbrokerSocket(); } -export { createAndMaintainTunnelbrokerWebsocket }; +export { + createAndMaintainTunnelbrokerWebsocket, + createAndMaintainAnonymousTunnelbrokerWebsocket, +}; diff --git a/lib/utils/conversion-utils.js b/lib/utils/conversion-utils.js --- a/lib/utils/conversion-utils.js +++ b/lib/utils/conversion-utils.js @@ -153,9 +153,25 @@ return input; } +// NOTE: This function should not be called from native. On native, we should +// use `convertObjToBytes` in native/backup/conversion-utils.js instead. +function convertObjToBytes(obj: T): Uint8Array { + const objStr = JSON.stringify(obj); + return new TextEncoder().encode(objStr ?? ''); +} + +// NOTE: This function should not be called from native. On native, we should +// use `convertBytesToObj` in native/backup/conversion-utils.js instead. +function convertBytesToObj(bytes: Uint8Array): T { + const str = new TextDecoder().decode(bytes.buffer); + return JSON.parse(str); +} + export { convertClientIDsToServerIDs, convertServerIDsToClientIDs, extractUserIDsFromPayload, convertObject, + convertObjToBytes, + convertBytesToObj, }; diff --git a/lib/utils/conversion-utils.test.js b/lib/utils/conversion-utils.test.js --- a/lib/utils/conversion-utils.test.js +++ b/lib/utils/conversion-utils.test.js @@ -7,6 +7,8 @@ extractUserIDsFromPayload, convertServerIDsToClientIDs, convertClientIDsToServerIDs, + convertBytesToObj, + convertObjToBytes, } from './conversion-utils.js'; import { tShape, tID, idSchemaRegex } from './validation-utils.js'; import { fetchMessageInfosResponseValidator } from '../types/validators/message-validators.js'; @@ -119,3 +121,14 @@ ).toEqual(['0', '1', '100', '200']); }); }); + +describe('convertObjToBytes and convertBytesToObj', () => { + it('should convert object to byte array and back', () => { + const obj = { hello: 'world', foo: 'bar', a: 2, b: false }; + + const bytes = convertObjToBytes(obj); + const restored = convertBytesToObj(bytes); + + expect(restored).toStrictEqual(obj); + }); +}); diff --git a/web/account/qr-code-login.react.js b/web/account/qr-code-login.react.js --- a/web/account/qr-code-login.react.js +++ b/web/account/qr-code-login.react.js @@ -19,6 +19,10 @@ qrCodeAuthMessagePayloadValidator, type QRCodeAuthMessagePayload, } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; +import { + convertBytesToObj, + convertObjToBytes, +} from 'lib/utils/conversion-utils.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; @@ -29,10 +33,6 @@ base64DecodeBuffer, base64EncodeBuffer, } from '../utils/base64-utils.js'; -import { - convertBytesToObj, - convertObjToBytes, -} from '../utils/conversion-utils.js'; async function composeTunnelbrokerMessage( encryptionKey: string, diff --git a/web/utils/conversion-utils.js b/web/utils/conversion-utils.js deleted file mode 100644 --- a/web/utils/conversion-utils.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow - -function convertObjToBytes(obj: T): Uint8Array { - const objStr = JSON.stringify(obj); - return new TextEncoder().encode(objStr ?? ''); -} - -function convertBytesToObj(bytes: Uint8Array): T { - const str = new TextDecoder().decode(bytes.buffer); - return JSON.parse(str); -} - -export { convertObjToBytes, convertBytesToObj }; diff --git a/web/utils/conversion-utils.test.js b/web/utils/conversion-utils.test.js deleted file mode 100644 --- a/web/utils/conversion-utils.test.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow - -import { convertBytesToObj, convertObjToBytes } from './conversion-utils.js'; - -describe('convertObjToBytes and convertBytesToObj', () => { - it('should convert object to byte array and back', () => { - const obj = { hello: 'world', foo: 'bar', a: 2, b: false }; - - const bytes = convertObjToBytes(obj); - const restored = convertBytesToObj(bytes); - - expect(restored).toStrictEqual(obj); - }); -});