diff --git a/keyserver/src/session/cookies.js b/keyserver/src/session/cookies.js --- a/keyserver/src/session/cookies.js +++ b/keyserver/src/session/cookies.js @@ -5,6 +5,10 @@ import invariant from 'invariant'; import url from 'url'; +import { + hasMinCodeVersion, + FUTURE_CODE_VERSION, +} from 'lib/shared/version-utils.js'; import type { Shape } from 'lib/types/core.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import { isWebPlatform } from 'lib/types/device-types.js'; @@ -826,9 +830,12 @@ if ( !viewer.platformDetails || (viewer.platformDetails.platform !== 'ios' && - viewer.platformDetails.platform !== 'android') || - !viewer.platformDetails.codeVersion || - viewer.platformDetails.codeVersion <= 222 + viewer.platformDetails.platform !== 'android' && + viewer.platformDetails.platform !== 'web') || + !hasMinCodeVersion(viewer.platformDetails, { + native: 222, + web: FUTURE_CODE_VERSION, + }) ) { return false; } diff --git a/lib/shared/crypto-utils.js b/lib/shared/crypto-utils.js --- a/lib/shared/crypto-utils.js +++ b/lib/shared/crypto-utils.js @@ -6,7 +6,11 @@ getOlmSessionInitializationData, getOlmSessionInitializationDataActionTypes, } from '../actions/user-actions.js'; -import type { OLMIdentityKeys, OLMOneTimeKeys } from '../types/crypto-types'; +import type { + OLMIdentityKeys, + OLMOneTimeKeys, + OLMPrekey, +} from '../types/crypto-types'; import type { OlmSessionInitializationInfo } from '../types/request-types'; import { useServerCall, @@ -15,6 +19,10 @@ import type { CallServerEndpointOptions } from '../utils/call-server-endpoint.js'; import { values } from '../utils/objects.js'; +const initialEncryptedMessageContent = { + type: 'init', +}; + function useInitialNotificationsEncryptedMessage( platformSpecificSessionCreator: ( notificationsIdentityKeys: OLMIdentityKeys, @@ -63,13 +71,26 @@ return values(oneTimeKeys.curve25519); } +function getPrekeyValue(prekey: OLMPrekey): string { + const [prekeyValue] = values(prekey.curve25519); + return prekeyValue; +} + function getOneTimeKeyValuesFromBlob(keyBlob: string): $ReadOnlyArray { const oneTimeKeys: OLMOneTimeKeys = JSON.parse(keyBlob); return getOneTimeKeyValues(oneTimeKeys); } +function getPrekeyValueFromBlob(prekeyBlob: string): string { + const prekey: OLMPrekey = JSON.parse(prekeyBlob); + return getPrekeyValue(prekey); +} + export { getOneTimeKeyValues, + getPrekeyValue, getOneTimeKeyValuesFromBlob, + getPrekeyValueFromBlob, + initialEncryptedMessageContent, useInitialNotificationsEncryptedMessage, }; diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -2,16 +2,25 @@ import olm from '@commapp/olm'; import invariant from 'invariant'; +import localforage from 'localforage'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import uuid from 'uuid'; +import { + initialEncryptedMessageContent, + getOneTimeKeyValuesFromBlob, + getPrekeyValueFromBlob, +} from 'lib/shared/crypto-utils.js'; import type { SignedIdentityKeysBlob, CryptoStore, IdentityKeysBlob, + OLMIdentityKeys, } from 'lib/types/crypto-types.js'; +import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; +import { NOTIFICATIONS_OLM_SESSION_KEY } from '../database/utils/constants.js'; import { initOlm } from '../olm/olm-utils.js'; import { setCryptoStore } from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; @@ -19,6 +28,11 @@ const CryptoStoreContext: React.Context> = React.createContext(null); +const WebNotificationsSessionCreatorContext: React.Context Promise> = React.createContext(null); + type Props = { +children: React.Node, }; @@ -123,4 +137,97 @@ }, [cryptoStorePromise]); } -export { useGetSignedIdentityKeysBlob, useCryptoStore, CryptoStoreProvider }; +function WebNotificationsSessionCreatorProvider(props: Props): React.Node { + const cryptoStorePromise = useCryptoStore(); + const sessionCreationPromiseRef = React.useRef>(null); + const sessionCreationFirstRunRef = React.useRef(false); + + React.useEffect(() => { + if (sessionCreationFirstRunRef.current) { + sessionCreationPromiseRef.current = null; + } + }, [cryptoStorePromise]); + + const value = React.useCallback( + ( + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + ) => { + if (sessionCreationPromiseRef.current) { + return sessionCreationPromiseRef.current; + } + + const newSessionCreationPromise = (async () => { + const [{ notificationAccount }] = await Promise.all([ + cryptoStorePromise, + initOlm(), + ]); + + const account = new olm.Account(); + account.unpickle( + notificationAccount.picklingKey, + notificationAccount.pickledAccount, + ); + + const notificationsPrekey = getPrekeyValueFromBlob( + notificationsInitializationInfo.prekey, + ); + const [notificationsOneTimeKey] = getOneTimeKeyValuesFromBlob( + notificationsInitializationInfo.oneTimeKey, + ); + + const session = new olm.Session(); + session.create_outbound( + account, + notificationsIdentityKeys.curve25519, + notificationsIdentityKeys.ed25519, + notificationsPrekey, + notificationsInitializationInfo.prekeySignature, + notificationsOneTimeKey, + ); + + const { body: initialNotificationsEncryptedMessage } = session.encrypt( + JSON.stringify(initialEncryptedMessageContent), + ); + + const pickledSession = session.pickle(notificationAccount.picklingKey); + await localforage.setItem( + NOTIFICATIONS_OLM_SESSION_KEY, + pickledSession, + ); + + sessionCreationFirstRunRef.current = true; + + return initialNotificationsEncryptedMessage; + })(); + + sessionCreationPromiseRef.current = newSessionCreationPromise; + return newSessionCreationPromise; + }, + [cryptoStorePromise], + ); + + return ( + + {props.children} + + ); +} + +function useWebNotificationsSessionCreator(): ( + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, +) => Promise { + const context = React.useContext(WebNotificationsSessionCreatorContext); + invariant(context, 'WebNotificationsSessionCreator not found.'); + + return context; +} + +export { + useGetSignedIdentityKeysBlob, + useCryptoStore, + WebNotificationsSessionCreatorProvider, + useWebNotificationsSessionCreator, + CryptoStoreProvider, +}; diff --git a/web/database/utils/constants.js b/web/database/utils/constants.js --- a/web/database/utils/constants.js +++ b/web/database/utils/constants.js @@ -14,6 +14,8 @@ export const COMM_SQLITE_DATABASE_PATH = 'comm.sqlite'; +export const NOTIFICATIONS_OLM_SESSION_KEY = 'notificationsOlmSessionKey'; + export const DB_SUPPORTED_OS: $ReadOnlyArray = [ 'Windows 10', 'Linux', diff --git a/web/root.js b/web/root.js --- a/web/root.js +++ b/web/root.js @@ -12,7 +12,10 @@ import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; -import { CryptoStoreProvider } from './account/account-hooks.js'; +import { + CryptoStoreProvider, + WebNotificationsSessionCreatorProvider, +} from './account/account-hooks.js'; import App from './app.react.js'; import { SQLiteDataHandler } from './database/sqlite-data-handler.js'; import { localforageConfig } from './database/utils/constants.js'; @@ -39,12 +42,14 @@ - - - - - - + + + + + + + + diff --git a/web/selectors/socket-selectors.js b/web/selectors/socket-selectors.js --- a/web/selectors/socket-selectors.js +++ b/web/selectors/socket-selectors.js @@ -47,6 +47,7 @@ type WebGetClientResponsesSelectorInputType = { +state: AppState, +getSignedIdentityKeysBlob: () => Promise, + +getInitialNotificationsEncryptedMessage: () => Promise, }; const webGetClientResponsesSelector: ( @@ -60,23 +61,26 @@ input.getSignedIdentityKeysBlob, (input: WebGetClientResponsesSelectorInputType) => input.state.navInfo.tab === 'calendar', + (input: WebGetClientResponsesSelectorInputType) => + input.getInitialNotificationsEncryptedMessage, ( getClientResponsesFunc: ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: () => Promise, - getInitialNotificationsEncryptedMessage: ?() => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, getSignedIdentityKeysBlob: () => Promise, calendarActive: boolean, + getInitialNotificationsEncryptedMessage: () => Promise, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc( calendarActive, null, getSignedIdentityKeysBlob, - null, + getInitialNotificationsEncryptedMessage, serverRequests, ), ); diff --git a/web/socket.react.js b/web/socket.react.js --- a/web/socket.react.js +++ b/web/socket.react.js @@ -12,13 +12,17 @@ connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; +import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; -import { useGetSignedIdentityKeysBlob } from './account/account-hooks.js'; +import { + useGetSignedIdentityKeysBlob, + useWebNotificationsSessionCreator, +} from './account/account-hooks.js'; import { useSelector } from './redux/redux-utils.js'; import { activeThreadSelector, @@ -51,8 +55,15 @@ const sessionIdentification = useSelector(sessionIdentificationSelector); const preRequestUserState = useSelector(preRequestUserStateSelector); const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); + const webNotificationsSessionCreator = useWebNotificationsSessionCreator(); + const getInitialNotificationsEncryptedMessage = + useInitialNotificationsEncryptedMessage(webNotificationsSessionCreator); const getClientResponses = useSelector(state => - webGetClientResponsesSelector({ state, getSignedIdentityKeysBlob }), + webGetClientResponsesSelector({ + state, + getSignedIdentityKeysBlob, + getInitialNotificationsEncryptedMessage, + }), ); const sessionStateFunc = useSelector(webSessionStateFuncSelector); const currentCalendarQuery = useSelector(webCalendarQuery);