Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/creators/account-creator.js b/keyserver/src/creators/account-creator.js
index 30dfa54a0..677bd5d87 100644
--- a/keyserver/src/creators/account-creator.js
+++ b/keyserver/src/creators/account-creator.js
@@ -1,322 +1,324 @@
// @flow
import invariant from 'invariant';
import { getRustAPI } from 'rust-node-addon';
import bcrypt from 'twin-bcrypt';
import ashoat from 'lib/facts/ashoat.js';
import bots from 'lib/facts/bots.js';
import genesis from 'lib/facts/genesis.js';
import { policyTypes } from 'lib/facts/policies.js';
import {
validUsernameRegex,
oldValidUsernameRegex,
} from 'lib/shared/account-utils.js';
import { hasMinCodeVersion } from 'lib/shared/version-utils.js';
import type {
RegisterResponse,
RegisterRequest,
} from 'lib/types/account-types.js';
import type {
SignedIdentityKeysBlob,
IdentityKeysBlob,
} from 'lib/types/crypto-types.js';
import type {
PlatformDetails,
DeviceTokenUpdateRequest,
} from 'lib/types/device-types.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import { messageTypes } from 'lib/types/message-types.js';
import type { SIWESocialProof } from 'lib/types/siwe-types.js';
import { threadTypes } from 'lib/types/thread-types.js';
import { ServerError } from 'lib/utils/errors.js';
import { values } from 'lib/utils/objects.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 {
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 { handleAsyncPromise } from '../responders/handlers.js';
import { createNewUserCookie, setNewSession } from '../session/cookies.js';
import { createScriptViewer } from '../session/scripts.js';
import type { Viewer } from '../session/viewer.js';
import { updateThread } from '../updaters/thread-updaters.js';
import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.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');
}
const usernameRegex = hasMinCodeVersion(viewer.platformDetails, 69)
? validUsernameRegex
: oldValidUsernameRegex;
if (request.username.search(usernameRegex) === -1) {
throw new ServerError('invalid_username');
}
const usernameQuery = SQL`
SELECT COUNT(id) AS count
FROM users
WHERE LCASE(username) = LCASE(${request.username})
`;
const promises = [dbQuery(usernameQuery)];
const { calendarQuery, signedIdentityKeysBlob } = request;
if (calendarQuery) {
promises.push(verifyCalendarQueryThreadIDs(calendarQuery));
}
const [[usernameResult]] = await Promise.all(promises);
if (
reservedUsernamesSet.has(request.username.toLowerCase()) ||
isValidEthereumAddress(request.username.toLowerCase())
) {
if (hasMinCodeVersion(viewer.platformDetails, 120)) {
throw new ServerError('username_reserved');
} else {
throw new ServerError('username_taken');
}
}
if (usernameResult[0].count !== 0) {
throw new ServerError('username_taken');
}
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, rustAPI] = await Promise.all([
+ const [userViewerData] = await Promise.all([
createNewUserCookie(id, {
platformDetails: request.platformDetails,
deviceToken,
signedIdentityKeysBlob,
}),
- getRustAPI(),
deleteCookie(viewer.cookieID),
dbQuery(newUserQuery),
]);
viewer.setNewCookie(userViewerData);
if (calendarQuery) {
await setNewSession(viewer, calendarQuery, 0);
}
await Promise.all([
updateThread(
createScriptViewer(ashoat.id),
{
threadID: genesis.id,
changes: { newMemberIDs: [id] },
},
{ forceAddMembers: true, silenceMessages: true, ignorePermissions: true },
),
viewerAcknowledgmentUpdater(viewer, policyTypes.tosAndPrivacyPolicy),
]);
const [privateThreadResult, ashoatThreadResult] = await Promise.all([
createPrivateThread(viewer, request.username),
createThread(
viewer,
{
type: threadTypes.PERSONAL,
initialMemberIDs: [ashoat.id],
},
{ forceAddMembers: true },
),
]);
const ashoatThreadID = ashoatThreadResult.newThreadInfo
? ashoatThreadResult.newThreadInfo.id
: ashoatThreadResult.newThreadID;
const privateThreadID = privateThreadResult.newThreadInfo
? privateThreadResult.newThreadInfo.id
: privateThreadResult.newThreadID;
invariant(
ashoatThreadID && privateThreadID,
'createThread should return either newThreadInfo or newThreadID',
);
let messageTime = Date.now();
const ashoatMessageDatas = ashoatMessages.map(message => ({
type: messageTypes.TEXT,
threadID: ashoatThreadID,
creatorID: ashoat.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,
];
if (signedIdentityKeysBlob) {
const identityKeys: IdentityKeysBlob = JSON.parse(
signedIdentityKeysBlob.payload,
);
handleAsyncPromise(
- rustAPI.registerUser(
- id,
- identityKeys.primaryIdentityPublicKeys.ed25519,
- request.username,
- request.password,
- signedIdentityKeysBlob,
- ),
+ (async () => {
+ const rustAPI = await getRustAPI();
+ await rustAPI.registerUser(
+ id,
+ identityKeys.primaryIdentityPublicKeys.ed25519,
+ request.username,
+ request.password,
+ signedIdentityKeysBlob,
+ );
+ })(),
);
}
return {
id,
rawMessageInfos,
currentUserInfo,
cookieChange: {
threadInfos: threadsResult.threadInfos,
userInfos: values(userInfos),
},
};
}
export type ProcessSIWEAccountCreationRequest = {
+address: string,
+calendarQuery: CalendarQuery,
+deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest,
+platformDetails: PlatformDetails,
+socialProof: SIWESocialProof,
+signedIdentityKeysBlob?: ?SignedIdentityKeysBlob,
};
// Note: `processSIWEAccountCreation(...)` assumes that the validity of
// `ProcessSIWEAccountCreationRequest` was checked at call site.
async function processSIWEAccountCreation(
viewer: Viewer,
request: ProcessSIWEAccountCreationRequest,
): Promise<string> {
const { calendarQuery, signedIdentityKeysBlob } = request;
await verifyCalendarQueryThreadIDs(calendarQuery);
const time = Date.now();
const deviceToken = request.deviceTokenUpdateRequest
? request.deviceTokenUpdateRequest.deviceToken
: viewer.deviceToken;
const [id] = await createIDs('users', 1);
const newUserRow = [id, request.address, request.address, time];
const newUserQuery = SQL`
INSERT INTO users(id, username, ethereum_address, creation_time)
VALUES ${[newUserRow]}
`;
const [userViewerData] = await Promise.all([
createNewUserCookie(id, {
platformDetails: request.platformDetails,
deviceToken,
socialProof: request.socialProof,
signedIdentityKeysBlob,
}),
deleteCookie(viewer.cookieID),
dbQuery(newUserQuery),
]);
viewer.setNewCookie(userViewerData);
await setNewSession(viewer, calendarQuery, 0);
await Promise.all([
updateThread(
createScriptViewer(ashoat.id),
{
threadID: genesis.id,
changes: { newMemberIDs: [id] },
},
{ forceAddMembers: true, silenceMessages: true, ignorePermissions: true },
),
viewerAcknowledgmentUpdater(viewer, policyTypes.tosAndPrivacyPolicy),
]);
const [privateThreadResult, ashoatThreadResult] = await Promise.all([
createPrivateThread(viewer, request.address),
createThread(
viewer,
{
type: threadTypes.PERSONAL,
initialMemberIDs: [ashoat.id],
},
{ forceAddMembers: true },
),
]);
const ashoatThreadID = ashoatThreadResult.newThreadInfo
? ashoatThreadResult.newThreadInfo.id
: ashoatThreadResult.newThreadID;
const privateThreadID = privateThreadResult.newThreadInfo
? privateThreadResult.newThreadInfo.id
: privateThreadResult.newThreadID;
invariant(
ashoatThreadID && privateThreadID,
'createThread should return either newThreadInfo or newThreadID',
);
let messageTime = Date.now();
const ashoatMessageDatas = ashoatMessages.map(message => ({
type: messageTypes.TEXT,
threadID: ashoatThreadID,
creatorID: ashoat.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)]);
return id;
}
export { createAccount, processSIWEAccountCreation };
diff --git a/keyserver/src/deleters/account-deleters.js b/keyserver/src/deleters/account-deleters.js
index 09e55c34a..e044833ca 100644
--- a/keyserver/src/deleters/account-deleters.js
+++ b/keyserver/src/deleters/account-deleters.js
@@ -1,146 +1,149 @@
// @flow
import { getRustAPI } from 'rust-node-addon';
import bcrypt from 'twin-bcrypt';
import type {
LogOutResponse,
DeleteAccountRequest,
} from 'lib/types/account-types.js';
import { updateTypes } from 'lib/types/update-types.js';
import type { UserInfo } from 'lib/types/user-types.js';
import { ServerError } from 'lib/utils/errors.js';
import { values } from 'lib/utils/objects.js';
import { promiseAll } from 'lib/utils/promises.js';
import { createUpdates } from '../creators/update-creator.js';
import { dbQuery, SQL } from '../database/database.js';
import { fetchKnownUserInfos } from '../fetchers/user-fetchers.js';
import { rescindPushNotifs } from '../push/rescind.js';
import { handleAsyncPromise } from '../responders/handlers.js';
import { createNewAnonymousCookie } from '../session/cookies.js';
import type { Viewer } from '../session/viewer.js';
async function deleteAccount(
viewer: Viewer,
request?: DeleteAccountRequest,
): Promise<?LogOutResponse> {
if (!viewer.loggedIn || (!request && !viewer.isScriptViewer)) {
throw new ServerError('not_logged_in');
}
if (request) {
const hashQuery = SQL`SELECT hash FROM users WHERE id = ${viewer.userID}`;
const [result] = await dbQuery(hashQuery);
if (result.length === 0) {
throw new ServerError('internal_error');
}
const row = result[0];
const requestPasswordConsistentWithDB = !!row.hash === !!request.password;
const shouldValidatePassword = !!row.hash;
if (
!requestPasswordConsistentWithDB ||
(shouldValidatePassword &&
!bcrypt.compareSync(request.password, row.hash))
) {
throw new ServerError('invalid_credentials');
}
}
const deletedUserID = viewer.userID;
await rescindPushNotifs(SQL`n.user = ${deletedUserID}`, SQL`NULL`);
- const rustAPIPromise = getRustAPI();
const knownUserInfos = await fetchKnownUserInfos(viewer);
const usersToUpdate = values(knownUserInfos).filter(
userID => userID !== deletedUserID,
);
// TODO: if this results in any orphaned orgs, convert them to chats
const deletionQuery = SQL`
START TRANSACTION;
DELETE FROM users WHERE id = ${deletedUserID};
DELETE FROM ids WHERE id = ${deletedUserID};
DELETE c, i
FROM cookies c
LEFT JOIN ids i ON i.id = c.id
WHERE c.user = ${deletedUserID};
DELETE FROM memberships WHERE user = ${deletedUserID};
DELETE FROM focused WHERE user = ${deletedUserID};
DELETE n, i
FROM notifications n
LEFT JOIN ids i ON i.id = n.id
WHERE n.user = ${deletedUserID};
DELETE u, i
FROM updates u
LEFT JOIN ids i ON i.id = u.id
WHERE u.user = ${deletedUserID};
DELETE s, i
FROM sessions s
LEFT JOIN ids i ON i.id = s.id
WHERE s.user = ${deletedUserID};
DELETE r, i
FROM reports r
LEFT JOIN ids i ON i.id = r.id
WHERE r.user = ${deletedUserID};
DELETE FROM relationships_undirected WHERE user1 = ${deletedUserID};
DELETE FROM relationships_undirected WHERE user2 = ${deletedUserID};
DELETE FROM relationships_directed WHERE user1 = ${deletedUserID};
DELETE FROM relationships_directed WHERE user2 = ${deletedUserID};
COMMIT;
`;
const promises = {};
promises.deletion = dbQuery(deletionQuery, { multipleStatements: true });
if (request) {
promises.anonymousViewerData = createNewAnonymousCookie({
platformDetails: viewer.platformDetails,
deviceToken: viewer.deviceToken,
});
}
- promises.rustAPI = rustAPIPromise;
- const { anonymousViewerData, rustAPI } = await promiseAll(promises);
- handleAsyncPromise(rustAPI.deleteUser(deletedUserID));
+ const { anonymousViewerData } = await promiseAll(promises);
+ handleAsyncPromise(
+ (async () => {
+ const rustAPI = await getRustAPI();
+ await rustAPI.deleteUser(deletedUserID);
+ })(),
+ );
if (anonymousViewerData) {
viewer.setNewCookie(anonymousViewerData);
}
const deletionUpdatesPromise = createAccountDeletionUpdates(
usersToUpdate,
deletedUserID,
);
if (request) {
handleAsyncPromise(deletionUpdatesPromise);
} else {
await deletionUpdatesPromise;
}
if (request) {
return {
currentUserInfo: {
id: viewer.id,
anonymous: true,
},
};
}
return null;
}
async function createAccountDeletionUpdates(
knownUserInfos: $ReadOnlyArray<UserInfo>,
deletedUserID: string,
): Promise<void> {
const time = Date.now();
const updateDatas = [];
for (const userInfo of knownUserInfos) {
const { id: userID } = userInfo;
updateDatas.push({
type: updateTypes.DELETE_ACCOUNT,
userID,
time,
deletedUserID,
});
}
await createUpdates(updateDatas);
}
export { deleteAccount };
diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js
index 07c1be5d5..558cf94e9 100644
--- a/keyserver/src/responders/user-responders.js
+++ b/keyserver/src/responders/user-responders.js
@@ -1,672 +1,674 @@
// @flow
import invariant from 'invariant';
import { getRustAPI } from 'rust-node-addon';
import { ErrorTypes, SiweMessage } from 'siwe';
import t from 'tcomb';
import bcrypt from 'twin-bcrypt';
import { baseLegalPolicies, policies } from 'lib/facts/policies.js';
import { hasMinCodeVersion } from 'lib/shared/version-utils.js';
import type {
ResetPasswordRequest,
LogOutResponse,
DeleteAccountRequest,
RegisterResponse,
RegisterRequest,
LogInResponse,
LogInRequest,
UpdatePasswordRequest,
UpdateUserSettingsRequest,
PolicyAcknowledgmentRequest,
} from 'lib/types/account-types.js';
import {
userSettingsTypes,
notificationTypeValues,
logInActionSources,
} from 'lib/types/account-types.js';
import type {
IdentityKeysBlob,
SignedIdentityKeysBlob,
} from 'lib/types/crypto-types.js';
import type { CalendarQuery } 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,
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 { promiseAll } from 'lib/utils/promises.js';
import {
getPublicKeyFromSIWEStatement,
isValidSIWEMessage,
isValidSIWEStatementWithPublicKey,
primaryIdentityPublicKeyRegex,
} from 'lib/utils/siwe-utils.js';
import {
tShape,
tPlatformDetails,
tPassword,
tEmail,
tOldValidUsername,
tRegex,
} from 'lib/utils/validation-utils.js';
import {
entryQueryInputValidator,
newEntryQueryInputValidator,
normalizeCalendarQuery,
verifyCalendarQueryThreadIDs,
} from './entry-responders.js';
import { handleAsyncPromise } from './handlers.js';
import {
createAccount,
processSIWEAccountCreation,
} from '../creators/account-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,
} from '../fetchers/user-fetchers.js';
import {
createNewAnonymousCookie,
createNewUserCookie,
setNewSession,
} from '../session/cookies.js';
import type { Viewer } from '../session/viewer.js';
import {
accountUpdater,
checkAndSendVerificationEmail,
checkAndSendPasswordResetEmail,
updatePassword,
updateUserSettings,
} from '../updaters/account-updaters.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 type { OLMUtility } from '../utils/olm-utils.js';
import { validateInput } from '../utils/validation-utils.js';
const subscriptionUpdateRequestInputValidator = tShape({
threadID: t.String,
updatedFields: tShape({
pushNotifs: t.maybe(t.Boolean),
home: t.maybe(t.Boolean),
}),
});
async function userSubscriptionUpdateResponder(
viewer: Viewer,
input: any,
): Promise<SubscriptionUpdateResponse> {
const request: SubscriptionUpdateRequest = input;
await validateInput(viewer, subscriptionUpdateRequestInputValidator, request);
const threadSubscription = await userSubscriptionUpdater(viewer, request);
return { threadSubscription };
}
const accountUpdateInputValidator = tShape({
updatedFields: tShape({
email: t.maybe(tEmail),
password: t.maybe(tPassword),
}),
currentPassword: tPassword,
});
async function passwordUpdateResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: PasswordUpdate = input;
await validateInput(viewer, accountUpdateInputValidator, request);
await accountUpdater(viewer, request);
}
async function sendVerificationEmailResponder(viewer: Viewer): Promise<void> {
await validateInput(viewer, null, null);
await checkAndSendVerificationEmail(viewer);
}
const resetPasswordRequestInputValidator = tShape({
usernameOrEmail: t.union([tEmail, tOldValidUsername]),
});
async function sendPasswordResetEmailResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: ResetPasswordRequest = input;
await validateInput(viewer, resetPasswordRequestInputValidator, request);
await checkAndSendPasswordResetEmail(request);
}
async function logOutResponder(viewer: Viewer): Promise<LogOutResponse> {
await validateInput(viewer, null, null);
if (viewer.loggedIn) {
const [anonymousViewerData] = await Promise.all([
createNewAnonymousCookie({
platformDetails: viewer.platformDetails,
deviceToken: viewer.deviceToken,
}),
deleteCookie(viewer.cookieID),
]);
viewer.setNewCookie(anonymousViewerData);
}
return {
currentUserInfo: {
id: viewer.id,
anonymous: true,
},
};
}
const deleteAccountRequestInputValidator = tShape({
password: t.maybe(tPassword),
});
async function accountDeletionResponder(
viewer: Viewer,
input: any,
): Promise<LogOutResponse> {
const request: DeleteAccountRequest = input;
await validateInput(viewer, deleteAccountRequestInputValidator, request);
const result = await deleteAccount(viewer, request);
invariant(result, 'deleteAccount should return result if handed request');
return result;
}
const deviceTokenUpdateRequestInputValidator = tShape({
deviceType: t.maybe(t.enums.of(['ios', 'android'])),
deviceToken: t.String,
});
const registerRequestInputValidator = 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),
});
async function accountCreationResponder(
viewer: Viewer,
input: any,
): Promise<RegisterResponse> {
const request: RegisterRequest = input;
await validateInput(viewer, registerRequestInputValidator, request);
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,
+input: any,
+userID: string,
+calendarQuery: ?CalendarQuery,
+socialProof?: ?SIWESocialProof,
+signedIdentityKeysBlob?: ?SignedIdentityKeysBlob,
};
async function processSuccessfulLogin(
params: ProcessSuccessfulLoginParams,
): Promise<LogInResponse> {
const {
viewer,
input,
userID,
calendarQuery,
socialProof,
signedIdentityKeysBlob,
} = params;
const request: LogInRequest = input;
const newServerTime = Date.now();
const deviceToken = request.deviceTokenUpdateRequest
? request.deviceTokenUpdateRequest.deviceToken
: viewer.deviceToken;
const [userViewerData, notAcknowledgedPolicies] = await Promise.all([
createNewUserCookie(userID, {
platformDetails: request.platformDetails,
deviceToken,
socialProof,
signedIdentityKeysBlob,
}),
fetchNotAcknowledgedPolicies(userID, baseLegalPolicies),
deleteCookie(viewer.cookieID),
]);
viewer.setNewCookie(userViewerData);
if (
notAcknowledgedPolicies.length &&
hasMinCodeVersion(viewer.platformDetails, 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 threadCursors = {};
for (const watchedThreadID of request.watchedIDs) {
threadCursors[watchedThreadID] = null;
}
const messageSelectionCriteria = { threadCursors, joinedThreads: true };
const [
threadsResult,
messagesResult,
entriesResult,
userInfos,
currentUserInfo,
] = await Promise.all([
fetchThreadInfos(viewer),
fetchMessageInfos(viewer, messageSelectionCriteria, defaultNumberPerThread),
calendarQuery ? fetchEntryInfos(viewer, [calendarQuery]) : undefined,
fetchKnownUserInfos(viewer),
fetchLoggedInUserInfo(viewer),
]);
const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null;
const response: LogInResponse = {
currentUserInfo,
rawMessageInfos: messagesResult.rawMessageInfos,
truncationStatuses: messagesResult.truncationStatuses,
serverTime: newServerTime,
userInfos: values(userInfos),
cookieChange: {
threadInfos: threadsResult.threadInfos,
userInfos: [],
},
};
if (rawEntryInfos) {
return {
...response,
rawEntryInfos,
};
}
return response;
}
const logInRequestInputValidator = tShape({
username: t.maybe(t.String),
usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])),
password: tPassword,
watchedIDs: t.list(t.String),
calendarQuery: t.maybe(entryQueryInputValidator),
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
source: t.maybe(t.enums.of(values(logInActionSources))),
// 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),
});
async function logInResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, logInRequestInputValidator, input);
const request: LogInRequest = input;
let identityKeys: ?IdentityKeysBlob;
const { signedIdentityKeysBlob } = 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 promises = {};
if (calendarQuery) {
promises.verifyCalendarQueryThreadIDs =
verifyCalendarQueryThreadIDs(calendarQuery);
}
const username = request.username ?? request.usernameOrEmail;
if (!username) {
if (hasMinCodeVersion(viewer.platformDetails, 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})
`;
promises.userQuery = dbQuery(userQuery);
- promises.rustAPI = getRustAPI();
const {
userQuery: [userResult],
- rustAPI,
} = await promiseAll(promises);
if (userResult.length === 0) {
if (hasMinCodeVersion(viewer.platformDetails, 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();
if (identityKeys && signedIdentityKeysBlob) {
+ const constIdentityKeys = identityKeys;
handleAsyncPromise(
- rustAPI.loginUserPake(
- id,
- identityKeys.primaryIdentityPublicKeys.ed25519,
- request.password,
- signedIdentityKeysBlob,
- ),
+ (async () => {
+ const rustAPI = await getRustAPI();
+ await rustAPI.loginUserPake(
+ id,
+ constIdentityKeys.primaryIdentityPublicKeys.ed25519,
+ request.password,
+ signedIdentityKeysBlob,
+ );
+ })(),
);
}
return await processSuccessfulLogin({
viewer,
input,
userID: id,
calendarQuery,
signedIdentityKeysBlob,
});
}
const siweAuthRequestInputValidator = tShape({
signature: t.String,
message: t.String,
calendarQuery: entryQueryInputValidator,
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
watchedIDs: t.list(t.String),
signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator),
});
async function siweAuthResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, siweAuthRequestInputValidator, input);
const request: SIWEAuthRequest = input;
const {
message,
signature,
deviceTokenUpdateRequest,
platformDetails,
signedIdentityKeysBlob,
} = 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. 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');
}
// 3. Validate SIWEMessage signature and handle possible errors.
try {
await siweMessage.validate(signature);
} catch (error) {
if (error === ErrorTypes.EXPIRED_MESSAGE) {
// Thrown when the `expirationTime` is present and in the past.
throw new ServerError('expired_message');
} else if (error === ErrorTypes.INVALID_SIGNATURE) {
// Thrown when the `validate()` function can't verify the message.
throw new ServerError('invalid_signature');
} else if (error === ErrorTypes.MALFORMED_SESSION) {
// Thrown when some required field is missing.
throw new ServerError('malformed_session');
} else {
throw new ServerError('unknown_error');
}
}
// 4. 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');
}
// 5. 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');
}
}
// 6. Ensure that `primaryIdentityPublicKeys.ed25519` matches SIWE
// statement `primaryIdentityPublicKey` if `identityKeys` exists.
if (
identityKeys &&
identityKeys.primaryIdentityPublicKeys.ed25519 !== primaryIdentityPublicKey
) {
throw new ServerError('primary_public_key_mismatch');
}
// 7. Construct `SIWESocialProof` object with the stringified
// SIWEMessage and the corresponding signature.
const socialProof: SIWESocialProof = {
siweMessage: siweMessage.toMessage(),
siweMessageSignature: signature,
};
// 8. Create account with call to `processSIWEAccountCreation(...)`
// if address does not correspond to an existing user.
let userID = await fetchUserIDForEthereumAddress(siweMessage.address);
if (!userID) {
const siweAccountCreationRequest = {
address: siweMessage.address,
calendarQuery,
deviceTokenUpdateRequest,
platformDetails,
socialProof,
};
userID = await processSIWEAccountCreation(
viewer,
siweAccountCreationRequest,
);
}
// 9. Try to double-write SIWE account info to the Identity service
const userIDCopy = userID;
if (identityKeys && signedIdentityKeysBlob) {
const identityKeysCopy = identityKeys;
handleAsyncPromise(
(async () => {
const rustAPI = await getRustAPI();
- rustAPI.loginUserWallet(
+ await rustAPI.loginUserWallet(
userIDCopy,
identityKeysCopy.primaryIdentityPublicKeys.ed25519,
siweMessage.toMessage(),
signature,
signedIdentityKeysBlob,
JSON.stringify(socialProof),
);
})(),
);
}
// 10. Complete login with call to `processSuccessfulLogin(...)`.
return await processSuccessfulLogin({
viewer,
input,
userID,
calendarQuery,
socialProof,
signedIdentityKeysBlob,
});
}
const updatePasswordRequestInputValidator = tShape({
code: t.String,
password: tPassword,
watchedIDs: t.list(t.String),
calendarQuery: t.maybe(entryQueryInputValidator),
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
});
async function oldPasswordUpdateResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, updatePasswordRequestInputValidator, input);
const request: UpdatePasswordRequest = input;
if (request.calendarQuery) {
request.calendarQuery = normalizeCalendarQuery(request.calendarQuery);
}
return await updatePassword(viewer, request);
}
const updateUserSettingsInputValidator = tShape({
name: t.irreducible(
userSettingsTypes.DEFAULT_NOTIFICATIONS,
x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS,
),
data: t.enums.of(notificationTypeValues),
});
async function updateUserSettingsResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: UpdateUserSettingsRequest = input;
await validateInput(viewer, updateUserSettingsInputValidator, request);
return await updateUserSettings(viewer, request);
}
const policyAcknowledgmentRequestInputValidator = tShape({
policy: t.maybe(t.enums.of(policies)),
});
async function policyAcknowledgmentResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: PolicyAcknowledgmentRequest = input;
await validateInput(
viewer,
policyAcknowledgmentRequestInputValidator,
request,
);
await viewerAcknowledgmentUpdater(viewer, request.policy);
}
export {
userSubscriptionUpdateResponder,
passwordUpdateResponder,
sendVerificationEmailResponder,
sendPasswordResetEmailResponder,
logOutResponder,
accountDeletionResponder,
accountCreationResponder,
logInResponder,
siweAuthResponder,
oldPasswordUpdateResponder,
updateUserSettingsResponder,
policyAcknowledgmentResponder,
};

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 10:14 AM (18 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690764
Default Alt Text
(36 KB)

Event Timeline