Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509921
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
38 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment