Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509170
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js
index 6984b5781..e6cec824b 100644
--- a/keyserver/src/endpoints.js
+++ b/keyserver/src/endpoints.js
@@ -1,247 +1,252 @@
// @flow
import { baseLegalPolicies } from 'lib/facts/policies.js';
import type { Endpoint } from 'lib/types/endpoints.js';
import {
updateActivityResponder,
threadSetUnreadStatusResponder,
} from './responders/activity-responders.js';
import { deviceTokenUpdateResponder } from './responders/device-responders.js';
import {
entryFetchResponder,
entryRevisionFetchResponder,
entryCreationResponder,
entryUpdateResponder,
entryDeletionResponder,
entryRestorationResponder,
calendarQueryUpdateResponder,
} from './responders/entry-responders.js';
import type { JSONResponder } from './responders/handlers.js';
import { getSessionPublicKeysResponder } from './responders/keys-responders.js';
import { messageReportCreationResponder } from './responders/message-report-responder.js';
import {
textMessageCreationResponder,
messageFetchResponder,
multimediaMessageCreationResponder,
reactionMessageCreationResponder,
} from './responders/message-responders.js';
import { updateRelationshipsResponder } from './responders/relationship-responders.js';
import {
reportCreationResponder,
reportMultiCreationResponder,
errorReportFetchInfosResponder,
} from './responders/report-responders.js';
import { userSearchResponder } from './responders/search-responders.js';
import { siweNonceResponder } from './responders/siwe-nonce-responders.js';
import {
threadDeletionResponder,
roleUpdateResponder,
memberRemovalResponder,
threadLeaveResponder,
threadUpdateResponder,
threadCreationResponder,
threadFetchMediaResponder,
threadJoinResponder,
} from './responders/thread-responders.js';
import {
userSubscriptionUpdateResponder,
passwordUpdateResponder,
sendVerificationEmailResponder,
sendPasswordResetEmailResponder,
logOutResponder,
accountDeletionResponder,
accountCreationResponder,
logInResponder,
siweAuthResponder,
oldPasswordUpdateResponder,
updateUserSettingsResponder,
policyAcknowledgmentResponder,
+ updateUserAvatarResponder,
} from './responders/user-responders.js';
import { codeVerificationResponder } from './responders/verification-responders.js';
import { uploadDeletionResponder } from './uploads/uploads.js';
const jsonEndpoints: { [id: Endpoint]: JSONResponder } = {
create_account: {
responder: accountCreationResponder,
requiredPolicies: [],
},
create_entry: {
responder: entryCreationResponder,
requiredPolicies: baseLegalPolicies,
},
create_error_report: {
responder: reportCreationResponder,
requiredPolicies: [],
},
create_message_report: {
responder: messageReportCreationResponder,
requiredPolicies: baseLegalPolicies,
},
create_multimedia_message: {
responder: multimediaMessageCreationResponder,
requiredPolicies: baseLegalPolicies,
},
create_reaction_message: {
responder: reactionMessageCreationResponder,
requiredPolicies: baseLegalPolicies,
},
create_report: {
responder: reportCreationResponder,
requiredPolicies: [],
},
create_reports: {
responder: reportMultiCreationResponder,
requiredPolicies: [],
},
create_text_message: {
responder: textMessageCreationResponder,
requiredPolicies: baseLegalPolicies,
},
create_thread: {
responder: threadCreationResponder,
requiredPolicies: baseLegalPolicies,
},
delete_account: {
responder: accountDeletionResponder,
requiredPolicies: [],
},
delete_entry: {
responder: entryDeletionResponder,
requiredPolicies: baseLegalPolicies,
},
delete_thread: {
responder: threadDeletionResponder,
requiredPolicies: baseLegalPolicies,
},
delete_upload: {
responder: uploadDeletionResponder,
requiredPolicies: baseLegalPolicies,
},
fetch_entries: {
responder: entryFetchResponder,
requiredPolicies: baseLegalPolicies,
},
fetch_entry_revisions: {
responder: entryRevisionFetchResponder,
requiredPolicies: baseLegalPolicies,
},
fetch_error_report_infos: {
responder: errorReportFetchInfosResponder,
requiredPolicies: baseLegalPolicies,
},
fetch_messages: {
responder: messageFetchResponder,
requiredPolicies: baseLegalPolicies,
},
fetch_thread_media: {
responder: threadFetchMediaResponder,
requiredPolicies: baseLegalPolicies,
},
get_session_public_keys: {
responder: getSessionPublicKeysResponder,
requiredPolicies: baseLegalPolicies,
},
join_thread: {
responder: threadJoinResponder,
requiredPolicies: baseLegalPolicies,
},
leave_thread: {
responder: threadLeaveResponder,
requiredPolicies: baseLegalPolicies,
},
log_in: {
responder: logInResponder,
requiredPolicies: [],
},
log_out: {
responder: logOutResponder,
requiredPolicies: [],
},
policy_acknowledgment: {
responder: policyAcknowledgmentResponder,
requiredPolicies: [],
},
remove_members: {
responder: memberRemovalResponder,
requiredPolicies: baseLegalPolicies,
},
restore_entry: {
responder: entryRestorationResponder,
requiredPolicies: baseLegalPolicies,
},
search_users: {
responder: userSearchResponder,
requiredPolicies: baseLegalPolicies,
},
send_password_reset_email: {
responder: sendPasswordResetEmailResponder,
requiredPolicies: [],
},
send_verification_email: {
responder: sendVerificationEmailResponder,
requiredPolicies: [],
},
set_thread_unread_status: {
responder: threadSetUnreadStatusResponder,
requiredPolicies: baseLegalPolicies,
},
update_account: {
responder: passwordUpdateResponder,
requiredPolicies: baseLegalPolicies,
},
update_activity: {
responder: updateActivityResponder,
requiredPolicies: baseLegalPolicies,
},
update_calendar_query: {
responder: calendarQueryUpdateResponder,
requiredPolicies: baseLegalPolicies,
},
update_user_settings: {
responder: updateUserSettingsResponder,
requiredPolicies: baseLegalPolicies,
},
update_device_token: {
responder: deviceTokenUpdateResponder,
requiredPolicies: [],
},
update_entry: {
responder: entryUpdateResponder,
requiredPolicies: baseLegalPolicies,
},
update_password: {
responder: oldPasswordUpdateResponder,
requiredPolicies: baseLegalPolicies,
},
update_relationships: {
responder: updateRelationshipsResponder,
requiredPolicies: baseLegalPolicies,
},
update_role: {
responder: roleUpdateResponder,
requiredPolicies: baseLegalPolicies,
},
update_thread: {
responder: threadUpdateResponder,
requiredPolicies: baseLegalPolicies,
},
update_user_subscription: {
responder: userSubscriptionUpdateResponder,
requiredPolicies: baseLegalPolicies,
},
verify_code: {
responder: codeVerificationResponder,
requiredPolicies: baseLegalPolicies,
},
siwe_nonce: {
responder: siweNonceResponder,
requiredPolicies: [],
},
siwe_auth: {
responder: siweAuthResponder,
requiredPolicies: [],
},
+ update_user_avatar: {
+ responder: updateUserAvatarResponder,
+ requiredPolicies: baseLegalPolicies,
+ },
};
export { jsonEndpoints };
diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js
index 230a0f1a6..e8697cf23 100644
--- a/keyserver/src/responders/user-responders.js
+++ b/keyserver/src/responders/user-responders.js
@@ -1,686 +1,697 @@
// @flow
import type { Utility as OlmUtility } from '@matrix-org/olm';
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 { UpdateUserAvatarRequest } from 'lib/types/avatar-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 { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.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 { 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);
const {
userQuery: [userResult],
} = 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(
(async () => {
const rustAPI = await getRustAPI();
try {
await rustAPI.loginUserPake(
id,
constIdentityKeys.primaryIdentityPublicKeys.ed25519,
request.password,
signedIdentityKeysBlob,
);
} catch (e) {
if (e.code === 'InvalidArg' && e.message === 'user not found') {
await rustAPI.registerUser(
id,
constIdentityKeys.primaryIdentityPublicKeys.ed25519,
username,
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();
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);
}
+async function updateUserAvatarResponder(
+ viewer: Viewer,
+ input: any,
+): Promise<void> {
+ const request: UpdateUserAvatarRequest = input;
+ await validateInput(viewer, updateUserAvatarRequestValidator, request);
+}
+
export {
userSubscriptionUpdateResponder,
passwordUpdateResponder,
sendVerificationEmailResponder,
sendPasswordResetEmailResponder,
logOutResponder,
accountDeletionResponder,
accountCreationResponder,
logInResponder,
siweAuthResponder,
oldPasswordUpdateResponder,
updateUserSettingsResponder,
policyAcknowledgmentResponder,
+ updateUserAvatarResponder,
};
diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js
index 8ab8356e4..d40e88378 100644
--- a/lib/types/endpoints.js
+++ b/lib/types/endpoints.js
@@ -1,115 +1,116 @@
// @flow
export type APIRequest = {
endpoint: Endpoint,
input: Object,
};
export type SocketAPIHandler = (request: APIRequest) => Promise<Object>;
export type Endpoint =
| HTTPOnlyEndpoint
| SocketOnlyEndpoint
| HTTPPreferredEndpoint
| SocketPreferredEndpoint;
// Endpoints that can cause session changes should occur over HTTP, since the
// socket code does not currently support changing sessions. In the future they
// could be made to work for native, but cookie changes on web require HTTP
// since websockets aren't able to Set-Cookie. Note that technically any
// endpoint can cause a sessionChange, and in that case the server will close
// the socket with a specific error code, and the client will proceed via HTTP.
const sessionChangingEndpoints = Object.freeze({
LOG_OUT: 'log_out',
DELETE_ACCOUNT: 'delete_account',
CREATE_ACCOUNT: 'create_account',
LOG_IN: 'log_in',
UPDATE_PASSWORD: 'update_password',
POLICY_ACKNOWLEDGMENT: 'policy_acknowledgment',
});
type SessionChangingEndpoint = $Values<typeof sessionChangingEndpoints>;
// We do uploads over HTTP as well. This is because Websockets use TCP, which
// guarantees ordering. That means that if we start an upload, any messages we
// try to send the server after the upload starts will have to wait until the
// upload ends. To avoid blocking other messages we upload using HTTP
// multipart/form-data.
const uploadEndpoints = Object.freeze({
UPLOAD_MULTIMEDIA: 'upload_multimedia',
});
type UploadEndpoint = $Values<typeof uploadEndpoints>;
type HTTPOnlyEndpoint = SessionChangingEndpoint | UploadEndpoint;
const socketOnlyEndpoints = Object.freeze({
UPDATE_ACTIVITY: 'update_activity',
UPDATE_CALENDAR_QUERY: 'update_calendar_query',
});
type SocketOnlyEndpoint = $Values<typeof socketOnlyEndpoints>;
const socketPreferredEndpoints = Object.freeze({
CREATE_ENTRY: 'create_entry',
CREATE_ERROR_REPORT: 'create_error_report',
CREATE_MESSAGE_REPORT: 'create_message_report',
CREATE_MULTIMEDIA_MESSAGE: 'create_multimedia_message',
CREATE_REACTION_MESSAGE: 'create_reaction_message',
CREATE_TEXT_MESSAGE: 'create_text_message',
CREATE_THREAD: 'create_thread',
DELETE_ENTRY: 'delete_entry',
DELETE_THREAD: 'delete_thread',
DELETE_UPLOAD: 'delete_upload',
FETCH_ENTRIES: 'fetch_entries',
FETCH_ENTRY_REVISIONS: 'fetch_entry_revisions',
FETCH_ERROR_REPORT_INFOS: 'fetch_error_report_infos',
FETCH_MESSAGES: 'fetch_messages',
FETCH_THREAD_MEDIA: 'fetch_thread_media',
GET_SESSION_PUBLIC_KEYS: 'get_session_public_keys',
JOIN_THREAD: 'join_thread',
LEAVE_THREAD: 'leave_thread',
REMOVE_MEMBERS: 'remove_members',
REQUEST_ACCESS: 'request_access',
RESTORE_ENTRY: 'restore_entry',
SEARCH_USERS: 'search_users',
SEND_PASSWORD_RESET_EMAIL: 'send_password_reset_email',
SEND_VERIFICATION_EMAIL: 'send_verification_email',
SET_THREAD_UNREAD_STATUS: 'set_thread_unread_status',
UPDATE_ACCOUNT: 'update_account',
UPDATE_USER_SETTINGS: 'update_user_settings',
UPDATE_DEVICE_TOKEN: 'update_device_token',
UPDATE_ENTRY: 'update_entry',
UPDATE_RELATIONSHIPS: 'update_relationships',
UPDATE_ROLE: 'update_role',
UPDATE_THREAD: 'update_thread',
UPDATE_USER_SUBSCRIPTION: 'update_user_subscription',
VERIFY_CODE: 'verify_code',
SIWE_NONCE: 'siwe_nonce',
SIWE_AUTH: 'siwe_auth',
+ UPDATE_USER_AVATAR: 'update_user_avatar',
});
type SocketPreferredEndpoint = $Values<typeof socketPreferredEndpoints>;
const httpPreferredEndpoints = Object.freeze({
CREATE_REPORT: 'create_report',
CREATE_REPORTS: 'create_reports',
});
type HTTPPreferredEndpoint = $Values<typeof httpPreferredEndpoints>;
const socketPreferredEndpointSet = new Set([
...Object.values(socketOnlyEndpoints),
...Object.values(socketPreferredEndpoints),
]);
export function endpointIsSocketPreferred(endpoint: Endpoint): boolean {
return socketPreferredEndpointSet.has(endpoint);
}
const socketSafeEndpointSet = new Set([
...Object.values(socketOnlyEndpoints),
...Object.values(socketPreferredEndpoints),
...Object.values(httpPreferredEndpoints),
]);
export function endpointIsSocketSafe(endpoint: Endpoint): boolean {
return socketSafeEndpointSet.has(endpoint);
}
const socketOnlyEndpointSet = new Set(Object.values(socketOnlyEndpoints));
export function endpointIsSocketOnly(endpoint: Endpoint): boolean {
return socketOnlyEndpointSet.has(endpoint);
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:59 AM (8 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690185
Default Alt Text
(33 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment