Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/creators/account-creator.js b/keyserver/src/creators/account-creator.js
index d91bf9e72..3cb3894dc 100644
--- a/keyserver/src/creators/account-creator.js
+++ b/keyserver/src/creators/account-creator.js
@@ -1,287 +1,253 @@
// @flow
import { getRustAPI } from 'rust-node-addon';
import bcrypt from 'twin-bcrypt';
import bots from 'lib/facts/bots.js';
import genesis from 'lib/facts/genesis.js';
import { policyTypes } from 'lib/facts/policies.js';
import { validUsernameRegex } from 'lib/shared/account-utils.js';
import type {
RegisterResponse,
RegisterRequest,
} from 'lib/types/account-types.js';
import type {
UserDetail,
ReservedUsernameMessage,
} from 'lib/types/crypto-types.js';
import { messageTypes } from 'lib/types/message-types-enum.js';
+import type { RawMessageInfo } from 'lib/types/message-types.js';
import { threadTypes } from 'lib/types/thread-types-enum.js';
import { ServerError } from 'lib/utils/errors.js';
import { values } from 'lib/utils/objects.js';
import { ignorePromiseRejections } from 'lib/utils/promises.js';
import { reservedUsernamesSet } from 'lib/utils/reserved-users.js';
import { isValidEthereumAddress } from 'lib/utils/siwe-utils.js';
import createIDs from './id-creator.js';
import createMessages from './message-creator.js';
import {
persistFreshOlmSession,
createOlmSession,
} from './olm-session-creator.js';
import {
createThread,
createPrivateThread,
privateThreadDescription,
} from './thread-creator.js';
import { dbQuery, SQL } from '../database/database.js';
import { deleteCookie } from '../deleters/cookie-deleters.js';
import { fetchThreadInfos } from '../fetchers/thread-fetchers.js';
import {
fetchLoggedInUserInfo,
fetchKnownUserInfos,
} from '../fetchers/user-fetchers.js';
import { verifyCalendarQueryThreadIDs } from '../responders/entry-responders.js';
import { searchForUser } from '../search/users.js';
import { createNewUserCookie, setNewSession } from '../session/cookies.js';
import { createScriptViewer } from '../session/scripts.js';
import type { Viewer } from '../session/viewer.js';
import { fetchOlmAccount } from '../updaters/olm-account-updater.js';
import { updateThread } from '../updaters/thread-updaters.js';
import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js';
import { thisKeyserverAdmin } from '../user/identity.js';
const { commbot } = bots;
const ashoatMessages = [
'welcome to Comm!',
'as you inevitably discover bugs, have feature requests, or design ' +
'suggestions, feel free to message them to me in the app.',
];
const privateMessages = [privateThreadDescription];
async function createAccount(
viewer: Viewer,
request: RegisterRequest,
): Promise<RegisterResponse> {
if (request.password.trim() === '') {
throw new ServerError('empty_password');
}
if (
request.username.search(validUsernameRegex) === -1 ||
isValidEthereumAddress(request.username.toLowerCase())
) {
throw new ServerError('invalid_username');
}
- const promises = [searchForUser(request.username), thisKeyserverAdmin()];
+ const promises = [searchForUser(request.username)];
const {
calendarQuery,
signedIdentityKeysBlob,
initialNotificationsEncryptedMessage,
} = request;
if (calendarQuery) {
promises.push(verifyCalendarQueryThreadIDs(calendarQuery));
}
- const [existingUser, admin] = await Promise.all(promises);
+ const [existingUser] = await Promise.all(promises);
if (reservedUsernamesSet.has(request.username.toLowerCase())) {
throw new ServerError('username_reserved');
}
if (existingUser) {
throw new ServerError('username_taken');
}
// 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) {
return await createOlmSession(
initialNotificationsEncryptedMessage,
'notifications',
);
}
return null;
})();
const hash = bcrypt.hashSync(request.password);
const time = Date.now();
const deviceToken = request.deviceTokenUpdateRequest
? request.deviceTokenUpdateRequest.deviceToken
: viewer.deviceToken;
const [id] = await createIDs('users', 1);
const newUserRow = [id, request.username, hash, time];
const newUserQuery = SQL`
INSERT INTO users(id, username, hash, creation_time)
VALUES ${[newUserRow]}
`;
const [userViewerData] = await Promise.all([
createNewUserCookie(id, {
platformDetails: request.platformDetails,
deviceToken,
signedIdentityKeysBlob,
}),
deleteCookie(viewer.cookieID),
dbQuery(newUserQuery),
]);
viewer.setNewCookie(userViewerData);
if (calendarQuery) {
await setNewSession(viewer, calendarQuery, 0);
}
const persistOlmNotifSessionPromise = (async () => {
if (olmNotifSession && userViewerData.cookieID) {
await persistFreshOlmSession(
olmNotifSession,
'notifications',
userViewerData.cookieID,
);
}
})();
await Promise.all([
- updateThread(
- createScriptViewer(admin.id),
- {
- threadID: genesis().id,
- changes: { newMemberIDs: [id] },
- },
- { forceAddMembers: true, silenceMessages: true, ignorePermissions: true },
- ),
viewerAcknowledgmentUpdater(viewer, policyTypes.tosAndPrivacyPolicy),
persistOlmNotifSessionPromise,
]);
- const [privateThreadResult, ashoatThreadResult] = await Promise.all([
- createPrivateThread(viewer),
- createThread(
- viewer,
- {
- type: threadTypes.PERSONAL,
- initialMemberIDs: [admin.id],
- },
- { forceAddMembers: true },
- ),
- ]);
- const ashoatThreadID = ashoatThreadResult.newThreadID;
- const privateThreadID = privateThreadResult.newThreadID;
+ const rawMessageInfos = await sendMessagesOnAccountCreation(viewer);
- let messageTime = Date.now();
- const ashoatMessageDatas = ashoatMessages.map(message => ({
- type: messageTypes.TEXT,
- threadID: ashoatThreadID,
- creatorID: admin.id,
- time: messageTime++,
- text: message,
- }));
- const privateMessageDatas = privateMessages.map(message => ({
- type: messageTypes.TEXT,
- threadID: privateThreadID,
- creatorID: commbot.userID,
- time: messageTime++,
- text: message,
- }));
- const messageDatas = [...ashoatMessageDatas, ...privateMessageDatas];
- const [messageInfos, threadsResult, userInfos, currentUserInfo] =
- await Promise.all([
- createMessages(viewer, messageDatas),
- fetchThreadInfos(viewer),
- fetchKnownUserInfos(viewer),
- fetchLoggedInUserInfo(viewer),
- ]);
- const rawMessageInfos = [
- ...ashoatThreadResult.newMessageInfos,
- ...privateThreadResult.newMessageInfos,
- ...messageInfos,
- ];
+ const [threadsResult, userInfos, currentUserInfo] = await Promise.all([
+ fetchThreadInfos(viewer),
+ fetchKnownUserInfos(viewer),
+ fetchLoggedInUserInfo(viewer),
+ ]);
ignorePromiseRejections(
createAndSendReservedUsernameMessage([
{ username: request.username, userID: id },
]),
);
return {
id,
rawMessageInfos,
currentUserInfo,
cookieChange: {
threadInfos: threadsResult.threadInfos,
userInfos: values(userInfos),
},
};
}
-async function processAccountCreationCommon(viewer: Viewer) {
+async function sendMessagesOnAccountCreation(
+ viewer: Viewer,
+): Promise<RawMessageInfo[]> {
const admin = await thisKeyserverAdmin();
await updateThread(
createScriptViewer(admin.id),
{
threadID: genesis().id,
changes: { newMemberIDs: [viewer.userID] },
},
{ forceAddMembers: true, silenceMessages: true, ignorePermissions: true },
);
const [privateThreadResult, ashoatThreadResult] = await Promise.all([
createPrivateThread(viewer),
createThread(
viewer,
{
type: threadTypes.PERSONAL,
initialMemberIDs: [admin.id],
},
{ forceAddMembers: true },
),
]);
const ashoatThreadID = ashoatThreadResult.newThreadID;
const privateThreadID = privateThreadResult.newThreadID;
let messageTime = Date.now();
const ashoatMessageDatas = ashoatMessages.map(message => ({
type: messageTypes.TEXT,
threadID: ashoatThreadID,
creatorID: admin.id,
time: messageTime++,
text: message,
}));
const privateMessageDatas = privateMessages.map(message => ({
type: messageTypes.TEXT,
threadID: privateThreadID,
creatorID: commbot.userID,
time: messageTime++,
text: message,
}));
const messageDatas = [...ashoatMessageDatas, ...privateMessageDatas];
- await Promise.all([createMessages(viewer, messageDatas)]);
+ const messageInfos = await createMessages(viewer, messageDatas);
+
+ return [
+ ...ashoatThreadResult.newMessageInfos,
+ ...privateThreadResult.newMessageInfos,
+ ...messageInfos,
+ ];
}
async function createAndSendReservedUsernameMessage(
payload: $ReadOnlyArray<UserDetail>,
) {
const issuedAt = new Date().toISOString();
const reservedUsernameMessage: ReservedUsernameMessage = {
statement: 'Add the following usernames to reserved list',
payload,
issuedAt,
};
const stringifiedMessage = JSON.stringify(reservedUsernameMessage);
const [rustAPI, accountInfo] = await Promise.all([
getRustAPI(),
fetchOlmAccount('content'),
]);
const signature = accountInfo.account.sign(stringifiedMessage);
await rustAPI.addReservedUsernames(stringifiedMessage, signature);
}
export {
createAccount,
- processAccountCreationCommon,
+ sendMessagesOnAccountCreation,
createAndSendReservedUsernameMessage,
};
diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js
index f6d716d69..edb504216 100644
--- a/keyserver/src/responders/user-responders.js
+++ b/keyserver/src/responders/user-responders.js
@@ -1,928 +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';
+} from 'lib/types/account-types.js';
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,
+ sendMessagesOnAccountCreation,
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<SubscriptionUpdateRequest> =
tShape<SubscriptionUpdateRequest>({
threadID: tID,
updatedFields: tShape({
pushNotifs: t.maybe(t.Boolean),
home: t.maybe(t.Boolean),
}),
});
async function userSubscriptionUpdateResponder(
viewer: Viewer,
request: SubscriptionUpdateRequest,
): Promise<SubscriptionUpdateResponse> {
const threadSubscription = await userSubscriptionUpdater(viewer, request);
return {
threadSubscription,
};
}
export const accountUpdateInputValidator: TInterface<PasswordUpdate> =
tShape<PasswordUpdate>({
updatedFields: tShape({
email: t.maybe(tEmail),
password: t.maybe(tPassword),
}),
currentPassword: tPassword,
});
async function passwordUpdateResponder(
viewer: Viewer,
request: PasswordUpdate,
): Promise<void> {
await passwordUpdater(viewer, request);
}
async function sendVerificationEmailResponder(viewer: Viewer): Promise<void> {
await checkAndSendVerificationEmail(viewer);
}
export const resetPasswordRequestInputValidator: TInterface<ResetPasswordRequest> =
tShape<ResetPasswordRequest>({
usernameOrEmail: t.union([tEmail, tOldValidUsername]),
});
async function sendPasswordResetEmailResponder(
viewer: Viewer,
request: ResetPasswordRequest,
): Promise<void> {
await checkAndSendPasswordResetEmail(request);
}
async function logOutResponder(viewer: Viewer): Promise<LogOutResponse> {
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<LogOutResponse> {
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<OldDeviceTokenUpdateRequest>({
deviceType: t.maybe(t.enums.of(['ios', 'android'])),
deviceToken: t.String,
});
export const registerRequestInputValidator: TInterface<RegisterRequest> =
tShape<RegisterRequest>({
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<RegisterResponse> {
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<string>,
+userID: string,
+calendarQuery: ?CalendarQuery,
+socialProof?: ?SIWESocialProof,
+signedIdentityKeysBlob?: ?SignedIdentityKeysBlob,
+initialNotificationsEncryptedMessage?: string,
+pickledContentOlmSession?: string,
+shouldMarkPoliciesAsAcceptedAfterCookieCreation?: boolean,
};
async function processSuccessfulLogin(
params: ProcessSuccessfulLoginParams,
): Promise<ServerLogInResponse> {
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<?FetchEntryInfosBase> = (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<LogInRequest> =
tShape<LogInRequest>({
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<ServerLogInResponse> {
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<SIWEAuthRequest> =
tShape<SIWEAuthRequest>({
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<ServerLogInResponse> {
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(...)`,
+ // 11. Create messages with call to `sendMessagesOnAccountCreation(...)`,
// if the account has just been registered. Also, set the username as
// reserved.
if (!existingUserID) {
- await processAccountCreationCommon(viewer);
+ await sendMessagesOnAccountCreation(viewer);
ignorePromiseRejections(
createAndSendReservedUsernameMessage([
{ username: siweMessage.address, userID },
]),
);
}
return result;
}
export const keyserverAuthRequestInputValidator: TInterface<KeyserverAuthRequest> =
tShape<KeyserverAuthRequest>({
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<ServerLogInResponse> {
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(...)`,
+ // 6. Create messages with call to `sendMessagesOnAccountCreation(...)`,
// if the account has just been registered.
if (!existingUsername) {
- await processAccountCreationCommon(viewer);
+ await sendMessagesOnAccountCreation(viewer);
}
return result;
}
export const updatePasswordRequestInputValidator: TInterface<UpdatePasswordRequest> =
tShape<UpdatePasswordRequest>({
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<ServerLogInResponse> {
if (request.calendarQuery) {
request.calendarQuery = normalizeCalendarQuery(request.calendarQuery);
}
return await updatePassword(viewer, request);
}
export const updateUserSettingsInputValidator: TInterface<UpdateUserSettingsRequest> =
tShape<UpdateUserSettingsRequest>({
name: t.irreducible(
userSettingsTypes.DEFAULT_NOTIFICATIONS,
x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS,
),
data: t.enums.of(notificationTypeValues),
});
async function updateUserSettingsResponder(
viewer: Viewer,
request: UpdateUserSettingsRequest,
): Promise<void> {
await updateUserSettings(viewer, request);
}
export const policyAcknowledgmentRequestInputValidator: TInterface<PolicyAcknowledgmentRequest> =
tShape<PolicyAcknowledgmentRequest>({
policy: t.maybe(t.enums.of(policies)),
});
async function policyAcknowledgmentResponder(
viewer: Viewer,
request: PolicyAcknowledgmentRequest,
): Promise<void> {
await viewerAcknowledgmentUpdater(viewer, request.policy);
}
async function updateUserAvatarResponder(
viewer: Viewer,
request: UpdateUserAvatarRequest,
): Promise<?ClientAvatar | UpdateUserAvatarResponse> {
return await updateUserAvatar(viewer, request);
}
async function claimUsernameResponder(
viewer: Viewer,
): Promise<ClaimUsernameResponse> {
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,
};

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 9:25 AM (16 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690712
Default Alt Text
(38 KB)

Event Timeline