diff --git a/keyserver/src/responders/relationship-responders.js b/keyserver/src/responders/relationship-responders.js index b387da820..7b02b4709 100644 --- a/keyserver/src/responders/relationship-responders.js +++ b/keyserver/src/responders/relationship-responders.js @@ -1,39 +1,39 @@ // @flow import t, { type TInterface, type TUnion } from 'tcomb'; import { type TraditionalRelationshipRequest, type RelationshipErrors, traditionalRelationshipActionsList, type RelationshipRequest, farcasterRelationshipRequestValidator, } from 'lib/types/relationship-types.js'; -import { tShape } from 'lib/utils/validation-utils.js'; +import { tShape, tUserID } from 'lib/utils/validation-utils.js'; import type { Viewer } from '../session/viewer.js'; import { updateRelationships } from '../updaters/relationship-updaters.js'; export const traditionalRelationshipRequestValidator: TInterface = tShape({ action: t.enums.of( traditionalRelationshipActionsList, 'relationship action', ), - userIDs: t.list(t.String), + userIDs: t.list(tUserID), }); export const updateRelationshipInputValidator: TUnion = t.union([ traditionalRelationshipRequestValidator, farcasterRelationshipRequestValidator, ]); async function updateRelationshipsResponder( viewer: Viewer, request: RelationshipRequest, ): Promise { return await updateRelationships(viewer, request); } export { updateRelationshipsResponder }; diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js index fff31af79..e31e4b4f9 100644 --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -1,271 +1,272 @@ // @flow import t from 'tcomb'; import type { TInterface, TUnion } from 'tcomb'; import { userSurfacedPermissionValidator } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadDeletionRequest, type RoleChangeRequest, type ChangeThreadSettingsResult, type RemoveMembersRequest, type LeaveThreadRequest, type LeaveThreadResult, type UpdateThreadRequest, type ServerNewThreadRequest, type NewThreadResponse, type ServerThreadJoinRequest, type ThreadJoinResult, type ThreadFetchMediaResult, type ThreadFetchMediaRequest, type ToggleMessagePinRequest, type ToggleMessagePinResult, type RoleModificationRequest, type RoleModificationResult, type RoleDeletionRequest, type RoleDeletionResult, } from 'lib/types/thread-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { values } from 'lib/utils/objects.js'; import { tShape, tNumEnum, tColor, tPassword, tID, + tUserID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { modifyRole } from '../creators/role-creator.js'; import { createThread } from '../creators/thread-creator.js'; import { deleteRole } from '../deleters/role-deleters.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { fetchMediaForThread } from '../fetchers/upload-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateRole, removeMembers, leaveThread, updateThread, joinThread, toggleMessagePinForThread, } from '../updaters/thread-updaters.js'; export const threadDeletionRequestInputValidator: TInterface = tShape({ threadID: tID, accountPassword: t.maybe(tPassword), }); async function threadDeletionResponder( viewer: Viewer, request: ThreadDeletionRequest, ): Promise { return await deleteThread(viewer, request); } export const roleChangeRequestInputValidator: TInterface = tShape({ threadID: tID, - memberIDs: t.list(t.String), + memberIDs: t.list(tUserID), role: t.refinement(tID, str => { if (str.indexOf('|') !== -1) { str = str.split('|')[1]; } const int = parseInt(str, 10); return String(int) === str && int > 0; }), }); async function roleUpdateResponder( viewer: Viewer, request: RoleChangeRequest, ): Promise { return await updateRole(viewer, request); } export const removeMembersRequestInputValidator: TInterface = tShape({ threadID: tID, - memberIDs: t.list(t.String), + memberIDs: t.list(tUserID), }); async function memberRemovalResponder( viewer: Viewer, request: RemoveMembersRequest, ): Promise { return await removeMembers(viewer, request); } export const leaveThreadRequestInputValidator: TInterface = tShape({ threadID: tID, }); async function threadLeaveResponder( viewer: Viewer, request: LeaveThreadRequest, ): Promise { return await leaveThread(viewer, request); } export const updateThreadRequestInputValidator: TInterface = tShape({ threadID: tID, changes: tShape({ type: t.maybe(tNumEnum(values(threadTypes))), name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), - newMemberIDs: t.maybe(t.list(t.String)), + newMemberIDs: t.maybe(t.list(tUserID)), avatar: t.maybe(updateUserAvatarRequestValidator), }), accountPassword: t.maybe(tPassword), }); async function threadUpdateResponder( viewer: Viewer, request: UpdateThreadRequest, ): Promise { return await updateThread(viewer, request); } const threadRequestValidationShape = { name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), - initialMemberIDs: t.maybe(t.list(t.String)), + initialMemberIDs: t.maybe(t.list(tUserID)), calendarQuery: t.maybe(entryQueryInputValidator), }; const newThreadRequestInputValidator: TUnion = t.union([ tShape({ type: tNumEnum([threadTypes.SIDEBAR]), sourceMessageID: tID, ...threadRequestValidationShape, }), tShape({ type: tNumEnum([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.PERSONAL, threadTypes.LOCAL, threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]), ...threadRequestValidationShape, }), ]); async function threadCreationResponder( viewer: Viewer, request: ServerNewThreadRequest, ): Promise { return await createThread(viewer, request, { silentlyFailMembers: request.type === threadTypes.SIDEBAR, }); } export const joinThreadRequestInputValidator: TInterface = tShape({ threadID: tID, calendarQuery: t.maybe(entryQueryInputValidator), inviteLinkSecret: t.maybe(t.String), }); async function threadJoinResponder( viewer: Viewer, request: ServerThreadJoinRequest, ): Promise { if (request.calendarQuery) { await verifyCalendarQueryThreadIDs(request.calendarQuery); } return await joinThread(viewer, request); } export const threadFetchMediaRequestInputValidator: TInterface = tShape({ threadID: tID, limit: t.Number, offset: t.Number, }); async function threadFetchMediaResponder( viewer: Viewer, request: ThreadFetchMediaRequest, ): Promise { return await fetchMediaForThread(viewer, request); } export const toggleMessagePinRequestInputValidator: TInterface = tShape({ messageID: tID, action: t.enums.of(['pin', 'unpin']), }); async function toggleMessagePinResponder( viewer: Viewer, request: ToggleMessagePinRequest, ): Promise { return await toggleMessagePinForThread(viewer, request); } export const roleModificationRequestInputValidator: TUnion = t.union([ tShape({ community: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['create_role']), }), tShape({ community: tID, existingRoleID: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['edit_role']), }), ]); async function roleModificationResponder( viewer: Viewer, request: RoleModificationRequest, ): Promise { return await modifyRole(viewer, request); } export const roleDeletionRequestInputValidator: TInterface = tShape({ community: tID, roleID: tID, }); async function roleDeletionResponder( viewer: Viewer, request: RoleDeletionRequest, ): Promise { return await deleteRole(viewer, request); } export { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadJoinResponder, threadFetchMediaResponder, newThreadRequestInputValidator, toggleMessagePinResponder, roleModificationResponder, roleDeletionResponder, }; diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js index e73607121..f6d716d69 100644 --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,927 +1,928 @@ // @flow import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { getRustAPI } from 'rust-node-addon'; import { SiweErrorType, SiweMessage } from 'siwe'; import t, { type TInterface } from 'tcomb'; import bcrypt from 'twin-bcrypt'; import { baseLegalPolicies, policies, policyTypes, } from 'lib/facts/policies.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { KeyserverAuthRequest, ResetPasswordRequest, LogOutResponse, RegisterResponse, RegisterRequest, ServerLogInResponse, LogInRequest, UpdatePasswordRequest, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, } from 'lib/types/account-types'; import { userSettingsTypes, notificationTypeValues, authActionSources, } from 'lib/types/account-types.js'; import { type ClientAvatar, type UpdateUserAvatarResponse, type UpdateUserAvatarRequest, } from 'lib/types/avatar-types.js'; import type { ReservedUsernameMessage, IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; import type { DeviceType, DeviceTokenUpdateRequest, PlatformDetails, } from 'lib/types/device-types'; import { type CalendarQuery, type FetchEntryInfosBase, } from 'lib/types/entry-types.js'; import { defaultNumberPerThread } from 'lib/types/message-types.js'; import type { SIWEAuthRequest, SIWEMessage, SIWESocialProof, } from 'lib/types/siwe-types.js'; import { type SubscriptionUpdateRequest, type SubscriptionUpdateResponse, } from 'lib/types/subscription-types.js'; import { type PasswordUpdate } from 'lib/types/user-types.js'; import { identityKeysBlobValidator, signedIdentityKeysBlobValidator, } from 'lib/utils/crypto-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { ignorePromiseRejections } from 'lib/utils/promises.js'; import { getPublicKeyFromSIWEStatement, isValidSIWEMessage, isValidSIWEStatementWithPublicKey, primaryIdentityPublicKeyRegex, } from 'lib/utils/siwe-utils.js'; import { tShape, tPlatformDetails, tPassword, tEmail, tOldValidUsername, tRegex, tID, + tUserID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, newEntryQueryInputValidator, normalizeCalendarQuery, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { createAndSendReservedUsernameMessage, processAccountCreationCommon, createAccount, } from '../creators/account-creator.js'; import createIDs from '../creators/id-creator.js'; import { createOlmSession, persistFreshOlmSession, } from '../creators/olm-session-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { deleteAccount } from '../deleters/account-deleters.js'; import { deleteCookie } from '../deleters/cookie-deleters.js'; import { checkAndInvalidateSIWENonceEntry } from '../deleters/siwe-nonce-deleters.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchKnownUserInfos, fetchLoggedInUserInfo, fetchUserIDForEthereumAddress, fetchUsername, } from '../fetchers/user-fetchers.js'; import { createNewAnonymousCookie, createNewUserCookie, setNewSession, } from '../session/cookies.js'; import type { Viewer } from '../session/viewer.js'; import { passwordUpdater, checkAndSendVerificationEmail, checkAndSendPasswordResetEmail, updatePassword, updateUserSettings, updateUserAvatar, } from '../updaters/account-updaters.js'; import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js'; import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js'; import { verifyUserLoggedIn } from '../user/login.js'; import { getOlmUtility, getContentSigningKey } from '../utils/olm-utils.js'; export const subscriptionUpdateRequestInputValidator: TInterface = tShape({ threadID: tID, updatedFields: tShape({ pushNotifs: t.maybe(t.Boolean), home: t.maybe(t.Boolean), }), }); async function userSubscriptionUpdateResponder( viewer: Viewer, request: SubscriptionUpdateRequest, ): Promise { const threadSubscription = await userSubscriptionUpdater(viewer, request); return { threadSubscription, }; } export const accountUpdateInputValidator: TInterface = tShape({ updatedFields: tShape({ email: t.maybe(tEmail), password: t.maybe(tPassword), }), currentPassword: tPassword, }); async function passwordUpdateResponder( viewer: Viewer, request: PasswordUpdate, ): Promise { await passwordUpdater(viewer, request); } async function sendVerificationEmailResponder(viewer: Viewer): Promise { await checkAndSendVerificationEmail(viewer); } export const resetPasswordRequestInputValidator: TInterface = tShape({ usernameOrEmail: t.union([tEmail, tOldValidUsername]), }); async function sendPasswordResetEmailResponder( viewer: Viewer, request: ResetPasswordRequest, ): Promise { await checkAndSendPasswordResetEmail(request); } async function logOutResponder(viewer: Viewer): Promise { if (viewer.loggedIn) { const [anonymousViewerData] = await Promise.all([ createNewAnonymousCookie({ platformDetails: viewer.platformDetails, deviceToken: viewer.deviceToken, }), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(anonymousViewerData); } return { currentUserInfo: { anonymous: true, }, }; } async function accountDeletionResponder( viewer: Viewer, ): Promise { const result = await deleteAccount(viewer); invariant(result, 'deleteAccount should return result if handed request'); return result; } type OldDeviceTokenUpdateRequest = { +deviceType?: ?DeviceType, +deviceToken: string, }; const deviceTokenUpdateRequestInputValidator = tShape({ deviceType: t.maybe(t.enums.of(['ios', 'android'])), deviceToken: t.String, }); export const registerRequestInputValidator: TInterface = tShape({ username: t.String, email: t.maybe(tEmail), password: tPassword, calendarQuery: t.maybe(newEntryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, // We include `primaryIdentityPublicKey` to avoid breaking // old clients, but we no longer do anything with it. primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), initialNotificationsEncryptedMessage: t.maybe(t.String), }); async function accountCreationResponder( viewer: Viewer, request: RegisterRequest, ): Promise { const { signedIdentityKeysBlob } = request; if (signedIdentityKeysBlob) { const identityKeys: IdentityKeysBlob = JSON.parse( signedIdentityKeysBlob.payload, ); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } return await createAccount(viewer, request); } type ProcessSuccessfulLoginParams = { +viewer: Viewer, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +userID: string, +calendarQuery: ?CalendarQuery, +socialProof?: ?SIWESocialProof, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, +pickledContentOlmSession?: string, +shouldMarkPoliciesAsAcceptedAfterCookieCreation?: boolean, }; async function processSuccessfulLogin( params: ProcessSuccessfulLoginParams, ): Promise { const { viewer, deviceTokenUpdateRequest, platformDetails, watchedIDs, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, pickledContentOlmSession, shouldMarkPoliciesAsAcceptedAfterCookieCreation, } = params; // Olm sessions have to be created before createNewUserCookie is called, // to avoid propagating a user cookie in case session creation fails const olmNotifSession = await (async () => { if (initialNotificationsEncryptedMessage && signedIdentityKeysBlob) { return await createOlmSession( initialNotificationsEncryptedMessage, 'notifications', ); } return null; })(); const newServerTime = Date.now(); const deviceToken = deviceTokenUpdateRequest ? deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const setNewCookiePromise = (async () => { const [userViewerData] = await Promise.all([ createNewUserCookie(userID, { platformDetails, deviceToken, socialProof, signedIdentityKeysBlob, }), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(userViewerData); })(); const policiesCheckAndUpdate = (async () => { if (shouldMarkPoliciesAsAcceptedAfterCookieCreation) { await setNewCookiePromise; await viewerAcknowledgmentUpdater( viewer, policyTypes.tosAndPrivacyPolicy, ); } return await fetchNotAcknowledgedPolicies(userID, baseLegalPolicies); })(); const [notAcknowledgedPolicies] = await Promise.all([ policiesCheckAndUpdate, setNewCookiePromise, ]); if ( notAcknowledgedPolicies.length && hasMinCodeVersion(viewer.platformDetails, { native: 181 }) ) { const currentUserInfo = await fetchLoggedInUserInfo(viewer); return { notAcknowledgedPolicies, currentUserInfo: currentUserInfo, rawMessageInfos: [], truncationStatuses: {}, userInfos: [], rawEntryInfos: [], serverTime: 0, cookieChange: { threadInfos: {}, userInfos: [], }, }; } if (calendarQuery) { await setNewSession(viewer, calendarQuery, newServerTime); } const persistOlmNotifSessionPromise = (async () => { if (olmNotifSession && viewer.cookieID) { await persistFreshOlmSession( olmNotifSession, 'notifications', viewer.cookieID, ); } })(); // `pickledContentOlmSession` is created in `keyserverAuthResponder(...)` in // order to authenticate the user. Here, we simply persist the session if it // exists. const persistOlmContentSessionPromise = (async () => { if (viewer.cookieID && pickledContentOlmSession) { await persistFreshOlmSession( pickledContentOlmSession, 'content', viewer.cookieID, ); } })(); const threadCursors: { [string]: null } = {}; for (const watchedThreadID of watchedIDs) { threadCursors[watchedThreadID] = null; } const messageSelectionCriteria = { threadCursors, joinedThreads: true }; const entriesPromise: Promise = (async () => { if (!calendarQuery) { return undefined; } return await fetchEntryInfos(viewer, [calendarQuery]); })(); const [ threadsResult, messagesResult, entriesResult, userInfos, currentUserInfo, ] = await Promise.all([ fetchThreadInfos(viewer), fetchMessageInfos(viewer, messageSelectionCriteria, defaultNumberPerThread), entriesPromise, fetchKnownUserInfos(viewer), fetchLoggedInUserInfo(viewer), persistOlmNotifSessionPromise, persistOlmContentSessionPromise, ]); const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null; const response: ServerLogInResponse = { currentUserInfo, rawMessageInfos: messagesResult.rawMessageInfos, truncationStatuses: messagesResult.truncationStatuses, serverTime: newServerTime, userInfos: values(userInfos), cookieChange: { threadInfos: threadsResult.threadInfos, userInfos: [], }, }; if (rawEntryInfos) { return { ...response, rawEntryInfos, }; } return response; } export const logInRequestInputValidator: TInterface = tShape({ username: t.maybe(t.String), usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])), password: tPassword, watchedIDs: t.list(tID), calendarQuery: t.maybe(entryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, source: t.maybe(t.enums.of(values(authActionSources))), // We include `primaryIdentityPublicKey` to avoid breaking // old clients, but we no longer do anything with it. primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), initialNotificationsEncryptedMessage: t.maybe(t.String), }); async function logInResponder( viewer: Viewer, request: LogInRequest, ): Promise { let identityKeys: ?IdentityKeysBlob; const { signedIdentityKeysBlob, initialNotificationsEncryptedMessage } = request; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } const calendarQuery = request.calendarQuery ? normalizeCalendarQuery(request.calendarQuery) : null; const verifyCalendarQueryThreadIDsPromise = (async () => { if (calendarQuery) { await verifyCalendarQueryThreadIDs(calendarQuery); } })(); const username = request.username ?? request.usernameOrEmail; if (!username) { if (hasMinCodeVersion(viewer.platformDetails, { native: 150 })) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userQuery = SQL` SELECT id, hash, username FROM users WHERE LCASE(username) = LCASE(${username}) `; const userQueryPromise = dbQuery(userQuery); const [[userResult]] = await Promise.all([ userQueryPromise, verifyCalendarQueryThreadIDsPromise, ]); if (userResult.length === 0) { if (hasMinCodeVersion(viewer.platformDetails, { native: 150 })) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userRow = userResult[0]; if (!userRow.hash || !bcrypt.compareSync(request.password, userRow.hash)) { throw new ServerError('invalid_credentials'); } const id = userRow.id.toString(); return await processSuccessfulLogin({ viewer, platformDetails: request.platformDetails, deviceTokenUpdateRequest: request.deviceTokenUpdateRequest, watchedIDs: request.watchedIDs, userID: id, calendarQuery, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, }); } export const siweAuthRequestInputValidator: TInterface = tShape({ signature: t.String, message: t.String, calendarQuery: entryQueryInputValidator, deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, watchedIDs: t.list(tID), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), initialNotificationsEncryptedMessage: t.maybe(t.String), doNotRegister: t.maybe(t.Boolean), }); async function siweAuthResponder( viewer: Viewer, request: SIWEAuthRequest, ): Promise { const { message, signature, deviceTokenUpdateRequest, platformDetails, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, doNotRegister, watchedIDs, } = request; const calendarQuery = normalizeCalendarQuery(request.calendarQuery); // 1. Ensure that `message` is a well formed Comm SIWE Auth message. const siweMessage: SIWEMessage = new SiweMessage(message); if (!isValidSIWEMessage(siweMessage)) { throw new ServerError('invalid_parameters'); } // 2. Check if there's already a user for this ETH address. // Verify calendarQuery. const [existingUserID] = await Promise.all([ fetchUserIDForEthereumAddress(siweMessage.address), verifyCalendarQueryThreadIDs(calendarQuery), ]); if (!existingUserID && doNotRegister) { throw new ServerError('account_does_not_exist'); } // 3. Ensure that the `nonce` exists in the `siwe_nonces` table // AND hasn't expired. If those conditions are met, delete the entry to // ensure that the same `nonce` can't be re-used in a future request. const wasNonceCheckedAndInvalidated = await checkAndInvalidateSIWENonceEntry( siweMessage.nonce, ); if (!wasNonceCheckedAndInvalidated) { throw new ServerError('invalid_parameters'); } // 4. Validate SIWEMessage signature and handle possible errors. try { await siweMessage.verify({ signature }); } catch (error) { if (error === SiweErrorType.EXPIRED_MESSAGE) { // Thrown when the `expirationTime` is present and in the past. throw new ServerError('expired_message'); } else if (error === SiweErrorType.INVALID_SIGNATURE) { // Thrown when the `validate()` function can't verify the message. throw new ServerError('invalid_signature'); } else { throw new ServerError('unknown_error'); } } // 5. Pull `primaryIdentityPublicKey` out from SIWEMessage `statement`. // We expect it to be included for BOTH native and web clients. const { statement } = siweMessage; const primaryIdentityPublicKey = statement && isValidSIWEStatementWithPublicKey(statement) ? getPublicKeyFromSIWEStatement(statement) : null; if (!primaryIdentityPublicKey) { throw new ServerError('invalid_siwe_statement_public_key'); } // 6. Verify `signedIdentityKeysBlob.payload` with included `signature` // if `signedIdentityKeysBlob` was included in the `SIWEAuthRequest`. let identityKeys: ?IdentityKeysBlob; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } // 7. Ensure that `primaryIdentityPublicKeys.ed25519` matches SIWE // statement `primaryIdentityPublicKey` if `identityKeys` exists. if ( identityKeys && identityKeys.primaryIdentityPublicKeys.ed25519 !== primaryIdentityPublicKey ) { throw new ServerError('primary_public_key_mismatch'); } // 8. Construct `SIWESocialProof` object with the stringified // SIWEMessage and the corresponding signature. const socialProof: SIWESocialProof = { siweMessage: siweMessage.toMessage(), siweMessageSignature: signature, }; // 9. Create account if address does not correspond to an existing user. const userID = await (async () => { if (existingUserID) { return existingUserID; } const time = Date.now(); const [id] = await createIDs('users', 1); const newUserRow = [id, siweMessage.address, siweMessage.address, time]; const newUserQuery = SQL` INSERT INTO users(id, username, ethereum_address, creation_time) VALUES ${[newUserRow]} `; await dbQuery(newUserQuery); return id; })(); // 10. Complete login with call to `processSuccessfulLogin(...)`. const result = await processSuccessfulLogin({ viewer, platformDetails, deviceTokenUpdateRequest, watchedIDs, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, shouldMarkPoliciesAsAcceptedAfterCookieCreation: !existingUserID, }); // 11. Create threads with call to `processAccountCreationCommon(...)`, // if the account has just been registered. Also, set the username as // reserved. if (!existingUserID) { await processAccountCreationCommon(viewer); ignorePromiseRejections( createAndSendReservedUsernameMessage([ { username: siweMessage.address, userID }, ]), ); } return result; } export const keyserverAuthRequestInputValidator: TInterface = tShape({ - userID: t.String, + userID: tUserID, deviceID: t.String, calendarQuery: entryQueryInputValidator, deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, watchedIDs: t.list(tID), initialContentEncryptedMessage: t.String, initialNotificationsEncryptedMessage: t.String, doNotRegister: t.Boolean, source: t.maybe(t.enums.of(values(authActionSources))), }); async function keyserverAuthResponder( viewer: Viewer, request: KeyserverAuthRequest, ): Promise { const { userID, deviceID, initialContentEncryptedMessage, initialNotificationsEncryptedMessage, doNotRegister, } = request; const calendarQuery = normalizeCalendarQuery(request.calendarQuery); // 1. Check if there's already a user for this userID. Simultaneously, get // info for identity service auth. const [existingUsername, authDeviceID, identityInfo, rustAPI] = await Promise.all([ fetchUsername(userID), getContentSigningKey(), verifyUserLoggedIn(), getRustAPI(), verifyCalendarQueryThreadIDs(calendarQuery), ]); if (!existingUsername && doNotRegister) { throw new ServerError('account_does_not_exist'); } if (!identityInfo) { throw new ServerError('account_not_registered_on_identity_service'); } // 2. Get user's keys from identity service. let inboundKeysForUser; try { inboundKeysForUser = await rustAPI.getInboundKeysForUserDevice( identityInfo.userId, authDeviceID, identityInfo.accessToken, userID, deviceID, ); } catch (e) { console.log(e); throw new ServerError('failed_to_retrieve_inbound_keys'); } const username = inboundKeysForUser.username ? inboundKeysForUser.username : inboundKeysForUser.walletAddress; if (!username) { throw new ServerError('user_identifier_missing'); } const identityKeys: IdentityKeysBlob = JSON.parse(inboundKeysForUser.payload); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } // 3. Create content olm session. (The notif session was introduced first and // as such is created in legacy auth responders as well. It's factored out // into in the shared utility `processSuccessfulLogin(...)`.) const pickledContentOlmSessionPromise = createOlmSession( initialContentEncryptedMessage, 'content', identityKeys.primaryIdentityPublicKeys.curve25519, ); // 4. Create account if username does not correspond to an existing user. const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: inboundKeysForUser.payload, signature: inboundKeysForUser.payloadSignature, }; const olmAccountCreationPromise = (async () => { if (existingUsername) { return; } const time = Date.now(); const newUserRow = [ userID, username, inboundKeysForUser.walletAddress, time, ]; const newUserQuery = SQL` INSERT INTO users(id, username, ethereum_address, creation_time) VALUES ${[newUserRow]} `; await dbQuery(newUserQuery); })(); const [pickledContentOlmSession] = await Promise.all([ pickledContentOlmSessionPromise, olmAccountCreationPromise, ]); // 5. Complete login with call to `processSuccessfulLogin(...)`. const result = await processSuccessfulLogin({ viewer, platformDetails: request.platformDetails, deviceTokenUpdateRequest: request.deviceTokenUpdateRequest, watchedIDs: request.watchedIDs, userID, calendarQuery, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, pickledContentOlmSession, shouldMarkPoliciesAsAcceptedAfterCookieCreation: !existingUsername, }); // 6. Create threads with call to `processAccountCreationCommon(...)`, // if the account has just been registered. if (!existingUsername) { await processAccountCreationCommon(viewer); } return result; } export const updatePasswordRequestInputValidator: TInterface = tShape({ code: t.String, password: tPassword, watchedIDs: t.list(tID), calendarQuery: t.maybe(entryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, }); async function oldPasswordUpdateResponder( viewer: Viewer, request: UpdatePasswordRequest, ): Promise { if (request.calendarQuery) { request.calendarQuery = normalizeCalendarQuery(request.calendarQuery); } return await updatePassword(viewer, request); } export const updateUserSettingsInputValidator: TInterface = tShape({ name: t.irreducible( userSettingsTypes.DEFAULT_NOTIFICATIONS, x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS, ), data: t.enums.of(notificationTypeValues), }); async function updateUserSettingsResponder( viewer: Viewer, request: UpdateUserSettingsRequest, ): Promise { await updateUserSettings(viewer, request); } export const policyAcknowledgmentRequestInputValidator: TInterface = tShape({ policy: t.maybe(t.enums.of(policies)), }); async function policyAcknowledgmentResponder( viewer: Viewer, request: PolicyAcknowledgmentRequest, ): Promise { await viewerAcknowledgmentUpdater(viewer, request.policy); } async function updateUserAvatarResponder( viewer: Viewer, request: UpdateUserAvatarRequest, ): Promise { return await updateUserAvatar(viewer, request); } async function claimUsernameResponder( viewer: Viewer, ): Promise { const [username, accountInfo] = await Promise.all([ fetchUsername(viewer.userID), fetchOlmAccount('content'), ]); if (!username) { throw new ServerError('invalid_credentials'); } const issuedAt = new Date().toISOString(); const reservedUsernameMessage: ReservedUsernameMessage = { statement: 'This user is the owner of the following username and user ID', payload: { username, userID: viewer.userID, }, issuedAt, }; const message = JSON.stringify(reservedUsernameMessage); const signature = accountInfo.account.sign(message); return { message, signature }; } export { userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, claimUsernameResponder, keyserverAuthResponder, };