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