fix: make pushChangesToGroupSwarm take an extraStoreRequest

pull/3052/head
Audric Ackermann 11 months ago
parent 9b124d384e
commit cd122c7252

@ -24,6 +24,7 @@ import {
SessionButtonType,
} from './basic/SessionButton';
import { SessionRadio } from './basic/SessionRadio';
import { hasClosedGroupV2QAButtons } from '../shared/env_vars';
const AvatarContainer = styled.div`
position: relative;
@ -215,6 +216,9 @@ const ResendPromoteButton = ({
pubkey: PubkeyType;
groupPk: GroupPubkeyType;
}) => {
if (!hasClosedGroupV2QAButtons()) {
return null;
}
return (
<SessionButton
dataTestId={'resend-promote-button'}

@ -139,6 +139,7 @@ import {
UserGroupsWrapperActions,
} from '../webworker/workers/browser/libsession_worker_interface';
import { markAttributesAsReadIfNeeded } from './messageFactory';
import { StoreGroupRequestFactory } from '../session/apis/snode_api/factories/StoreGroupRequestFactory';
type InMemoryConvoInfos = {
mentionedUs: boolean;
@ -1122,17 +1123,20 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
updatedExpirationSeconds: expireUpdate.expireTimer,
});
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[v2groupMessage],
group
);
await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk: this.id,
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
encryptedSupplementKeys: [],
});
await GroupSync.storeGroupUpdateMessages({
groupPk: this.id,
updateMessages: [v2groupMessage],
supplementalKeysSubRequest: [],
extraStoreRequests,
});
await GroupSync.queueNewJobIfNeeded(this.id);
return true;
}

@ -355,7 +355,6 @@ async function handleGroupMemberLeftMessage({
memberLeft: author,
})
);
}
async function handleGroupUpdateMemberLeftNotificationMessage({
@ -491,7 +490,6 @@ async function handleGroupUpdateInviteResponseMessage({
}
window.inboxStore.dispatch(groupInfoActions.inviteResponseReceived({ groupPk, member: author }));
}
async function handleGroupUpdatePromoteMessage({
@ -531,7 +529,6 @@ async function handleGroupUpdatePromoteMessage({
secret: groupKeypair.privateKey,
})
);
}
async function handle1o1GroupUpdateMessage(

@ -25,6 +25,7 @@ import {
WithSignature,
WithTimestamp,
} from './types';
import { TTL_DEFAULT } from '../../constants';
type WithMaxSize = { max_size?: number };
export type WithShortenOrExtend = { shortenOrExtend: 'shorten' | 'extend' | '' };
@ -818,32 +819,29 @@ export class StoreGroupMessageSubRequest extends SnodeAPISubRequest {
}
}
export class StoreGroupConfigSubRequest extends SnodeAPISubRequest {
abstract class StoreGroupConfigSubRequest<
T extends SnodeNamespacesGroupConfig | SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
> extends SnodeAPISubRequest {
public method = 'store' as const;
public readonly namespace:
| SnodeNamespacesGroupConfig
| SnodeNamespaces.ClosedGroupRevokedRetrievableMessages;
public readonly namespace: T;
public readonly destination: GroupPubkeyType;
public readonly ttlMs: number;
public readonly encryptedData: Uint8Array;
// this is mandatory for a group config store, if it is null, we throw
public readonly secretKey: Uint8Array | null;
public readonly authData: Uint8Array | null;
constructor(
args: WithGroupPubkey & {
namespace: SnodeNamespacesGroupConfig | SnodeNamespaces.ClosedGroupRevokedRetrievableMessages;
ttlMs: number;
namespace: T;
encryptedData: Uint8Array;
authData: Uint8Array | null;
secretKey: Uint8Array | null;
}
) {
super();
this.namespace = args.namespace;
this.destination = args.groupPk;
this.ttlMs = args.ttlMs;
this.ttlMs = TTL_DEFAULT.CONFIG_MESSAGE;
this.encryptedData = args.encryptedData;
this.authData = args.authData;
this.secretKey = args.secretKey;
if (isEmpty(this.encryptedData)) {
@ -852,13 +850,8 @@ export class StoreGroupConfigSubRequest extends SnodeAPISubRequest {
if (!PubKey.is03Pubkey(this.destination)) {
throw new Error('StoreGroupConfigSubRequest: groupconfig namespace required a 03 pubkey');
}
if (isEmpty(this.secretKey) && isEmpty(this.authData)) {
throw new Error('StoreGroupConfigSubRequest needs either authData or secretKey to be set');
}
if (SnodeNamespace.isGroupConfigNamespace(this.namespace) && isEmpty(this.secretKey)) {
throw new Error(
`StoreGroupConfigSubRequest: groupconfig namespace [${this.namespace}] requires an adminSecretKey`
);
if (isEmpty(this.secretKey)) {
throw new Error('StoreGroupConfigSubRequest needs secretKey to be set');
}
}
@ -872,7 +865,7 @@ export class StoreGroupConfigSubRequest extends SnodeAPISubRequest {
const signDetails = await SnodeGroupSignature.getSnodeGroupSignature({
method: this.method,
namespace: this.namespace,
group: { authData: this.authData, pubkeyHex: this.destination, secretKey: this.secretKey },
group: { authData: null, pubkeyHex: this.destination, secretKey: this.secretKey },
});
if (!signDetails) {
@ -897,6 +890,36 @@ export class StoreGroupConfigSubRequest extends SnodeAPISubRequest {
}
}
export class StoreGroupInfoSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupInfo> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupInfo });
}
}
export class StoreGroupMembersSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupMembers> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupMembers });
}
}
export class StoreGroupKeysSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupKeys> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupKeys });
}
}
export class StoreGroupRevokedRetrievableSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupRevokedRetrievableMessages> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages });
}
}
export class StoreUserConfigSubRequest extends SnodeAPISubRequest {
public method = 'store' as const;
public readonly namespace: SnodeNamespacesUserConfig;
@ -1136,8 +1159,11 @@ export type RawSnodeSubRequests =
| RetrieveLegacyClosedGroupSubRequest
| RetrieveUserSubRequest
| RetrieveGroupSubRequest
| StoreGroupConfigSubRequest
| StoreGroupInfoSubRequest
| StoreGroupMembersSubRequest
| StoreGroupKeysSubRequest
| StoreGroupMessageSubRequest
| StoreGroupRevokedRetrievableSubRequest
| StoreUserConfigSubRequest
| SwarmForSubRequest
| OnsResolveSubRequest
@ -1159,13 +1185,16 @@ export type BuiltSnodeSubRequests =
| ReturnType<RetrieveLegacyClosedGroupSubRequest['build']>
| AwaitedReturn<RetrieveUserSubRequest['buildAndSignParameters']>
| AwaitedReturn<RetrieveGroupSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupConfigSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupInfoSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupMembersSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupKeysSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupMessageSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupRevokedRetrievableSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreUserConfigSubRequest['buildAndSignParameters']>
| ReturnType<SwarmForSubRequest['build']>
| ReturnType<OnsResolveSubRequest['build']>
| ReturnType<GetServiceNodesSubRequest['build']>
| ReturnType<NetworkTimeSubRequest['build']>
| AwaitedReturn<SwarmForSubRequest['build']>
| AwaitedReturn<OnsResolveSubRequest['build']>
| AwaitedReturn<GetServiceNodesSubRequest['build']>
| AwaitedReturn<NetworkTimeSubRequest['build']>
| AwaitedReturn<DeleteHashesFromGroupNodeSubRequest['buildAndSignParameters']>
| AwaitedReturn<DeleteHashesFromUserNodeSubRequest['buildAndSignParameters']>
| AwaitedReturn<DeleteAllFromUserNodeSubRequest['buildAndSignParameters']>

@ -0,0 +1,35 @@
import { UserGroupsGet } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { ed25519Str } from '../../../utils/String';
import { DeleteHashesFromGroupNodeSubRequest } from '../SnodeRequestTypes';
function makeGroupHashesToDeleteSubRequest({
allOldHashes,
group,
}: {
group: Pick<UserGroupsGet, 'secretKey' | 'pubkeyHex'>;
allOldHashes: Set<string>;
}) {
const groupPk = group.pubkeyHex;
const allOldHashesArray = [...allOldHashes];
if (allOldHashesArray.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`makeGroupHashesToDeleteSubRequest: ${ed25519Str(groupPk)}: allOldHashesArray not empty but we do not have the secretKey`
);
throw new Error(
'makeGroupHashesToDeleteSubRequest: allOldHashesArray not empty but we do not have the secretKey'
);
}
return new DeleteHashesFromGroupNodeSubRequest({
messagesHashes: [...allOldHashes],
groupPk,
secretKey: group.secretKey,
});
}
return null;
}
export const DeleteGroupHashesFactory = { makeGroupHashesToDeleteSubRequest };

@ -0,0 +1,184 @@
import { UserGroupsGet } from 'libsession_util_nodejs';
import { compact, isEmpty } from 'lodash';
import { SignalService } from '../../../../protobuf';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { GroupUpdateInfoChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { GroupUpdateMemberChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage';
import { MessageSender } from '../../../sending';
import { ed25519Str } from '../../../utils/String';
import { PendingChangesForGroup } from '../../../utils/libsession/libsession_utils';
import {
StoreGroupExtraData,
StoreGroupInfoSubRequest,
StoreGroupKeysSubRequest,
StoreGroupMembersSubRequest,
StoreGroupMessageSubRequest,
} from '../SnodeRequestTypes';
import { SnodeNamespaces } from '../namespaces';
export type StoreMessageToSubRequestType =
| GroupUpdateMemberChangeMessage
| GroupUpdateInfoChangeMessage;
async function makeGroupMessageSubRequest(
updateMessages: Array<StoreMessageToSubRequestType | null>,
group: Pick<UserGroupsGet, 'authData' | 'secretKey'>
) {
const compactedMessages = compact(updateMessages);
if (isEmpty(compactedMessages)) {
return [];
}
const groupPk = compactedMessages[0].destination;
const allForSameDestination = compactedMessages.every(m => m.destination === groupPk);
if (!allForSameDestination) {
throw new Error('makeGroupMessageSubRequest: not all messages are for the same destination');
}
const messagesToEncrypt: Array<StoreGroupExtraData> = compactedMessages.map(updateMessage => {
const wrapped = MessageSender.wrapContentIntoEnvelope(
SignalService.Envelope.Type.SESSION_MESSAGE,
undefined,
updateMessage.createAtNetworkTimestamp, // message is signed with this timestmap
updateMessage.plainTextBuffer()
);
return {
namespace: SnodeNamespaces.ClosedGroupMessages,
pubkey: updateMessage.destination,
ttl: updateMessage.ttl(),
networkTimestamp: updateMessage.createAtNetworkTimestamp,
data: SignalService.Envelope.encode(wrapped).finish(),
dbMessageIdentifier: updateMessage.identifier,
};
});
const encryptedContent = messagesToEncrypt.length
? await MetaGroupWrapperActions.encryptMessages(
groupPk,
messagesToEncrypt.map(m => m.data)
)
: [];
if (encryptedContent.length !== messagesToEncrypt.length) {
throw new Error(
'makeGroupMessageSubRequest: MetaGroupWrapperActions.encryptMessages did not return the right count of items'
);
}
const updateMessagesEncrypted = messagesToEncrypt.map((requestDetails, index) => ({
...requestDetails,
data: encryptedContent[index],
}));
const updateMessagesRequests = updateMessagesEncrypted.map(m => {
return new StoreGroupMessageSubRequest({
encryptedData: m.data,
groupPk,
ttlMs: m.ttl,
dbMessageIdentifier: m.dbMessageIdentifier,
...group,
createdAtNetworkTimestamp: m.networkTimestamp,
});
});
return updateMessagesRequests;
}
function makeStoreGroupKeysSubRequest({
encryptedSupplementKeys,
group,
}: {
group: Pick<UserGroupsGet, 'secretKey' | 'pubkeyHex'>;
encryptedSupplementKeys: Array<Uint8Array>;
}) {
const groupPk = group.pubkeyHex;
if (!encryptedSupplementKeys.length) {
return [];
}
// supplementalKeys are already encrypted, but we still need the secretKey to sign the request
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: keysEncryptedmessage not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: keysEncryptedmessage not empty but we do not have the secretKey'
);
}
return encryptedSupplementKeys.map(encryptedData => {
return new StoreGroupKeysSubRequest({
encryptedData,
groupPk,
secretKey: group.secretKey,
});
});
}
function makeStoreGroupConfigSubRequest({
group,
pendingConfigData,
}: {
group: Pick<UserGroupsGet, 'secretKey' | 'pubkeyHex'>;
pendingConfigData: Array<PendingChangesForGroup>;
}) {
if (!pendingConfigData.length) {
return [];
}
const groupPk = group.pubkeyHex;
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: pendingConfigMsgs not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: pendingConfigMsgs not empty but we do not have the secretKey'
);
}
const groupInfoSubRequests = compact(
pendingConfigData.map(m =>
m.namespace === SnodeNamespaces.ClosedGroupInfo
? new StoreGroupInfoSubRequest({
encryptedData: m.ciphertext,
groupPk,
secretKey: group.secretKey,
})
: null
)
);
const groupMembersSubRequests = compact(
pendingConfigData.map(m =>
m.namespace === SnodeNamespaces.ClosedGroupMembers
? new StoreGroupMembersSubRequest({
encryptedData: m.ciphertext,
groupPk,
secretKey: group.secretKey,
})
: null
)
);
const groupKeysSubRequests = compact(
pendingConfigData.map(m =>
m.namespace === SnodeNamespaces.ClosedGroupKeys
? new StoreGroupKeysSubRequest({
encryptedData: m.ciphertext,
groupPk,
secretKey: group.secretKey,
})
: null
)
);
// we want to store first the keys (as the info and members might already be encrypted with them)
return [...groupKeysSubRequests, ...groupInfoSubRequests, ...groupMembersSubRequests];
}
export const StoreGroupRequestFactory = {
makeGroupMessageSubRequest,
makeStoreGroupConfigSubRequest,
makeStoreGroupKeysSubRequest,
};

@ -329,8 +329,9 @@ class ConvoController {
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
encryptedSupplementKeys: [],
supplementalKeysSubRequest: [],
deleteAllMessagesSubRequest,
extraStoreRequests: [],
});
if (lastPushResult !== RunJobResult.Success) {
throw new Error(`Failed to destroyGroupDetails for pk ${ed25519Str(groupPk)}`);

@ -30,8 +30,11 @@ import {
RetrieveGroupSubRequest,
RetrieveLegacyClosedGroupSubRequest,
RetrieveUserSubRequest,
StoreGroupConfigSubRequest,
StoreGroupInfoSubRequest,
StoreGroupKeysSubRequest,
StoreGroupMembersSubRequest,
StoreGroupMessageSubRequest,
StoreGroupRevokedRetrievableSubRequest,
StoreLegacyGroupMessageSubRequest,
StoreUserConfigSubRequest,
StoreUserMessageSubRequest,
@ -89,7 +92,12 @@ type StoreRequest05 =
| StoreUserConfigSubRequest
| StoreUserMessageSubRequest
| StoreLegacyGroupMessageSubRequest;
type StoreRequest03 = StoreGroupConfigSubRequest | StoreGroupMessageSubRequest;
type StoreRequest03 =
| StoreGroupInfoSubRequest
| StoreGroupMembersSubRequest
| StoreGroupKeysSubRequest
| StoreGroupRevokedRetrievableSubRequest
| StoreGroupMessageSubRequest;
type PubkeyToRequestType<T extends GroupPubkeyType | PubkeyType> = T extends PubkeyType
? StoreRequest05
@ -366,7 +374,10 @@ async function signSubRequests(
p instanceof DeleteHashesFromUserNodeSubRequest ||
p instanceof DeleteHashesFromGroupNodeSubRequest ||
p instanceof DeleteAllFromUserNodeSubRequest ||
p instanceof StoreGroupConfigSubRequest ||
p instanceof StoreGroupInfoSubRequest ||
p instanceof StoreGroupMembersSubRequest ||
p instanceof StoreGroupKeysSubRequest ||
p instanceof StoreGroupRevokedRetrievableSubRequest ||
p instanceof StoreGroupMessageSubRequest ||
p instanceof StoreLegacyGroupMessageSubRequest ||
p instanceof StoreUserConfigSubRequest ||

@ -13,11 +13,10 @@ import {
MultiEncryptWrapperActions,
UserGroupsWrapperActions,
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import { StoreGroupRevokedRetrievableSubRequest } from '../../../apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../../../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../../../apis/snode_api/namespaces';
import { RevokeChanges, SnodeAPIRevoke } from '../../../apis/snode_api/revokeSubaccount';
import { WithSecretKey } from '../../../apis/snode_api/types';
import { TTL_DEFAULT } from '../../../constants';
import { concatUInt8Array } from '../../../crypto';
import { MessageSender } from '../../../sending';
import { fromHexToArray } from '../../String';
@ -29,7 +28,6 @@ import {
RunJobResult,
} from '../PersistedJob';
import { GroupSync } from './GroupSyncJob';
import { StoreGroupConfigSubRequest } from '../../../apis/snode_api/SnodeRequestTypes';
export type WithAddWithoutHistoryMembers = { withoutHistory: Array<PubkeyType> };
export type WithAddWithHistoryMembers = { withHistory: Array<PubkeyType> };
@ -154,13 +152,10 @@ class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersisted
secretKey: group.secretKey,
});
const multiEncryptRequest = new StoreGroupConfigSubRequest({
const multiEncryptRequest = new StoreGroupRevokedRetrievableSubRequest({
encryptedData: multiEncryptedMessage,
groupPk,
namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
ttlMs: TTL_DEFAULT.CONTENT_MESSAGE,
secretKey: group.secretKey,
authData: null,
});
const result = await MessageSender.sendEncryptedDataToSnode({

@ -1,9 +1,8 @@
/* eslint-disable no-await-in-loop */
import { GroupPubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isArray, isEmpty, isNumber } from 'lodash';
import { to_hex } from 'libsodium-wrappers-sumo';
import { isArray, isEmpty, isNumber } from 'lodash';
import { UserUtils } from '../..';
import { SignalService } from '../../../../protobuf';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { isSignInByLinking } from '../../../../util/storage';
import {
@ -12,21 +11,18 @@ import {
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
DeleteAllFromGroupMsgNodeSubRequest,
DeleteHashesFromGroupNodeSubRequest,
StoreGroupConfigSubRequest,
StoreGroupExtraData,
StoreGroupKeysSubRequest,
StoreGroupMessageSubRequest,
} from '../../../apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../../../apis/snode_api/getNetworkTime';
import { DeleteGroupHashesFactory } from '../../../apis/snode_api/factories/DeleteGroupHashesRequestFactory';
import { StoreGroupRequestFactory } from '../../../apis/snode_api/factories/StoreGroupRequestFactory';
import { SnodeNamespaces } from '../../../apis/snode_api/namespaces';
import { WithRevokeSubRequest } from '../../../apis/snode_api/types';
import { TTL_DEFAULT } from '../../../constants';
import { ConvoHub } from '../../../conversations';
import { GroupUpdateInfoChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { GroupUpdateMemberChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage';
import { MessageSender } from '../../../sending/MessageSender';
import { PubKey } from '../../../types';
import { allowOnlyOneAtATime } from '../../Promise';
import { ed25519Str } from '../../String';
import { GroupSuccessfulChange, LibSessionUtil } from '../../libsession/libsession_utils';
import { runners } from '../JobRunner';
import {
@ -35,7 +31,6 @@ import {
PersistedJob,
RunJobResult,
} from '../PersistedJob';
import { ed25519Str } from '../../String';
const defaultMsBetweenRetries = 15000; // 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;
@ -81,98 +76,18 @@ async function confirmPushedAndDump(
return LibSessionUtil.saveDumpsToDb(groupPk);
}
async function storeGroupUpdateMessages({
updateMessages,
groupPk,
}: WithGroupPubkey & {
updateMessages: Array<GroupUpdateMemberChangeMessage | GroupUpdateInfoChangeMessage>;
}) {
if (!updateMessages.length) {
return true;
}
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group) {
window.log.warn(
`storeGroupUpdateMessages for ${ed25519Str(groupPk)}: no group found in wrapper`
);
return false;
}
const updateMessagesToEncrypt: Array<StoreGroupExtraData> = updateMessages.map(updateMessage => {
const wrapped = MessageSender.wrapContentIntoEnvelope(
SignalService.Envelope.Type.SESSION_MESSAGE,
undefined,
updateMessage.createAtNetworkTimestamp, // message is signed with this timestmap
updateMessage.plainTextBuffer()
);
return {
namespace: SnodeNamespaces.ClosedGroupMessages,
pubkey: groupPk,
ttl: updateMessage.ttl(),
networkTimestamp: updateMessage.createAtNetworkTimestamp,
data: SignalService.Envelope.encode(wrapped).finish(),
dbMessageIdentifier: updateMessage.identifier,
};
});
const encryptedUpdate = updateMessagesToEncrypt
? await MetaGroupWrapperActions.encryptMessages(
groupPk,
updateMessagesToEncrypt.map(m => m.data)
)
: [];
const updateMessagesEncrypted = updateMessagesToEncrypt.map((requestDetails, index) => ({
...requestDetails,
data: encryptedUpdate[index],
}));
const updateMessagesRequests = updateMessagesEncrypted.map(m => {
return new StoreGroupMessageSubRequest({
encryptedData: m.data,
groupPk,
ttlMs: m.ttl,
dbMessageIdentifier: m.dbMessageIdentifier,
...group,
createdAtNetworkTimestamp: m.networkTimestamp,
});
});
const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests: [...updateMessagesRequests],
destination: groupPk,
deleteHashesSubRequest: null,
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
});
const expectedReplyLength = updateMessagesRequests.length; // each of those messages are sent as a subrequest
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) {
window.log.info(
`GroupSyncJob: unexpected result length: expected ${expectedReplyLength} but got ${result?.length}`
);
// this might be a 421 error (already handled) so let's retry this request a little bit later
return false;
}
return true;
}
async function pushChangesToGroupSwarmIfNeeded({
revokeSubRequest,
unrevokeSubRequest,
groupPk,
encryptedSupplementKeys,
supplementalKeysSubRequest,
deleteAllMessagesSubRequest,
extraStoreRequests,
}: WithGroupPubkey &
WithRevokeSubRequest & {
encryptedSupplementKeys: Array<Uint8Array>;
supplementalKeysSubRequest: Array<StoreGroupKeysSubRequest>;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
extraStoreRequests: Array<StoreGroupMessageSubRequest>;
}): Promise<RunJobResult> {
// save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc
await LibSessionUtil.saveDumpsToDb(groupPk);
@ -182,10 +97,11 @@ async function pushChangesToGroupSwarmIfNeeded({
// is updated we want to try and run immediately so don't schedule another run in this case)
if (
isEmpty(pendingConfigData) &&
!encryptedSupplementKeys.length &&
!revokeSubRequest &&
!unrevokeSubRequest &&
!deleteAllMessagesSubRequest
isEmpty(supplementalKeysSubRequest) &&
isEmpty(revokeSubRequest) &&
isEmpty(unrevokeSubRequest) &&
isEmpty(deleteAllMessagesSubRequest) &&
isEmpty(extraStoreRequests)
) {
window.log.debug(`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: nothing to push`);
return RunJobResult.Success;
@ -205,109 +121,20 @@ async function pushChangesToGroupSwarmIfNeeded({
);
}
const networkTimestamp = GetNetworkTime.now();
const pendingConfigMsgs = pendingConfigData.map(item => {
return {
namespace: item.namespace,
pubkey: groupPk,
networkTimestamp,
ttl: TTL_DEFAULT.CONFIG_MESSAGE,
data: item.ciphertext,
};
const pendingConfigRequests = StoreGroupRequestFactory.makeStoreGroupConfigSubRequest({
group,
pendingConfigData,
});
// supplementKeys are already encrypted by libsession
const keysEncryptedMessages: Array<StoreGroupExtraData> = encryptedSupplementKeys.map(key => ({
namespace: SnodeNamespaces.ClosedGroupKeys,
pubkey: groupPk,
ttl: TTL_DEFAULT.CONFIG_MESSAGE,
networkTimestamp,
data: key,
dbMessageIdentifier: null,
}));
let pendingConfigRequests: Array<StoreGroupConfigSubRequest> = [];
let keysEncryptedRequests: Array<StoreGroupConfigSubRequest> = [];
if (pendingConfigMsgs.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: pendingConfigMsgs not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: pendingConfigMsgs not empty but we do not have the secretKey'
);
}
pendingConfigRequests = pendingConfigMsgs.map(m => {
return new StoreGroupConfigSubRequest({
encryptedData: m.data,
groupPk,
namespace: m.namespace,
ttlMs: m.ttl,
secretKey: group.secretKey,
authData: null,
});
});
}
if (keysEncryptedMessages.length) {
// supplementalKeys are already encrypted, but we still need the secretKey to sign the request
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: keysEncryptedmessage not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: keysEncryptedmessage not empty but we do not have the secretKey'
);
}
keysEncryptedRequests = keysEncryptedMessages.map(m => {
return new StoreGroupConfigSubRequest({
encryptedData: m.data,
groupPk,
namespace: SnodeNamespaces.ClosedGroupKeys,
ttlMs: m.ttl,
secretKey: group.secretKey,
authData: null,
});
});
}
let deleteHashesSubRequest: DeleteHashesFromGroupNodeSubRequest | null = null;
const allOldHashesArray = [...allOldHashes];
if (allOldHashesArray.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: allOldHashesArray not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: allOldHashesArray not empty but we do not have the secretKey'
);
}
deleteHashesSubRequest = new DeleteHashesFromGroupNodeSubRequest({
messagesHashes: [...allOldHashes],
groupPk,
secretKey: group.secretKey,
const deleteHashesSubRequest = DeleteGroupHashesFactory.makeGroupHashesToDeleteSubRequest({
group,
allOldHashes,
});
}
if (
revokeSubRequest?.revokeTokenHex.length === 0 ||
unrevokeSubRequest?.revokeTokenHex.length === 0
) {
throw new Error(
'revokeSubRequest and unrevoke request must be null when not doing token change'
);
}
const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests: [...pendingConfigRequests, ...keysEncryptedRequests],
// Note: this is on purpose that supplementalKeysSubRequest is before pendingConfigRequests
// as this is to avoid a race condition where a device polls while we are posting the configs (already encrypted with the new keys)
storeRequests: [...supplementalKeysSubRequest, ...pendingConfigRequests, ...extraStoreRequests],
destination: groupPk,
deleteHashesSubRequest,
revokeSubRequest,
@ -316,12 +143,13 @@ async function pushChangesToGroupSwarmIfNeeded({
});
const expectedReplyLength =
pendingConfigRequests.length + // each of those messages are sent as a subrequest
keysEncryptedRequests.length + // each of those messages are sent as a subrequest
pendingConfigRequests.length + // each of those are sent as a subrequest
supplementalKeysSubRequest.length + // each of those are sent as a subrequest
(allOldHashes.size ? 1 : 0) + // we are sending all hashes changes as a single subrequest
(revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest
(unrevokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest
(deleteAllMessagesSubRequest ? 1 : 0); // a delete_all sub request is a single subrequest
(deleteAllMessagesSubRequest ? 1 : 0) + // a delete_all sub request is a single subrequest
(extraStoreRequests ? 1 : 0); // each of those are sent as a subrequest
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) {
@ -341,9 +169,9 @@ async function pushChangesToGroupSwarmIfNeeded({
if (isEmpty(changes)) {
return RunJobResult.RetryJobIfPossible;
}
// Now that we have the successful changes, we need to mark them as pushed and
// generate any config dumps which need to be stored
await confirmPushedAndDump(changes, groupPk);
return RunJobResult.Success;
}
@ -393,7 +221,8 @@ class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
groupPk: thisJobDestination,
revokeSubRequest: null,
unrevokeSubRequest: null,
encryptedSupplementKeys: [],
supplementalKeysSubRequest: [],
extraStoreRequests: [],
});
// eslint-disable-next-line no-useless-catch
@ -473,7 +302,6 @@ async function queueNewJobIfNeeded(groupPk: GroupPubkeyType) {
export const GroupSync = {
GroupSyncJob,
pushChangesToGroupSwarmIfNeeded,
storeGroupUpdateMessages,
queueNewJobIfNeeded: (groupPk: GroupPubkeyType) =>
allowOnlyOneAtATime(`GroupSyncJob-oneAtAtTime-${groupPk}`, () => queueNewJobIfNeeded(groupPk)),
};

@ -70,7 +70,6 @@ async function insertContactFromDBIntoWrapperAndRefresh(
const expirationMode = foundConvo.get('expirationMode') || undefined;
const expireTimer = foundConvo.get('expireTimer') || 0;
const wrapperContact = getContactInfoFromDBValues({
id,
dbApproved,

@ -27,5 +27,5 @@ export function isTestIntegration() {
}
export function hasClosedGroupV2QAButtons() {
return !!window.sessionFeatureFlags.useClosedGroupV2QAButtons
return !!window.sessionFeatureFlags.useClosedGroupV2QAButtons;
}

@ -17,6 +17,7 @@ import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { StoreGroupRequestFactory } from '../../session/apis/snode_api/factories/StoreGroupRequestFactory';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime';
import { ConvoHub } from '../../session/conversations';
import { getSodiumRenderer } from '../../session/crypto';
@ -51,6 +52,7 @@ import {
import { StateType } from '../reducer';
import { openConversationWithMessages } from './conversations';
import { resetLeftOverlayMode } from './section';
import { ed25519Str } from '../../session/utils/String';
type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message.
export type GroupState = {
@ -177,19 +179,7 @@ const initNewGroupInWrapper = createAsyncThunk(
const convo = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2);
await convo.setIsApproved(true, false);
await convo.commit(); // commit here too, as the poll needs it to be approved
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
encryptedSupplementKeys: [],
deleteAllMessagesSubRequest: null,
});
if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
throw new Error('failed to pushChangesToGroupSwarmIfNeeded');
}
let groupMemberChange: GroupUpdateMemberChangeMessage | null = null;
// push one group change message were initial members are added to the group
if (membersFromWrapper.length) {
const membersHex = uniq(membersFromWrapper.map(m => m.pubkeyHex));
@ -202,7 +192,7 @@ const initNewGroupInWrapper = createAsyncThunk(
convo,
markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier
});
const groupChange = await getWithoutHistoryControlMessage({
groupMemberChange = await getWithoutHistoryControlMessage({
adminSecretKey: groupSecretKey,
convo,
groupPk,
@ -210,12 +200,24 @@ const initNewGroupInWrapper = createAsyncThunk(
createAtNetworkTimestamp: sentAt,
dbMsgIdentifier: msgModel.id,
});
if (groupChange) {
await GroupSync.storeGroupUpdateMessages({
}
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[groupMemberChange],
{ authData: null, secretKey: newGroup.secretKey }
);
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
updateMessages: [groupChange],
revokeSubRequest: null,
unrevokeSubRequest: null,
supplementalKeysSubRequest: [],
deleteAllMessagesSubRequest: null,
extraStoreRequests,
});
}
if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
throw new Error('failed to pushChangesToGroupSwarmIfNeeded');
}
await convo.commit();
@ -647,7 +649,8 @@ async function handleMemberAddedFromUI({
groupPk,
});
// first, get the unrevoke requests for people who are added
const revokeUnrevokeParams = await GroupPendingRemovals.getPendingRevokeParams({
const { revokeSubRequest, unrevokeSubRequest } =
await GroupPendingRemovals.getPendingRevokeParams({
groupPk,
withHistory,
withoutHistory,
@ -659,6 +662,11 @@ async function handleMemberAddedFromUI({
// this adds them to the members wrapper etc
const encryptedSupplementKeys = await handleWithHistoryMembers({ groupPk, withHistory });
const supplementalKeysSubRequest = StoreGroupRequestFactory.makeStoreGroupKeysSubRequest({
group,
encryptedSupplementKeys,
});
// then handle the addition without history of messages (full rotation of keys).
// this adds them to the members wrapper etc
await handleWithoutHistoryMembers({ groupPk, withoutHistory });
@ -666,27 +674,6 @@ async function handleMemberAddedFromUI({
await LibSessionUtil.saveDumpsToDb(groupPk);
// push new members & key supplement in a single batch call
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
encryptedSupplementKeys,
...revokeUnrevokeParams,
deleteAllMessagesSubRequest: null,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
'handleMemberAddedFromUIOrNot: pushChangesToGroupSwarmIfNeeded did not return success'
);
}
// schedule send invite details, auth signature, etc. to the new users
await scheduleGroupInviteJobs(groupPk, withHistory, withoutHistory);
await LibSessionUtil.saveDumpsToDb(groupPk);
convo.set({
active_at: createAtNetworkTimestamp,
});
const expireDetails = DisappearingMessages.getExpireDetailsForOutgoingMesssage(
convo,
createAtNetworkTimestamp
@ -698,7 +685,6 @@ async function handleMemberAddedFromUI({
expireUpdate: expireDetails,
markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier
};
const updateMessagesToPush: Array<GroupUpdateMemberChangeMessage> = [];
if (withHistory.length) {
const msgModel = await ClosedGroup.addUpdateMessage({
@ -735,8 +721,35 @@ async function handleMemberAddedFromUI({
}
}
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
updateMessagesToPush,
group
);
// push new members & key supplement in a single batch call
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
supplementalKeysSubRequest,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest: null,
extraStoreRequests,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
'handleMemberAddedFromUIOrNot: pushChangesToGroupSwarmIfNeeded did not return success'
);
}
// schedule send invite details, auth signature, etc. to the new users
await scheduleGroupInviteJobs(groupPk, withHistory, withoutHistory);
await LibSessionUtil.saveDumpsToDb(groupPk);
convo.set({
active_at: createAtNetworkTimestamp,
});
await convo.commit();
await GroupSync.storeGroupUpdateMessages({ groupPk, updateMessages: updateMessagesToPush });
}
/**
@ -782,32 +795,14 @@ async function handleMemberRemovedFromUI({
const createAtNetworkTimestamp = GetNetworkTime.now();
await LibSessionUtil.saveDumpsToDb(groupPk);
// revoked pubkeys, update messages, and libsession groups config in a single batch call
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
encryptedSupplementKeys: [],
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
'currentDeviceGroupMembersChange: pushChangesToGroupSwarmIfNeeded did not return success'
);
}
await LibSessionUtil.saveDumpsToDb(groupPk);
convo.set({
active_at: createAtNetworkTimestamp,
});
const expiringDetails = DisappearingMessages.getExpireDetailsForOutgoingMesssage(
convo,
createAtNetworkTimestamp
);
const shared = {
let removedControlMessage: GroupUpdateMemberChangeMessage | null = null;
if (removed.length && !fromMemberLeftMessage) {
const msgModel = await ClosedGroup.addUpdateMessage({
diff: { type: 'kicked', kicked: removed },
convo,
sender: us,
sentAt: createAtNetworkTimestamp,
@ -819,16 +814,9 @@ async function handleMemberRemovedFromUI({
? createAtNetworkTimestamp + expiringDetails.expireTimer
: null,
},
};
await convo.commit();
if (removed.length && !fromMemberLeftMessage) {
const msgModel = await ClosedGroup.addUpdateMessage({
diff: { type: 'kicked', kicked: removed },
...shared,
markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier
});
const removedControlMessage = await getRemovedControlMessage({
removedControlMessage = await getRemovedControlMessage({
adminSecretKey: group.secretKey,
convo,
groupPk,
@ -837,13 +825,34 @@ async function handleMemberRemovedFromUI({
fromMemberLeftMessage,
dbMsgIdentifier: msgModel.id,
});
if (removedControlMessage) {
await GroupSync.storeGroupUpdateMessages({
}
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[removedControlMessage],
group
);
// revoked pubkeys, update messages, and libsession groups config in a single batch call
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
updateMessages: [removedControlMessage],
supplementalKeysSubRequest: [],
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
extraStoreRequests,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
'currentDeviceGroupMembersChange: pushChangesToGroupSwarmIfNeeded did not return success'
);
}
}
await LibSessionUtil.saveDumpsToDb(groupPk);
convo.set({
active_at: createAtNetworkTimestamp,
});
await convo.commit();
}
async function handleNameChangeFromUI({
@ -901,12 +910,18 @@ async function handleNameChangeFromUI({
...DisappearingMessages.getExpireDetailsForOutgoingMesssage(convo, createAtNetworkTimestamp),
});
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[nameChangeMsg],
group
);
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
encryptedSupplementKeys: [],
supplementalKeysSubRequest: [],
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
extraStoreRequests,
});
if (batchResult !== RunJobResult.Success) {
@ -916,7 +931,6 @@ async function handleNameChangeFromUI({
}
await UserSync.queueNewJobIfNeeded();
await GroupSync.storeGroupUpdateMessages({ groupPk, updateMessages: [nameChangeMsg] });
convo.set({
active_at: createAtNetworkTimestamp,
@ -1018,10 +1032,24 @@ const triggerFakeAvatarUpdate = createAsyncThunk(
secretKey: group.secretKey,
sodium: await getSodiumRenderer(),
});
await GroupSync.storeGroupUpdateMessages({
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[updateMsg],
group
);
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
updateMessages: [updateMsg],
supplementalKeysSubRequest: [],
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
extraStoreRequests,
});
if (!batchResult) {
window.log.warn(`failed to send avatarChange message for group ${ed25519Str(groupPk)}`);
throw new Error('failed to send avatarChange message');
}
}
);

@ -277,7 +277,8 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
encryptedSupplementKeys: [],
supplementalKeysSubRequest: [],
extraStoreRequests: [],
});
expect(result).to.be.eq(RunJobResult.Success);
expect(sendStub.callCount).to.be.eq(0);
@ -302,7 +303,8 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
encryptedSupplementKeys: [],
supplementalKeysSubRequest: [],
extraStoreRequests: [],
});
sendStub.resolves(undefined);
@ -376,7 +378,8 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
encryptedSupplementKeys: [],
supplementalKeysSubRequest: [],
extraStoreRequests: [],
});
expect(sendStub.callCount).to.be.eq(1);

Loading…
Cancel
Save