fix: add setadmin on promote accepts

also sign/verify group update messages
pull/2963/head
Audric Ackermann 1 year ago
parent 16e7ee1cd6
commit 9595f09085

@ -90,13 +90,12 @@ message GroupUpdateInviteMessage {
required string name = 2;
required bytes memberAuthData = 3;
// @required
required bytes adminSignature = 6;
required bytes adminSignature = 4;
}
message GroupUpdateDeleteMessage {
// @required
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
// @required
required bytes adminSignature = 2;
repeated string memberSessionIds = 1;
required bytes adminSignature = 2;
}
@ -111,6 +110,7 @@ message GroupUpdateInfoChangeMessage {
required Type type = 1;
optional string updatedName = 2;
optional uint32 updatedExpiration = 3;
required bytes adminSignature = 4;
}
message GroupUpdateMemberChangeMessage {
@ -122,7 +122,8 @@ message GroupUpdateMemberChangeMessage {
// @required
required Type type = 1;
repeated string memberSessionIds = 2;
repeated string memberSessionIds = 2;
required bytes adminSignature = 3;
}
@ -142,9 +143,9 @@ message GroupUpdateInviteResponseMessage {
message GroupUpdateDeleteMemberContentMessage {
repeated string memberSessionIds = 1;
// @required
required bytes adminSignature = 2;
repeated string memberSessionIds = 1;
repeated string messageHashes = 2;
optional bytes adminSignature = 3;
}
message GroupUpdateMessage {

@ -997,14 +997,12 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (!this.id) {
throw new Error('A message always needs an id');
}
perfStart(`messageCommit-${this.id}`);
console.warn('this.attributes', JSON.stringify(this.attributes));
// because the saving to db calls _cleanData which mutates the field for cleaning, we need to save a copy
const id = await Data.saveMessage(cloneDeep(this.attributes));
if (triggerUIUpdate) {
this.dispatchMessageUpdate();
}
perfEnd(`messageCommit-${this.id}`, 'messageCommit');
return id;
}

@ -38,8 +38,6 @@ import { ECKeyPair } from './keypairs';
export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try {
console.warn('444 envelope.source', envelope.source);
console.warn('444 envelope.senderIdentity', envelope.senderIdentity);
const decryptedForAll = await decrypt(envelope);
if (!decryptedForAll || !decryptedForAll.decryptedContent || isEmpty(decryptedForAll)) {

@ -1,16 +1,22 @@
import { PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isEmpty, isFinite, isNumber } from 'lodash';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { ConvoHub } from '../../session/conversations';
import { getSodiumRenderer } from '../../session/crypto';
import { ClosedGroup } from '../../session/group/closed-group';
import { ed25519Str } from '../../session/onions/onionPath';
import { PubKey } from '../../session/types';
import { UserUtils } from '../../session/utils';
import { stringToUint8Array } from '../../session/utils/String';
import { PreConditionFailed } from '../../session/utils/errors';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils';
import { groupInfoActions } from '../../state/ducks/groups';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { BlockedNumberController } from '../../util';
import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
@ -27,11 +33,7 @@ type GroupInviteDetails = {
} & WithEnvelopeTimestamp &
WithAuthor;
type GroupMemberChangeDetails = {
memberChangeDetails: SignalService.GroupUpdateMemberChangeMessage;
} & WithEnvelopeTimestamp &
WithGroupPubkey &
WithAuthor;
type GroupUpdateGeneric<T> = { change: T } & WithEnvelopeTimestamp & WithGroupPubkey & WithAuthor;
type GroupUpdateDetails = {
updateMessage: SignalService.GroupUpdateMessage;
@ -43,22 +45,39 @@ async function handleGroupInviteMessage({
envelopeTimestamp,
}: GroupInviteDetails) {
if (!PubKey.is03Pubkey(inviteMessage.groupSessionId)) {
// invite to a group which has not a 03 prefix, we can just drop it.
return;
}
if (BlockedNumberController.isBlocked(author)) {
window.log.info(
`received invite to group ${ed25519Str(
inviteMessage.groupSessionId
)} by blocked user:${ed25519Str(author)}... dropping it`
);
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(inviteMessage.groupSessionId),
signature: inviteMessage.adminSignature,
data: stringToUint8Array(`INVITE${UserUtils.getOurPubKeyStrFromCache()}${envelopeTimestamp}`),
});
if (!sigValid) {
window.log.warn('received group invite with invalid signature. dropping');
return;
}
window.log.debug(
`received invite to group ${ed25519Str(inviteMessage.groupSessionId)} by user:${ed25519Str(
author
)}`
);
// TODO verify sig invite adminSignature
const convo = await ConvoHub.use().getOrCreateAndWait(
inviteMessage.groupSessionId,
ConversationTypeEnum.GROUPV2
);
convo.set({
active_at: envelopeTimestamp,
didApproveMe: true,
});
if (inviteMessage.name && isEmpty(convo.getRealSessionUsername())) {
@ -94,33 +113,107 @@ async function handleGroupInviteMessage({
).buffer,
});
await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache());
await UserSync.queueNewJobIfNeeded();
// TODO use the pending so we actually don't start polling here unless it is not in the pending state.
// once everything is ready, start polling using that authData to get the keys, members, details of that group, and its messages.
getSwarmPollingInstance().addGroupId(inviteMessage.groupSessionId);
}
async function handleGroupMemberChangeMessage({
memberChangeDetails,
async function verifySig({
data,
pubKey,
signature,
}: {
data: Uint8Array;
signature: Uint8Array;
pubKey: Uint8Array;
}) {
const sodium = await getSodiumRenderer();
return sodium.crypto_sign_verify_detached(signature, data, pubKey);
}
async function handleGroupInfoChangeMessage({
change,
groupPk,
envelopeTimestamp,
author,
}: GroupMemberChangeDetails) {
if (!PubKey.is03Pubkey(groupPk)) {
// invite to a group which has not a 03 prefix, we can just drop it.
}: GroupUpdateGeneric<SignalService.GroupUpdateInfoChangeMessage>) {
const sigValid = await verifySig({
pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(`INFO_CHANGE${change.type}${envelopeTimestamp}`),
});
if (!sigValid) {
window.log.warn('received group info change with invalid signature. dropping');
return;
}
// TODO verify sig invite adminSignature
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
switch (memberChangeDetails.type) {
switch (change.type) {
case SignalService.GroupUpdateInfoChangeMessage.Type.NAME: {
await ClosedGroup.addUpdateMessage(
convo,
{ newName: change.updatedName },
author,
envelopeTimestamp
);
break;
}
case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR: {
console.warn('Not implemented');
throw new Error('Not implemented');
}
case SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES: {
if (
change.updatedExpiration &&
isNumber(change.updatedExpiration) &&
isFinite(change.updatedExpiration) &&
change.updatedExpiration >= 0
) {
await convo.updateExpireTimer(change.updatedExpiration, author, envelopeTimestamp);
}
break;
}
default:
return;
}
convo.set({
active_at: envelopeTimestamp,
});
}
async function handleGroupMemberChangeMessage({
change,
groupPk,
envelopeTimestamp,
author,
}: GroupUpdateGeneric<SignalService.GroupUpdateMemberChangeMessage>) {
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(`MEMBER_CHANGE${change.type}${envelopeTimestamp}`),
});
if (!sigValid) {
window.log.warn('received group member change with invalid signature. dropping');
return;
}
switch (change.type) {
case SignalService.GroupUpdateMemberChangeMessage.Type.ADDED: {
await ClosedGroup.addUpdateMessage(
convo,
{ joiningMembers: memberChangeDetails.memberSessionIds },
{ joiningMembers: change.memberSessionIds },
author,
envelopeTimestamp
);
@ -130,7 +223,7 @@ async function handleGroupMemberChangeMessage({
case SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED: {
await ClosedGroup.addUpdateMessage(
convo,
{ kickedMembers: memberChangeDetails.memberSessionIds },
{ kickedMembers: change.memberSessionIds },
author,
envelopeTimestamp
);
@ -139,7 +232,7 @@ async function handleGroupMemberChangeMessage({
case SignalService.GroupUpdateMemberChangeMessage.Type.PROMOTED: {
await ClosedGroup.addUpdateMessage(
convo,
{ promotedMembers: memberChangeDetails.memberSessionIds },
{ promotedMembers: change.memberSessionIds },
author,
envelopeTimestamp
);
@ -151,27 +244,187 @@ async function handleGroupMemberChangeMessage({
convo.set({
active_at: envelopeTimestamp,
didApproveMe: true,
});
}
async function handleGroupUpdateMessage(
async function handleGroupMemberLeftMessage({
groupPk,
envelopeTimestamp,
author,
}: GroupUpdateGeneric<SignalService.GroupUpdateMemberLeftMessage>) {
// No need to verify sig, the author is already verified with the libsession.decrypt()
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
await ClosedGroup.addUpdateMessage(
convo,
{ leavingMembers: [author] },
author,
envelopeTimestamp
);
convo.set({
active_at: envelopeTimestamp,
});
// TODO We should process this message type even if the sender is blocked
}
async function handleGroupDeleteMemberContentMessage({
groupPk,
envelopeTimestamp,
change,
}: GroupUpdateGeneric<SignalService.GroupUpdateDeleteMemberContentMessage>) {
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(
`DELETE_CONTENT${envelopeTimestamp}${change.memberSessionIds.join()}${change.messageHashes.join()}`
),
});
if (!sigValid) {
window.log.warn('received group member delete content with invalid signature. dropping');
return;
}
// TODO we should process this message type even if the sender is blocked
console.warn('Not implemented');
convo.set({
active_at: envelopeTimestamp,
});
throw new Error('Not implemented');
}
async function handleGroupUpdateDeleteMessage({
groupPk,
envelopeTimestamp,
change,
}: GroupUpdateGeneric<SignalService.GroupUpdateDeleteMessage>) {
// TODO verify sig?
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
const sigValid = await verifySig({
pubKey: HexString.fromHexString(groupPk),
signature: change.adminSignature,
data: stringToUint8Array(`DELETE${envelopeTimestamp}${change.memberSessionIds.join()}`),
});
if (!sigValid) {
window.log.warn('received group delete message with invalid signature. dropping');
return;
}
convo.set({
active_at: envelopeTimestamp,
});
console.warn('Not implemented');
throw new Error('Not implemented');
// TODO We should process this message type even if the sender is blocked
}
async function handleGroupUpdateInviteResponseMessage({
groupPk,
envelopeTimestamp,
}: GroupUpdateGeneric<SignalService.GroupUpdateInviteResponseMessage>) {
// no sig verify for this type of messages
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
convo.set({
active_at: envelopeTimestamp,
});
console.warn('Not implemented');
// TODO We should process this message type even if the sender is blocked
throw new Error('Not implemented');
}
async function handleGroupUpdatePromoteMessage({
change,
}: Omit<GroupUpdateGeneric<SignalService.GroupUpdatePromoteMessage>, 'groupPk'>) {
const seed = change.groupIdentitySeed;
const sodium = await getSodiumRenderer();
const groupKeypair = sodium.crypto_sign_seed_keypair(seed);
const groupPk = `03${HexString.toHexString(groupKeypair.publicKey)}` as GroupPubkeyType;
const convo = ConvoHub.use().get(groupPk);
if (!convo) {
return;
}
// no group update message here, another message is sent to the group's swarm for the update message.
// this message is just about the keys that we need to save, and accepting the promotion.
const found = await UserGroupsWrapperActions.getGroup(groupPk);
if (!found) {
// could have been removed by the user already so let's not force create it
window.log.info(
'received group promote message but that group is not in the usergroups wrapper'
);
return;
}
found.secretKey = groupKeypair.privateKey;
await UserGroupsWrapperActions.setGroup(found);
await UserSync.queueNewJobIfNeeded();
window.inboxStore.dispatch(
groupInfoActions.markUsAsAdmin({
groupPk,
})
);
// TODO we should process this even if the sender is blocked
}
async function handle1o1GroupUpdateMessage(
details: GroupUpdateDetails & WithUncheckedSource & WithUncheckedSenderIdentity
) {
if (details.updateMessage.inviteMessage) {
// the invite message is received from our own swarm, so source is the sender, and senderIdentity is empty
const author = details.source;
if (!PubKey.is05Pubkey(author)) {
window.log.warn('received group inviteMessage with invalid author');
return;
// the message types below are received from our own swarm, so source is the sender, and senderIdentity is empty
if (details.updateMessage.inviteMessage || details.updateMessage.promoteMessage) {
if (!PubKey.is05Pubkey(details.source)) {
window.log.warn('received group invite/promote with invalid author');
throw new PreConditionFailed('received group invite/promote with invalid author');
}
await handleGroupInviteMessage({
inviteMessage: details.updateMessage.inviteMessage as SignalService.GroupUpdateInviteMessage,
...details,
author,
});
if (details.updateMessage.inviteMessage) {
await handleGroupInviteMessage({
inviteMessage: details.updateMessage
.inviteMessage as SignalService.GroupUpdateInviteMessage,
...details,
author: details.source,
});
} else if (details.updateMessage.promoteMessage) {
await handleGroupUpdatePromoteMessage({
change: details.updateMessage.promoteMessage as SignalService.GroupUpdatePromoteMessage,
...details,
author: details.source,
});
}
// returns true for all cases where this message was expected to be a 1o1 message, even if not processed
return true;
}
return false;
}
async function handleGroupUpdateMessage(
details: GroupUpdateDetails & WithUncheckedSource & WithUncheckedSenderIdentity
) {
const was1o1Message = await handle1o1GroupUpdateMessage(details);
if (was1o1Message) {
return;
}
// other messages are received from the groups swarm, so source is the groupPk, and senderIdentity is the author
const author = details.senderIdentity;
const groupPk = details.source;
@ -179,16 +432,57 @@ async function handleGroupUpdateMessage(
window.log.warn('received group update message with invalid author or groupPk');
return;
}
const detailsWithContext = { ...details, author, groupPk };
if (details.updateMessage.memberChangeMessage) {
await handleGroupMemberChangeMessage({
memberChangeDetails: details.updateMessage
change: details.updateMessage
.memberChangeMessage as SignalService.GroupUpdateMemberChangeMessage,
...details,
author,
groupPk,
...detailsWithContext,
});
return;
}
if (details.updateMessage.infoChangeMessage) {
await handleGroupInfoChangeMessage({
change: details.updateMessage.infoChangeMessage as SignalService.GroupUpdateInfoChangeMessage,
...detailsWithContext,
});
return;
}
if (details.updateMessage.memberLeftMessage) {
await handleGroupMemberLeftMessage({
change: details.updateMessage.memberLeftMessage as SignalService.GroupUpdateMemberLeftMessage,
...detailsWithContext,
});
return;
}
if (details.updateMessage.deleteMemberContent) {
await handleGroupDeleteMemberContentMessage({
change: details.updateMessage
.deleteMemberContent as SignalService.GroupUpdateDeleteMemberContentMessage,
...detailsWithContext,
});
return;
}
if (details.updateMessage.deleteMessage) {
await handleGroupUpdateDeleteMessage({
change: details.updateMessage.deleteMessage as SignalService.GroupUpdateDeleteMessage,
...detailsWithContext,
});
return;
}
if (details.updateMessage.inviteResponse) {
await handleGroupUpdateInviteResponseMessage({
change: details.updateMessage
.inviteResponse as SignalService.GroupUpdateInviteResponseMessage,
...detailsWithContext,
});
return;
}
window.log.warn('received group update of unknown type. Discarding...');
}
export const GroupV2Receiver = { handleGroupUpdateMessage };

@ -31,6 +31,11 @@ export enum SnodeNamespaces {
*/
UserGroups = 5,
/**
* This is the namespace that revoked members can still poll messages from
*/
ClosedGroupRevokedRetrievableMessages = -11,
/**
* This is the namespace used to sync the closed group messages for each closed group
*/
@ -97,6 +102,7 @@ function isUserConfigNamespace(namespace: SnodeNamespaces): namespace is UserCon
case SnodeNamespaces.ClosedGroupMembers:
case SnodeNamespaces.ClosedGroupMessages:
case SnodeNamespaces.LegacyClosedGroup:
case SnodeNamespaces.ClosedGroupRevokedRetrievableMessages:
case SnodeNamespaces.Default:
// user messages is not hosting config based messages
return false;
@ -125,6 +131,7 @@ function isGroupConfigNamespace(
case SnodeNamespaces.ConvoInfoVolatile:
case SnodeNamespaces.LegacyClosedGroup:
case SnodeNamespaces.ClosedGroupMessages:
case SnodeNamespaces.ClosedGroupRevokedRetrievableMessages:
return false;
case SnodeNamespaces.ClosedGroupInfo:
case SnodeNamespaces.ClosedGroupKeys:
@ -153,6 +160,9 @@ function isGroupNamespace(namespace: SnodeNamespaces): namespace is SnodeNamespa
if (namespace === SnodeNamespaces.ClosedGroupMessages) {
return true;
}
if (namespace === SnodeNamespaces.ClosedGroupRevokedRetrievableMessages) {
return true;
}
switch (namespace) {
case SnodeNamespaces.Default:
case SnodeNamespaces.UserContacts:
@ -185,6 +195,7 @@ function namespacePriority(namespace: SnodeNamespaces): 10 | 1 {
case SnodeNamespaces.ClosedGroupInfo:
case SnodeNamespaces.ClosedGroupMembers:
case SnodeNamespaces.ClosedGroupKeys:
case SnodeNamespaces.ClosedGroupRevokedRetrievableMessages:
return 1;
default:

@ -1,11 +1,13 @@
import _, { isFinite, isNumber } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { PubkeyType } from 'libsession_util_nodejs';
import { getMessageQueue } from '..';
import { Data } from '../../data/data';
import { ConversationModel } from '../../models/conversation';
import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes';
import { MessageModel } from '../../models/message';
import { MessageGroupUpdate } from '../../models/messageType';
import { SignalService } from '../../protobuf';
import {
addKeyPairToCacheAndDBIfNeeded,
@ -37,10 +39,6 @@ export type GroupInfo = {
admins?: Array<string>;
};
export interface GroupDiff extends MemberChanges {
newName?: string;
}
export interface MemberChanges {
joiningMembers?: Array<string>;
leavingMembers?: Array<string>;
@ -48,6 +46,10 @@ export interface MemberChanges {
promotedMembers?: Array<string>;
}
export interface GroupDiff extends MemberChanges {
newName?: string;
}
/**
* This function is only called when the local user makes a change to a group.
* So this function is not called on group updates from the network, even from another of our devices.
@ -141,7 +143,7 @@ async function addUpdateMessage(
sender: string,
sentAt: number
): Promise<MessageModel> {
const groupUpdate: any = {};
const groupUpdate: MessageGroupUpdate = {};
if (diff.newName) {
groupUpdate.name = diff.newName;
@ -149,14 +151,14 @@ async function addUpdateMessage(
if (diff.joiningMembers) {
groupUpdate.joined = diff.joiningMembers;
}
if (diff.leavingMembers) {
} else if (diff.leavingMembers) {
groupUpdate.left = diff.leavingMembers;
}
if (diff.kickedMembers) {
} else if (diff.kickedMembers) {
groupUpdate.kicked = diff.kickedMembers;
} else if (diff.promotedMembers) {
groupUpdate.promoted = diff.promotedMembers as Array<PubkeyType>;
} else {
throw new Error('addUpdateMessage with unknown type of change');
}
if (UserUtils.isUsFromCache(sender)) {

@ -1,8 +1,14 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { SignalService } from '../../../../../protobuf';
import { LibSodiumWrappers } from '../../../../crypto';
import { DataMessage } from '../../DataMessage';
import { MessageParams } from '../../Message';
export type AdminSigDetails = {
secretKey: Uint8Array;
sodium: LibSodiumWrappers;
};
export interface GroupUpdateMessageParams extends MessageParams {
groupPk: GroupPubkeyType;
}

@ -1,21 +1,31 @@
import { isEmpty, isFinite } from 'lodash';
import { SignalService } from '../../../../../../protobuf';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
import { LibSodiumWrappers } from '../../../../../crypto';
import { stringToUint8Array } from '../../../../../utils/String';
import { PreConditionFailed } from '../../../../../utils/errors';
import {
AdminSigDetails,
GroupUpdateMessage,
GroupUpdateMessageParams,
} from '../GroupUpdateMessage';
type NameChangeParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.NAME;
updatedName: string;
};
type NameChangeParams = GroupUpdateMessageParams &
AdminSigDetails & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.NAME;
updatedName: string;
};
type AvatarChangeParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR;
};
type AvatarChangeParams = GroupUpdateMessageParams &
AdminSigDetails & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR;
};
type DisappearingMessageChangeParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES;
updatedExpirationSeconds: number;
};
type DisappearingMessageChangeParams = GroupUpdateMessageParams &
AdminSigDetails & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES;
updatedExpirationSeconds: number;
};
/**
* GroupUpdateInfoChangeMessage is sent as a message to group's swarm.
@ -25,12 +35,16 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
public readonly updatedName: string = '';
public readonly updatedExpirationSeconds: number = 0;
public readonly namespace = SnodeNamespaces.ClosedGroupMessages;
private readonly secretKey: Uint8Array; // not sent, only used for signing content as part of the message
private readonly sodium: LibSodiumWrappers;
constructor(params: NameChangeParams | AvatarChangeParams | DisappearingMessageChangeParams) {
super(params);
const types = SignalService.GroupUpdateInfoChangeMessage.Type;
this.typeOfChange = params.typeOfChange;
this.secretKey = params.secretKey;
this.sodium = params.sodium;
switch (params.typeOfChange) {
case types.NAME: {
@ -42,6 +56,7 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
}
case types.AVATAR:
// nothing to do for avatar
throw new PreConditionFailed('not implemented');
break;
case types.DISAPPEARING_MESSAGES: {
if (!isFinite(params.updatedExpirationSeconds) || params.updatedExpirationSeconds < 0) {
@ -58,16 +73,26 @@ export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
public dataProto(): SignalService.DataMessage {
const infoChangeMessage = new SignalService.GroupUpdateInfoChangeMessage({
type: this.typeOfChange,
adminSignature: this.sodium.crypto_sign_detached(
stringToUint8Array(`INFO_CHANGE${this.typeOfChange}${this.timestamp}`),
this.secretKey
),
});
switch (this.typeOfChange) {
case SignalService.GroupUpdateInfoChangeMessage.Type.NAME:
infoChangeMessage.updatedName = this.updatedName;
if (this.typeOfChange === SignalService.GroupUpdateInfoChangeMessage.Type.NAME) {
infoChangeMessage.updatedName = this.updatedName;
}
if (
this.typeOfChange === SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES
) {
infoChangeMessage.updatedExpiration = this.updatedExpirationSeconds;
break;
case SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES:
infoChangeMessage.updatedExpiration = this.updatedExpirationSeconds;
break;
case SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR:
default:
break;
}
return new SignalService.DataMessage({ groupUpdateMessage: { infoChangeMessage } });
}

@ -3,7 +3,13 @@ import { isEmpty } from 'lodash';
import { SignalService } from '../../../../../../protobuf';
import { assertUnreachable } from '../../../../../../types/sqlSharedTypes';
import { SnodeNamespaces } from '../../../../../apis/snode_api/namespaces';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
import { LibSodiumWrappers } from '../../../../../crypto';
import { stringToUint8Array } from '../../../../../utils/String';
import {
AdminSigDetails,
GroupUpdateMessage,
GroupUpdateMessageParams,
} from '../GroupUpdateMessage';
type MembersAddedMessageParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED;
@ -27,15 +33,24 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage {
public readonly typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type;
public readonly memberSessionIds: Array<PubkeyType> = []; // added, removed, promoted based on the type.
public readonly namespace = SnodeNamespaces.ClosedGroupMessages;
private readonly secretKey: Uint8Array; // not sent, only used for signing content as part of the message
private readonly sodium: LibSodiumWrappers;
constructor(
params: MembersAddedMessageParams | MembersRemovedMessageParams | MembersPromotedMessageParams
params: (
| MembersAddedMessageParams
| MembersRemovedMessageParams
| MembersPromotedMessageParams
) &
AdminSigDetails
) {
super(params);
const { Type } = SignalService.GroupUpdateMemberChangeMessage;
const { typeOfChange } = params;
this.typeOfChange = typeOfChange;
this.secretKey = params.secretKey;
this.sodium = params.sodium;
switch (typeOfChange) {
case Type.ADDED: {
@ -68,6 +83,10 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage {
const memberChangeMessage = new SignalService.GroupUpdateMemberChangeMessage({
type: this.typeOfChange,
memberSessionIds: this.memberSessionIds,
adminSignature: this.sodium.crypto_sign_detached(
stringToUint8Array(`MEMBER_CHANGE${this.typeOfChange}${this.timestamp}`),
this.secretKey
),
});
return new SignalService.DataMessage({ groupUpdateMessage: { memberChangeMessage } });

@ -7,7 +7,7 @@ interface Params extends GroupUpdateMessageParams {
}
/**
* GroupUpdateDeleteMessage is sent as a 1o1 message to the recipient, not through the group's swarm.
* GroupUpdateDeleteMessage is sent to the group's swarm on the `revokedRetrievableGroupMessages`
*/
export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
public readonly adminSignature: Params['adminSignature'];
@ -27,9 +27,9 @@ export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
public dataProto(): SignalService.DataMessage {
const deleteMessage = new SignalService.GroupUpdateDeleteMessage({
groupSessionId: this.destination,
adminSignature: this.adminSignature,
});
throw new Error('Not implemented');
return new SignalService.DataMessage({ groupUpdateMessage: { deleteMessage } });
}

@ -61,7 +61,9 @@ function overwriteOutgoingTimestampWithNetworkTimestamp(message: { plainTextBuff
if (
dataMessage.syncTarget ||
dataMessage.groupUpdateMessage?.inviteMessage ||
dataMessage.groupUpdateMessage?.infoChangeMessage ||
dataMessage.groupUpdateMessage?.deleteMemberContent ||
dataMessage.groupUpdateMessage?.memberChangeMessage ||
dataMessage.groupUpdateMessage?.deleteMessage
) {
return {

@ -21,6 +21,7 @@ import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces';
import { RevokeChanges, SnodeAPIRevoke } from '../../session/apis/snode_api/revokeSubaccount';
import { SnodeGroupSignature } from '../../session/apis/snode_api/signature/groupSignature';
import { ConvoHub } from '../../session/conversations';
import { getSodiumRenderer } from '../../session/crypto';
import { ClosedGroup } from '../../session/group/closed-group';
import { GroupUpdateInfoChangeMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { GroupUpdateMemberChangeMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage';
@ -32,6 +33,7 @@ import { PreConditionFailed } from '../../session/utils/errors';
import { RunJobResult } from '../../session/utils/job_runners/PersistedJob';
import { GroupInvite } from '../../session/utils/job_runners/jobs/GroupInviteJob';
import { GroupSync } from '../../session/utils/job_runners/jobs/GroupSyncJob';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import {
getGroupPubkeyFromWrapperType,
@ -129,7 +131,7 @@ const initNewGroupInWrapper = createAsyncThunk(
const member = uniqMembers[index];
const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member);
if (created.pubkeyHex === us) {
await MetaGroupWrapperActions.memberSetPromoted(groupPk, created.pubkeyHex, false);
await MetaGroupWrapperActions.memberSetAdmin(groupPk, created.pubkeyHex);
} else {
await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false);
}
@ -474,11 +476,14 @@ async function handleRemoveMembers({
timestamp,
adminSignature: from_base64(adminSignature.signature, base64_variants.ORIGINAL),
});
console.warn(
'TODO: poll from namespace -11, handle messages and sig for it, batch request handle 401/403, but 200 ok for this -11 namespace'
);
const sentStatus = await getMessageQueue().sendToPubKeyNonDurably({
pubkey: PubKey.cast(m),
message: deleteMessage,
namespace: SnodeNamespaces.Default,
namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
});
if (!sentStatus) {
window.log.warn('Failed to send a GroupUpdateDeleteMessage to a member removed: ', m);
@ -581,6 +586,7 @@ async function handleMemberChangeFromUIOrNot({
const member = withHistory[index];
await GroupInvite.addGroupInviteJob({ groupPk, member });
}
const sodium = await getSodiumRenderer();
const allAdded = [...withHistory, ...withoutHistory]; // those are already enforced to be unique (and without intersection) in `validateMemberChange()`
const timestamp = Date.now();
@ -598,6 +604,8 @@ async function handleMemberChangeFromUIOrNot({
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED,
identifier: msg.id,
timestamp,
secretKey: group.secretKey,
sodium,
}),
});
}
@ -614,7 +622,9 @@ async function handleMemberChangeFromUIOrNot({
groupPk,
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED,
identifier: msg.id,
timestamp: Date.now(),
timestamp: GetNetworkTime.getNowWithNetworkOffset(),
secretKey: group.secretKey,
sodium,
}),
});
}
@ -637,6 +647,10 @@ async function handleNameChangeFromUIOrNot({
if (!group || !group.secretKey || isEmpty(group.secretKey)) {
throw new Error('tried to make change to group but we do not have the admin secret key');
}
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
if (!infos) {
throw new PreConditionFailed('nameChange infoGet is empty');
}
// this throws if the name is the same, or empty
const { newName, convo, us } = validateNameChange({
@ -646,8 +660,10 @@ async function handleNameChangeFromUIOrNot({
});
group.name = newName;
infos.name = newName;
await UserGroupsWrapperActions.setGroup(group);
console.warn('after set and refetcj', await UserGroupsWrapperActions.getGroup(group.pubkeyHex));
await MetaGroupWrapperActions.infoSet(groupPk, infos);
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded(groupPk, []);
if (batchResult !== RunJobResult.Success) {
throw new Error(
@ -655,6 +671,8 @@ async function handleNameChangeFromUIOrNot({
);
}
await UserSync.queueNewJobIfNeeded();
const timestamp = Date.now();
if (fromCurrentDevice) {
@ -666,6 +684,8 @@ async function handleNameChangeFromUIOrNot({
updatedName: newName,
identifier: msg.id,
timestamp: Date.now(),
secretKey: group.secretKey,
sodium: await getSodiumRenderer(),
}),
});
}
@ -712,6 +732,41 @@ const currentDeviceGroupMembersChange = createAsyncThunk(
}
);
const markUsAsAdmin = createAsyncThunk(
'group/markUsAsAdmin',
async (
{
groupPk,
}: {
groupPk: GroupPubkeyType;
},
payloadCreator
): Promise<GroupDetailsUpdate> => {
const state = payloadCreator.getState() as StateType;
if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) {
throw new PreConditionFailed('markUsAsAdmin group not present in redux slice');
}
const us = UserUtils.getOurPubKeyStrFromCache();
if (state.groups.members[groupPk].find(m => m.pubkeyHex === us)?.admin) {
// we are already an admin, nothing to do
return {
groupPk,
infos: await MetaGroupWrapperActions.infoGet(groupPk),
members: await MetaGroupWrapperActions.memberGetAll(groupPk),
};
}
await MetaGroupWrapperActions.memberSetAdmin(groupPk, us);
await GroupSync.queueNewJobIfNeeded(groupPk);
return {
groupPk,
infos: await MetaGroupWrapperActions.infoGet(groupPk),
members: await MetaGroupWrapperActions.memberGetAll(groupPk),
};
}
);
const currentDeviceGroupNameChange = createAsyncThunk(
'group/currentDeviceGroupNameChange',
async (
@ -863,6 +918,17 @@ const groupSlice = createSlice({
builder.addCase(currentDeviceGroupNameChange.pending, state => {
state.nameChangesFromUIPending = true;
});
builder.addCase(markUsAsAdmin.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload;
state.infos[groupPk] = infos;
state.members[groupPk] = members;
window.log.debug(`groupInfo after markUsAsAdmin: ${stringify(infos)}`);
window.log.debug(`groupMembers after markUsAsAdmin: ${stringify(members)}`);
});
builder.addCase(markUsAsAdmin.rejected, (_state, action) => {
window.log.error('a markUsAsAdmin was rejected', action.error);
});
},
});
@ -873,6 +939,7 @@ export const groupInfoActions = {
refreshGroupDetailsFromWrapper,
handleUserGroupUpdate,
currentDeviceGroupMembersChange,
markUsAsAdmin,
currentDeviceGroupNameChange,
...groupSlice.actions,
};

@ -28,6 +28,7 @@ function emptyMember(pubkeyHex: PubkeyType): GroupMemberGet {
promoted: false,
promotionFailed: false,
promotionPending: false,
admin: false,
pubkeyHex,
};
}

@ -438,6 +438,10 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
pubkeyHex,
failed,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetPromoted']>>,
memberSetAdmin: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) =>
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'memberSetAdmin', pubkeyHex]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['memberSetAdmin']>
>,
memberSetInvited: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType, failed: boolean) =>
callLibSessionWorker([
`MetaGroupConfig-${groupPk}`,

Loading…
Cancel
Save