diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index eaa33093f..a3133d7d6 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,210 +1,210 @@ // @flow import t, { type TInterface, type TList } from 'tcomb'; import { identityKeysBlobValidator, type IdentityKeysBlob, signedPrekeysValidator, type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, } from './request-types.js'; import { 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/lib.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof: ?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/lib.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof?: ?string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type DeviceOlmOutboundKeys = { +identityKeysBlob: IdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, +payloadSignature: string, +socialProof: ?string, }; export const deviceOlmOutboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, payloadSignature: t.String, socialProof: t.maybe(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, }; 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 { +deleteUser: () => Promise; +logOut: () => Promise; +getKeyserverKeys: string => Promise; +registerPasswordUser?: ( username: string, password: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: 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>; // 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; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, nonceChallengeResponse: SignedMessage, ) => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, }; export const identityAuthResultValidator: TInterface = tShape({ userID: t.String, accessToken: t.String, username: t.String, }); -export type IdentityDeviceKeyUpload = { +export type IdentityNewDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +contentOneTimeKeys: $ReadOnlyArray, +notifOneTimeKeys: $ReadOnlyArray, }; // Device list types export type RawDeviceList = { +devices: $ReadOnlyArray, +timestamp: number, }; export type SignedDeviceList = { // JSON-stringified RawDeviceList +rawDeviceList: string, }; export const signedDeviceListValidator: TInterface = tShape({ rawDeviceList: t.String, }); export const signedDeviceListHistoryValidator: TList> = t.list(signedDeviceListValidator); export type NonceChallenge = { +nonce: string, }; export type SignedMessage = { +message: string, +signature: 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, }); diff --git a/web/account/account-hooks.js b/web/account/account-hooks.js index ecb3950c1..ca6b97365 100644 --- a/web/account/account-hooks.js +++ b/web/account/account-hooks.js @@ -1,435 +1,435 @@ // @flow import olm from '@commapp/olm'; import invariant from 'invariant'; import localforage from 'localforage'; import * as React from 'react'; import uuid from 'uuid'; import { initialEncryptedMessageContent } from 'lib/shared/crypto-utils.js'; import { OlmSessionCreatorContext } from 'lib/shared/olm-session-creator-context.js'; import { hasMinCodeVersion, NEXT_CODE_VERSION, } from 'lib/shared/version-utils.js'; import type { SignedIdentityKeysBlob, CryptoStore, IdentityKeysBlob, CryptoStoreContextType, OLMIdentityKeys, NotificationsOlmDataType, } from 'lib/types/crypto-types.js'; -import { type IdentityDeviceKeyUpload } from 'lib/types/identity-service-types.js'; +import { type IdentityNewDeviceKeyUpload } from 'lib/types/identity-service-types.js'; import type { OlmSessionInitializationInfo } from 'lib/types/request-types.js'; import { getConfig } from 'lib/utils/config.js'; import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { generateCryptoKey, encryptData, exportKeyToJWK, } from '../crypto/aes-gcm-crypto-utils.js'; import { initOlm } from '../olm/olm-utils.js'; import { getOlmDataContentKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, } from '../push-notif/notif-crypto-utils.js'; import { setCryptoStore } from '../redux/crypto-store-reducer.js'; import { useSelector } from '../redux/redux-utils.js'; import { isDesktopSafari } from '../shared-worker/utils/db-utils.js'; const CryptoStoreContext: React.Context = React.createContext(null); type Props = { +children: React.Node, }; function GetOrCreateCryptoStoreProvider(props: Props): React.Node { const dispatch = useDispatch(); const createCryptoStore = React.useCallback(async () => { await initOlm(); const identityAccount = new olm.Account(); identityAccount.create(); const { ed25519: identityED25519, curve25519: identityCurve25519 } = JSON.parse(identityAccount.identity_keys()); const identityAccountPicklingKey = uuid.v4(); const pickledIdentityAccount = identityAccount.pickle( identityAccountPicklingKey, ); const notificationAccount = new olm.Account(); notificationAccount.create(); const { ed25519: notificationED25519, curve25519: notificationCurve25519 } = JSON.parse(notificationAccount.identity_keys()); const notificationAccountPicklingKey = uuid.v4(); const pickledNotificationAccount = notificationAccount.pickle( notificationAccountPicklingKey, ); const newCryptoStore = { primaryAccount: { picklingKey: identityAccountPicklingKey, pickledAccount: pickledIdentityAccount, }, primaryIdentityKeys: { ed25519: identityED25519, curve25519: identityCurve25519, }, notificationAccount: { picklingKey: notificationAccountPicklingKey, pickledAccount: pickledNotificationAccount, }, notificationIdentityKeys: { ed25519: notificationED25519, curve25519: notificationCurve25519, }, }; dispatch({ type: setCryptoStore, payload: newCryptoStore }); return newCryptoStore; }, [dispatch]); const currentCryptoStore = useSelector(state => state.cryptoStore); const createCryptoStorePromiseRef = React.useRef>(null); const getCryptoStorePromise = React.useCallback(() => { if (currentCryptoStore) { return Promise.resolve(currentCryptoStore); } const currentCreateCryptoStorePromiseRef = createCryptoStorePromiseRef.current; if (currentCreateCryptoStorePromiseRef) { return currentCreateCryptoStorePromiseRef; } const newCreateCryptoStorePromise = (async () => { try { return await createCryptoStore(); } catch (e) { createCryptoStorePromiseRef.current = undefined; throw e; } })(); createCryptoStorePromiseRef.current = newCreateCryptoStorePromise; return newCreateCryptoStorePromise; }, [createCryptoStore, currentCryptoStore]); const isCryptoStoreSet = !!currentCryptoStore; React.useEffect(() => { if (!isCryptoStoreSet) { createCryptoStorePromiseRef.current = undefined; } }, [isCryptoStoreSet]); const contextValue = React.useMemo( () => ({ getInitializedCryptoStore: getCryptoStorePromise, }), [getCryptoStorePromise], ); return ( {props.children} ); } function useGetOrCreateCryptoStore(): () => Promise { const context = React.useContext(CryptoStoreContext); invariant(context, 'CryptoStoreContext not found'); return context.getInitializedCryptoStore; } function useGetSignedIdentityKeysBlob(): () => Promise { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); return React.useCallback(async () => { const [{ primaryAccount, primaryIdentityKeys, notificationIdentityKeys }] = await Promise.all([getOrCreateCryptoStore(), initOlm()]); const primaryOLMAccount = new olm.Account(); primaryOLMAccount.unpickle( primaryAccount.picklingKey, primaryAccount.pickledAccount, ); const identityKeysBlob: IdentityKeysBlob = { primaryIdentityPublicKeys: primaryIdentityKeys, notificationIdentityPublicKeys: notificationIdentityKeys, }; const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: payloadToBeSigned, signature: primaryOLMAccount.sign(payloadToBeSigned), }; return signedIdentityKeysBlob; }, [getOrCreateCryptoStore]); } -function useGetDeviceKeyUpload(): () => Promise { +function useGetNewDeviceKeyUpload(): () => Promise { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); // `getSignedIdentityKeysBlob()` will initialize OLM, so no need to do it // again const getSignedIdentityKeysBlob = useGetSignedIdentityKeysBlob(); const dispatch = useDispatch(); return React.useCallback(async () => { const [signedIdentityKeysBlob, cryptoStore] = await Promise.all([ getSignedIdentityKeysBlob(), getOrCreateCryptoStore(), ]); const primaryOLMAccount = new olm.Account(); const notificationOLMAccount = new olm.Account(); primaryOLMAccount.unpickle( cryptoStore.primaryAccount.picklingKey, cryptoStore.primaryAccount.pickledAccount, ); notificationOLMAccount.unpickle( cryptoStore.notificationAccount.picklingKey, cryptoStore.notificationAccount.pickledAccount, ); const primaryAccountKeysSet = retrieveAccountKeysSet(primaryOLMAccount); const notificationAccountKeysSet = retrieveAccountKeysSet( notificationOLMAccount, ); const pickledPrimaryAccount = primaryOLMAccount.pickle( cryptoStore.primaryAccount.picklingKey, ); const pickledNotificationAccount = notificationOLMAccount.pickle( cryptoStore.notificationAccount.picklingKey, ); const updatedCryptoStore = { primaryAccount: { picklingKey: cryptoStore.primaryAccount.picklingKey, pickledAccount: pickledPrimaryAccount, }, primaryIdentityKeys: cryptoStore.primaryIdentityKeys, notificationAccount: { picklingKey: cryptoStore.notificationAccount.picklingKey, pickledAccount: pickledNotificationAccount, }, notificationIdentityKeys: cryptoStore.notificationIdentityKeys, }; dispatch({ type: setCryptoStore, payload: updatedCryptoStore }); return { keyPayload: signedIdentityKeysBlob.payload, keyPayloadSignature: signedIdentityKeysBlob.signature, contentPrekey: primaryAccountKeysSet.prekey, contentPrekeySignature: primaryAccountKeysSet.prekeySignature, notifPrekey: notificationAccountKeysSet.prekey, notifPrekeySignature: notificationAccountKeysSet.prekeySignature, contentOneTimeKeys: primaryAccountKeysSet.oneTimeKeys, notifOneTimeKeys: notificationAccountKeysSet.oneTimeKeys, }; }, [dispatch, getOrCreateCryptoStore, getSignedIdentityKeysBlob]); } function OlmSessionCreatorProvider(props: Props): React.Node { const getOrCreateCryptoStore = useGetOrCreateCryptoStore(); const currentCryptoStore = useSelector(state => state.cryptoStore); const platformDetails = getConfig().platformDetails; const createNewNotificationsSession = React.useCallback( async ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => { const [{ notificationAccount }, encryptionKey] = await Promise.all([ getOrCreateCryptoStore(), generateCryptoKey({ extractable: isDesktopSafari }), initOlm(), ]); const account = new olm.Account(); const { picklingKey, pickledAccount } = notificationAccount; account.unpickle(picklingKey, pickledAccount); const notificationsPrekey = notificationsInitializationInfo.prekey; const session = new olm.Session(); session.create_outbound( account, notificationsIdentityKeys.curve25519, notificationsIdentityKeys.ed25519, notificationsPrekey, notificationsInitializationInfo.prekeySignature, notificationsInitializationInfo.oneTimeKey, ); const { body: initialNotificationsEncryptedMessage } = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); const mainSession = session.pickle(picklingKey); const notificationsOlmData: NotificationsOlmDataType = { mainSession, pendingSessionUpdate: mainSession, updateCreationTimestamp: Date.now(), picklingKey, }; const encryptedOlmData = await encryptData( new TextEncoder().encode(JSON.stringify(notificationsOlmData)), encryptionKey, ); let notifsOlmDataContentKey; let notifsOlmDataEncryptionKeyDBLabel; if ( hasMinCodeVersion(platformDetails, { majorDesktop: NEXT_CODE_VERSION }) ) { notifsOlmDataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForCookie( cookie, keyserverID, ); notifsOlmDataContentKey = getOlmDataContentKeyForCookie( cookie, keyserverID, ); } else { notifsOlmDataEncryptionKeyDBLabel = getOlmEncryptionKeyDBLabelForCookie(cookie); notifsOlmDataContentKey = getOlmDataContentKeyForCookie(cookie); } const persistEncryptionKeyPromise = (async () => { let cryptoKeyPersistentForm; if (isDesktopSafari) { // Safari doesn't support structured clone algorithm in service // worker context so we have to store CryptoKey as JSON cryptoKeyPersistentForm = await exportKeyToJWK(encryptionKey); } else { cryptoKeyPersistentForm = encryptionKey; } await localforage.setItem( notifsOlmDataEncryptionKeyDBLabel, cryptoKeyPersistentForm, ); })(); await Promise.all([ localforage.setItem(notifsOlmDataContentKey, encryptedOlmData), persistEncryptionKeyPromise, ]); return initialNotificationsEncryptedMessage; }, [getOrCreateCryptoStore, platformDetails], ); const createNewContentSession = React.useCallback( async ( contentIdentityKeys: OLMIdentityKeys, contentInitializationInfo: OlmSessionInitializationInfo, ) => { const [{ primaryAccount }] = await Promise.all([ getOrCreateCryptoStore(), initOlm(), ]); const account = new olm.Account(); const { picklingKey, pickledAccount } = primaryAccount; account.unpickle(picklingKey, pickledAccount); const contentPrekey = contentInitializationInfo.prekey; const session = new olm.Session(); session.create_outbound( account, contentIdentityKeys.curve25519, contentIdentityKeys.ed25519, contentPrekey, contentInitializationInfo.prekeySignature, contentInitializationInfo.oneTimeKey, ); const { body: initialContentEncryptedMessage } = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); return initialContentEncryptedMessage; }, [getOrCreateCryptoStore], ); const perKeyserverNotificationsSessionPromises = React.useRef<{ [keyserverID: string]: ?Promise, }>({}); const createNotificationsSession = React.useCallback( async ( cookie: ?string, notificationsIdentityKeys: OLMIdentityKeys, notificationsInitializationInfo: OlmSessionInitializationInfo, keyserverID: string, ) => { if (perKeyserverNotificationsSessionPromises.current[keyserverID]) { return perKeyserverNotificationsSessionPromises.current[keyserverID]; } const newNotificationsSessionPromise = (async () => { try { return await createNewNotificationsSession( cookie, notificationsIdentityKeys, notificationsInitializationInfo, keyserverID, ); } catch (e) { perKeyserverNotificationsSessionPromises.current[keyserverID] = undefined; throw e; } })(); perKeyserverNotificationsSessionPromises.current[keyserverID] = newNotificationsSessionPromise; return newNotificationsSessionPromise; }, [createNewNotificationsSession], ); const isCryptoStoreSet = !!currentCryptoStore; React.useEffect(() => { if (!isCryptoStoreSet) { perKeyserverNotificationsSessionPromises.current = {}; } }, [isCryptoStoreSet]); const contextValue = React.useMemo( () => ({ notificationsSessionCreator: createNotificationsSession, contentSessionCreator: createNewContentSession, }), [createNewContentSession, createNotificationsSession], ); return ( {props.children} ); } export { useGetSignedIdentityKeysBlob, useGetOrCreateCryptoStore, OlmSessionCreatorProvider, GetOrCreateCryptoStoreProvider, - useGetDeviceKeyUpload, + useGetNewDeviceKeyUpload, }; diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index 4126eadde..ded3a944d 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,562 +1,562 @@ // @flow import { Login } from '@commapp/opaque-ke-wasm'; import identityServiceConfig from 'lib/facts/identity-service.js'; import type { OneTimeKeysResultValues, SignedPrekeys, } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import { type SignedDeviceList, signedDeviceListHistoryValidator, type SignedMessage, type IdentityServiceAuthLayer, type IdentityServiceClient, type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, type UserDevicesOlmOutboundKeys, type IdentityAuthResult, - type IdentityDeviceKeyUpload, + type IdentityNewDeviceKeyUpload, identityDeviceTypes, identityAuthResultValidator, type UserDevicesOlmInboundKeys, type DeviceOlmInboundKeys, deviceOlmInboundKeysValidator, userDeviceOlmInboundKeysValidator, } from 'lib/types/identity-service-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { VersionInterceptor, AuthInterceptor } from './interceptor.js'; import { initOpaque } from '../crypto/opaque-utils.js'; import * as IdentityAuthClient from '../protobufs/identity-auth-client.cjs'; import * as IdentityAuthStructs from '../protobufs/identity-auth-structs.cjs'; import { DeviceKeyUpload, Empty, IdentityKeyInfo, OpaqueLoginFinishRequest, OpaqueLoginStartRequest, Prekey, WalletAuthRequest, SecondaryDeviceKeysUploadRequest, } from '../protobufs/identity-unauth-structs.cjs'; import * as IdentityUnauthClient from '../protobufs/identity-unauth.cjs'; class IdentityServiceClientWrapper implements IdentityServiceClient { overridedOpaqueFilepath: ?string; authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; - getDeviceKeyUpload: () => Promise; + getNewDeviceKeyUpload: () => Promise; constructor( platformDetails: PlatformDetails, overridedOpaqueFilepath: ?string, authLayer: ?IdentityServiceAuthLayer, - getDeviceKeyUpload: () => Promise, + getNewDeviceKeyUpload: () => Promise, ) { this.overridedOpaqueFilepath = overridedOpaqueFilepath; if (authLayer) { this.authClient = IdentityServiceClientWrapper.createAuthClient( platformDetails, authLayer, ); } this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(platformDetails); - this.getDeviceKeyUpload = getDeviceKeyUpload; + this.getNewDeviceKeyUpload = getNewDeviceKeyUpload; } static determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } static createAuthClient( platformDetails: PlatformDetails, authLayer: IdentityServiceAuthLayer, ): IdentityAuthClient.IdentityClientServicePromiseClient { const { userID, deviceID, commServicesAccessToken } = authLayer; const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor( platformDetails, ); const authInterceptor = new AuthInterceptor( userID, deviceID, commServicesAccessToken, ); const authClientOpts = { unaryInterceptors: [versionInterceptor, authInterceptor], }; return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } static createUnauthClient( platformDetails: PlatformDetails, ): IdentityUnauthClient.IdentityClientServicePromiseClient { const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor( platformDetails, ); const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; return new IdentityUnauthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, unauthClientOpts, ); } deleteUser: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.deleteUser(new Empty()); }; logOut: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.logOutUser(new Empty()); }; getKeyserverKeys: (keyserverID: string) => Promise = async (keyserverID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); request.setUserId(keyserverID); const response = await client.getKeyserverKeys(request); const keyserverInfo = response.getKeyserverInfo(); const identityInfo = keyserverInfo?.getIdentityInfo(); const contentPreKey = keyserverInfo?.getContentPrekey(); const notifPreKey = keyserverInfo?.getNotifPrekey(); const payload = identityInfo?.getPayload(); const keyserverKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: contentPreKey?.getPrekey(), prekeySignature: contentPreKey?.getPrekeySignature(), oneTimeKey: keyserverInfo?.getOneTimeContentPrekey(), }, notifInitializationInfo: { prekey: notifPreKey?.getPrekey(), prekeySignature: notifPreKey?.getPrekeySignature(), oneTimeKey: keyserverInfo?.getOneTimeNotifPrekey(), }, payloadSignature: identityInfo?.getPayloadSignature(), socialProof: identityInfo?.getSocialProof(), }; if (!keyserverKeys.contentInitializationInfo.oneTimeKey) { throw new Error('Missing content one time key'); } if (!keyserverKeys.notifInitializationInfo.oneTimeKey) { throw new Error('Missing notif one time key'); } return assertWithValidator(keyserverKeys, deviceOlmOutboundKeysValidator); }; getOutboundKeysForUser: ( userID: string, ) => Promise = async (userID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); request.setUserId(userID); const response = await client.getOutboundKeysForUser(request); const devicesMap = response.toObject()?.devicesMap; if (!devicesMap || !Array.isArray(devicesMap)) { throw new Error('Invalid devicesMap'); } const devicesKeys: (?UserDevicesOlmOutboundKeys)[] = devicesMap.map( ([deviceID, outboundKeysInfo]) => { const identityInfo = outboundKeysInfo?.identityInfo; const payload = identityInfo?.payload; const contentPreKey = outboundKeysInfo?.contentPrekey; const notifPreKey = outboundKeysInfo?.notifPrekey; if (typeof deviceID !== 'string') { console.log(`Invalid deviceID in devicesMap: ${deviceID}`); return null; } if ( !outboundKeysInfo.oneTimeContentPrekey || !outboundKeysInfo.oneTimeNotifPrekey ) { console.log(`Missing one time key for device ${deviceID}`); return { deviceID, keys: null, }; } const deviceKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: contentPreKey?.prekey, prekeySignature: contentPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: notifPreKey?.prekey, prekeySignature: notifPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeNotifPrekey, }, payloadSignature: identityInfo?.payloadSignature, socialProof: identityInfo?.socialProof, }; try { const validatedKeys = assertWithValidator( deviceKeys, deviceOlmOutboundKeysValidator, ); return { deviceID, keys: validatedKeys, }; } catch (e) { console.log(e); return { deviceID, keys: null, }; } }, ); return devicesKeys.filter(Boolean); }; getInboundKeysForUser: ( userID: string, ) => Promise = async (userID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.InboundKeysForUserRequest(); request.setUserId(userID); const response = await client.getInboundKeysForUser(request); const devicesMap = response.toObject()?.devicesMap; if (!devicesMap || !Array.isArray(devicesMap)) { throw new Error('Invalid devicesMap'); } const devicesKeys: { [deviceID: string]: ?DeviceOlmInboundKeys, } = {}; devicesMap.forEach(([deviceID, inboundKeys]) => { const identityInfo = inboundKeys?.identityInfo; const payload = identityInfo?.payload; const contentPreKey = inboundKeys?.contentPrekey; const notifPreKey = inboundKeys?.notifPrekey; if (typeof deviceID !== 'string') { console.log(`Invalid deviceID in devicesMap: ${deviceID}`); return; } const deviceKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, signedPrekeys: { contentPrekey: contentPreKey?.prekey, contentPrekeySignature: contentPreKey?.prekeySignature, notifPrekey: notifPreKey?.prekey, notifPrekeySignature: notifPreKey?.prekeySignature, }, payloadSignature: identityInfo?.payloadSignature, }; try { devicesKeys[deviceID] = assertWithValidator( deviceKeys, deviceOlmInboundKeysValidator, ); } catch (e) { console.log(e); devicesKeys[deviceID] = null; } }); const identityInfo = response?.getIdentity(); const inboundUserKeys = { keys: devicesKeys, username: identityInfo?.getUsername(), walletAddress: identityInfo?.getEthIdentity()?.getWalletAddress(), }; return assertWithValidator( inboundUserKeys, userDeviceOlmInboundKeysValidator, ); }; uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise = async (oneTimeKeys: OneTimeKeysResultValues) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const contentOneTimeKeysArray = [...oneTimeKeys.contentOneTimeKeys]; const notifOneTimeKeysArray = [...oneTimeKeys.notificationsOneTimeKeys]; const request = new IdentityAuthStructs.UploadOneTimeKeysRequest(); request.setContentOneTimePrekeysList(contentOneTimeKeysArray); request.setNotifOneTimePrekeysList(notifOneTimeKeysArray); await client.uploadOneTimeKeys(request); }; logInPasswordUser: ( username: string, password: string, ) => Promise = async ( username: string, password: string, ) => { const client = this.unauthClient; if (!client) { throw new Error('Identity service client is not initialized'); } const [identityDeviceKeyUpload] = await Promise.all([ - this.getDeviceKeyUpload(), + this.getNewDeviceKeyUpload(), initOpaque(this.overridedOpaqueFilepath), ]); const opaqueLogin = new Login(); const startRequestBytes = opaqueLogin.start(password); - const deviceKeyUpload = authDeviceKeyUpload(identityDeviceKeyUpload); + const deviceKeyUpload = authNewDeviceKeyUpload(identityDeviceKeyUpload); const loginStartRequest = new OpaqueLoginStartRequest(); loginStartRequest.setUsername(username); loginStartRequest.setOpaqueLoginRequest(startRequestBytes); loginStartRequest.setDeviceKeyUpload(deviceKeyUpload); let loginStartResponse; try { loginStartResponse = await client.logInPasswordUserStart(loginStartRequest); } catch (e) { console.log('Error calling logInPasswordUserStart:', e); throw new Error(getMessageForException(e) ?? 'unknown'); } const finishRequestBytes = opaqueLogin.finish( loginStartResponse.getOpaqueLoginResponse_asU8(), ); const loginFinishRequest = new OpaqueLoginFinishRequest(); loginFinishRequest.setSessionId(loginStartResponse.getSessionId()); loginFinishRequest.setOpaqueLoginUpload(finishRequestBytes); let loginFinishResponse; try { loginFinishResponse = await client.logInPasswordUserFinish(loginFinishRequest); } catch (e) { console.log('Error calling logInPasswordUserFinish:', e); throw new Error(getMessageForException(e) ?? 'unknown'); } const userID = loginFinishResponse.getUserId(); const accessToken = loginFinishResponse.getAccessToken(); const identityAuthResult = { accessToken, userID, username }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise = async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { - const identityDeviceKeyUpload = await this.getDeviceKeyUpload(); - const deviceKeyUpload = authDeviceKeyUpload(identityDeviceKeyUpload); + const identityDeviceKeyUpload = await this.getNewDeviceKeyUpload(); + const deviceKeyUpload = authNewDeviceKeyUpload(identityDeviceKeyUpload); const loginRequest = new WalletAuthRequest(); loginRequest.setSiweMessage(siweMessage); loginRequest.setSiweSignature(siweSignature); loginRequest.setDeviceKeyUpload(deviceKeyUpload); let loginResponse; try { loginResponse = await this.unauthClient.logInWalletUser(loginRequest); } catch (e) { console.log('Error calling logInWalletUser:', e); throw new Error(getMessageForException(e) ?? 'unknown'); } const userID = loginResponse.getUserId(); const accessToken = loginResponse.getAccessToken(); const identityAuthResult = { accessToken, userID, username: walletAddress }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; uploadKeysForRegisteredDeviceAndLogIn: ( ownerUserID: string, nonceChallengeResponse: SignedMessage, ) => Promise = async ( ownerUserID, nonceChallengeResponse, ) => { - const identityDeviceKeyUpload = await this.getDeviceKeyUpload(); - const deviceKeyUpload = authDeviceKeyUpload(identityDeviceKeyUpload); + const identityDeviceKeyUpload = await this.getNewDeviceKeyUpload(); + const deviceKeyUpload = authNewDeviceKeyUpload(identityDeviceKeyUpload); const challengeResponse = JSON.stringify(nonceChallengeResponse); const request = new SecondaryDeviceKeysUploadRequest(); request.setUserId(ownerUserID); request.setChallengeResponse(challengeResponse); request.setDeviceKeyUpload(deviceKeyUpload); let response; try { response = await this.unauthClient.uploadKeysForRegisteredDeviceAndLogIn(request); } catch (e) { console.log('Error calling uploadKeysForRegisteredDeviceAndLogIn:', e); throw new Error(getMessageForException(e) ?? 'unknown'); } const userID = response.getUserId(); const accessToken = response.getAccessToken(); const identityAuthResult = { accessToken, userID, username: '' }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; generateNonce: () => Promise = async () => { const result = await this.unauthClient.generateNonce(new Empty()); return result.getNonce(); }; publishWebPrekeys: (prekeys: SignedPrekeys) => Promise = async ( prekeys: SignedPrekeys, ) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const contentPrekeyUpload = new Prekey(); contentPrekeyUpload.setPrekey(prekeys.contentPrekey); contentPrekeyUpload.setPrekeySignature(prekeys.contentPrekeySignature); const notifPrekeyUpload = new Prekey(); notifPrekeyUpload.setPrekey(prekeys.notifPrekey); notifPrekeyUpload.setPrekeySignature(prekeys.notifPrekeySignature); const request = new IdentityAuthStructs.RefreshUserPrekeysRequest(); request.setNewContentPrekeys(contentPrekeyUpload); request.setNewNotifPrekeys(notifPrekeyUpload); await client.refreshUserPrekeys(request); }; getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray> = async ( userID, sinceTimestamp, ) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.GetDeviceListRequest(); request.setUserId(userID); if (sinceTimestamp) { request.setSinceTimestamp(sinceTimestamp); } const response = await client.getDeviceListForUser(request); const rawPayloads = response.getDeviceListUpdatesList(); const deviceListUpdates: SignedDeviceList[] = rawPayloads.map(payload => JSON.parse(payload), ); return assertWithValidator( deviceListUpdates, signedDeviceListHistoryValidator, ); }; } -function authDeviceKeyUpload( - uploadData: IdentityDeviceKeyUpload, +function authNewDeviceKeyUpload( + uploadData: IdentityNewDeviceKeyUpload, ): DeviceKeyUpload { const { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, contentOneTimeKeys, notifOneTimeKeys, } = uploadData; const contentOneTimeKeysArray = [...contentOneTimeKeys]; const notifOneTimeKeysArray = [...notifOneTimeKeys]; const identityKeyInfo = new IdentityKeyInfo(); identityKeyInfo.setPayload(keyPayload); identityKeyInfo.setPayloadSignature(keyPayloadSignature); const contentPrekeyUpload = new Prekey(); contentPrekeyUpload.setPrekey(contentPrekey); contentPrekeyUpload.setPrekeySignature(contentPrekeySignature); const notifPrekeyUpload = new Prekey(); notifPrekeyUpload.setPrekey(notifPrekey); notifPrekeyUpload.setPrekeySignature(notifPrekeySignature); const deviceKeyUpload = new DeviceKeyUpload(); deviceKeyUpload.setDeviceKeyInfo(identityKeyInfo); deviceKeyUpload.setContentUpload(contentPrekeyUpload); deviceKeyUpload.setNotifUpload(notifPrekeyUpload); deviceKeyUpload.setOneTimeContentPrekeysList(contentOneTimeKeysArray); deviceKeyUpload.setOneTimeNotifPrekeysList(notifOneTimeKeysArray); deviceKeyUpload.setDeviceType(identityDeviceTypes.WEB); return deviceKeyUpload; } export { IdentityServiceClientWrapper }; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js index 7ca2e7ee8..37b1f2afb 100644 --- a/web/grpc/identity-service-context-provider.react.js +++ b/web/grpc/identity-service-context-provider.react.js @@ -1,75 +1,75 @@ // @flow import * as React from 'react'; import { IdentityClientContext, type AuthMetadata, } from 'lib/shared/identity-client-context.js'; import { getConfig } from 'lib/utils/config.js'; import { IdentityServiceClientSharedProxy } from './identity-service-client-proxy.js'; import { IdentityServiceClientWrapper } from './identity-service-client-wrapper.js'; -import { useGetDeviceKeyUpload } from '../account/account-hooks.js'; +import { useGetNewDeviceKeyUpload } from '../account/account-hooks.js'; import { usingSharedWorker } from '../crypto/olm-api.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { +children: React.Node, }; function IdentityServiceContextProvider(props: Props): React.Node { const { children } = props; const userID = useSelector(state => state.currentUserInfo?.id); const accessToken = useSelector(state => state.commServicesAccessToken); const deviceID = useSelector( state => state.cryptoStore?.primaryIdentityKeys.ed25519, ); - const getDeviceKeyUpload = useGetDeviceKeyUpload(); + const getNewDeviceKeyUpload = useGetNewDeviceKeyUpload(); const client = React.useMemo(() => { let authLayer = null; if (userID && deviceID && accessToken) { authLayer = { userID, deviceID, commServicesAccessToken: accessToken, }; } if (usingSharedWorker) { return new IdentityServiceClientSharedProxy(authLayer); } else { return new IdentityServiceClientWrapper( getConfig().platformDetails, null, authLayer, - getDeviceKeyUpload, + getNewDeviceKeyUpload, ); } - }, [accessToken, deviceID, getDeviceKeyUpload, userID]); + }, [accessToken, deviceID, getNewDeviceKeyUpload, userID]); const getAuthMetadata = React.useCallback<() => Promise>( async () => ({ userID, deviceID, accessToken, }), [accessToken, deviceID, userID], ); const value = React.useMemo( () => ({ identityClient: client, getAuthMetadata, }), [client, getAuthMetadata], ); return ( {children} ); } export default IdentityServiceContextProvider; diff --git a/web/shared-worker/worker/identity-client.js b/web/shared-worker/worker/identity-client.js index 75339766e..1371f43ef 100644 --- a/web/shared-worker/worker/identity-client.js +++ b/web/shared-worker/worker/identity-client.js @@ -1,61 +1,61 @@ // @flow -import { getDeviceKeyUpload } from './worker-crypto.js'; +import { getNewDeviceKeyUpload } from './worker-crypto.js'; import { IdentityServiceClientWrapper } from '../../grpc/identity-service-client-wrapper.js'; import { type WorkerResponseMessage, type WorkerRequestMessage, workerRequestMessageTypes, workerResponseMessageTypes, } from '../../types/worker-types.js'; import type { EmscriptenModule } from '../types/module.js'; import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; let identityClient: ?IdentityServiceClientWrapper = null; async function processAppIdentityClientRequest( sqliteQueryExecutor: SQLiteQueryExecutor, dbModule: EmscriptenModule, message: WorkerRequestMessage, ): Promise { if ( message.type === workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT ) { identityClient = new IdentityServiceClientWrapper( message.platformDetails, message.opaqueWasmPath, message.authLayer, - async () => getDeviceKeyUpload(), + async () => getNewDeviceKeyUpload(), ); return undefined; } if (!identityClient) { throw new Error('Identity client not created'); } if (message.type === workerRequestMessageTypes.CALL_IDENTITY_CLIENT_METHOD) { // Flow doesn't allow us to access methods like this (it needs an index // signature declaration in the object type) // $FlowFixMe const method = identityClient[message.method]; if (typeof method !== 'function') { throw new Error( `Couldn't find identity client method with name '${message.method}'`, ); } const result = await method(...message.args); return { type: workerResponseMessageTypes.CALL_IDENTITY_CLIENT_METHOD, result, }; } return undefined; } function getIdentityClient(): ?IdentityServiceClientWrapper { return identityClient; } export { processAppIdentityClientRequest, getIdentityClient }; diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js index 081a806c8..7eb91a7f9 100644 --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,230 +1,230 @@ // @flow import olm from '@commapp/olm'; import uuid from 'uuid'; import type { CryptoStore, PickledOLMAccount, IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; -import type { IdentityDeviceKeyUpload } from 'lib/types/identity-service-types.js'; +import type { IdentityNewDeviceKeyUpload } from 'lib/types/identity-service-types.js'; import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; import { getDBModule, getSQLiteQueryExecutor } from './worker-database.js'; import { type WorkerRequestMessage, type WorkerResponseMessage, workerRequestMessageTypes, } from '../../types/worker-types.js'; type WorkerCryptoStore = { +contentAccountPickleKey: string, +contentAccount: olm.Account, +notificationAccountPickleKey: string, +notificationAccount: olm.Account, }; let cryptoStore: ?WorkerCryptoStore = null; function clearCryptoStore() { cryptoStore = null; } function persistCryptoStore() { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error( "Couldn't persist crypto store because database is not initialized", ); } if (!cryptoStore) { throw new Error("Couldn't persist crypto store because it doesn't exist"); } const { contentAccountPickleKey, contentAccount, notificationAccountPickleKey, notificationAccount, } = cryptoStore; const pickledContentAccount: PickledOLMAccount = { picklingKey: contentAccountPickleKey, pickledAccount: contentAccount.pickle(contentAccountPickleKey), }; const pickledNotificationAccount: PickledOLMAccount = { picklingKey: notificationAccountPickleKey, pickledAccount: notificationAccount.pickle(notificationAccountPickleKey), }; try { sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getContentAccountID(), JSON.stringify(pickledContentAccount), ); sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getNotifsAccountID(), JSON.stringify(pickledNotificationAccount), ); } catch (err) { throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } } function getOrCreateOlmAccount(accountIDInDB: number): { +picklingKey: string, +account: olm.Account, } { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error('Database not initialized'); } const account = new olm.Account(); let picklingKey; let accountDBString; try { accountDBString = sqliteQueryExecutor.getOlmPersistAccountDataWeb(accountIDInDB); } catch (err) { throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } if (accountDBString.isNull) { picklingKey = uuid.v4(); account.create(); } else { const dbAccount: PickledOLMAccount = JSON.parse(accountDBString.value); picklingKey = dbAccount.picklingKey; account.unpickle(picklingKey, dbAccount.pickledAccount); } return { picklingKey, account }; } function unpickleInitialCryptoStoreAccount( account: PickledOLMAccount, ): olm.Account { const { picklingKey, pickledAccount } = account; const olmAccount = new olm.Account(); olmAccount.unpickle(picklingKey, pickledAccount); return olmAccount; } async function initializeCryptoAccount( olmWasmPath: string, initialCryptoStore: ?CryptoStore, ) { const sqliteQueryExecutor = getSQLiteQueryExecutor(); if (!sqliteQueryExecutor) { throw new Error('Database not initialized'); } await olm.init({ locateFile: () => olmWasmPath }); if (initialCryptoStore) { cryptoStore = { contentAccountPickleKey: initialCryptoStore.primaryAccount.picklingKey, contentAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.primaryAccount, ), notificationAccountPickleKey: initialCryptoStore.notificationAccount.picklingKey, notificationAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.notificationAccount, ), }; persistCryptoStore(); return; } const contentAccountResult = getOrCreateOlmAccount( sqliteQueryExecutor.getContentAccountID(), ); const notificationAccountResult = getOrCreateOlmAccount( sqliteQueryExecutor.getNotifsAccountID(), ); cryptoStore = { contentAccountPickleKey: contentAccountResult.picklingKey, contentAccount: contentAccountResult.account, notificationAccountPickleKey: notificationAccountResult.picklingKey, notificationAccount: notificationAccountResult.account, }; persistCryptoStore(); } async function processAppOlmApiRequest( message: WorkerRequestMessage, ): Promise { if (message.type === workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT) { await initializeCryptoAccount( message.olmWasmPath, message.initialCryptoStore, ); } } function getSignedIdentityKeysBlob(): SignedIdentityKeysBlob { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, notificationAccount } = cryptoStore; const identityKeysBlob: IdentityKeysBlob = { notificationIdentityPublicKeys: JSON.parse( notificationAccount.identity_keys(), ), primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()), }; const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: payloadToBeSigned, signature: contentAccount.sign(payloadToBeSigned), }; return signedIdentityKeysBlob; } -function getDeviceKeyUpload(): IdentityDeviceKeyUpload { +function getNewDeviceKeyUpload(): IdentityNewDeviceKeyUpload { if (!cryptoStore) { throw new Error('Crypto account not initialized'); } const { contentAccount, notificationAccount } = cryptoStore; const signedIdentityKeysBlob = getSignedIdentityKeysBlob(); const primaryAccountKeysSet = retrieveAccountKeysSet(contentAccount); const notificationAccountKeysSet = retrieveAccountKeysSet(notificationAccount); persistCryptoStore(); return { keyPayload: signedIdentityKeysBlob.payload, keyPayloadSignature: signedIdentityKeysBlob.signature, contentPrekey: primaryAccountKeysSet.prekey, contentPrekeySignature: primaryAccountKeysSet.prekeySignature, notifPrekey: notificationAccountKeysSet.prekey, notifPrekeySignature: notificationAccountKeysSet.prekeySignature, contentOneTimeKeys: primaryAccountKeysSet.oneTimeKeys, notifOneTimeKeys: notificationAccountKeysSet.oneTimeKeys, }; } export { clearCryptoStore, processAppOlmApiRequest, getSignedIdentityKeysBlob, - getDeviceKeyUpload, + getNewDeviceKeyUpload, };