diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -155,6 +155,8 @@ roleModificationResultValidator, } from './responders/thread-responders.js'; import { + keyserverAuthRequestInputValidator, + keyserverAuthResponder, userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, @@ -380,6 +382,12 @@ threadJoinResultValidator, baseLegalPolicies, ), + keyserver_auth: createJSONResponder( + keyserverAuthResponder, + keyserverAuthRequestInputValidator, + logInResponseValidator, + [], + ), leave_thread: createJSONResponder( threadLeaveResponder, leaveThreadRequestInputValidator, 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 @@ -2,6 +2,7 @@ import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; +import { getRustAPI } from 'rust-node-addon'; import { ErrorTypes, SiweMessage } from 'siwe'; import t, { type TInterface, type TUnion, type TEnums } from 'tcomb'; import bcrypt from 'twin-bcrypt'; @@ -14,6 +15,7 @@ import { rawThreadInfoValidator } from 'lib/permissions/minimally-encoded-thread-permissions-validators.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { + KeyserverAuthRequest, ResetPasswordRequest, LogOutResponse, RegisterResponse, @@ -24,7 +26,7 @@ UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, -} from 'lib/types/account-types.js'; +} from 'lib/types/account-types'; import { userSettingsTypes, notificationTypeValues, @@ -98,10 +100,15 @@ verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { + processOLMAccountCreation, createAccount, processSIWEAccountCreation, } from '../creators/account-creator.js'; -import { createAndPersistOlmSession } from '../creators/olm-session-creator.js'; +import { + createOlmSession, + persistFreshOlmSession, + createAndPersistOlmSession, +} 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'; @@ -133,7 +140,8 @@ 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 { getOlmUtility } from '../utils/olm-utils.js'; +import { fetchIdentityInfo } from '../user/identity.js'; +import { getOlmUtility, getContentSigningKey } from '../utils/olm-utils.js'; export const subscriptionUpdateRequestInputValidator: TInterface = tShape({ @@ -293,6 +301,8 @@ +socialProof?: ?SIWESocialProof, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, + +pickledContentOlmSession?: string, + +cookieHasBeenSet?: boolean, }; async function processSuccessfulLogin( @@ -306,6 +316,8 @@ socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, + pickledContentOlmSession, + cookieHasBeenSet, } = params; const request: LogInRequest = input; @@ -313,17 +325,25 @@ const deviceToken = request.deviceTokenUpdateRequest ? request.deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; - const [userViewerData, notAcknowledgedPolicies] = await Promise.all([ - createNewUserCookie(userID, { - platformDetails: request.platformDetails, - deviceToken, - socialProof, - signedIdentityKeysBlob, - }), + const setNewCookiePromise = (async () => { + if (cookieHasBeenSet) { + return; + } + const [userViewerData] = await Promise.all([ + createNewUserCookie(userID, { + platformDetails: request.platformDetails, + deviceToken, + socialProof, + signedIdentityKeysBlob, + }), + deleteCookie(viewer.cookieID), + ]); + viewer.setNewCookie(userViewerData); + })(); + const [notAcknowledgedPolicies] = await Promise.all([ fetchNotAcknowledgedPolicies(userID, baseLegalPolicies), - deleteCookie(viewer.cookieID), + setNewCookiePromise, ]); - viewer.setNewCookie(userViewerData); if ( notAcknowledgedPolicies.length && @@ -348,16 +368,28 @@ if (calendarQuery) { await setNewSession(viewer, calendarQuery, newServerTime); } - const olmSessionPromise = (async () => { + const olmNotifSessionPromise = (async () => { if ( - userViewerData.cookieID && + viewer.cookieID && initialNotificationsEncryptedMessage && signedIdentityKeysBlob ) { await createAndPersistOlmSession( initialNotificationsEncryptedMessage, 'notifications', - userViewerData.cookieID, + viewer.cookieID, + ); + } + })(); + // `pickledContentOlmSession` is created in `keyserverAuthResponder(...)` in + // order to authenticate the user. Here, we simply persist the session if it + // exists. + const olmContentSessionPromise = (async () => { + if (viewer.cookieID && pickledContentOlmSession) { + await persistFreshOlmSession( + pickledContentOlmSession, + 'content', + viewer.cookieID, ); } })(); @@ -387,7 +419,8 @@ entriesPromise, fetchKnownUserInfos(viewer), fetchLoggedInUserInfo(viewer), - olmSessionPromise, + olmNotifSessionPromise, + olmContentSessionPromise, ]); const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null; @@ -666,6 +699,127 @@ }); } +export const keyserverAuthRequestInputValidator: TInterface = + tShape({ + userID: t.String, + deviceID: t.String, + calendarQuery: entryQueryInputValidator, + deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), + platformDetails: tPlatformDetails, + watchedIDs: t.list(tID), + initialContentEncryptedMessage: t.String, + initialNotificationsEncryptedMessage: t.String, + doNotRegister: t.Boolean, + }); + +async function keyserverAuthResponder( + viewer: Viewer, + request: KeyserverAuthRequest, +): Promise { + const { + userID, + deviceID, + deviceTokenUpdateRequest, + platformDetails, + 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(), + fetchIdentityInfo(), + getRustAPI(), + ]); + 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) { + 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 is not required for auth + // and will be created later in `processSuccessfulLogin(...)`.) + const pickledContentOlmSessionPromise = createOlmSession( + initialContentEncryptedMessage, + 'content', + identityKeys.primaryIdentityPublicKeys.curve25519, + ); + + // 4. Create account with call to `processOLMAccountCreation(...)` + // if username does not correspond to an existing user. If we successfully + // create a new account, we set `cookieHasBeenSet` to true to avoid + // creating a new cookie again in `processSuccessfulLogin`. + const signedIdentityKeysBlob: SignedIdentityKeysBlob = { + payload: inboundKeysForUser.payload, + signature: inboundKeysForUser.payloadSignature, + }; + const olmAccountCreationPromise = (async () => { + if (existingUsername) { + return; + } + const olmAccountCreationRequest = { + userID, + username: username, + walletAddress: inboundKeysForUser.walletAddress, + signedIdentityKeysBlob, + calendarQuery, + deviceTokenUpdateRequest, + platformDetails, + }; + await processOLMAccountCreation(viewer, olmAccountCreationRequest); + })(); + + const [pickledContentOlmSession] = await Promise.all([ + pickledContentOlmSessionPromise, + olmAccountCreationPromise, + ]); + + // 5. Complete login with call to `processSuccessfulLogin(...)`. + return await processSuccessfulLogin({ + viewer, + input: request, + userID, + calendarQuery, + signedIdentityKeysBlob, + initialNotificationsEncryptedMessage, + pickledContentOlmSession, + cookieHasBeenSet: !existingUsername, + }); +} + export const updatePasswordRequestInputValidator: TInterface = tShape({ code: t.String, @@ -781,4 +935,5 @@ policyAcknowledgmentResponder, updateUserAvatarResponder, claimUsernameResponder, + keyserverAuthResponder, };