diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js index 194e0dff0..3e8225115 100644 --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -1,220 +1,229 @@ // @flow import olm from '@commapp/olm'; import cluster from 'cluster'; import cookieParser from 'cookie-parser'; import crypto from 'crypto'; import express from 'express'; import expressWs from 'express-ws'; import os from 'os'; import qrcode from 'qrcode'; import './cron/cron.js'; import { qrCodeLinkURL } from 'lib/facts/links.js'; import { migrate } from './database/migrations.js'; import { jsonEndpoints } from './endpoints.js'; import { emailSubscriptionResponder } from './responders/comm-landing-responders.js'; import { jsonHandler, downloadHandler, handleAsyncPromise, htmlHandler, uploadHandler, } from './responders/handlers.js'; import landingHandler from './responders/landing-handler.js'; import { errorReportDownloadResponder } from './responders/report-responders.js'; import { inviteResponder, websiteResponder, } 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 { multerProcessor, multimediaUploadResponder, uploadDownloadResponder, } from './uploads/uploads.js'; import { verifyUserLoggedIn } from './user/login.js'; import { initENSCache } from './utils/ens-cache.js'; +import { getContentSigningKey } from './utils/olm-utils.js'; import { prefetchAllURLFacts, getSquadCalURLFacts, getLandingURLFacts, getCommAppURLFacts, } from './utils/urls.js'; const shouldDisplayQRCodeInTerminal = false; (async () => { await Promise.all([olm.init(), prefetchAllURLFacts(), initENSCache()]); const squadCalBaseRoutePath = getSquadCalURLFacts()?.baseRoutePath; const landingBaseRoutePath = getLandingURLFacts()?.baseRoutePath; const commAppBaseRoutePath = getCommAppURLFacts()?.baseRoutePath; const compiledFolderOptions = process.env.NODE_ENV === 'development' ? undefined : { maxAge: '1y', immutable: true }; if (cluster.isMaster) { const didMigrationsSucceed: boolean = await migrate(); if (!didMigrationsSucceed) { // The following line uses exit code 2 to ensure nodemon exits // in a dev environment, instead of restarting. Context provided // in https://github.com/remy/nodemon/issues/751 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. handleAsyncPromise(createAndMaintainTunnelbrokerWebsocket(identityInfo)); } catch (e) { console.warn('failed_identity_login'); } if (shouldDisplayQRCodeInTerminal) { try { const aes256Key = crypto.randomBytes(32).toString('hex'); - const ed25519Key = 'ed25519Key'; + const ed25519Key = await getContentSigningKey(); + + console.log( + '\nOpen the Comm app on your phone and scan the QR code below\n', + ); + console.log('How to find the scanner:\n'); + console.log('Go to \x1b[1mProfile\x1b[0m'); + console.log('Select \x1b[1mLinked devices\x1b[0m'); + console.log('Click \x1b[1mAdd\x1b[0m on the top right'); const url = qrCodeLinkURL(aes256Key, ed25519Key); qrcode.toString(url, (error, encodedURL) => console.log(encodedURL)); } catch (e) { console.log('Error generating QR code', e); } } const cpuCount = os.cpus().length; for (let i = 0; i < cpuCount; i++) { cluster.fork(); } cluster.on('exit', () => cluster.fork()); } else { const server = express(); expressWs(server); server.use(express.json({ limit: '250mb' })); server.use(cookieParser()); const setupAppRouter = router => { router.use('/images', express.static('images')); router.use('/fonts', express.static('fonts')); router.use('/misc', express.static('misc')); router.use( '/.well-known', express.static( '.well-known', // Necessary for apple-app-site-association file { setHeaders: res => res.setHeader('Content-Type', 'application/json'), }, ), ); router.use( '/compiled', express.static('app_compiled', compiledFolderOptions), ); router.use('/', express.static('icons')); for (const endpoint in jsonEndpoints) { // $FlowFixMe Flow thinks endpoint is string const responder = jsonEndpoints[endpoint]; const expectCookieInvalidation = endpoint === 'log_out'; router.post( `/${endpoint}`, jsonHandler(responder, expectCookieInvalidation), ); } router.get( '/download_error_report/:reportID', downloadHandler(errorReportDownloadResponder), ); router.get( '/upload/:uploadID/:secret', downloadHandler(uploadDownloadResponder), ); router.get('/invite/:secret', inviteResponder); // $FlowFixMe express-ws has side effects that can't be typed router.ws('/ws', onConnection); router.get('/worker/:worker', webWorkerResponder); router.get('*', htmlHandler(websiteResponder)); router.post( '/upload_multimedia', multerProcessor, uploadHandler(multimediaUploadResponder), ); }; // Note - the order of router declarations matters. On prod we have // squadCalBaseRoutePath configured to '/', which means it's a catch-all. If // we call server.use on squadCalRouter first, it will catch all requests // and prevent commAppRouter and landingRouter from working correctly. So we // make sure that squadCalRouter goes last server.get('/invite/:secret', inviteResponder); if (landingBaseRoutePath) { const landingRouter = express.Router(); landingRouter.get('/invite/:secret', inviteResponder); landingRouter.use( '/.well-known', express.static( '.well-known', // Necessary for apple-app-site-association file { setHeaders: res => res.setHeader('Content-Type', 'application/json'), }, ), ); landingRouter.use('/images', express.static('images')); landingRouter.use('/fonts', express.static('fonts')); landingRouter.use( '/compiled', express.static('landing_compiled', compiledFolderOptions), ); landingRouter.use('/', express.static('landing_icons')); landingRouter.post('/subscribe_email', emailSubscriptionResponder); landingRouter.get('*', landingHandler); server.use(landingBaseRoutePath, landingRouter); } if (commAppBaseRoutePath) { const commAppRouter = express.Router(); setupAppRouter(commAppRouter); server.use(commAppBaseRoutePath, commAppRouter); } if (squadCalBaseRoutePath) { const squadCalRouter = express.Router(); setupAppRouter(squadCalRouter); server.use(squadCalBaseRoutePath, squadCalRouter); } const listenAddress = (() => { if (process.env.COMM_LISTEN_ADDR) { return process.env.COMM_LISTEN_ADDR; } else if (process.env.NODE_ENV === 'development') { return undefined; } else { return 'localhost'; } })(); server.listen(parseInt(process.env.PORT, 10) || 3000, listenAddress); } })(); diff --git a/keyserver/src/scripts/get-keyserver-public-key.js b/keyserver/src/scripts/get-keyserver-public-key.js index 9a9519f8f..dee10b68a 100644 --- a/keyserver/src/scripts/get-keyserver-public-key.js +++ b/keyserver/src/scripts/get-keyserver-public-key.js @@ -1,12 +1,12 @@ // @flow import { main } from './utils.js'; -import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; +import { getContentSigningKey } from '../utils/olm-utils.js'; // Outputs the keyserver's signing ed25519 public key async function getKeyserverPublicKey() { - const info = await fetchOlmAccount('content'); - console.log(JSON.parse(info.account.identity_keys()).ed25519); + const contentSigningKey = await getContentSigningKey(); + console.log(contentSigningKey); } main([getKeyserverPublicKey]); diff --git a/keyserver/src/socket/tunnelbroker.js b/keyserver/src/socket/tunnelbroker.js index 56e41e2c4..c01f940d0 100644 --- a/keyserver/src/socket/tunnelbroker.js +++ b/keyserver/src/socket/tunnelbroker.js @@ -1,105 +1,105 @@ // @flow import WebSocket from 'ws'; import { refreshKeysTBMessageValidator, type TBKeyserverConnectionInitializationMessage, type MessageFromTunnelbroker, tunnelbrokerMessageTypes, } from 'lib/types/tunnelbroker-messages.js'; import { getCommConfig } from 'lib/utils/comm-config.js'; import { ServerError } from 'lib/utils/errors.js'; -import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; import { type IdentityInfo } from '../user/identity.js'; -import { uploadNewOneTimeKeys } from '../utils/olm-utils.js'; +import { + uploadNewOneTimeKeys, + getContentSigningKey, +} from '../utils/olm-utils.js'; type TBConnectionInfo = { +url: string, }; async function getTBConnectionInfo(): Promise { const tbConfig = await getCommConfig({ folder: 'facts', name: 'tunnelbroker', }); if (tbConfig) { return tbConfig; } console.warn('Defaulting to local Tunnelbroker instance'); return { url: 'ws://127.0.0.1:51001', }; } async function createAndMaintainTunnelbrokerWebsocket( identityInfo: IdentityInfo, ) { - const [accountInfo, tbConnectionInfo] = await Promise.all([ - fetchOlmAccount('content'), + const [deviceID, tbConnectionInfo] = await Promise.all([ + getContentSigningKey(), getTBConnectionInfo(), ]); - const deviceID = JSON.parse(accountInfo.account.identity_keys()).ed25519; - openTunnelbrokerConnection( deviceID, identityInfo.userId, identityInfo.accessToken, tbConnectionInfo.url, ); } function handleTBMessageEvent(event: ArrayBuffer): Promise { const rawMessage = JSON.parse(event.toString()); if (!refreshKeysTBMessageValidator.is(rawMessage)) { throw new ServerError('unsupported_tunnelbroker_message'); } const message: MessageFromTunnelbroker = rawMessage; 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, tbUrl: string, ) { try { const tunnelbrokerSocket = new WebSocket(tbUrl); tunnelbrokerSocket.on('open', () => { const message: TBKeyserverConnectionInitializationMessage = { type: 'sessionRequest', accessToken, deviceID, deviceType: 'keyserver', 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 114ebaf43..739eb1820 100644 --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -1,155 +1,164 @@ // @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 { getOneTimeKeyValuesFromBlob } from 'lib/shared/crypto-utils.js'; import { olmEncryptedMessageTypes } from 'lib/types/crypto-types.js'; import { ServerError } from 'lib/utils/errors.js'; -import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; +import { + fetchCallUpdateOlmAccount, + fetchOlmAccount, +} 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(); } } 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 = getOneTimeKeyValuesFromBlob( 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 = getOneTimeKeyValuesFromBlob( notifAccount.one_time_keys(), ); await rustAPI.uploadOneTimeKeys( identityInfo.userId, deviceID, identityInfo.accessToken, contentOneTimeKeys, notifOneTimeKeys, ); notifAccount.mark_keys_as_published(); contentAccount.mark_keys_as_published(); }, ); }); } +async function getContentSigningKey(): Promise { + const accountInfo = await fetchOlmAccount('content'); + return JSON.parse(accountInfo.account.identity_keys()).ed25519; +} + export { createPickledOlmAccount, createPickledOlmSession, getOlmUtility, unpickleOlmAccount, unpickleOlmSession, validateAccountPrekey, uploadNewOneTimeKeys, + getContentSigningKey, };