diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js index 20c5d00c5..b6f453267 100644 --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -1,327 +1,332 @@ // @flow import olm from '@commapp/olm'; import cluster from 'cluster'; import compression from 'compression'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import crypto from 'crypto'; import express from 'express'; import type { $Request, $Response } 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 { identityDeviceTypes } from 'lib/types/identity-service-types.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { ignorePromiseRejections } from 'lib/utils/promises.js'; import { migrate } from './database/migrations.js'; import { jsonEndpoints } from './endpoints.js'; import { logEndpointMetrics } from './middleware/endpoint-profiling.js'; import { emailSubscriptionResponder } from './responders/comm-landing-responders.js'; import { jsonHandler, downloadHandler, 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 { createAuthoritativeKeyserverConfigFiles } from './user/create-configs.js'; import { fetchIdentityInfo } from './user/identity.js'; import { verifyUserLoggedIn } from './user/login.js'; import { initENSCache } from './utils/ens-cache.js'; import { initFCCache } from './utils/fc-cache.js'; import { getContentSigningKey } from './utils/olm-utils.js'; import { isPrimaryNode, isSecondaryNode, } from './utils/primary-secondary-utils.js'; import { getRunServerConfig } from './utils/server-utils.js'; import { prefetchAllURLFacts, getKeyserverURLFacts, getLandingURLFacts, getWebAppURLFacts, getWebAppCorsConfig, } from './utils/urls.js'; const shouldDisplayQRCodeInTerminal = false; void (async () => { const [webAppCorsConfig] = await Promise.all([ getWebAppCorsConfig(), olm.init(), prefetchAllURLFacts(), initENSCache(), initFCCache(), ]); const keyserverURLFacts = getKeyserverURLFacts(); const keyserverBaseRoutePath = keyserverURLFacts?.baseRoutePath; const landingBaseRoutePath = getLandingURLFacts()?.baseRoutePath; const webAppURLFacts = getWebAppURLFacts(); const webAppBaseRoutePath = webAppURLFacts?.baseRoutePath; const compiledFolderOptions = process.env.NODE_ENV === 'development' ? undefined : { maxAge: '1y', immutable: true }; let keyserverCorsOptions = null; if (webAppCorsConfig) { keyserverCorsOptions = { origin: webAppCorsConfig.domain, methods: ['GET', 'POST'], }; } const isCPUProfilingEnabled = process.env.KEYSERVER_CPU_PROFILING_ENABLED; const areEndpointMetricsEnabled = process.env.KEYSERVER_ENDPOINT_METRICS_ENABLED; if (cluster.isMaster) { if (isPrimaryNode) { 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); } } if (shouldDisplayQRCodeInTerminal && isPrimaryNode) { try { const aes256Key = crypto.randomBytes(32).toString('hex'); const ed25519Key = await getContentSigningKey(); const [identityInfo] = await Promise.all([ fetchIdentityInfo(), createAndMaintainTunnelbrokerWebsocket(aes256Key), ]); if (!identityInfo) { console.log( '\nOpen the Comm app on your phone and scan the QR code below, or copy and paste this URL:\n', ); - const url = qrCodeLinkURL(aes256Key, ed25519Key); + const url = qrCodeLinkURL( + aes256Key, + ed25519Key, + identityDeviceTypes.KEYSERVER, + ); console.log(url, '\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'); qrcode.toString(url, (error, encodedURL) => console.log(encodedURL)); } } catch (e) { console.log('Error generating QR code', e); } } else { // Allow login to be optional until staging environment is available try { await (async () => { // Should not be run by Landing or WebApp nodes if (!isPrimaryNode && !isSecondaryNode) { return; } // 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(); if (!isPrimaryNode) { return; } // 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(null)); if (process.env.NODE_ENV !== 'development') { return; } await createAuthoritativeKeyserverConfigFiles(identityInfo.userId); })(); } catch (e) { console.warn( 'Failed identity login. Login optional until staging environment is available', ); } } if (!isCPUProfilingEnabled) { const cpuCount = os.cpus().length; for (let i = 0; i < cpuCount; i++) { cluster.fork(); } cluster.on('exit', () => cluster.fork()); } } if (!cluster.isMaster || isCPUProfilingEnabled) { const server = express(); server.use(compression()); expressWs(server); server.use(express.json({ limit: '250mb' })); server.use(cookieParser()); server.get('/health', (req: $Request, res: $Response) => { res.send('OK'); }); // Note - the order of router declarations matters. On prod we have // keyserverBaseRoutePath configured to '/', which means it's a catch-all. // If we call server.use on keyserverRouter first, it will catch all // requests and prevent webAppRouter and landingRouter from working // correctly. So we make sure that keyserverRouter goes last const runServerConfig = await getRunServerConfig(); if (landingBaseRoutePath && runServerConfig.runLanding) { const landingRouter = express.Router<$Request, $Response>(); 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 (webAppBaseRoutePath && runServerConfig.runWebApp) { const webAppRouter = express.Router<$Request, $Response>(); webAppRouter.use('/images', express.static('images')); webAppRouter.use('/fonts', express.static('fonts')); webAppRouter.use('/misc', express.static('misc')); webAppRouter.use( '/.well-known', express.static( '.well-known', // Necessary for apple-app-site-association file { setHeaders: res => res.setHeader('Content-Type', 'application/json'), }, ), ); webAppRouter.use( '/compiled', express.static('app_compiled', compiledFolderOptions), ); webAppRouter.use('/', express.static('icons')); webAppRouter.get('/invite/:secret', inviteResponder); webAppRouter.get('/worker/:worker', webWorkerResponder); if (keyserverURLFacts) { webAppRouter.get( '/upload/:uploadID/:secret', (req: $Request, res: $Response) => { const { uploadID, secret } = req.params; const url = `${keyserverURLFacts.baseDomain}${keyserverURLFacts.basePath}upload/${uploadID}/${secret}`; res.redirect(url); }, ); } webAppRouter.get('*', htmlHandler(websiteResponder)); server.use(webAppBaseRoutePath, webAppRouter); } if (keyserverBaseRoutePath && runServerConfig.runKeyserver) { const keyserverRouter = express.Router<$Request, $Response>(); if (areEndpointMetricsEnabled) { keyserverRouter.use(logEndpointMetrics); } if (keyserverCorsOptions) { keyserverRouter.use(cors(keyserverCorsOptions)); } for (const endpoint in jsonEndpoints) { // $FlowFixMe Flow thinks endpoint is string const responder = jsonEndpoints[endpoint]; const expectCookieInvalidation = endpoint === 'log_out'; keyserverRouter.post( `/${endpoint}`, jsonHandler(responder, expectCookieInvalidation), ); } keyserverRouter.get( '/download_error_report/:reportID', downloadHandler(errorReportDownloadResponder), ); keyserverRouter.get( '/upload/:uploadID/:secret', downloadHandler(uploadDownloadResponder), ); // $FlowFixMe express-ws has side effects that can't be typed keyserverRouter.ws('/ws', onConnection); keyserverRouter.post( '/upload_multimedia', multerProcessor, uploadHandler(multimediaUploadResponder), ); server.use(keyserverBaseRoutePath, keyserverRouter); } if (isDev && webAppURLFacts) { const oldPath = '/comm/'; server.all(`${oldPath}*`, (req: $Request, res: $Response) => { const endpoint = req.url.slice(oldPath.length); const newURL = `${webAppURLFacts.baseDomain}${webAppURLFacts.basePath}${endpoint}`; res.redirect(newURL); }); } 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 044c19809..0c5f68655 100644 --- a/lib/facts/links.js +++ b/lib/facts/links.js @@ -1,68 +1,98 @@ // @flow +import { + type IdentityDeviceType, + assertIdentityDeviceType, +} from '../types/identity-service-types.js'; import { isDev } from '../utils/dev-utils.js'; /* Invite Links */ function inviteLinkURL(secret: string): string { if (isDev) { return `http://localhost:3000/invite/${secret}`; } return `https://comm.app/invite/${secret}`; } /* QR Code */ -function qrCodeLinkURL(aes256Param: string, ed25519Param: string): string { +function qrCodeLinkURL( + aes256Param: string, + ed25519Param: string, + deviceType: IdentityDeviceType, +): string { const keys = { aes256: aes256Param, ed25519: ed25519Param, }; const keysString = encodeURIComponent(JSON.stringify(keys)); - return `comm://qr-code/${keysString}`; + return `comm://qr-code/${deviceType}/${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 }, + +data: { +keys: string, +deviceType?: IdentityDeviceType }, }; export type ParsedDeepLinkData = ParsedInviteLinkData | ParsedQRCodeData | null; function parseDataFromDeepLink(url: string): ParsedDeepLinkData { const inviteLinkSecretRegex = /invite\/(\S+)$/; - const qrCodeKeysRegex = /qr-code\/(\S+)$/; + const qrCodeKeysRegex = /qr-code\/(?:([0-9]+)\/)?(\S+)$/; const inviteLinkSecretMatch = inviteLinkSecretRegex.exec(url); if (inviteLinkSecretMatch) { return { type: 'invite-link', data: { secret: inviteLinkSecretMatch[1] }, }; } const qrCodeKeysMatch = qrCodeKeysRegex.exec(url); - if (qrCodeKeysMatch) { + if (!qrCodeKeysMatch) { + return null; + } + + const [, deviceTypeStr, keys] = qrCodeKeysMatch; + if (!deviceTypeStr) { return { type: 'qr-code', - data: { keys: qrCodeKeysMatch[1] }, + data: { keys }, }; } - return null; + const parsedDeviceTypeNumber = parseInt(deviceTypeStr); + try { + const deviceType = assertIdentityDeviceType(parsedDeviceTypeNumber); + return { + type: 'qr-code', + data: { + keys, + deviceType: deviceType, + }, + }; + } catch (e) { + console.warn('QR code contains invalid device type'); + + return { + type: 'qr-code', + data: { keys }, + }; + } } export { inviteLinkURL, qrCodeLinkURL, parseInstallReferrerFromInviteLinkURL, parseDataFromDeepLink, }; diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index d666ee4e2..c3dcf7c85 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,403 +1,425 @@ // @flow +import invariant from 'invariant'; import t, { type TInterface, type TList, type TDict, type TEnums } from 'tcomb'; import { identityKeysBlobValidator, type IdentityKeysBlob, signedPrekeysValidator, type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; import { type Platform } from './device-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, } from './request-types.js'; import { currentUserInfoValidator, type CurrentUserInfo, } from './user-types.js'; import { values } from '../utils/objects.js'; import { tUserID, tShape } from '../utils/validation-utils.js'; export type UserAuthMetadata = { +userID: string, +accessToken: string, }; // This type should not be altered without also updating OutboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +oneTimeContentPrekey: ?string, +oneTimeNotifPrekey: ?string, }; // This type should not be altered without also updating InboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type DeviceOlmOutboundKeys = { +identityKeysBlob: IdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, +payloadSignature: string, }; export const deviceOlmOutboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, payloadSignature: t.String, }); export type UserDevicesOlmOutboundKeys = { +deviceID: string, +keys: ?DeviceOlmOutboundKeys, }; export type DeviceOlmInboundKeys = { +identityKeysBlob: IdentityKeysBlob, +signedPrekeys: SignedPrekeys, +payloadSignature: string, }; export const deviceOlmInboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, signedPrekeys: signedPrekeysValidator, payloadSignature: t.String, }); export type UserDevicesOlmInboundKeys = { +keys: { +[deviceID: string]: ?DeviceOlmInboundKeys, }, +username?: ?string, +walletAddress?: ?string, }; // This type should not be altered without also updating FarcasterUser in // keyserver/addons/rust-node-addon/src/identity_client/get_farcaster_users.rs export type FarcasterUser = { +userID: string, +username: string, +farcasterID: string, }; export const farcasterUserValidator: TInterface = tShape({ userID: tUserID, username: t.String, farcasterID: t.String, }); export const farcasterUsersValidator: TList> = t.list( farcasterUserValidator, ); export const userDeviceOlmInboundKeysValidator: TInterface = tShape({ keys: t.dict(t.String, t.maybe(deviceOlmInboundKeysValidator)), username: t.maybe(t.String), walletAddress: t.maybe(t.String), }); export interface IdentityServiceClient { // Only a primary device can initiate account deletion, and web cannot be a // primary device +deleteWalletUser?: () => Promise; // Only a primary device can initiate account deletion, and web cannot be a // primary device +deletePasswordUser?: (password: string) => Promise; +logOut: () => Promise; // This log out type is specific to primary device, and web cannot be a // primary device +logOutPrimaryDevice?: () => Promise; +logOutSecondaryDevice: () => Promise; +getKeyserverKeys: string => Promise; // Users cannot register from web +registerPasswordUser?: ( username: string, password: string, fid: ?string, ) => Promise; // Users cannot register from web +registerReservedPasswordUser?: ( username: string, password: string, keyserverMessage: string, keyserverSignature: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; // Users cannot register from web +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => Promise; +logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; // on native, publishing prekeys to Identity is called directly from C++, // there is no need to expose it to JS +publishWebPrekeys?: (prekeys: SignedPrekeys) => Promise; +getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray>; +getDeviceListsForUsers: ( userIDs: $ReadOnlyArray, ) => Promise; // updating device list is possible only on Native // web cannot be a primary device, so there's no need to expose it to JS +updateDeviceList?: (newDeviceList: SignedDeviceList) => Promise; +syncPlatformDetails: () => Promise; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, signedNonce: SignedNonce, ) => Promise; +getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>; +linkFarcasterAccount: (farcasterID: string) => Promise; +unlinkFarcasterAccount: () => Promise; +findUserIdentities: ( userIDs: $ReadOnlyArray, ) => Promise; +versionSupported: () => Promise; +changePassword: (oldPassword: string, newPassword: string) => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, +preRequestUserState?: ?CurrentUserInfo, }; export const identityAuthResultValidator: TInterface = tShape({ userID: tUserID, accessToken: t.String, username: t.String, preRequestUserState: t.maybe(currentUserInfoValidator), }); export type IdentityNewDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +contentOneTimeKeys: $ReadOnlyArray, +notifOneTimeKeys: $ReadOnlyArray, }; export type IdentityExistingDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; // Device list types export type RawDeviceList = { +devices: $ReadOnlyArray, +timestamp: number, }; export const rawDeviceListValidator: TInterface = tShape({ devices: t.list(t.String), timestamp: t.Number, }); export type UsersRawDeviceLists = { +[userID: string]: RawDeviceList, }; // User Identity types export type EthereumIdentity = { walletAddress: string, siweMessage: string, siweSignature: string, }; export type Identity = { +username: string, +ethIdentity: ?EthereumIdentity, +farcasterID: ?string, }; export type Identities = { +[userID: string]: Identity, }; export const ethereumIdentityValidator: TInterface = tShape({ walletAddress: t.String, siweMessage: t.String, siweSignature: t.String, }); export const identityValidator: TInterface = tShape({ username: t.String, ethIdentity: t.maybe(ethereumIdentityValidator), farcasterID: t.maybe(t.String), }); export const identitiesValidator: TDict = t.dict( t.String, identityValidator, ); export type ReservedUserIdentifiers = { +[userID: string]: string, }; export const reservedIdentifiersValidator: TDict = t.dict(t.String, t.String); export type UserIdentitiesResponse = { +identities: Identities, +reservedUserIdentifiers: ReservedUserIdentifiers, }; export const userIdentitiesResponseValidator: TInterface = tShape({ identities: identitiesValidator, reservedUserIdentifiers: reservedIdentifiersValidator, }); export type SignedDeviceList = { // JSON-stringified RawDeviceList +rawDeviceList: string, // Current primary device signature. Absent for Identity Service generated // device lists. +curPrimarySignature?: string, // Previous primary device signature. Present only if primary device // has changed since last update. +lastPrimarySignature?: string, }; export const signedDeviceListValidator: TInterface = tShape({ rawDeviceList: t.String, curPrimarySignature: t.maybe(t.String), lastPrimarySignature: t.maybe(t.String), }); export const signedDeviceListHistoryValidator: TList> = t.list(signedDeviceListValidator); export type UsersSignedDeviceLists = { +[userID: string]: SignedDeviceList, }; export const usersSignedDeviceListsValidator: TDict = t.dict(t.String, signedDeviceListValidator); export type SignedNonce = { +nonce: string, +nonceSignature: string, }; export const ONE_TIME_KEYS_NUMBER = 10; export const identityDeviceTypes = Object.freeze({ KEYSERVER: 0, WEB: 1, IOS: 2, ANDROID: 3, WINDOWS: 4, MAC_OS: 5, }); export type IdentityDeviceType = $Values; +function isIdentityDeviceType(deviceType: number): boolean %checks { + return ( + deviceType === 0 || + deviceType === 1 || + deviceType === 2 || + deviceType === 3 || + deviceType === 4 || + deviceType === 5 + ); +} + +export function assertIdentityDeviceType( + deviceType: number, +): IdentityDeviceType { + invariant( + isIdentityDeviceType(deviceType), + 'number is not IdentityDeviceType enum', + ); + return deviceType; +} + export const identityDeviceTypeToPlatform: { +[identityDeviceType: IdentityDeviceType]: Platform, } = Object.freeze({ [identityDeviceTypes.WEB]: 'web', [identityDeviceTypes.ANDROID]: 'android', [identityDeviceTypes.IOS]: 'ios', [identityDeviceTypes.WINDOWS]: 'windows', [identityDeviceTypes.MAC_OS]: 'macos', }); export const platformToIdentityDeviceType: { +[platform: Platform]: IdentityDeviceType, } = Object.freeze({ web: identityDeviceTypes.WEB, android: identityDeviceTypes.ANDROID, ios: identityDeviceTypes.IOS, windows: identityDeviceTypes.WINDOWS, macos: identityDeviceTypes.MAC_OS, }); export const identityDeviceTypeValidator: TEnums = t.enums.of( values(identityDeviceTypes), ); export type IdentityPlatformDetails = { +deviceType: IdentityDeviceType, +codeVersion: number, +stateVersion?: number, +majorDesktopVersion?: number, }; export const identityPlatformDetailsValidator: TInterface = tShape({ deviceType: identityDeviceTypeValidator, codeVersion: t.Number, stateVersion: t.maybe(t.Number), majorDesktopVersion: t.maybe(t.Number), }); export type UserDevicesPlatformDetails = { +[deviceID: string]: IdentityPlatformDetails, }; export const userDevicesPlatformDetailsValidator: TDict = t.dict(t.String, identityPlatformDetailsValidator); export type UsersDevicesPlatformDetails = { +[userID: string]: UserDevicesPlatformDetails, }; export const usersDevicesPlatformDetailsValidator: TDict = t.dict(t.String, userDevicesPlatformDetailsValidator); export type PeersDeviceLists = { +usersSignedDeviceLists: UsersSignedDeviceLists, +usersDevicesPlatformDetails: UsersDevicesPlatformDetails, }; export const peersDeviceListsValidator: TInterface = tShape({ usersSignedDeviceLists: usersSignedDeviceListsValidator, usersDevicesPlatformDetails: usersDevicesPlatformDetailsValidator, }); diff --git a/native/qr-code/qr-code-screen.react.js b/native/qr-code/qr-code-screen.react.js index 5cdc4aa89..badd061b8 100644 --- a/native/qr-code/qr-code-screen.react.js +++ b/native/qr-code/qr-code-screen.react.js @@ -1,100 +1,108 @@ // @flow import * as React from 'react'; import { View, Text } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; import { useQRAuthContext } from 'lib/components/qr-auth-provider.react.js'; import { qrCodeLinkURL } from 'lib/facts/links.js'; +import { platformToIdentityDeviceType } from 'lib/types/identity-service-types.js'; +import { getConfig } from 'lib/utils/config.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'; type QRCodeScreenProps = { +navigation: QRCodeSignInNavigationProp<'QRCodeScreen'>, +route: NavigationRoute<'QRCodeScreen'>, }; +const { platform } = getConfig().platformDetails; + // eslint-disable-next-line no-unused-vars function QRCodeScreen(props: QRCodeScreenProps): React.Node { const { qrData, generateQRCode } = useQRAuthContext(); React.useEffect(() => { void generateQRCode(); }, [generateQRCode]); - const qrCodeURL = React.useMemo( - () => (qrData ? qrCodeLinkURL(qrData.aesKey, qrData.deviceID) : undefined), - [qrData], - ); + const qrCodeURL = React.useMemo(() => { + if (!qrData) { + return undefined; + } + + const deviceType = platformToIdentityDeviceType[platform]; + return qrCodeLinkURL(qrData.aesKey, qrData.deviceID, deviceType); + }, [qrData]); 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 8fb41ccae..524752878 100644 --- a/web/account/qr-code-login.react.js +++ b/web/account/qr-code-login.react.js @@ -1,46 +1,55 @@ // @flow import { QRCodeSVG } from 'qrcode.react'; import * as React from 'react'; import { useQRAuthContext } from 'lib/components/qr-auth-provider.react.js'; import { qrCodeLinkURL } from 'lib/facts/links.js'; +import { platformToIdentityDeviceType } from 'lib/types/identity-service-types.js'; +import { getConfig } from 'lib/utils/config.js'; import css from './qr-code-login.css'; +const { platform } = getConfig().platformDetails; + function QRCodeLogin(): React.Node { const { qrData, generateQRCode } = useQRAuthContext(); React.useEffect(() => { void generateQRCode(); }, [generateQRCode]); - const qrCodeURL = React.useMemo( - () => (qrData ? qrCodeLinkURL(qrData.aesKey, qrData.deviceID) : undefined), - [qrData], - ); + const qrCodeURL = React.useMemo(() => { + if (!qrData) { + return undefined; + } + + const identityDeviceType = platformToIdentityDeviceType[platform]; + + return qrCodeLinkURL(qrData.aesKey, qrData.deviceID, identityDeviceType); + }, [qrData]); 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;