fix: don't update state on updates already included in our syncmessage

This commit is contained in:
Audric Ackermann 2023-06-02 09:14:50 +10:00
parent 2068737cdd
commit db9fa14213
15 changed files with 275 additions and 120 deletions

View File

@ -14,6 +14,11 @@ const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp';
const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed';
// user config tracking timestamps (to discard incoming messages which would make a change we reverted in the last config message we merged)
const latestUserProfileEnvelopeTimestamp = 'latestUserProfileEnvelopeTimestamp';
const latestUserGroupEnvelopeTimestamp = 'latestUserGroupEnvelopeTimestamp';
const latestUserContactsEnvelopeTimestamp = 'latestUserContactsEnvelopeTimestamp';
export const SettingsKey = {
settingsReadReceipt,
settingsTypingIndicator,
@ -29,6 +34,9 @@ export const SettingsKey = {
hasSyncedInitialConfigurationItem,
lastAvatarUploadTimestamp,
hasLinkPreviewPopupBeenDisplayed,
latestUserProfileEnvelopeTimestamp,
latestUserGroupEnvelopeTimestamp,
latestUserContactsEnvelopeTimestamp,
} as const;
export const KNOWN_BLINDED_KEYS_ITEM = 'KNOWN_BLINDED_KEYS_ITEM';

View File

@ -1,25 +1,29 @@
import { Data } from '../../ts/data/data';
import { SignalService } from '../protobuf';
import { removeFromCache } from './cache';
import { EnvelopePlus } from './types';
import { PubKey } from '../session/types';
import { toHex } from '../session/utils/String';
import { getMessageQueue } from '../session';
import { getConversationController } from '../session/conversations';
import * as ClosedGroup from '../session/group/closed-group';
import { PubKey } from '../session/types';
import { toHex } from '../session/utils/String';
import { BlockedNumberController } from '../util';
import { getMessageQueue } from '../session';
import { removeFromCache } from './cache';
import { decryptWithSessionProtocol } from './contentMessage';
import { Data } from '../../ts/data/data';
import { EnvelopePlus } from './types';
import { ECKeyPair, HexKeyPair } from './keypairs';
import { UserUtils } from '../session/utils';
import _, { isNumber, toNumber } from 'lodash';
import { ConversationModel } from '../models/conversation';
import _ from 'lodash';
import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage';
import { queueAllCachedFromSource } from './receiver';
import { getSwarmPollingInstance } from '../session/apis/snode_api';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { getSwarmPollingInstance } from '../session/apis/snode_api';
import { SnodeNamespaces } from '../session/apis/snode_api/namespaces';
import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage';
import { UserUtils } from '../session/utils';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { ReleasedFeatures } from '../util/releaseFeature';
import { Storage } from '../util/storage';
import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions';
import { getSettingsKeyFromLibsessionWrapper } from './configMessage';
import { ECKeyPair, HexKeyPair } from './keypairs';
import { queueAllCachedFromSource } from './receiver';
export const distributingClosedGroupEncryptionKeyPairs = new Map<string, ECKeyPair>();
@ -113,7 +117,7 @@ export async function handleClosedGroupControlMessage(
);
return;
}
await handleNewClosedGroup(envelope, groupUpdate);
await handleNewClosedGroup(envelope, groupUpdate, false);
return;
}
@ -199,9 +203,48 @@ function sanityCheckNewGroup(
return true;
}
/**
* If we merged a more recent wrapper, we must not apply the changes from some incoming messages as it would override a change already set in the wrapper.
*
* This is mostly to take care of the link a device logic, where we apply the changes from a wrapper, and then start polling from our swarm namespace 0.
* Some messages on our swarm might unhide a contact which was marked hidden after that message was already received on another device. Same for groups left/joined etc.
*
* @returns true if the user config release is live AND the latest processed corresponding wrapper is supposed to have already included the changes this message did.
* So if that message should not make any changes to the ata tracked in the wrappers (just add messages if needed, but don't set members, unhide contact etc).
*/
export async function sentAtMoreRecentThanWrapper(
envelopeSentAtMs: number,
variant: ConfigWrapperObjectTypes
): Promise<'unknown' | 'wrapper_more_recent' | 'envelope_more_recent'> {
const userConfigReleased = await ReleasedFeatures.checkIsUserConfigFeatureReleased();
if (!userConfigReleased) {
return 'unknown';
}
const settingsKey = getSettingsKeyFromLibsessionWrapper(variant);
if (!settingsKey) {
return 'unknown';
}
const latestProcessedEnvelope = Storage.get(settingsKey);
if (!isNumber(latestProcessedEnvelope) || !latestProcessedEnvelope) {
// We want to process the message if we do not have valid data in the db.
// Also, we DO want to process a message if we DO NOT have a latest processed timestamp for that wrapper yet
return 'envelope_more_recent';
}
// this must return true if the message we are considering should have already been handled based on our `latestProcessedEnvelope`.
// so if that message was sent before `latestProcessedEnvelope - 2 mins`, we must return true;
const latestProcessedEnvelopeLess2Mins = latestProcessedEnvelope - 2 * 60 * 1000;
return envelopeSentAtMs > latestProcessedEnvelopeLess2Mins
? 'envelope_more_recent'
: 'wrapper_more_recent';
}
export async function handleNewClosedGroup(
envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
fromLegacyConfig: boolean
) {
if (groupUpdate.type !== SignalService.DataMessage.ClosedGroupControlMessage.Type.NEW) {
return;
@ -229,10 +272,21 @@ export async function handleNewClosedGroup(
const groupId = toHex(publicKey);
const members = membersAsData.map(toHex);
const admins = adminsAsData.map(toHex);
const envelopeTimestamp = _.toNumber(envelope.timestamp);
const envelopeTimestamp = toNumber(envelope.timestamp);
// a type new is sent and received on one to one so do not use envelope.senderIdentity here
const sender = envelope.source;
if (
!fromLegacyConfig &&
(await sentAtMoreRecentThanWrapper(envelopeTimestamp, 'UserGroupsConfig')) ===
'wrapper_more_recent'
) {
// not from legacy config, so this is a new closed group deposited on our swarm by a user.
// we do not want to process it if our wrapper is more recent that that invite to group envelope.
window.log.info('dropping invite to legacy group because our wrapper is more recent');
return removeFromCache(envelope);
}
if (!members.includes(ourNumber.key)) {
window?.log?.info(
'Got a new group message but apparently we are not a member of it. Dropping it.'
@ -276,7 +330,7 @@ export async function handleNewClosedGroup(
groupConvo.set({
left: false,
isKickedFromGroup: false,
lastJoinedTimestamp: _.toNumber(envelope.timestamp),
lastJoinedTimestamp: toNumber(envelope.timestamp),
// we just got readded. Consider the zombie list to have been cleared
zombies: [],
@ -493,7 +547,7 @@ async function performIfValid(
lastJoinedTimestamp = aYearAgo;
}
const envelopeTimestamp = _.toNumber(envelope.timestamp);
const envelopeTimestamp = toNumber(envelope.timestamp);
if (envelopeTimestamp <= lastJoinedTimestamp) {
window?.log?.warn(
'Got a group update with an older timestamp than when we joined this group last time. Dropping it.'
@ -513,26 +567,28 @@ async function performIfValid(
// make sure the conversation with this user exist (even if it's just hidden)
await getConversationController().getOrCreateAndWait(sender, ConversationTypeEnum.PRIVATE);
const moreRecentOrNah = await sentAtMoreRecentThanWrapper(envelopeTimestamp, 'UserGroupsConfig');
const shouldNotApplyGroupChange = moreRecentOrNah === 'wrapper_more_recent';
if (groupUpdate.type === Type.NAME_CHANGE) {
await handleClosedGroupNameChanged(envelope, groupUpdate, convo);
await handleClosedGroupNameChanged(envelope, groupUpdate, convo, shouldNotApplyGroupChange);
} else if (groupUpdate.type === Type.MEMBERS_ADDED) {
await handleClosedGroupMembersAdded(envelope, groupUpdate, convo);
await handleClosedGroupMembersAdded(envelope, groupUpdate, convo, shouldNotApplyGroupChange);
} else if (groupUpdate.type === Type.MEMBERS_REMOVED) {
await handleClosedGroupMembersRemoved(envelope, groupUpdate, convo);
await handleClosedGroupMembersRemoved(envelope, groupUpdate, convo, shouldNotApplyGroupChange);
} else if (groupUpdate.type === Type.MEMBER_LEFT) {
await handleClosedGroupMemberLeft(envelope, convo);
await handleClosedGroupMemberLeft(envelope, convo, shouldNotApplyGroupChange);
} else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) {
await removeFromCache(envelope);
}
// if you add a case here, remember to add it where performIfValid is called too.
return true;
}
async function handleClosedGroupNameChanged(
envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
convo: ConversationModel
convo: ConversationModel,
shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
) {
// Only add update message if we have something to show
const newName = groupUpdate.name;
@ -546,9 +602,11 @@ async function handleClosedGroupNameChanged(
convo,
groupDiff,
envelope.senderIdentity,
_.toNumber(envelope.timestamp)
toNumber(envelope.timestamp)
);
convo.set({ displayNameInProfile: newName });
if (!shouldOnlyAddUpdateMessage) {
convo.set({ displayNameInProfile: newName });
}
convo.updateLastMessage();
await convo.commit();
}
@ -559,7 +617,8 @@ async function handleClosedGroupNameChanged(
async function handleClosedGroupMembersAdded(
envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
convo: ConversationModel
convo: ConversationModel,
shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
) {
const { members: addedMembersBinary } = groupUpdate;
const addedMembers = (addedMembersBinary || []).map(toHex);
@ -602,10 +661,12 @@ async function handleClosedGroupMembersAdded(
convo,
groupDiff,
envelope.senderIdentity,
_.toNumber(envelope.timestamp)
toNumber(envelope.timestamp)
);
convo.set({ members });
if (!shouldOnlyAddUpdateMessage) {
convo.set({ members });
}
convo.updateLastMessage();
await convo.commit();
@ -625,7 +686,8 @@ async function areWeAdmin(groupConvo: ConversationModel) {
async function handleClosedGroupMembersRemoved(
envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
convo: ConversationModel
convo: ConversationModel,
shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
) {
// Check that the admin wasn't removed
const currentMembers = convo.get('members');
@ -681,7 +743,7 @@ async function handleClosedGroupMembersRemoved(
convo,
groupDiff,
envelope.senderIdentity,
_.toNumber(envelope.timestamp)
toNumber(envelope.timestamp)
);
convo.updateLastMessage();
}
@ -689,9 +751,10 @@ async function handleClosedGroupMembersRemoved(
// Update the group
const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z));
convo.set({ members: membersAfterUpdate });
convo.set({ zombies });
if (!shouldOnlyAddUpdateMessage) {
convo.set({ members: membersAfterUpdate });
convo.set({ zombies });
}
await convo.commit();
}
await removeFromCache(envelope);
@ -761,7 +824,11 @@ async function handleClosedGroupLeftOurself(groupId: string, envelope: EnvelopeP
await removeFromCache(envelope);
}
async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: ConversationModel) {
async function handleClosedGroupMemberLeft(
envelope: EnvelopePlus,
convo: ConversationModel,
shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
) {
const sender = envelope.senderIdentity;
const groupPublicKey = envelope.source;
const didAdminLeave = convo.get('groupAdmins')?.includes(sender) || false;
@ -778,7 +845,6 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
// if the admin leaves, the group is disabled for everyone
if (didAdminLeave) {
await handleClosedGroupAdminMemberLeft(groupPublicKey, envelope);
return;
@ -801,14 +867,16 @@ async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: Conver
convo,
groupDiff,
envelope.senderIdentity,
_.toNumber(envelope.timestamp)
toNumber(envelope.timestamp)
);
convo.updateLastMessage();
// if a user just left and we are the admin, we remove him right away for everyone by sending a MEMBERS_REMOVED message so no need to add him as a zombie
if (oldMembers.includes(sender)) {
addMemberToZombies(envelope, PubKey.cast(sender), convo);
}
convo.set('members', newMembers);
if (!shouldOnlyAddUpdateMessage) {
convo.set('members', newMembers);
}
await convo.commit();

View File

@ -146,6 +146,50 @@ async function mergeConfigsWithIncomingUpdates(
}
}
export function getSettingsKeyFromLibsessionWrapper(
wrapperType: ConfigWrapperObjectTypes
): string | null {
switch (wrapperType) {
case 'UserConfig':
return SettingsKey.latestUserProfileEnvelopeTimestamp;
case 'ContactsConfig':
return SettingsKey.latestUserContactsEnvelopeTimestamp;
case 'UserGroupsConfig':
return SettingsKey.latestUserGroupEnvelopeTimestamp;
case 'ConvoInfoVolatileConfig':
return null; // we don't really care about the convo info volatile one
default:
try {
assertUnreachable(
wrapperType,
`getSettingsKeyFromLibsessionWrapper unknown type: ${wrapperType}`
);
} catch (e) {
window.log.warn('assertUnreachable:', e.message);
}
return null;
}
}
async function updateLibsessionLatestProcessedUserTimestamp(
wrapperType: ConfigWrapperObjectTypes,
latestEnvelopeTimestamp: number
) {
const settingsKey = getSettingsKeyFromLibsessionWrapper(wrapperType);
if (!settingsKey) {
return;
}
const currentLatestEnvelopeProcessed = Storage.get(settingsKey) || 0;
const newLatestProcessed = Math.max(
latestEnvelopeTimestamp,
isNumber(currentLatestEnvelopeProcessed) ? currentLatestEnvelopeProcessed : 0
);
if (newLatestProcessed !== currentLatestEnvelopeProcessed || currentLatestEnvelopeProcessed) {
await Storage.put(settingsKey, newLatestProcessed);
}
}
async function handleUserProfileUpdate(result: IncomingConfResult): Promise<IncomingConfResult> {
const updateUserInfo = await UserConfigWrapperActions.getUserInfo();
if (!updateUserInfo) {
@ -161,6 +205,17 @@ async function handleUserProfileUpdate(result: IncomingConfResult): Promise<Inco
updateUserInfo.priority
);
const settingsKey = SettingsKey.latestUserProfileEnvelopeTimestamp;
const currentLatestEnvelopeProcessed = Storage.get(settingsKey) || 0;
const newLatestProcessed = Math.max(
result.latestEnvelopeTimestamp,
isNumber(currentLatestEnvelopeProcessed) ? currentLatestEnvelopeProcessed : 0
);
if (newLatestProcessed !== currentLatestEnvelopeProcessed) {
await Storage.put(settingsKey, newLatestProcessed);
}
return result;
}
@ -715,6 +770,14 @@ async function processMergingResults(results: Map<ConfigWrapperObjectTypes, Inco
}
}
const variant = LibSessionUtil.kindToVariant(kind);
try {
await updateLibsessionLatestProcessedUserTimestamp(
variant,
incomingResult.latestEnvelopeTimestamp
);
} catch (e) {
window.log.error(`updateLibsessionLatestProcessedUserTimestamp failed with "${e.message}"`);
}
if (incomingResult.needsDump) {
// The config data had changes so regenerate the dump and save it
@ -917,7 +980,7 @@ const handleClosedGroupsFromConfigLegacy = async (
publicKey: c.publicKey,
});
try {
await handleNewClosedGroup(envelope, groupUpdate);
await handleNewClosedGroup(envelope, groupUpdate, true);
} catch (e) {
window?.log?.warn('failed to handle a new closed group from configuration message');
}

View File

@ -12,7 +12,11 @@ import {
deleteMessagesFromSwarmAndCompletelyLocally,
deleteMessagesFromSwarmAndMarkAsDeletedLocally,
} from '../interactions/conversations/unsendingInteractions';
import { ConversationTypeEnum, READ_MESSAGE_STATE } from '../models/conversationAttributes';
import {
CONVERSATION_PRIORITIES,
ConversationTypeEnum,
READ_MESSAGE_STATE,
} from '../models/conversationAttributes';
import { findCachedBlindedMatchOrLookupOnAllServers } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { getConversationController } from '../session/conversations';
import { concatUInt8Array, getSodiumRenderer } from '../session/crypto';
@ -26,9 +30,10 @@ import { BlockedNumberController } from '../util';
import { ReadReceipts } from '../util/readReceipts';
import { Storage } from '../util/storage';
import { handleCallMessage } from './callMessage';
import { getAllCachedECKeyPair } from './closedGroups';
import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGroups';
import { ConfigMessageHandler } from './configMessage';
import { ECKeyPair } from './keypairs';
import { ContactsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try {
@ -287,6 +292,38 @@ async function decrypt(envelope: EnvelopePlus): Promise<any> {
}
}
async function shouldDropIncomingPrivateMessage(sentAtTimestamp: number, envelope: EnvelopePlus) {
// sentAtMoreRecentThanWrapper is going to be true, if the latest contact wrapper we processed was roughly more recent that this message timestamp
const moreRecentOrNah = await sentAtMoreRecentThanWrapper(sentAtTimestamp, 'ContactsConfig');
if (moreRecentOrNah === 'wrapper_more_recent') {
// we need to check if that conversation is already in the wrapper, and if yes
try {
const privateConvoInWrapper = await ContactsWrapperActions.get(envelope.source);
if (
!privateConvoInWrapper ||
privateConvoInWrapper.priority <= CONVERSATION_PRIORITIES.hidden
) {
// the wrapper is more recent that this message and there is no such private conversation. Just drop this incoming message.
window.log.info(
`received message from contact ${envelope.source} which appears to be hidden/removed in our most recent libsession contactconfig, sentAt: ${sentAtTimestamp}. Dropping it`
);
return true;
} else {
window.log.info(
`received message from contact ${envelope.source} which appears to NOT be hidden/removed in our most recent libsession contactconfig, sentAt: ${sentAtTimestamp}. `
);
}
} catch (e) {
window.log.warn(
'ContactsWrapperActions.get in handleSwarmDataMessage failed with',
e.message
);
}
}
return false;
}
function shouldDropBlockedUserMessage(
content: SignalService.Content,
groupPubkey: string
@ -373,6 +410,12 @@ export async function innerHandleSwarmContentMessage(
// if this is a closed group message, envelope.senderIdentity is the sender's pubkey and envelope.source is the closed group's pubkey
const isPrivateConversationMessage = !envelope.senderIdentity;
if (isPrivateConversationMessage) {
if (await shouldDropIncomingPrivateMessage(sentAtTimestamp, envelope)) {
return removeFromCache(envelope);
}
}
/**
* For a closed group message, this holds the conversation with that specific user outside of the closed group.
* For a private conversation message, this is just the conversation with that user
@ -396,7 +439,7 @@ export async function innerHandleSwarmContentMessage(
if (content.dataMessage) {
// because typescript is funky with incoming protobufs
if (content.dataMessage.profileKey && content.dataMessage.profileKey.length === 0) {
if (isEmpty(content.dataMessage.profileKey)) {
content.dataMessage.profileKey = null;
}
perfStart(`handleSwarmDataMessage-${envelope.id}`);

View File

@ -1,28 +1,28 @@
import { SignalService } from './../protobuf';
import { removeFromCache } from './cache';
import { EnvelopePlus } from './types';
import { getEnvelopeId } from './common';
import { EnvelopePlus } from './types';
import { PubKey } from '../session/types';
import { handleMessageJob, toRegularMessage } from './queuedJob';
import { isEmpty, isFinite, noop, omit, toNumber } from 'lodash';
import { StringUtils, UserUtils } from '../session/utils';
import { getConversationController } from '../session/conversations';
import { handleClosedGroupControlMessage } from './closedGroups';
import { Data } from '../../ts/data/data';
import { ConversationModel } from '../models/conversation';
import { getConversationController } from '../session/conversations';
import { PubKey } from '../session/types';
import { StringUtils, UserUtils } from '../session/utils';
import { handleClosedGroupControlMessage } from './closedGroups';
import { handleMessageJob, toRegularMessage } from './queuedJob';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { MessageModel } from '../models/message';
import {
createSwarmMessageSentFromNotUs,
createSwarmMessageSentFromUs,
} from '../models/messageFactory';
import { MessageModel } from '../models/message';
import { isUsFromCache } from '../session/utils/User';
import { toLogFormat } from '../types/attachments/Errors';
import { ConversationTypeEnum } from '../models/conversationAttributes';
import { Reactions } from '../util/reactions';
import { Action, Reaction } from '../types/Reaction';
import { ProfileManager } from '../session/profile_manager/ProfileManager';
import { isUsFromCache } from '../session/utils/User';
import { Action, Reaction } from '../types/Reaction';
import { toLogFormat } from '../types/attachments/Errors';
import { Reactions } from '../util/reactions';
function cleanAttachment(attachment: any) {
return {
@ -185,8 +185,6 @@ export async function handleSwarmDataMessage(
if (isSyncedMessage && !isMe) {
window?.log?.warn('Got a sync message from someone else than me. Dropping it.');
return removeFromCache(envelope);
} else if (isSyncedMessage) {
// we should create the synTarget convo but I have no idea how to know if this is a private or closed group convo?
}
const convoIdToAddTheMessageTo = PubKey.removeTextSecurePrefixIfNeeded(
isSyncedMessage ? cleanDataMessage.syncTarget : envelope.source
@ -201,16 +199,16 @@ export async function handleSwarmDataMessage(
typeOfConvo = ConversationTypeEnum.GROUP;
}
window?.log?.info(
`Handle dataMessage about convo ${convoIdToAddTheMessageTo} from user: ${convoIdOfSender}`
);
// remove the prefix from the source object so this is correct for all other
const convoToAddMessageTo = await getConversationController().getOrCreateAndWait(
convoIdToAddTheMessageTo,
typeOfConvo
);
window?.log?.info(
`Handle dataMessage about convo ${convoIdToAddTheMessageTo} from user: ${convoIdOfSender}`
);
// remove the prefix from the source object so this is correct for all other
// Check if we need to update any profile names
if (
!isMe &&

View File

@ -25,6 +25,7 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
return null;
}
const method = 'delete_all' as const;
const namespace = 'all' as const;
try {
const maliciousSnodes = await pRetry(
@ -41,12 +42,12 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
async () => {
const signOpts = await SnodeSignature.getSnodeSignatureParams({
method,
namespace: null,
namespace,
pubkey: userX25519PublicKey,
});
const ret = await doSnodeBatchRequest(
[{ method, params: signOpts }],
[{ method, params: { ...signOpts, namespace } }],
snodeToMakeRequestTo,
10000,
userX25519PublicKey
@ -112,12 +113,18 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
return snodePubkey;
}
const hashes = snodeJson.deleted as Array<string>;
const deletedObj = snodeJson.deleted as Record<number, Array<string>>;
const hashes: Array<string> = [];
for (const key in deletedObj) {
hashes.push(...deletedObj[key]);
}
const sortedHashes = hashes.sort();
const signatureSnode = snodeJson.signature as string;
// The signature format is ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
const dataToVerify = `${userX25519PublicKey}${signOpts.timestamp}${hashes.join(
''
)}`;
// The signature format is (with sortedHashes) ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
const dataToVerify = `${userX25519PublicKey}${
signOpts.timestamp
}${sortedHashes.join('')}`;
const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8');
const isValid = sodium.crypto_sign_verify_detached(
fromBase64ToArray(signatureSnode),

View File

@ -101,6 +101,7 @@ export type StoreOnNodeParamsNoSig = Pick<
export type DeleteFromNodeWithTimestampParams = {
timestamp: string | number;
namespace: number | null | 'all';
} & DeleteSigParameters;
export type DeleteByHashesFromNodeParams = { messages: Array<string> } & DeleteSigParameters;

View File

@ -1,5 +1,6 @@
import { last, orderBy } from 'lodash';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { PickEnum } from '../../../types/Enums';
export enum SnodeNamespaces {
/**
@ -36,10 +37,6 @@ export enum SnodeNamespaces {
// ClosedGroupInfo = 1,
}
type PickEnum<T, K extends T> = {
[P in keyof K]: P extends K ? P : never;
};
export type SnodeNamespacesGroup = PickEnum<
SnodeNamespaces,
SnodeNamespaces.ClosedGroupMessage // | SnodeNamespaces.ClosedGroupInfo

View File

@ -54,7 +54,7 @@ async function getSnodeSignatureByHashesParams({
async function getSnodeSignatureParams(params: {
pubkey: string;
namespace: number | null;
namespace: number | null | 'all'; // 'all' can be used to clear all namespaces (during account deletion)
method: 'retrieve' | 'store' | 'delete_all';
}): Promise<SnodeSignatureResult> {
const ourEd25519Key = await UserUtils.getUserED25519KeyPair();
@ -69,10 +69,12 @@ async function getSnodeSignatureParams(params: {
const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset();
const withoutNamespace = `${params.method}${signatureTimestamp}`;
const withNamespace = `${params.method}${namespace}${signatureTimestamp}`;
const verificationData =
namespace === 0
? StringUtils.encode(`${params.method}${signatureTimestamp}`, 'utf8')
: StringUtils.encode(`${params.method}${namespace}${signatureTimestamp}`, 'utf8');
? StringUtils.encode(withoutNamespace, 'utf8')
: StringUtils.encode(withNamespace, 'utf8');
const message = new Uint8Array(verificationData);

View File

@ -393,8 +393,8 @@ async function generateAndSendNewEncryptionKeyPair(
return;
}
const ourNumber = UserUtils.getOurPubKeyFromCache();
if (!groupConvo.get('groupAdmins')?.includes(ourNumber.key)) {
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
if (!groupConvo.get('groupAdmins')?.includes(ourNumber)) {
window?.log?.warn('generateAndSendNewEncryptionKeyPair: cannot send it as a non admin');
return;
}

View File

@ -20,6 +20,7 @@ export type ByteKeyPair = {
/**
* Check if this pubkey is us, using the cache.
* This does not check for us blinded. To check for us or us blinded, use isUsAnySogsFromCache()
* Throws an error if our pubkey is not set
*/
export function isUsFromCache(pubKey: string | PubKey | undefined): boolean {

View File

@ -18,6 +18,7 @@ import {
} from '../PersistedJob';
import { ReleasedFeatures } from '../../../../util/releaseFeature';
import { allowOnlyOneAtATime } from '../../Promise';
import { isSignInByLinking } from '../../../../util/storage';
const defaultMsBetweenRetries = 30000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s)
const defaultMaxAttempts = 2;
@ -284,6 +285,11 @@ class ConfigurationSyncJob extends PersistedJob<ConfigurationSyncPersistedData>
* A ConfigurationSyncJob can only be added if there is none of the same type queued already.
*/
async function queueNewJobIfNeeded() {
if (isSignInByLinking()) {
window.log.info('NOT Scheduling ConfSyncJob: as we are linking a device');
return;
}
if (
!lastRunConfigSyncJobTimestamp ||
lastRunConfigSyncJobTimestamp < Date.now() - defaultMsBetweenRetries

View File

@ -71,7 +71,6 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise<voi
priority,
expirationTimerSeconds,
});
try {
window.log.debug('inserting into contact wrapper: ', JSON.stringify(wrapperContact));
await ContactsWrapperActions.set(wrapperContact);

View File

@ -1,41 +0,0 @@
// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression
import chai from 'chai';
import { describe } from 'mocha';
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised as any);
// tslint:disable-next-line: max-func-body-length
describe('ClosedGroupUpdates', () => {
// const ourDevice = TestUtils.generateFakePubKey();
// const ourNumber = ourDevice.key;
// const groupId = TestUtils.generateFakePubKey().key;
// const members = TestUtils.generateFakePubKeys(10);
// const sender = members[3].key;
// const getConvo = sandbox.stub(ConversationController, 'get');
// beforeEach(async () => {
// // Utils Stubs
// sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber);
// });
// afterEach(() => {
// sandbox.restore();
// });
// describe('handleClosedGroupControlMessage', () => {
// describe('performIfValid', () => {
// it('does not perform if convo does not exist', async () => {
// const envelope = generateEnvelopePlusClosedGroup(groupId, sender);
// const groupUpdate = generateGroupUpdateNameChange(groupId);
// getConvo.returns(undefined as any);
// await handleClosedGroupControlMessage(envelope, groupUpdate);
// });
// });
// // describe('handleClosedGroupNameChanged', () => {
// // it('does not trigger an update of the group if the name is the same', async () => {
// // const envelope = generateEnvelopePlusClosedGroup(groupId, sender);
// // const groupUpdate = generateGroupUpdateNameChange(groupId);
// // await handleClosedGroupControlMessage(envelope, groupUpdate);
// // });
// // });
// });
});

3
ts/types/Enums.ts Normal file
View File

@ -0,0 +1,3 @@
export type PickEnum<T, K extends T> = {
[P in keyof K]: P extends K ? P : never;
};