fix: add still broken promote handling with set_sig_keys

pull/2963/head
Audric Ackermann 1 year ago
parent bbb376fc2a
commit b259d18443

@ -130,7 +130,8 @@
"semver": "^7.5.4",
"styled-components": "5.1.1",
"uuid": "8.3.2",
"webrtc-adapter": "^4.1.1"
"webrtc-adapter": "^4.1.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",

@ -80,7 +80,6 @@ message GroupUpdateDeleteMessage {
required bytes adminSignature = 2;
}
message GroupUpdateInfoChangeMessage {
enum Type {
NAME = 1;
@ -232,7 +231,8 @@ message DataMessage {
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
optional bool blocksCommunityMessageRequests = 106;
optional GroupUpdateMessage groupUpdateMessage = 120;}
optional GroupUpdateMessage groupUpdateMessage = 120;
}
message CallMessage {

@ -6,6 +6,7 @@ import { useConversationUsernameOrShorten } from '../hooks/useParamSelector';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';
import { GroupInvite } from '../session/utils/job_runners/jobs/GroupInviteJob';
import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob';
import {
useMemberInviteFailed,
useMemberInvitePending,
@ -14,7 +15,12 @@ import {
} from '../state/selectors/groups';
import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar';
import { Flex } from './basic/Flex';
import { SessionButton, SessionButtonShape, SessionButtonType } from './basic/SessionButton';
import {
SessionButton,
SessionButtonColor,
SessionButtonShape,
SessionButtonType,
} from './basic/SessionButton';
import { SessionRadio } from './basic/SessionRadio';
const AvatarContainer = styled.div`
@ -92,7 +98,7 @@ type MemberListItemProps = {
groupPk?: string;
};
const ResendInviteContainer = ({
const ResendContainer = ({
displayGroupStatus,
groupPk,
pubkey,
@ -105,8 +111,14 @@ const ResendInviteContainer = ({
!UserUtils.isUsFromCache(pubkey)
) {
return (
<Flex container={true} margin="0 0 0 auto" padding="0 var(--margins-lg)">
<Flex
container={true}
margin="0 0 0 auto"
padding="0 var(--margins-lg)"
gap="var(--margins-sm)"
>
<ResendInviteButton groupPk={groupPk} pubkey={pubkey} />
<ResendPromoteButton groupPk={groupPk} pubkey={pubkey} />
</Flex>
);
}
@ -177,7 +189,28 @@ const ResendInviteButton = ({
buttonType={SessionButtonType.Solid}
text={window.i18n('resend')}
onClick={() => {
void GroupInvite.addGroupInviteJob({ groupPk, member: pubkey });
void GroupInvite.addJob({ groupPk, member: pubkey });
}}
/>
);
};
const ResendPromoteButton = ({
groupPk,
pubkey,
}: {
pubkey: PubkeyType;
groupPk: GroupPubkeyType;
}) => {
return (
<SessionButton
dataTestId="resend-promote-button"
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
buttonColor={SessionButtonColor.Danger}
text="ReSEND PRomote"
onClick={() => {
void GroupPromote.addJob({ groupPk, member: pubkey });
}}
/>
);
@ -234,11 +267,7 @@ export const MemberListItem = ({
</Flex>
</StyledInfo>
<ResendInviteContainer
pubkey={pubkey}
displayGroupStatus={displayGroupStatus}
groupPk={groupPk}
/>
<ResendContainer pubkey={pubkey} displayGroupStatus={displayGroupStatus} groupPk={groupPk} />
{!inMentions && (
<StyledCheckContainer>

@ -27,6 +27,7 @@ export interface FlexProps {
| 'inherit';
// Child Props
flexGrow?: number;
gap?: string;
flexShrink?: number;
flexBasis?: number;
// Common Layout Props
@ -52,6 +53,7 @@ export const Flex = styled.div<FlexProps>`
align-items: ${props => props.alignItems || 'stretch'};
margin: ${props => props.margin || '0'};
padding: ${props => props.padding || '0'};
gap: ${props => props.gap || undefined};
width: ${props => props.width || 'auto'};
height: ${props => props.height || 'auto'};
max-width: ${props => props.maxWidth || 'none'};

@ -1,7 +1,7 @@
import React from 'react';
import { PubkeyType } from 'libsession_util_nodejs';
import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector';
import { useConversationsUsernameWithQuoteOrShortPk } from '../../../../hooks/useParamSelector';
import { arrayContainsUsOnly } from '../../../../models/message';
import { PreConditionFailed } from '../../../../session/utils/errors';
import {
@ -89,7 +89,7 @@ const ChangeItemJoined = (added: Array<PubkeyType>): string => {
if (!added.length) {
throw new Error('Group update add is missing contacts');
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(added);
const names = useConversationsUsernameWithQuoteOrShortPk(added);
const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr();
if (isGroupV2) {
@ -107,7 +107,7 @@ const ChangeItemKicked = (removed: Array<PubkeyType>): string => {
if (!removed.length) {
throw new Error('Group update removed is missing contacts');
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(removed);
const names = useConversationsUsernameWithQuoteOrShortPk(removed);
const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr();
if (isGroupV2) {
@ -130,7 +130,7 @@ const ChangeItemPromoted = (promoted: Array<PubkeyType>): string => {
if (!promoted.length) {
throw new Error('Group update promoted is missing contacts');
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(promoted);
const names = useConversationsUsernameWithQuoteOrShortPk(promoted);
const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr();
if (isGroupV2) {
@ -148,7 +148,7 @@ const ChangeItemLeft = (left: Array<PubkeyType>): string => {
throw new Error('Group update remove is missing contacts');
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(left);
const names = useConversationsUsernameWithQuoteOrShortPk(left);
if (arrayContainsUsOnly(left)) {
return window.i18n('youLeftTheGroup');

@ -759,6 +759,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
expireTimer,
serverTimestamp: this.isPublic() ? networkTimestamp : undefined,
groupInvitation,
sent_at: networkTimestamp, // overriden later, but we need one to have the sorting done in the UI even when the sending is pending
});
// We're offline!
@ -1781,9 +1782,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const { id } = message;
const destination = this.id as string;
const sentAt = message.get('sent_at');
if (sentAt) {
throw new Error('sendMessageJob() sent_at is already set.');
const sentAt = message.get('sent_at'); // this is used to store the timestamp when we tried sending that message, it should be set by the caller
if (!sentAt) {
throw new Error('sendMessageJob() sent_at is not set.');
}
const networkTimestamp = GetNetworkTime.now();

@ -131,7 +131,7 @@ async function mergeUserConfigsWithIncomingUpdates(
const needsPush = await GenericWrapperActions.needsPush(variant);
const mergedTimestamps = sameVariant
.filter(m => hashesMerged.includes(m.hash))
.map(m => m.timestamp);
.map(m => m.storedAt);
const latestEnvelopeTimestamp = Math.max(...mergedTimestamps);
window.log.debug(

@ -44,12 +44,13 @@ export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageH
return;
}
const sentAtTimestamp = toNumber(envelope.timestamp);
const envelopeTimestamp = toNumber(envelope.timestamp);
// swarm messages already comes with a timestamp in milliseconds, so this sentAtTimestamp is correct.
// the sogs messages do not come as milliseconds but just seconds, so we override it
await innerHandleSwarmContentMessage({
envelope,
sentAtTimestamp,
envelopeTimestamp,
contentDecrypted: decryptedForAll.decryptedContent,
messageHash,
});
@ -400,10 +401,10 @@ export async function innerHandleSwarmContentMessage({
contentDecrypted,
envelope,
messageHash,
sentAtTimestamp,
envelopeTimestamp,
}: {
envelope: EnvelopePlus;
sentAtTimestamp: number;
envelopeTimestamp: number;
contentDecrypted: ArrayBuffer;
messageHash: string;
}): Promise<void> {
@ -437,7 +438,7 @@ export async function innerHandleSwarmContentMessage({
const isPrivateConversationMessage = !envelope.senderIdentity;
if (isPrivateConversationMessage) {
if (await shouldDropIncomingPrivateMessage(sentAtTimestamp, envelope, content)) {
if (await shouldDropIncomingPrivateMessage(envelopeTimestamp, envelope, content)) {
await IncomingMessageCache.removeFromCache(envelope);
return;
}
@ -468,7 +469,7 @@ export async function innerHandleSwarmContentMessage({
}
await handleSwarmDataMessage(
envelope,
sentAtTimestamp,
envelopeTimestamp,
content.dataMessage as SignalService.DataMessage,
messageHash,
senderConversationModel

@ -147,7 +147,7 @@ export function cleanIncomingDataMessage(rawDataMessage: SignalService.DataMessa
*/
export async function handleSwarmDataMessage(
envelope: EnvelopePlus,
sentAtTimestamp: number,
envelopeTimestamp: number,
rawDataMessage: SignalService.DataMessage,
messageHash: string,
senderConversationModel: ConversationModel
@ -158,7 +158,7 @@ export async function handleSwarmDataMessage(
if (cleanDataMessage.groupUpdateMessage) {
await GroupV2Receiver.handleGroupUpdateMessage({
envelopeTimestamp: sentAtTimestamp,
envelopeTimestamp,
updateMessage: rawDataMessage.groupUpdateMessage as SignalService.GroupUpdateMessage,
source: envelope.source,
senderIdentity: envelope.senderIdentity,
@ -252,19 +252,19 @@ export async function handleSwarmDataMessage(
? createSwarmMessageSentFromUs({
conversationId: convoIdToAddTheMessageTo,
messageHash,
sentAt: sentAtTimestamp,
sentAt: envelopeTimestamp,
})
: createSwarmMessageSentFromNotUs({
conversationId: convoIdToAddTheMessageTo,
messageHash,
sender: senderConversationModel.id,
sentAt: sentAtTimestamp,
sentAt: envelopeTimestamp,
});
await handleSwarmMessage(
msgModel,
messageHash,
sentAtTimestamp,
envelopeTimestamp,
cleanDataMessage,
convoToAddMessageTo,
// eslint-disable-next-line @typescript-eslint/no-misused-promises

@ -59,7 +59,7 @@ async function handleGroupInviteMessage({
);
return;
}
debugger;
const sigValid = await verifySig({
pubKey: HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId),
signature: inviteMessage.adminSignature,
@ -120,7 +120,6 @@ async function handleGroupInviteMessage({
await UserSync.queueNewJobIfNeeded();
// TODO currently sending auto-accept of invite. needs to be removed once we get the Group message request logic
debugger;
console.warn('currently sending auto accept invite response');
await getMessageQueue().sendToGroupV2({
message: new GroupUpdateInviteResponseMessage({
@ -396,6 +395,7 @@ async function handleGroupUpdatePromoteMessage({
window.inboxStore.dispatch(
groupInfoActions.markUsAsAdmin({
groupPk,
secret: groupKeypair.privateKey,
})
);

@ -1,5 +1,5 @@
/* eslint-disable more/no-then */
import _, { isEmpty, last } from 'lodash';
import { isEmpty, last, toNumber } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { EnvelopePlus } from './types';
@ -27,12 +27,12 @@ const incomingMessagePromises: Array<Promise<any>> = [];
export async function handleSwarmContentDecryptedWithTimeout({
envelope,
messageHash,
sentAtTimestamp,
envelopeTimestamp,
contentDecrypted,
}: {
envelope: EnvelopePlus;
messageHash: string;
sentAtTimestamp: number;
envelopeTimestamp: number;
contentDecrypted: ArrayBuffer;
}) {
let taskDone = false;
@ -54,7 +54,7 @@ export async function handleSwarmContentDecryptedWithTimeout({
envelope,
messageHash,
contentDecrypted,
sentAtTimestamp,
envelopeTimestamp,
});
await IncomingMessageCache.removeFromCache(envelope);
} catch (e) {
@ -297,16 +297,14 @@ async function handleDecryptedEnvelope({
contentDecrypted: ArrayBuffer;
messageHash: string;
}) {
if (envelope.content) {
const sentAtTimestamp = _.toNumber(envelope.timestamp);
await innerHandleSwarmContentMessage({
envelope,
sentAtTimestamp,
contentDecrypted,
messageHash,
});
} else {
if (!envelope.content) {
await IncomingMessageCache.removeFromCache(envelope);
}
return innerHandleSwarmContentMessage({
envelope,
contentDecrypted,
messageHash,
envelopeTimestamp: toNumber(envelope.timestamp),
});
}

@ -475,7 +475,7 @@ async function handleInboxOutboxMessages(
await innerHandleSwarmContentMessage({
envelope: builtEnvelope,
sentAtTimestamp: postedAtInMs,
envelopeTimestamp: postedAtInMs,
contentDecrypted: builtEnvelope.content,
messageHash: '',
});

@ -134,13 +134,15 @@ async function buildRetrieveRequest(
): Promise<Array<RetrieveSubRequestType>> {
const isUs = pubkey === ourPubkey;
const maxSizeMap = SnodeNamespace.maxSizeMap(namespaces);
const now = GetNetworkTime.now();
const retrieveRequestsParams: Array<RetrieveSubRequestType> = await Promise.all(
namespaces.map(async (namespace, index) => {
const foundMaxSize = maxSizeMap.find(m => m.namespace === namespace)?.maxSize;
const retrieveParam = {
pubkey,
last_hash: lastHashes.at(index) || '',
timestamp: GetNetworkTime.now(),
timestamp: now,
max_size: foundMaxSize,
};
@ -197,7 +199,7 @@ async function buildRetrieveRequest(
params: {
messages: configHashesToBump,
expiry,
...signResult,
...omit(signResult, 'timestamp'),
pubkey,
},
};
@ -262,13 +264,12 @@ async function retrieveNextMessages(
`_retrieveNextMessages - retrieve result is not 200 with ${targetNode.ip}:${targetNode.port} but ${firstResult.code}`
);
}
if (!window.inboxStore?.getState().onionPaths.isOnline) {
window.inboxStore?.dispatch(updateIsOnline(true));
}
try {
// we rely on the code of the first one to check for online status
const bodyFirstResult = firstResult.body;
if (!window.inboxStore?.getState().onionPaths.isOnline) {
window.inboxStore?.dispatch(updateIsOnline(true));
}
GetNetworkTime.handleTimestampOffsetFromNetwork('retrieve', bodyFirstResult.t);
@ -280,9 +281,7 @@ async function retrieveNextMessages(
}));
} catch (e) {
window?.log?.warn('exception while parsing json of nextMessage:', e);
if (!window.inboxStore?.getState().onionPaths.isOnline) {
window.inboxStore?.dispatch(updateIsOnline(true));
}
throw new Error(
`_retrieveNextMessages - exception while parsing json of nextMessage ${targetNode.ip}:${targetNode.port}: ${e?.message}`
);

@ -12,6 +12,7 @@ import {
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import { getSodiumRenderer } from '../../../crypto/MessageEncrypter';
import { GroupUpdateInviteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage';
import { GroupUpdatePromoteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdatePromoteMessage';
import { StringUtils, UserUtils } from '../../../utils';
import { fromUInt8ArrayToBase64, stringToUint8Array } from '../../../utils/String';
import { PreConditionFailed } from '../../../utils/errors';
@ -38,7 +39,6 @@ async function getGroupInviteMessage({
if (UserUtils.isUsFromCache(member)) {
throw new Error('getGroupInviteMessage: we cannot invite ourselves');
}
debugger;
// Note: as the signature is built with the timestamp here, we cannot override the timestamp later on the sending pipeline
const adminSignature = sodium.crypto_sign_detached(
@ -57,6 +57,29 @@ async function getGroupInviteMessage({
return invite;
}
async function getGroupPromoteMessage({
member,
secretKey,
groupPk,
}: {
member: PubkeyType;
secretKey: Uint8ArrayLen64; // len 64
groupPk: GroupPubkeyType;
}) {
const createAtNetworkTimestamp = GetNetworkTime.now();
if (UserUtils.isUsFromCache(member)) {
throw new Error('getGroupPromoteMessage: we cannot promote ourselves');
}
const msg = new GroupUpdatePromoteMessage({
groupPk,
createAtNetworkTimestamp,
groupIdentitySeed: secretKey.slice(0, 32), // the seed is the first 32 bytes of the secretkey
});
return msg;
}
type ParamsShared = {
groupPk: GroupPubkeyType;
namespace: SnodeNamespacesGroup;
@ -275,6 +298,7 @@ async function getGroupSignatureByHashesParams({
export const SnodeGroupSignature = {
generateUpdateExpiryGroupSignature,
getGroupInviteMessage,
getGroupPromoteMessage,
getSnodeGroupSignature,
getGroupSignatureByHashesParams,
signDataWithAdminSecret,

@ -2,6 +2,8 @@
/* eslint-disable more/no-then */
/* eslint-disable @typescript-eslint/no-misused-promises */
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { z } from 'zod';
import {
compact,
concat,
@ -9,6 +11,7 @@ import {
flatten,
isArray,
last,
omit,
sample,
toNumber,
uniqBy,
@ -23,7 +26,6 @@ import * as snodePool from './snodePool';
import { ConversationModel } from '../../../models/conversation';
import { ConversationTypeEnum } from '../../../models/conversationAttributes';
import { EnvelopePlus } from '../../../receiver/types';
import { updateIsOnline } from '../../../state/ducks/onion';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
import {
@ -35,6 +37,7 @@ import { DURATION, SWARM_POLLING_TIMEOUT } from '../../constants';
import { ConvoHub } from '../../conversations';
import { ed25519Str } from '../../onions/onionPath';
import { StringUtils, UserUtils } from '../../utils';
import { sleepFor } from '../../utils/Promise';
import { PreConditionFailed } from '../../utils/errors';
import { LibSessionUtil } from '../../utils/libsession/libsession_utils';
import { SnodeNamespace, SnodeNamespaces, UserConfigNamespaces } from './namespaces';
@ -313,6 +316,7 @@ export class SwarmPolling {
return;
}
if (type === ConversationTypeEnum.GROUPV2 && PubKey.is03Pubkey(pubkey)) {
await sleepFor(100);
await SwarmPollingGroupConfig.handleGroupSharedConfigMessages(confMessages, pubkey);
}
}
@ -671,11 +675,30 @@ export class SwarmPolling {
}
}
function retrieveItemWithNamespace(results: Array<RetrieveRequestResult>) {
// zod schema for retrieve items as returned by the snodes
const retrieveItemSchema = z.object({
hash: z.string(),
data: z.string(),
expiration: z.number(),
timestamp: z.number(),
});
function retrieveItemWithNamespace(
results: Array<RetrieveRequestResult>
): Array<RetrieveMessageItemWithNamespace> {
return flatten(
compact(
results.map(
result => result.messages.messages?.map(r => ({ ...r, namespace: result.namespace }))
result =>
result.messages.messages?.map(r => {
// throws if the result is not expected
const parsedItem = retrieveItemSchema.parse(r);
return {
...omit(parsedItem, 'timestamp'),
namespace: result.namespace,
storedAt: parsedItem.timestamp,
};
})
)
)
);
@ -698,7 +721,6 @@ function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>(
);
const confMessages = retrieveItemWithNamespace(userConfs);
const otherMessages = retrieveItemWithNamespace(userOthers);
return { confMessages, otherMessages: uniqBy(otherMessages, x => x.hash) };
@ -719,7 +741,6 @@ function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>(
);
const groupConfMessages = retrieveItemWithNamespace(groupConfs);
const groupOtherMessages = retrieveItemWithNamespace(groupOthers);
return {
@ -733,11 +754,7 @@ function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>(
}
}
async function decryptForGroupV2(retrieveResult: {
groupPk: string;
content: Uint8Array;
sentTimestamp: number;
}): Promise<EnvelopePlus | null> {
async function decryptForGroupV2(retrieveResult: { groupPk: string; content: Uint8Array }) {
window?.log?.info('received closed group message v2');
try {
const groupPk = retrieveResult.groupPk;
@ -746,28 +763,22 @@ async function decryptForGroupV2(retrieveResult: {
}
const decrypted = await MetaGroupWrapperActions.decryptMessage(groupPk, retrieveResult.content);
const envelopePlus: EnvelopePlus = {
// just try to parse what we have, it should be a protobuf content decrypted already
const parsedEnvelope = SignalService.Envelope.decode(new Uint8Array(decrypted.plaintext));
// not doing anything, just enforcing that the content is indeed a protobuf object of type Content, or throws
SignalService.Content.decode(parsedEnvelope.content);
// the receiving pipeline relies on the envelope.senderIdentity field to know who is the author of a message
return {
id: v4(),
senderIdentity: decrypted.pubkeyHex,
receivedAt: Date.now(),
content: decrypted.plaintext,
content: parsedEnvelope.content,
source: groupPk,
type: SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE,
timestamp: retrieveResult.sentTimestamp,
timestamp: parsedEnvelope.timestamp,
};
try {
// just try to parse what we have, it should be a protobuf content decrypted already
const parsedEnvelope = SignalService.Envelope.decode(new Uint8Array(decrypted.plaintext));
SignalService.Content.decode(parsedEnvelope.content);
envelopePlus.content = parsedEnvelope.content;
} catch (e) {
throw new Error('content got from libsession does not look to be envelope+decryptedContent');
}
// the receiving pipeline relies on the envelope.senderIdentity field to know who is the author of a message
return envelopePlus;
} catch (e) {
window.log.warn('failed to decrypt message with error: ', e.message);
return null;
@ -785,7 +796,6 @@ async function handleMessagesForGroupV2(
const envelopePlus = await decryptForGroupV2({
content: retrieveResult,
groupPk,
sentTimestamp: msg.timestamp,
});
if (!envelopePlus) {
throw new Error('decryptForGroupV2 returned empty envelope');
@ -797,7 +807,7 @@ async function handleMessagesForGroupV2(
envelope: envelopePlus,
contentDecrypted: envelopePlus.content,
messageHash: msg.hash,
sentAtTimestamp: toNumber(envelopePlus.timestamp),
envelopeTimestamp: toNumber(envelopePlus.timestamp),
});
} catch (e) {
window.log.warn('failed to handle groupv2 otherMessage because of: ', e.message);

@ -8,32 +8,37 @@ import { SnodeNamespaces } from '../namespaces';
import { RetrieveMessageItemWithNamespace } from '../types';
async function handleGroupSharedConfigMessages(
groupConfigMessagesMerged: Array<RetrieveMessageItemWithNamespace>,
groupConfigMessages: Array<RetrieveMessageItemWithNamespace>,
groupPk: GroupPubkeyType
) {
try {
window.log.info(
`received groupConfigMessagesMerged count: ${
groupConfigMessagesMerged.length
} for groupPk:${ed25519Str(groupPk)}`
`received groupConfigMessages count: ${groupConfigMessages.length} for groupPk:${ed25519Str(
groupPk
)}`
);
const infos = groupConfigMessagesMerged
if (groupConfigMessages.find(m => !m.storedAt)) {
debugger;
throw new Error('all incoming group config message should have a timestamp');
}
const infos = groupConfigMessages
.filter(m => m.namespace === SnodeNamespaces.ClosedGroupInfo)
.map(info => {
return { data: fromBase64ToArray(info.data), hash: info.hash };
});
const members = groupConfigMessagesMerged
const members = groupConfigMessages
.filter(m => m.namespace === SnodeNamespaces.ClosedGroupMembers)
.map(info => {
return { data: fromBase64ToArray(info.data), hash: info.hash };
});
const keys = groupConfigMessagesMerged
const keys = groupConfigMessages
.filter(m => m.namespace === SnodeNamespaces.ClosedGroupKeys)
.map(info => {
return {
data: fromBase64ToArray(info.data),
hash: info.hash,
timestampMs: info.timestamp,
timestampMs: info.storedAt,
};
});
const toMerge = {
@ -42,6 +47,11 @@ async function handleGroupSharedConfigMessages(
groupMember: members,
};
window.log.info(
`received keys: ${toMerge.groupKeys.length},infos: ${toMerge.groupInfo.length},members: ${
toMerge.groupMember.length
} for groupPk:${ed25519Str(groupPk)}`
);
// do the merge with our current state
await MetaGroupWrapperActions.metaMerge(groupPk, toMerge);
// save updated dumps to the DB right away
@ -55,7 +65,7 @@ async function handleGroupSharedConfigMessages(
);
} catch (e) {
window.log.warn(
`handleGroupSharedConfigMessages of ${groupConfigMessagesMerged.length} failed with ${e.message}`
`handleGroupSharedConfigMessages of ${groupConfigMessages.length} failed with ${e.message}`
);
// not rethrowing
}

@ -3,8 +3,8 @@ import { SnodeNamespaces } from './namespaces';
export type RetrieveMessageItem = {
hash: string;
expiration: number;
data: string; // base64 encrypted content of the emssage
timestamp: number;
data: string; // base64 encrypted content of the message
storedAt: number; // **not** the envelope timestamp, but when the message was effectively stored on the snode
};
export type RetrieveMessageItemWithNamespace = RetrieveMessageItem & {

@ -1,4 +1,5 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { MessageEncrypter, concatUInt8Array, getSodiumRenderer } from '.';
import { Data } from '../../data/data';
import { SignalService } from '../../protobuf';
@ -116,14 +117,14 @@ export async function encryptUsingSessionProtocol(
);
const signature = sodium.crypto_sign_detached(verificationData, userED25519SecretKeyBytes);
if (!signature || signature.length === 0) {
if (isEmpty(signature)) {
throw new Error("Couldn't sign message");
}
const plaintextWithMetadata = concatUInt8Array(plaintext, userED25519PubKeyBytes, signature);
const ciphertext = sodium.crypto_box_seal(plaintextWithMetadata, recipientX25519PublicKey);
if (!ciphertext) {
if (isEmpty(ciphertext)) {
throw new Error("Couldn't encrypt message.");
}
return ciphertext;

@ -147,9 +147,7 @@ async function addUpdateMessage(
if (diff.newName) {
groupUpdate.name = diff.newName;
}
if (diff.joiningMembers) {
} else if (diff.joiningMembers) {
groupUpdate.joined = diff.joiningMembers;
} else if (diff.leavingMembers) {
groupUpdate.left = diff.leavingMembers;

@ -39,6 +39,7 @@ import { GroupUpdateMemberChangeMessage } from '../messages/outgoing/controlMess
import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage';
import { GroupUpdateDeleteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateDeleteMessage';
import { GroupUpdateInviteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage';
import { GroupUpdatePromoteMessage } from '../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdatePromoteMessage';
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
type ClosedGroupMessageType =
@ -269,6 +270,7 @@ export class MessageQueue {
| CallMessage
| ClosedGroupMemberLeftMessage
| GroupUpdateInviteMessage
| GroupUpdatePromoteMessage
| GroupUpdateDeleteMessage;
namespace: SnodeNamespaces;
}): Promise<number | null> {
@ -283,6 +285,7 @@ export class MessageQueue {
);
return effectiveTimestamp;
} catch (error) {
window.log.error('failed to send message with: ', error.message);
if (rawMessage) {
await MessageSentHandler.handleMessageSentFailure(rawMessage, error);
}

@ -136,7 +136,7 @@ export class PendingMessageCache {
!isNumber(message.networkTimestampCreated) ||
message.networkTimestampCreated <= 0
) {
throw new Error('networkTimestampCreated is emptyo <=0');
throw new Error('networkTimestampCreated is empty <=0');
}
const plainTextBuffer = from_hex(message.plainTextBufferHex); // if a plaintextBufferHex is unset or not hex, this throws and we remove that message entirely

@ -203,13 +203,6 @@ export async function timeout<T>(promise: Promise<T>, timeoutMs: number): Promis
return Promise.race([timeoutPromise, promise]);
}
export async function delay(timeoutMs: number = 2000): Promise<boolean> {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, timeoutMs);
});
}
export const sleepFor = async (ms: number, showLog = false) => {
if (showLog) {

@ -6,6 +6,7 @@ import { persistedJobFromData } from './JobDeserialization';
import {
AvatarDownloadPersistedData,
GroupInvitePersistedData,
GroupPromotePersistedData,
GroupSyncPersistedData,
PersistedJob,
RunJobResult,
@ -359,14 +360,21 @@ const avatarDownloadRunner = new PersistedJobRunner<AvatarDownloadPersistedData>
'AvatarDownloadJob',
null
);
const groupInviteJobRunner = new PersistedJobRunner<GroupInvitePersistedData>(
'GroupInviteJob',
null
);
const groupPromoteJobRunner = new PersistedJobRunner<GroupPromotePersistedData>(
'GroupPromoteJob',
null
);
export const runners = {
userSyncRunner,
groupSyncRunner,
avatarDownloadRunner,
groupInviteJobRunner,
groupPromoteJobRunner,
};

@ -6,6 +6,7 @@ export type PersistedJobType =
| 'GroupSyncJobType'
| 'AvatarDownloadJobType'
| 'GroupInviteJobType'
| 'GroupPromoteJobType'
| 'FakeSleepForJobType'
| 'FakeSleepForJobMultiType';
@ -40,6 +41,12 @@ export interface GroupInvitePersistedData extends PersistedJobData {
member: PubkeyType;
}
export interface GroupPromotePersistedData extends PersistedJobData {
jobType: 'GroupPromoteJobType';
groupPk: GroupPubkeyType;
member: PubkeyType;
}
export interface UserSyncPersistedData extends PersistedJobData {
jobType: 'UserSyncJobType';
}
@ -53,7 +60,8 @@ export type TypeOfPersistedData =
| FakeSleepJobData
| FakeSleepForMultiJobData
| GroupSyncPersistedData
| GroupInvitePersistedData;
| GroupInvitePersistedData
| GroupPromotePersistedData;
export type AddJobCheckReturn = 'skipAddSameJobPresent' | 'sameJobDataAlreadyInQueue' | null;

@ -26,7 +26,7 @@ type JobExtraArgs = {
member: PubkeyType;
};
export function shouldAddGroupInviteJob(args: JobExtraArgs) {
export function shouldAddJob(args: JobExtraArgs) {
if (UserUtils.isUsFromCache(args.member)) {
return false;
}
@ -42,8 +42,8 @@ const invitesFailed = new Map<
}
>();
async function addGroupInviteJob({ groupPk, member }: JobExtraArgs) {
if (shouldAddGroupInviteJob({ groupPk, member })) {
async function addJob({ groupPk, member }: JobExtraArgs) {
if (shouldAddJob({ groupPk, member })) {
const groupInviteJob = new GroupInviteJob({
groupPk,
member,
@ -190,7 +190,7 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
export const GroupInvite = {
GroupInviteJob,
addGroupInviteJob,
addJob,
};
function updateFailedStateForMember(groupPk: GroupPubkeyType, member: PubkeyType, failed: boolean) {
let thisGroupFailure = invitesFailed.get(groupPk);

@ -0,0 +1,151 @@
import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
import { isNumber } from 'lodash';
import { v4 } from 'uuid';
import { UserUtils } from '../..';
import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import { SnodeNamespaces } from '../../../apis/snode_api/namespaces';
import { SnodeGroupSignature } from '../../../apis/snode_api/signature/groupSignature';
import { getMessageQueue } from '../../../sending';
import { PubKey } from '../../../types';
import { runners } from '../JobRunner';
import {
AddJobCheckReturn,
GroupPromotePersistedData,
PersistedJob,
RunJobResult,
} from '../PersistedJob';
const defaultMsBetweenRetries = 10000;
const defaultMaxAttemps = 1;
type JobExtraArgs = {
groupPk: GroupPubkeyType;
member: PubkeyType;
};
export function shouldAddJob(args: JobExtraArgs) {
if (UserUtils.isUsFromCache(args.member)) {
return false;
}
return true;
}
async function addJob({ groupPk, member }: JobExtraArgs) {
if (shouldAddJob({ groupPk, member })) {
const groupPromoteJob = new GroupPromoteJob({
groupPk,
member,
nextAttemptTimestamp: Date.now(),
});
window.log.debug(`addGroupPromoteJob: adding group promote for ${groupPk}:${member} `);
await runners.groupPromoteJobRunner.addJob(groupPromoteJob);
}
}
class GroupPromoteJob extends PersistedJob<GroupPromotePersistedData> {
constructor({
groupPk,
member,
nextAttemptTimestamp,
maxAttempts,
currentRetry,
identifier,
}: Pick<GroupPromotePersistedData, 'groupPk' | 'member'> &
Partial<
Pick<
GroupPromotePersistedData,
| 'nextAttemptTimestamp'
| 'identifier'
| 'maxAttempts'
| 'delayBetweenRetries'
| 'currentRetry'
>
>) {
super({
jobType: 'GroupPromoteJobType',
identifier: identifier || v4(),
member,
groupPk,
delayBetweenRetries: defaultMsBetweenRetries,
maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttemps,
nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries,
currentRetry: isNumber(currentRetry) ? currentRetry : 0,
});
}
public async run(): Promise<RunJobResult> {
const { groupPk, member, jobType, identifier } = this.persistedData;
window.log.info(
`running job ${jobType} with groupPk:"${groupPk}" member: ${member} id:"${identifier}" `
);
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group || !group.secretKey || !group.name) {
window.log.warn(`GroupPromoteJob: Did not find group in wrapper or no valid info in wrapper`);
return RunJobResult.PermanentFailure;
}
if (UserUtils.isUsFromCache(member)) {
return RunJobResult.Success;
}
let failed = true;
try {
const message = await SnodeGroupSignature.getGroupPromoteMessage({
member,
secretKey: group.secretKey,
groupPk,
});
const storedAt = await getMessageQueue().sendToPubKeyNonDurably({
message,
namespace: SnodeNamespaces.Default,
pubkey: PubKey.cast(member),
});
if (storedAt !== null) {
failed = false;
}
} finally {
try {
await MetaGroupWrapperActions.memberSetPromoted(groupPk, member, failed);
} catch (e) {
window.log.warn('GroupPromoteJob memberSetPromoted failed with', e.message);
}
}
// return true so this job is marked as a success and we don't need to retry it
return RunJobResult.Success;
}
public serializeJob(): GroupPromotePersistedData {
return super.serializeBase();
}
public nonRunningJobsToRemove(_jobs: Array<GroupPromotePersistedData>) {
return [];
}
public addJobCheck(jobs: Array<GroupPromotePersistedData>): AddJobCheckReturn {
// avoid adding the same job if the exact same one is already planned
const hasSameJob = jobs.some(j => {
return j.groupPk === this.persistedData.groupPk && j.member === this.persistedData.member;
});
if (hasSameJob) {
return 'skipAddSameJobPresent';
}
return null;
}
public getJobTimeoutMs(): number {
return 15000;
}
}
export const GroupPromote = {
GroupPromoteJob,
addJob,
};

@ -4,4 +4,5 @@ export type JobRunnerType =
| 'FakeSleepForJob'
| 'FakeSleepForMultiJob'
| 'AvatarDownloadJob'
| 'GroupInviteJob';
| 'GroupInviteJob'
| 'GroupPromoteJob';

@ -5,6 +5,7 @@ import {
GroupMemberGet,
GroupPubkeyType,
PubkeyType,
Uint8ArrayLen64,
UserGroupsGet,
WithGroupPubkey,
} from 'libsession_util_nodejs';
@ -17,7 +18,6 @@ import { SignalService } from '../../protobuf';
import { getMessageQueue } from '../../session';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime';
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';
@ -76,9 +76,10 @@ type GroupDetailsUpdate = {
async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) {
const us = UserUtils.getOurPubKeyStrFromCache();
const inGroup = await MetaGroupWrapperActions.memberGet(groupPk, us);
const haveAdminkey = await UserGroupsWrapperActions.getGroup(groupPk);
if (!haveAdminkey || inGroup?.promoted) {
const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us);
const inUserGroup = await UserGroupsWrapperActions.getGroup(groupPk);
if (isEmpty(inUserGroup?.secretKey) || !usInGroup?.promoted) {
throw new Error(`checkWeAreAdminOrThrow failed with ctx: ${context}`);
}
}
@ -185,7 +186,7 @@ const initNewGroupInWrapper = createAsyncThunk(
// can update the groupwrapper with a failed state if a message fails to be sent.
for (let index = 0; index < membersFromWrapper.length; index++) {
const member = membersFromWrapper[index];
await GroupInvite.addGroupInviteJob({ member: member.pubkeyHex, groupPk });
await GroupInvite.addJob({ member: member.pubkeyHex, groupPk });
}
await openConversationWithMessages({ conversationKey: groupPk, messageId: null });
@ -460,6 +461,10 @@ async function handleWithoutHistoryMembers({
const created = await MetaGroupWrapperActions.memberGetOrConstruct(groupPk, member);
await MetaGroupWrapperActions.memberSetInvited(groupPk, created.pubkeyHex, false);
}
if (!isEmpty(withoutHistory)) {
await MetaGroupWrapperActions.keyRekey(groupPk);
}
}
async function handleRemoveMembers({
@ -471,6 +476,7 @@ async function handleRemoveMembers({
if (!fromCurrentDevice) {
return;
}
await MetaGroupWrapperActions.memberEraseAndRekey(groupPk, removed);
const createAtNetworkTimestamp = GetNetworkTime.now();
@ -489,15 +495,14 @@ async function handleRemoveMembers({
'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.ClosedGroupRevokedRetrievableMessages,
});
if (!sentStatus) {
window.log.warn('Failed to send a GroupUpdateDeleteMessage to a member removed: ', m);
throw new Error('Failed to send a GroupUpdateDeleteMessage to a member removed');
}
// const sentStatus = await getMessageQueue().sendToPubKeyNonDurably({
// pubkey: PubKey.cast(m),
// message: deleteMessage,
// namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
// });
// if (!sentStatus) {
// window.log.warn('Failed to send a GroupUpdateDeleteMessage to a member removed: ', m);
// }
})
);
}
@ -591,11 +596,11 @@ async function handleMemberChangeFromUIOrNot({
// schedule send invite details, auth signature, etc. to the new users
for (let index = 0; index < withoutHistory.length; index++) {
const member = withoutHistory[index];
await GroupInvite.addGroupInviteJob({ groupPk, member });
await GroupInvite.addJob({ groupPk, member });
}
for (let index = 0; index < withHistory.length; index++) {
const member = withHistory[index];
await GroupInvite.addGroupInviteJob({ groupPk, member });
await GroupInvite.addJob({ groupPk, member });
}
const sodium = await getSodiumRenderer();
@ -755,8 +760,10 @@ const markUsAsAdmin = createAsyncThunk(
async (
{
groupPk,
secret,
}: {
groupPk: GroupPubkeyType;
secret: Uint8ArrayLen64;
},
payloadCreator
): Promise<GroupDetailsUpdate> => {
@ -764,6 +771,12 @@ const markUsAsAdmin = createAsyncThunk(
if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) {
throw new PreConditionFailed('markUsAsAdmin group not present in redux slice');
}
if (secret.length !== 64) {
throw new PreConditionFailed('markUsAsAdmin secret needs to be 64');
}
console.warn('before setSigKeys ', groupPk, stringify(secret));
await MetaGroupWrapperActions.setSigKeys(groupPk, secret);
console.warn('after setSigKeys');
const us = UserUtils.getOurPubKeyStrFromCache();
if (state.groups.members[groupPk].find(m => m.pubkeyHex === us)?.admin) {
@ -801,10 +814,15 @@ const inviteResponseReceived = createAsyncThunk(
if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) {
throw new PreConditionFailed('inviteResponseReceived group but not present in redux slice');
}
await checkWeAreAdminOrThrow(groupPk, 'inviteResponseReceived');
try {
await checkWeAreAdminOrThrow(groupPk, 'inviteResponseReceived');
await MetaGroupWrapperActions.memberSetAccepted(groupPk, member);
await GroupSync.queueNewJobIfNeeded(groupPk);
await MetaGroupWrapperActions.memberSetAccepted(groupPk, member);
await GroupSync.queueNewJobIfNeeded(groupPk);
} catch (e) {
window.log.info('inviteResponseReceived failed with', e.message);
// only admins can do the steps above, but we don't want to throw if we are not an admin
}
return {
groupPk,

@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
import { createSelector } from '@reduxjs/toolkit';
import { filter, isEmpty, isNumber, pick, sortBy, toNumber, isFinite } from 'lodash';
import { filter, isEmpty, isFinite, isNumber, pick, sortBy, toNumber } from 'lodash';
import {
ConversationLookupType,

@ -290,7 +290,7 @@ export function toFixedUint8ArrayOfLength<T extends number>(
}
export function stringify(obj: unknown) {
return JSON.stringify(obj, (_key, value) =>
value instanceof Uint8Array ? `Uint8Array(${value.length}): ${toHex(value)}` : value
);
return JSON.stringify(obj, (_key, value) => {
return value instanceof Uint8Array ? `Uint8Array(${value.length}): ${toHex(value)}` : value;
});
}

@ -106,7 +106,7 @@ export class WorkerInterface {
reject: (error: any) => {
this._removeJob(id);
const end = Date.now();
window.log.info(
window.log.debug(
`Worker job ${id} (${fnName}) failed in ${end - start}ms with ${error.message}`
);
return reject(error);

@ -14,6 +14,7 @@ import {
ProfilePicture,
PubkeyType,
Uint8ArrayLen100,
Uint8ArrayLen64,
UserConfigWrapperActionsCalls,
UserGroupsSet,
UserGroupsWrapperActionsCalls,
@ -540,6 +541,11 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
'swarmVerifySubAccount',
signingValue,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['swarmVerifySubAccount']>>,
setSigKeys: async (groupPk: GroupPubkeyType, secret: Uint8ArrayLen64) => {
return callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'setSigKeys', secret]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['setSigKeys']>
>;
},
};
export const callLibSessionWorker = async (

@ -7870,3 +7870,8 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.22.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==

Loading…
Cancel
Save