diff --git a/keyserver/src/creators/account-creator.js b/keyserver/src/creators/account-creator.js --- a/keyserver/src/creators/account-creator.js +++ b/keyserver/src/creators/account-creator.js @@ -94,9 +94,9 @@ ? request.deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const [id] = await createIDs('users', 1); - const newUserRow = [id, request.username, hash, time]; + const newUserRow = [id, request.username, hash, time, request.address]; const newUserQuery = SQL` - INSERT INTO users(id, username, hash, creation_time) + INSERT INTO users(id, username, hash, creation_time, ethereum_address) VALUES ${[newUserRow]} `; const [userViewerData] = await Promise.all([ diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -51,6 +51,7 @@ logInResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, + siweResponder, } from './responders/user-responders'; import { codeVerificationResponder } from './responders/verification-responders'; import { uploadDeletionResponder } from './uploads/uploads'; @@ -84,6 +85,7 @@ send_password_reset_email: sendPasswordResetEmailResponder, send_verification_email: sendVerificationEmailResponder, set_thread_unread_status: threadSetUnreadStatusResponder, + siwe: siweResponder, update_account: passwordUpdateResponder, update_activity: updateActivityResponder, update_calendar_query: calendarQueryUpdateResponder, diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,6 +1,7 @@ // @flow import invariant from 'invariant'; +import { SiweMessage, ErrorTypes } from 'siwe'; import t from 'tcomb'; import bcrypt from 'twin-bcrypt'; @@ -13,6 +14,8 @@ RegisterRequest, LogInResponse, LogInRequest, + SIWERequest, + SIWEResponse, UpdatePasswordRequest, UpdateUserSettingsRequest, } from 'lib/types/account-types'; @@ -193,55 +196,18 @@ source: t.maybe(t.enums.of(values(logInActionSources))), }); -async function logInResponder( - viewer: Viewer, - input: any, -): Promise { - await validateInput(viewer, logInRequestInputValidator, input); - const request: LogInRequest = input; - +async function logInQueries(viewer: Viewer, input: any, userId: string) { + const request: LogInRequest | SIWERequest = input; const calendarQuery = request.calendarQuery ? normalizeCalendarQuery(request.calendarQuery) : null; - const promises = {}; - if (calendarQuery) { - promises.verifyCalendarQueryThreadIDs = verifyCalendarQueryThreadIDs( - calendarQuery, - ); - } - const username = request.username ?? request.usernameOrEmail; - if (!username) { - throw new ServerError('invalid_parameters'); - } - const userQuery = SQL` - SELECT id, hash, username - FROM users - WHERE LCASE(username) = LCASE(${username}) - `; - promises.userQuery = dbQuery(userQuery); - const { - userQuery: [userResult], - } = await promiseAll(promises); - - if (userResult.length === 0) { - throw new ServerError('invalid_parameters'); - } - const userRow = userResult[0]; - if (!userRow.hash || !bcrypt.compareSync(request.password, userRow.hash)) { - if (hasMinCodeVersion(viewer.platformDetails, 99999)) { - throw new ServerError('invalid_parameters'); - } else { - throw new ServerError('invalid_credentials'); - } - } - const id = userRow.id.toString(); const newServerTime = Date.now(); const deviceToken = request.deviceTokenUpdateRequest ? request.deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const [userViewerData] = await Promise.all([ - createNewUserCookie(id, { + createNewUserCookie(userId, { platformDetails: request.platformDetails, deviceToken, }), @@ -290,6 +256,116 @@ return response; } +async function logInResponder( + viewer: Viewer, + input: any, +): Promise { + await validateInput(viewer, logInRequestInputValidator, input); + const request: LogInRequest = input; + + const calendarQuery = request.calendarQuery + ? normalizeCalendarQuery(request.calendarQuery) + : null; + const promises = {}; + if (calendarQuery) { + promises.verifyCalendarQueryThreadIDs = verifyCalendarQueryThreadIDs( + calendarQuery, + ); + } + const username = request.username ?? request.usernameOrEmail; + if (!username) { + throw new ServerError('invalid_parameters'); + } + const userQuery = SQL` + SELECT id, hash, username + FROM users + WHERE LCASE(username) = LCASE(${username}) + `; + promises.userQuery = dbQuery(userQuery); + const { + userQuery: [userResult], + } = await promiseAll(promises); + + if (userResult.length === 0) { + throw new ServerError('invalid_parameters'); + } + const userRow = userResult[0]; + if (!userRow.hash || !bcrypt.compareSync(request.password, userRow.hash)) { + if (hasMinCodeVersion(viewer.platformDetails, 99999)) { + throw new ServerError('invalid_parameters'); + } else { + throw new ServerError('invalid_credentials'); + } + } + const id = userRow.id.toString(); + + return await logInQueries(viewer, input, id); +} + +const siweRequestInputValidator = tShape({ + address: t.String, + signature: t.String, + message: t.String, + watchedIDs: t.list(t.String), + calendarQuery: t.maybe(entryQueryInputValidator), + deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), + platformDetails: tPlatformDetails, + source: t.maybe(t.enums.of(values(logInActionSources))), +}); + +async function siweResponder( + viewer: Viewer, + input: any, +): Promise { + await validateInput(viewer, siweRequestInputValidator, input); + const request: SIWERequest = input; + + const { address, message, signature } = request; + if (!address) { + throw new ServerError('invalid_parameters'); + } + + try { + const siweMessage = new SiweMessage(message); + await siweMessage.validate(signature); + } catch (error) { + switch (error) { + case ErrorTypes.EXPIRED_MESSAGE: + throw new ServerError('expired_signature', { status: 440 }); + case ErrorTypes.INVALID_SIGNATURE: + throw new ServerError('invalid_signature', { status: 422 }); + default: + throw new ServerError('oops', { status: 500 }); + } + } + // addresses are case insensitive to the network but not to sql queries + // lowercasing just in case + const userQuery = SQL` + SELECT id, hash, username + FROM users + WHERE LCASE(ethereum_address) = LCASE(${address}) + `; + const [userResult] = await dbQuery(userQuery); + if (userResult.length === 0) { + // breaking out variables for flow's sake + const { + message: noop, + signature: noop2, + watchedIDs: noop3, + ...rest + } = request; + return await createAccount(viewer, { + username: address, + password: signature, + ...rest, + }); + } + const userRow = userResult[0]; + const id = userRow.id.toString(); + + return await logInQueries(viewer, input, id); +} + const updatePasswordRequestInputValidator = tShape({ code: t.String, password: tPassword, @@ -339,4 +415,5 @@ logInResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, + siweResponder, }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -7,6 +7,8 @@ LogInResult, RegisterResult, RegisterInfo, + SIWEServerCall, + SIWEResult, UpdateUserSettingsRequest, } from '../types/account-types'; import type { GetSessionPublicKeysArgs } from '../types/request-types'; @@ -91,6 +93,33 @@ }; }; +const siweActionTypes = Object.freeze({ + started: 'SIWE_STARTED', + success: 'SIWE_SUCCESS', + failed: 'SIWE_FAILED', +}); +const siwe = ( + callServerEndpoint: CallServerEndpoint, +): ((siweInfo: SIWEServerCall) => Promise) => async siweInfo => { + const watchedIDs = threadWatcher.getWatchedIDs(); + const response = await callServerEndpoint( + 'siwe', + { + ...siweInfo, + watchedIDs, + platformDetails: getConfig().platformDetails, + }, + registerCallServerEndpointOptions, + ); + return { + currentUserInfo: response.currentUserInfo, + rawMessageInfos: response.rawMessageInfos, + threadInfos: response.cookieChange.threadInfos, + userInfos: response.cookieChange.userInfos, + calendarQuery: siweInfo.calendarQuery, + }; +}; + function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] { const merged = {}; for (const userInfoArray of userInfoArrays) { @@ -238,6 +267,8 @@ searchUsersActionTypes, setUserSettings, setUserSettingsActionTypes, + siwe, + siweActionTypes, updateSubscription, updateSubscriptionActionTypes, }; diff --git a/lib/reducers/data-loaded-reducer.js b/lib/reducers/data-loaded-reducer.js --- a/lib/reducers/data-loaded-reducer.js +++ b/lib/reducers/data-loaded-reducer.js @@ -5,6 +5,7 @@ deleteAccountActionTypes, logInActionTypes, registerActionTypes, + siweActionTypes, } from '../actions/user-actions'; import type { BaseAction } from '../types/redux-types'; import { setNewSessionActionType } from '../utils/action-utils'; @@ -15,7 +16,8 @@ ): boolean { if ( action.type === logInActionTypes.success || - action.type === registerActionTypes.success + action.type === registerActionTypes.success || + action.type === siweActionTypes.success ) { return true; } else if ( diff --git a/lib/reducers/user-reducer.js b/lib/reducers/user-reducer.js --- a/lib/reducers/user-reducer.js +++ b/lib/reducers/user-reducer.js @@ -13,6 +13,7 @@ logInActionTypes, registerActionTypes, setUserSettingsActionTypes, + siweActionTypes, } from '../actions/user-actions'; import type { BaseAction } from '../types/redux-types'; import { @@ -45,6 +46,7 @@ if ( action.type === logInActionTypes.success || action.type === registerActionTypes.success || + action.type === siweActionTypes.success || action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success ) { diff --git a/lib/types/account-types.js b/lib/types/account-types.js --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -50,6 +50,7 @@ +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, + +address?: ?string, }; export type RegisterResponse = { @@ -140,6 +141,26 @@ +logInActionSource: LogInActionSource, }; +export type SIWERequest = { + +address: string, + +message: string, + +signature: string, + +calendarQuery?: ?CalendarQuery, + +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, + +platformDetails: PlatformDetails, + +watchedIDs: $ReadOnlyArray, +}; + +export type SIWEServerCall = { + +address: string, + +message: string, + +signature: string, + ...LogInExtraInfo, +}; + +export type SIWEResponse = RegisterResponse | LogInResponse; + +export type SIWEResult = RegisterResult | LogInResult; export type UpdatePasswordRequest = { code: string, password: string, diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -24,6 +24,7 @@ CREATE_ACCOUNT: 'create_account', LOG_IN: 'log_in', UPDATE_PASSWORD: 'update_password', + SIWE: 'siwe', }); type SessionChangingEndpoint = $Values; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -7,6 +7,7 @@ LogInResult, RegisterResult, DefaultNotificationPayload, + SIWEResult, } from './account-types'; import type { ActivityUpdateSuccessPayload, @@ -818,6 +819,22 @@ +error: true, +payload: Error, +loadingInfo: LoadingInfo, + } + | { + +type: 'SIWE_STARTED', + +payload?: void, + +loadingInfo: LoadingInfo, + } + | { + +type: 'SIWE_SUCCESS', + +payload: SIWEResult, + +loadingInfo: LoadingInfo, + } + | { + +type: 'SIWE_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -320,12 +320,14 @@ const promptButtonsSize = Platform.OS === 'ios' ? 40 : 61; const logInContainerSize = 140; const registerPanelSize = Platform.OS === 'ios' ? 181 : 180; + const siwePanelSize = 250; const containerSize = add( headerHeight, cond(not(isPastPrompt(this.modeValue)), promptButtonsSize, 0), cond(eq(this.modeValue, modeNumbers['log-in']), logInContainerSize, 0), cond(eq(this.modeValue, modeNumbers['register']), registerPanelSize, 0), + cond(eq(this.modeValue, modeNumbers['siwe']), siwePanelSize, 0), ); const potentialPanelPaddingTop = divide( max(sub(this.contentHeight, this.keyboardHeightValue, containerSize), 0), diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -3,11 +3,11 @@ import Animated from 'react-native-reanimated'; import WebView from 'react-native-webview'; -import { registerActionTypes, register } from 'lib/actions/user-actions'; +import { siweActionTypes, siwe } from 'lib/actions/user-actions'; import type { - RegisterInfo, + SIWEServerCall, + SIWEResult, LogInExtraInfo, - RegisterResult, LogInStartingPayload, } from 'lib/types/account-types'; import { @@ -20,7 +20,6 @@ import { useSelector } from '../redux/redux-utils'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors'; import { defaultLandingURLPrefix } from '../utils/url-utils'; -import { setNativeCredentials } from './native-credentials'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; @@ -35,40 +34,40 @@ // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs - +registerAction: (registerInfo: RegisterInfo) => Promise, + +siweAction: (siweInfo: SIWEServerCall) => Promise, }; function SIWEPanel({ logInExtraInfo, dispatchActionPromise, - registerAction, + siweAction, }: Props) { const handleSIWE = React.useCallback( - ({ address, signature }) => { + ({ address, message, signature }) => { // this is all mocked from register-panel const extraInfo = logInExtraInfo(); dispatchActionPromise( - registerActionTypes, - registerAction({ - username: address, - password: signature, + siweActionTypes, + siweAction({ + address, + message, + signature, ...extraInfo, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); - setNativeCredentials({ username: address, password: signature }); }, - [logInExtraInfo, dispatchActionPromise, registerAction], + [logInExtraInfo, dispatchActionPromise, siweAction], ); const handleMessage = React.useCallback( event => { const { nativeEvent: { data }, } = event; - const { address, signature } = JSON.parse(data); + const { address, message, signature } = JSON.parse(data); if (address && signature) { - handleSIWE({ address, signature }); + handleSIWE({ address, message, signature }); } }, [handleSIWE], @@ -87,14 +86,14 @@ ); const dispatchActionPromise = useDispatchActionPromise(); - const callRegister = useServerCall(register); + const callSiwe = useServerCall(siwe); return ( ); }, diff --git a/native/utils/url-utils.js b/native/utils/url-utils.js --- a/native/utils/url-utils.js +++ b/native/utils/url-utils.js @@ -40,7 +40,13 @@ return getDevNodeServerURLFromHostname(hostname); } +// the SIWE message prompt hangs indefinitely if it doesn't originate from HTTPs +const canRainbowKitSignOverHTTPYet = false; + function getDevLandingURL(): string { + if (!canRainbowKitSignOverHTTPYet) { + return productionLandingURL; + } invariant(__DEV__, 'getDevLandingURL called from production'); const hostname = getDevServerHostname(); return getDevLandingURLFromHostname(hostname);