diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index 40078fad5..14cc5ea69 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,273 +1,269 @@ // @flow import threadWatcher from '../shared/thread-watcher.js'; import type { LogOutResult, LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, } from '../types/account-types.js'; import type { GetSessionPublicKeysArgs } from '../types/request-types.js'; import type { UserSearchResult } from '../types/search-types.js'; import type { SessionPublicKeys, PreRequestUserState, } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { UserInfo, PasswordUpdate } from '../types/user-types.js'; import type { CallServerEndpoint } from '../utils/call-server-endpoint.js'; import { getConfig } from '../utils/config.js'; import sleep from '../utils/sleep.js'; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callServerEndpoint: CallServerEndpoint, ): ((preRequestUserState: PreRequestUserState) => Promise) => async preRequestUserState => { let response = null; try { response = await Promise.race([ callServerEndpoint('log_out', {}), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? response.currentUserInfo : null; return { currentUserInfo, preRequestUserState }; }; const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); const deleteAccount = ( callServerEndpoint: CallServerEndpoint, ): (( password: ?string, preRequestUserState: PreRequestUserState, ) => Promise) => async (password, preRequestUserState) => { const response = await callServerEndpoint('delete_account', { password }); return { currentUserInfo: response.currentUserInfo, preRequestUserState }; }; const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', failed: 'REGISTER_FAILED', }); const registerCallServerEndpointOptions = { timeout: 60000 }; const register = ( callServerEndpoint: CallServerEndpoint, ): ((registerInfo: RegisterInfo) => Promise) => async registerInfo => { const response = await callServerEndpoint( 'create_account', { ...registerInfo, platformDetails: getConfig().platformDetails, }, registerCallServerEndpointOptions, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] { const merged = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallServerEndpointOptions = { timeout: 60000 }; const logIn = ( callServerEndpoint: CallServerEndpoint, ): ((logInInfo: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); - const { - logInActionSource, - primaryIdentityPublicKeys, - notificationIdentityPublicKeys, - ...restLogInInfo - } = logInInfo; + const { logInActionSource, signedIdentityKeysBlob, ...restLogInInfo } = + logInInfo; const response = await callServerEndpoint( 'log_in', { ...restLogInInfo, source: logInActionSource, watchedIDs, platformDetails: getConfig().platformDetails, }, logInCallServerEndpointOptions, ); const userInfos = mergeUserInfos( response.userInfos, response.cookieChange.userInfos, ); return { threadInfos: response.cookieChange.threadInfos, currentUserInfo: response.currentUserInfo, calendarResult: { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: response.rawEntryInfos, }, messagesResult: { messageInfos: response.rawMessageInfos, truncationStatus: response.truncationStatuses, watchedIDsAtRequestTime: watchedIDs, currentAsOf: response.serverTime, }, userInfos, updatesCurrentAsOf: response.serverTime, logInActionSource: logInInfo.logInActionSource, notAcknowledgedPolicies: response.notAcknowledgedPolicies, }; }; const changeUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_USER_PASSWORD_STARTED', success: 'CHANGE_USER_PASSWORD_SUCCESS', failed: 'CHANGE_USER_PASSWORD_FAILED', }); const changeUserPassword = ( callServerEndpoint: CallServerEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callServerEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callServerEndpoint: CallServerEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callServerEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callServerEndpoint: CallServerEndpoint, ): (( subscriptionUpdate: SubscriptionUpdateRequest, ) => Promise) => async subscriptionUpdate => { const response = await callServerEndpoint( 'update_user_subscription', subscriptionUpdate, ); return { threadID: subscriptionUpdate.threadID, subscription: response.threadSubscription, }; }; const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callServerEndpoint: CallServerEndpoint, ): ((userSettingsRequest: UpdateUserSettingsRequest) => Promise) => async userSettingsRequest => { await callServerEndpoint('update_user_settings', userSettingsRequest); }; const getSessionPublicKeys = ( callServerEndpoint: CallServerEndpoint, ): ((data: GetSessionPublicKeysArgs) => Promise) => async data => { return await callServerEndpoint('get_session_public_keys', data); }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callServerEndpoint: CallServerEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callServerEndpoint('policy_acknowledgment', policyRequest); }; export { changeUserPasswordActionTypes, changeUserPassword, deleteAccount, deleteAccountActionTypes, getSessionPublicKeys, mergeUserInfos, logIn, logInActionTypes, logOut, logOutActionTypes, register, registerActionTypes, searchUsers, searchUsersActionTypes, setUserSettings, setUserSettingsActionTypes, updateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, }; diff --git a/lib/types/account-types.js b/lib/types/account-types.js index bb3f5f4e4..8fae55d86 100644 --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,193 +1,192 @@ // @flow -import type { OLMIdentityKeys } from './crypto-types.js'; +import type { SignedIdentityKeysBlob } from './crypto-types.js'; import type { PlatformDetails } from './device-types.js'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState } from './session-types.js'; import type { RawThreadInfo } from './thread-types.js'; import type { UserInfo, LoggedOutUserInfo, LoggedInUserInfo, OldLoggedInUserInfo, } from './user-types.js'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, }; export type LogOutResult = { +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: PreRequestUserState, }; export type LogOutResponse = { +currentUserInfo: LoggedOutUserInfo, }; export type RegisterInfo = { ...LogInExtraInfo, +username: string, +password: string, +primaryIdentityPublicKey?: string, }; type DeviceTokenUpdateRequest = { +deviceToken: string, }; export type RegisterRequest = { +username: string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +primaryIdentityPublicKey?: ?string, }; export type RegisterResponse = { id: string, rawMessageInfos: $ReadOnlyArray, currentUserInfo: OldLoggedInUserInfo | LoggedInUserInfo, cookieChange: { threadInfos: { +[id: string]: RawThreadInfo }, userInfos: $ReadOnlyArray, }, }; export type RegisterResult = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +threadInfos: { +[id: string]: RawThreadInfo }, +userInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type DeleteAccountRequest = { +password: ?string, }; export const logInActionSources = Object.freeze({ cookieInvalidationResolutionAttempt: 'COOKIE_INVALIDATION_RESOLUTION_ATTEMPT', appStartCookieLoggedInButInvalidRedux: 'APP_START_COOKIE_LOGGED_IN_BUT_INVALID_REDUX', appStartReduxLoggedInButInvalidCookie: 'APP_START_REDUX_LOGGED_IN_BUT_INVALID_COOKIE', socketAuthErrorResolutionAttempt: 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT', sqliteOpFailure: 'SQLITE_OP_FAILURE', sqliteLoadFailure: 'SQLITE_LOAD_FAILURE', logInFromWebForm: 'LOG_IN_FROM_WEB_FORM', logInFromNativeForm: 'LOG_IN_FROM_NATIVE_FORM', logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE', corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION', refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT', }); export type LogInActionSource = $Values; export type LogInStartingPayload = { +calendarQuery: CalendarQuery, +logInActionSource?: LogInActionSource, }; export type LogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +primaryIdentityPublicKey?: string, - +primaryIdentityPublicKeys?: ?OLMIdentityKeys, - +notificationIdentityPublicKeys?: ?OLMIdentityKeys, + +signedIdentityKeysBlob?: SignedIdentityKeysBlob, }; export type LogInInfo = { ...LogInExtraInfo, +username: string, +password: string, +logInActionSource: LogInActionSource, }; export type LogInRequest = { +usernameOrEmail?: ?string, +username?: ?string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +source?: LogInActionSource, }; export type LogInResponse = { +currentUserInfo: LoggedInUserInfo | OldLoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +rawEntryInfos?: ?$ReadOnlyArray, +serverTime: number, +cookieChange: { +threadInfos: { +[id: string]: RawThreadInfo }, +userInfos: $ReadOnlyArray, }, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type LogInResult = { +threadInfos: { +[id: string]: RawThreadInfo }, +currentUserInfo: LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: number, +logInActionSource: LogInActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type UpdatePasswordRequest = { code: string, password: string, calendarQuery?: ?CalendarQuery, deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, platformDetails: PlatformDetails, watchedIDs: $ReadOnlyArray, }; export type PolicyAcknowledgmentRequest = { +policy: PolicyType, }; export type EmailSubscriptionRequest = { +email: string, }; export type UpdateUserSettingsRequest = { +name: 'default_user_notifications', +data: NotificationTypes, }; export const userSettingsTypes = Object.freeze({ DEFAULT_NOTIFICATIONS: 'default_user_notifications', }); export type DefaultNotificationPayload = { +default_user_notifications: ?NotificationTypes, }; export const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', BACKGROUND: 'background', }); export type NotificationTypes = $Values; export const notificationTypeValues: $ReadOnlyArray = values(notificationTypes); diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index 2c2fab9ac..5192c6360 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,18 +1,23 @@ // @flow export type OLMIdentityKeys = { +ed25519: string, +curve25519: string, }; export type PickledOLMAccount = { +picklingKey: string, +pickledAccount: string, }; export type CryptoStore = { +primaryAccount: ?PickledOLMAccount, +primaryIdentityKeys: ?OLMIdentityKeys, +notificationAccount: ?PickledOLMAccount, +notificationIdentityKeys: ?OLMIdentityKeys, }; + +export type SignedIdentityKeysBlob = { + +payload: string, + +signature: string, +}; diff --git a/web/account/traditional-login-form.react.js b/web/account/traditional-login-form.react.js index 6faccab6b..e71e171a0 100644 --- a/web/account/traditional-login-form.react.js +++ b/web/account/traditional-login-form.react.js @@ -1,210 +1,237 @@ // @flow +import olm from '@matrix-org/olm'; import invariant from 'invariant'; import * as React from 'react'; import { logIn, logInActionTypes } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { oldValidUsernameRegex, validEmailRegex, } from 'lib/shared/account-utils.js'; import type { LogInExtraInfo, LogInStartingPayload, } from 'lib/types/account-types.js'; import { logInActionSources } from 'lib/types/account-types.js'; -import type { OLMIdentityKeys } from 'lib/types/crypto-types.js'; +import type { + OLMIdentityKeys, + PickledOLMAccount, + SignedIdentityKeysBlob, +} from 'lib/types/crypto-types.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import HeaderSeparator from './header-separator.react.js'; import css from './log-in-form.css'; import PasswordInput from './password-input.react.js'; import Button from '../components/button.react.js'; import LoadingIndicator from '../loading-indicator.react.js'; import Input from '../modals/input.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { webLogInExtraInfoSelector } from '../selectors/account-selectors.js'; const loadingStatusSelector = createLoadingStatusSelector(logInActionTypes); function TraditionalLoginForm(): React.Node { const inputDisabled = useSelector(loadingStatusSelector) === 'loading'; const loginExtraInfo = useSelector(webLogInExtraInfoSelector); const callLogIn = useServerCall(logIn); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); const primaryIdentityPublicKeys: ?OLMIdentityKeys = useSelector( state => state.cryptoStore.primaryIdentityKeys, ); const notificationIdentityPublicKeys: ?OLMIdentityKeys = useSelector( state => state.cryptoStore.notificationIdentityKeys, ); + const primaryAccount: ?PickledOLMAccount = useSelector( + state => state.cryptoStore.primaryAccount, + ); const usernameInputRef = React.useRef(); React.useEffect(() => { usernameInputRef.current?.focus(); }, []); const [username, setUsername] = React.useState(''); const onUsernameChange = React.useCallback(e => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setUsername(e.target.value); }, []); const onUsernameBlur = React.useCallback(() => { setUsername(untrimmedUsername => untrimmedUsername.trim()); }, []); const [password, setPassword] = React.useState(''); const onPasswordChange = React.useCallback(e => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setPassword(e.target.value); }, []); const [errorMessage, setErrorMessage] = React.useState(''); const logInAction = React.useCallback( async (extraInfo: LogInExtraInfo) => { try { invariant( primaryIdentityPublicKeys, 'primaryIdentityPublicKeys must be set in logInAction', ); invariant( notificationIdentityPublicKeys, 'notificationIdentityPublicKeys must be set in logInAction', ); + invariant(primaryAccount, 'primaryAccount must be set in logInAction'); + + const primaryOLMAccount = new olm.Account(); + primaryOLMAccount.unpickle( + primaryAccount.picklingKey, + primaryAccount.pickledAccount, + ); + + const payloadToBeSigned = JSON.stringify({ + primaryIdentityPublicKeys, + notificationIdentityPublicKeys, + }); + const signedIdentityKeysBlob: SignedIdentityKeysBlob = { + payload: payloadToBeSigned, + signature: primaryOLMAccount.sign(payloadToBeSigned), + }; + const result = await callLogIn({ ...extraInfo, username, password, logInActionSource: logInActionSources.logInFromWebForm, primaryIdentityPublicKey: primaryIdentityPublicKeys.ed25519, - primaryIdentityPublicKeys, - notificationIdentityPublicKeys, + signedIdentityKeysBlob, }); modalContext.popModal(); return result; } catch (e) { setUsername(''); setPassword(''); if (e.message === 'invalid_credentials') { setErrorMessage('incorrect username or password'); } else { setErrorMessage('unknown error'); } usernameInputRef.current?.focus(); throw e; } }, [ callLogIn, modalContext, notificationIdentityPublicKeys, password, + primaryAccount, primaryIdentityPublicKeys, username, ], ); const onSubmit = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); if (username.search(validEmailRegex) > -1) { setUsername(''); setErrorMessage('usernames only, not emails'); usernameInputRef.current?.focus(); return; } else if (username.search(oldValidUsernameRegex) === -1) { setUsername(''); setErrorMessage('alphanumeric usernames only'); usernameInputRef.current?.focus(); return; } else if (password === '') { setErrorMessage('password is empty'); usernameInputRef.current?.focus(); return; } const extraInfo = loginExtraInfo(); dispatchActionPromise( logInActionTypes, logInAction(extraInfo), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); }, [dispatchActionPromise, logInAction, loginExtraInfo, username, password], ); const loginButtonContent = React.useMemo(() => { if (inputDisabled) { return ; } return 'Sign in'; }, [inputDisabled]); const signInButtonColor = React.useMemo( () => ({ backgroundColor: '#6A20E3' }), [], ); return (

Sign in to Comm

Username
Password
{errorMessage}
); } export default TraditionalLoginForm;