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 @@ -6,6 +6,10 @@ import bcrypt from 'twin-bcrypt'; 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'; @@ -827,9 +831,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 @@ -1,16 +1,27 @@ // @flow 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, CompleteCryptoStore, IdentityKeysBlob, + OLMIdentityKeys, + PickledOLMAccount, } 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 { setPrimaryIdentityKeys, @@ -140,4 +151,74 @@ }, [getOrCreateCryptoStore]); } -export { useGetSignedIdentityKeysBlob, useGetOrCreateCryptoStore }; +function useWebNotificationsSessionCreator(): ( + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, +) => Promise { + const notificationsAccount = useSelector( + state => state.cryptoStore.notificationAccount, + ); + const dispatch = useDispatch(); + + return React.useCallback( + async ( + notificationsIdentityKeys: OLMIdentityKeys, + notificationsInitializationInfo: OlmSessionInitializationInfo, + ) => { + invariant( + notificationsAccount, + 'SignedIdentityKeysBlob must be uploaded to the keyserver.', + ); + + await initOlm(); + const account = new olm.Account(); + account.unpickle( + notificationsAccount.picklingKey, + notificationsAccount.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 updatedNotificationsAccount: PickledOLMAccount = { + pickledAccount: account.pickle(notificationsAccount.picklingKey), + picklingKey: notificationsAccount.picklingKey, + }; + + await dispatch({ + type: setPickledNotificationAccount, + payload: updatedNotificationsAccount, + }); + + const pickledSession = session.pickle(notificationsAccount.picklingKey); + await localforage.setItem(NOTIFICATIONS_OLM_SESSION_KEY, pickledSession); + + return initialNotificationsEncryptedMessage; + }, + [notificationsAccount, dispatch], + ); +} + +export { + useGetSignedIdentityKeysBlob, + useGetOrCreateCryptoStore, + useWebNotificationsSessionCreator, +}; 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/selectors/socket-selectors.js b/web/selectors/socket-selectors.js --- a/web/selectors/socket-selectors.js +++ b/web/selectors/socket-selectors.js @@ -42,6 +42,7 @@ type WebGetClientResponsesSelectorInputType = { state: AppState, getSignedIdentityKeysBlob: () => Promise, + getInitialNotificationsEncryptedMessage: () => Promise, }; const webGetClientResponsesSelector: ( @@ -55,23 +56,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);