diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js index 9d1f9b2eb..194e0dff0 100644 --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -1,220 +1,220 @@ // @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 { 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); + const aes256Key = crypto.randomBytes(32).toString('hex'); const ed25519Key = 'ed25519Key'; 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/lib/facts/links.js b/lib/facts/links.js index 8437ee383..551306eb7 100644 --- a/lib/facts/links.js +++ b/lib/facts/links.js @@ -1,63 +1,63 @@ // @flow /* Invite Links */ function inviteLinkURL(secret: string): string { return `https://comm.app/invite/${secret}`; } /* QR Code */ -function qrCodeLinkURL(aes256Param: Uint8Array, ed25519Param: string): string { +function qrCodeLinkURL(aes256Param: string, ed25519Param: string): string { const keys = { aes256: aes256Param, ed25519: ed25519Param, }; const keysString = encodeURIComponent(JSON.stringify(keys)); return `comm://qr-code/${keysString}`; } /* Deep Link Utils */ function parseInstallReferrerFromInviteLinkURL(referrer: string): ?string { const referrerRegex = /utm_source=(invite\/(\S+))$/; const match = referrerRegex.exec(referrer); return match?.[1]; } type ParsedInviteLinkData = { +type: 'invite-link', +data: { +secret: string }, }; type ParsedQRCodeData = { +type: 'qr-code', +data: { +keys: string }, }; export type ParsedDeepLinkData = ParsedInviteLinkData | ParsedQRCodeData | null; function parseDataFromDeepLink(url: string): ParsedDeepLinkData { const inviteLinkSecretRegex = /invite\/(\S+)$/; const qrCodeKeysRegex = /qr-code\/(\S+)$/; const inviteLinkSecretMatch = inviteLinkSecretRegex.exec(url); if (inviteLinkSecretMatch) { return { type: 'invite-link', data: { secret: inviteLinkSecretMatch[1] }, }; } const qrCodeKeysMatch = qrCodeKeysRegex.exec(url); if (qrCodeKeysMatch) { return { type: 'qr-code', data: { keys: qrCodeKeysMatch[1] }, }; } return null; } export { inviteLinkURL, qrCodeLinkURL, parseInstallReferrerFromInviteLinkURL, parseDataFromDeepLink, }; diff --git a/native/qr-code/qr-code-screen.react.js b/native/qr-code/qr-code-screen.react.js index 879d76851..be198da30 100644 --- a/native/qr-code/qr-code-screen.react.js +++ b/native/qr-code/qr-code-screen.react.js @@ -1,108 +1,111 @@ // @flow import * as React from 'react'; import { View, Text } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; import { qrCodeLinkURL } from 'lib/facts/links.js'; +import { uintArrayToHexString } from 'lib/media/data-utils.js'; import type { QRCodeSignInNavigationProp } from './qr-code-sign-in-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import * as AES from '../utils/aes-crypto-module.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; type QRCodeScreenProps = { +navigation: QRCodeSignInNavigationProp<'QRCodeScreen'>, +route: NavigationRoute<'QRCodeScreen'>, }; // eslint-disable-next-line no-unused-vars function QRCodeScreen(props: QRCodeScreenProps): React.Node { const [qrCodeValue, setQrCodeValue] = React.useState(); const generateQRCode = React.useCallback(async () => { try { - const aes256Key: Uint8Array = await AES.generateKey(); + const rawAESKey: Uint8Array = await AES.generateKey(); + const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); + const ed25519Key: string = await getContentSigningKey(); - const url = qrCodeLinkURL(aes256Key, ed25519Key); + const url = qrCodeLinkURL(aesKeyAsHexString, ed25519Key); setQrCodeValue(url); } catch (err) { console.error('Failed to generate QR Code:', err); } }, []); React.useEffect(() => { generateQRCode(); }, [generateQRCode]); const styles = useStyles(unboundStyles); return ( Log in to Comm Open the Comm app on your phone and scan the QR code below How to find the scanner: Go to Profile Select Linked devices Click Add on the top right ); } const unboundStyles = { container: { flex: 1, alignItems: 'center', marginTop: 125, }, heading: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 12, }, headingSubtext: { fontSize: 12, color: 'panelForegroundLabel', paddingBottom: 30, }, instructionsBox: { alignItems: 'center', width: 300, marginTop: 40, padding: 15, borderColor: 'panelForegroundLabel', borderWidth: 2, borderRadius: 8, }, instructionsTitle: { fontSize: 12, color: 'panelForegroundLabel', paddingBottom: 15, }, instructionsStep: { fontSize: 12, padding: 1, color: 'panelForegroundLabel', }, instructionsBold: { fontWeight: 'bold', }, }; export default QRCodeScreen; diff --git a/web/account/qr-code-login.react.js b/web/account/qr-code-login.react.js index 4b50214dc..3ef86788c 100644 --- a/web/account/qr-code-login.react.js +++ b/web/account/qr-code-login.react.js @@ -1,59 +1,62 @@ // @flow import { QRCodeSVG } from 'qrcode.react'; import * as React from 'react'; import { qrCodeLinkURL } from 'lib/facts/links.js'; +import { uintArrayToHexString } from 'lib/media/data-utils.js'; import css from './qr-code-login.css'; import { generateKey } from '../media/aes-crypto-utils.js'; import { useSelector } from '../redux/redux-utils.js'; function QrCodeLogin(): React.Node { const [qrCodeValue, setQrCodeValue] = React.useState(); const ed25519Key = useSelector( state => state.cryptoStore.primaryIdentityKeys?.ed25519, ); const generateQRCode = React.useCallback(async () => { try { if (!ed25519Key) { return; } - const aes256Key: Uint8Array = await generateKey(); - const url = qrCodeLinkURL(aes256Key, ed25519Key); + const rawAESKey: Uint8Array = await generateKey(); + const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); + + const url = qrCodeLinkURL(aesKeyAsHexString, ed25519Key); setQrCodeValue(url); } catch (err) { console.error('Failed to generate QR Code:', err); } }, [ed25519Key]); React.useEffect(() => { generateQRCode(); }, [generateQRCode]); return (
Log in to Comm
Open the Comm app on your phone and scan the QR code below
How to find the scanner:
Go to Profile
Select Linked devices
Click Add on the top right
); } export default QrCodeLogin;