diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 65abd867b..43ffde283 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -21,7 +21,10 @@ import { from_hex } from 'libsodium-wrappers-sumo'; import { SignalService } from '../protobuf'; import { getMessageQueue } from '../session'; import { getConversationController } from '../session/conversations'; -import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; +import { + ClosedGroupV3VisibleMessage, + ClosedGroupVisibleMessage, +} from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; import { PubKey } from '../session/types'; import { ToastUtils, UserUtils } from '../session/utils'; import { BlockedNumberController } from '../util'; @@ -221,7 +224,9 @@ export class ConversationModel extends Backbone.Model { } public isClosedGroupV3(): boolean { - return Boolean(this.get('type') === ConversationTypeEnum.GROUPV3 && this.id.startsWith('03')); + return Boolean( + this.get('type') === ConversationTypeEnum.GROUPV3 && PubKey.isClosedGroupV2(this.id) + ); } public isPrivate() { @@ -1701,6 +1706,7 @@ export class ConversationModel extends Backbone.Model { // we are trying to send a message to someone. Make sure this convo is not hidden await this.unhideIfNeeded(true); + // TODO break down those functions (sendMessage and retrySend into smaller functions and narrow the VisibleMessageParams to preview, etc. with checks of types) // an OpenGroupV2 message is just a visible message const chatMessageParams: VisibleMessageParams = { body, @@ -1761,8 +1767,7 @@ export class ConversationModel extends Backbone.Model { if (this.isPrivate()) { if (this.isMe()) { - chatMessageParams.syncTarget = this.id; - const chatMessageMe = new VisibleMessage(chatMessageParams); + const chatMessageMe = new VisibleMessage({ ...chatMessageParams, syncTarget: this.id }); await getMessageQueue().sendSyncMessage({ namespace: SnodeNamespaces.Default, @@ -1798,6 +1803,12 @@ export class ConversationModel extends Backbone.Model { return; } + if (this.isClosedGroupV3()) { + // we need the return await so that errors are caught in the catch {} + await this.sendMessageToGroupV3(chatMessageParams); + return; + } + if (this.isClosedGroup()) { const chatMessageMediumGroup = new VisibleMessage(chatMessageParams); const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({ @@ -1819,6 +1830,20 @@ export class ConversationModel extends Backbone.Model { } } + private async sendMessageToGroupV3(chatMessageParams: VisibleMessageParams) { + const visibleMessage = new VisibleMessage(chatMessageParams); + const groupVisibleMessage = new ClosedGroupV3VisibleMessage({ + chatMessage: visibleMessage, + destination: this.id, + namespace: SnodeNamespaces.ClosedGroupMessages, + }); + + // we need the return await so that errors are caught in the catch {} + await getMessageQueue().sendToGroupV3({ + message: groupVisibleMessage, + }); + } + private async sendBlindedMessageRequest(messageParams: VisibleMessageParams) { const ourSignKeyBytes = await UserUtils.getUserED25519KeyPairBytes(); const groupUrl = this.getSogsOriginMessage(); diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 8d5118d98..c5ba8b879 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -87,7 +87,7 @@ export async function handleClosedGroupControlMessage( }` ); - if (PubKey.isClosedGroupV3(envelope.source)) { + if (PubKey.isClosedGroupV2(envelope.source)) { window?.log?.warn( 'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage ' ); @@ -168,7 +168,7 @@ function sanityCheckNewGroup( return false; } - if (PubKey.isClosedGroupV3(hexGroupPublicKey)) { + if (PubKey.isClosedGroupV2(hexGroupPublicKey)) { window?.log?.warn('sanityCheckNewGroup: got a v3 new group as a ClosedGroupControlMessage. '); return false; } @@ -520,7 +520,7 @@ async function performIfValid( const groupPublicKey = envelope.source; const sender = envelope.senderIdentity; - if (PubKey.isClosedGroupV3(groupPublicKey)) { + if (PubKey.isClosedGroupV2(groupPublicKey)) { window?.log?.warn( 'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage ' ); diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 1057634e2..df4a41e31 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -33,7 +33,11 @@ import { Storage } from '../util/storage'; import { handleCallMessage } from './callMessage'; import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGroups'; import { ECKeyPair } from './keypairs'; -import { ContactsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface'; +import { + ContactsWrapperActions, + MetaGroupWrapperActions, +} from '../webworker/workers/browser/libsession_worker_interface'; +import { PreConditionFailed } from '../session/utils/errors'; export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) { try { @@ -54,6 +58,18 @@ export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageH } } +async function decryptForGroupV2(envelope: EnvelopePlus) { + window?.log?.info('received closed group message v2'); + // try { + const groupPk = envelope.source; + if (!PubKey.isClosedGroupV2(groupPk)) { + throw new PreConditionFailed('decryptForGroupV2: not a 03 prefixed group'); + } + + return await MetaGroupWrapperActions.decryptMessage(groupPk, envelope.content); + // } catch (e) {} +} + async function decryptForClosedGroup(envelope: EnvelopePlus) { // case .closedGroupCiphertext: for ios window?.log?.info('received closed group message'); @@ -266,7 +282,11 @@ async function decrypt(envelope: EnvelopePlus): Promise { plaintext = await decryptEnvelopeWithOurKey(envelope); break; case SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE: - plaintext = await decryptForClosedGroup(envelope); + if (PubKey.isClosedGroupV2(envelope.source)) { + plaintext = await decryptForGroupV2(envelope); + } else { + plaintext = await decryptForClosedGroup(envelope); + } break; default: assertUnreachable(envelope.type, `Unknown message type:${envelope.type}`); diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index edea30808..ac32e58d0 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -195,7 +195,7 @@ export async function handleSwarmDataMessage( ); const isGroupMessage = !!envelope.senderIdentity; - const isGroupV3Message = isGroupMessage && PubKey.isClosedGroupV3(envelope.source); + const isGroupV3Message = isGroupMessage && PubKey.isClosedGroupV2(envelope.source); let typeOfConvo = ConversationTypeEnum.PRIVATE; if (isGroupV3Message) { typeOfConvo = ConversationTypeEnum.GROUPV3; diff --git a/ts/session/apis/snode_api/batchRequest.ts b/ts/session/apis/snode_api/batchRequest.ts index 3e23b1ad6..a5212ecf1 100644 --- a/ts/session/apis/snode_api/batchRequest.ts +++ b/ts/session/apis/snode_api/batchRequest.ts @@ -4,6 +4,12 @@ import { processOnionRequestErrorAtDestination, SnodeResponse } from './onions'; import { snodeRpc } from './sessionRpc'; import { NotEmptyArrayOfBatchResults, SnodeApiSubRequests } from './SnodeRequestTypes'; +function logSubRequests(requests: Array) { + return requests.map(m => + m.method === 'retrieve' || m.method === 'store' ? `${m.method}-${m.params.namespace}` : m.method + ); +} + /** * This is the equivalent to the batch send on sogs. The target node runs each sub request and returns a list of all the sub status and bodies. * If the global status code is not 200, an exception is thrown. @@ -23,7 +29,7 @@ export async function doSnodeBatchRequest( ): Promise { console.warn( `doSnodeBatchRequest "${method}":`, - JSON.stringify(subRequests.map(m => m.method)) + JSON.stringify(logSubRequests(subRequests)) // subRequests ); const result = await snodeRpc({ diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 75833206f..2ac729006 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -93,7 +93,7 @@ async function retrieveRequestForGroup({ namespace: SnodeNamespaces; retrieveParam: RetrieveParams; }) { - if (!PubKey.isClosedGroupV3(groupPk)) { + if (!PubKey.isClosedGroupV2(groupPk)) { throw new Error('retrieveRequestForGroup: not a 03 group'); } if (!SnodeNamespace.isGroupNamespace(namespace)) { @@ -147,7 +147,7 @@ async function buildRetrieveRequest( return retrieveRequestForLegacyGroup({ namespace, ourPubkey, pubkey, retrieveParam }); } - if (PubKey.isClosedGroupV3(pubkey)) { + if (PubKey.isClosedGroupV2(pubkey)) { if (!SnodeNamespace.isGroupNamespace(namespace)) { // either config or messages namespaces for 03 groups throw new Error(`tried to poll from a non 03 group namespace ${namespace}`); diff --git a/ts/session/apis/snode_api/snodeSignatures.ts b/ts/session/apis/snode_api/snodeSignatures.ts index 91c09a168..b2d46ecd7 100644 --- a/ts/session/apis/snode_api/snodeSignatures.ts +++ b/ts/session/apis/snode_api/snodeSignatures.ts @@ -77,7 +77,7 @@ function isSigParamsForGroupAdmin( sigParams: SnodeSigParamsAdminGroup | SnodeSigParamsUs ): sigParams is SnodeSigParamsAdminGroup { const asGr = sigParams as SnodeSigParamsAdminGroup; - return PubKey.isClosedGroupV3(asGr.groupPk) && !!asGr.privKey; + return PubKey.isClosedGroupV2(asGr.groupPk) && !!asGr.privKey; } async function getSnodeShared(params: SnodeSigParamsAdminGroup | SnodeSigParamsUs) { diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 8e9362888..d29c713c7 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -191,7 +191,7 @@ export class SwarmPolling { window?.log?.debug( `Polling for ${loggingId}; timeout: ${convoPollingTimeout}; diff: ${diff} ` ); - if (PubKey.isClosedGroupV3(key)) { + if (PubKey.isClosedGroupV2(key)) { return this.pollOnceForKey([key, ConversationTypeEnum.GROUPV3]); } return this.pollOnceForKey([key, ConversationTypeEnum.GROUP]); @@ -263,7 +263,7 @@ export class SwarmPolling { } else if ( type === ConversationTypeEnum.GROUPV3 && confMessages?.length && - PubKey.isClosedGroupV3(pubkey) + PubKey.isClosedGroupV2(pubkey) ) { await SwarmPollingGroupConfig.handleGroupSharedConfigMessages(confMessages, pubkey); } diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index fc889bb08..38b7fec95 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -96,7 +96,7 @@ export class ConversationController { throw new TypeError(`'type' must be 'private' or 'group' or 'groupv3' but got: '${type}'`); } - if (type === ConversationTypeEnum.GROUPV3 && !PubKey.isClosedGroupV3(id)) { + if (type === ConversationTypeEnum.GROUPV3 && !PubKey.isClosedGroupV2(id)) { throw new Error( 'required v3 closed group` ` but the pubkey does not match the 03 prefix for them' ); @@ -226,7 +226,7 @@ export class ConversationController { // if we were kicked or sent our left message, we have nothing to do more with that group. // Just delete everything related to it, not trying to add update message or send a left message. await this.removeGroupOrCommunityFromDBAndRedux(groupId); - if (PubKey.isClosedGroupV3(groupId)) { + if (PubKey.isClosedGroupV2(groupId)) { await remove03GroupFromWrappers(groupId); } else { await removeLegacyGroupFromWrappers(groupId); diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 66eff6517..20334b24b 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -1,9 +1,13 @@ +import { GroupPubkeyType } from 'libsession_util_nodejs'; +import { MessageEncrypter, concatUInt8Array, getSodiumRenderer } from '.'; +import { Data } from '../../data/data'; import { SignalService } from '../../protobuf'; +import { assertUnreachable } from '../../types/sqlSharedTypes'; +import { MetaGroupWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; import { PubKey } from '../types'; -import { concatUInt8Array, getSodiumRenderer, MessageEncrypter } from '.'; -import { fromHexToArray } from '../utils/String'; -import { Data } from '../../data/data'; import { UserUtils } from '../utils'; +import { fromHexToArray } from '../utils/String'; +import { SigningFailed } from '../utils/errors'; import { addMessagePadding } from './BufferPadding'; export { concatUInt8Array, getSodiumRenderer }; @@ -13,57 +17,106 @@ type EncryptResult = { cipherText: Uint8Array; }; +async function encryptWithLibSession(destination: GroupPubkeyType, plainText: Uint8Array) { + try { + return MetaGroupWrapperActions.encryptMessage(destination, plainText, true); + } catch (e) { + window.log.warn('encrypt message for group failed with', e.message); + throw new SigningFailed(e.message); + } +} + +async function encryptForLegacyGroup(destination: PubKey, plainText: Uint8Array) { + const hexEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(destination.key); + if (!hexEncryptionKeyPair) { + window?.log?.warn("Couldn't get key pair for closed group during encryption"); + throw new Error("Couldn't get key pair for closed group"); + } + + const destinationX25519Pk = PubKey.cast(hexEncryptionKeyPair.publicHex); + + const cipherTextClosedGroup = await MessageEncrypter.encryptUsingSessionProtocol( + destinationX25519Pk, + plainText + ); + + return { + envelopeType: SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE, + cipherText: cipherTextClosedGroup, + }; +} + /** - * Encrypt `plainTextBuffer` with given `encryptionType` for `device`. + * Encrypt `plainTextBuffer` with given `encryptionType` for `destination`. * - * @param device The device `PubKey` to encrypt for. + * @param destination The device `PubKey` to encrypt for. * @param plainTextBuffer The unpadded plaintext buffer. It will be padded * @param encryptionType The type of encryption. * @returns The envelope type and the base64 encoded cipher text */ export async function encrypt( - device: PubKey, + destination: PubKey, plainTextBuffer: Uint8Array, encryptionType: SignalService.Envelope.Type ): Promise { const { CLOSED_GROUP_MESSAGE, SESSION_MESSAGE } = SignalService.Envelope.Type; - - if (encryptionType !== CLOSED_GROUP_MESSAGE && encryptionType !== SESSION_MESSAGE) { - throw new Error(`Invalid encryption type:${encryptionType}`); - } - - const encryptForClosedGroup = encryptionType === CLOSED_GROUP_MESSAGE; - const plainText = addMessagePadding(plainTextBuffer); - - if (encryptForClosedGroup) { - // window?.log?.info( - // 'Encrypting message with SessionProtocol and envelope type is CLOSED_GROUP_MESSAGE' - // ); - const hexEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(device.key); - if (!hexEncryptionKeyPair) { - window?.log?.warn("Couldn't get key pair for closed group during encryption"); - throw new Error("Couldn't get key pair for closed group"); + const plainTextPadded = addMessagePadding(plainTextBuffer); + + switch (encryptionType) { + case SESSION_MESSAGE: { + // if (destination.isPrivate || destination.isUS) { + const cipherText = await MessageEncrypter.encryptUsingSessionProtocol( + PubKey.cast(destination.key), + plainTextPadded + ); + return { envelopeType: SESSION_MESSAGE, cipherText }; + // } + + // if (destination.isGroupV2 || destination.isLegacyGroup) { + // throw new PreConditionFailed( + // 'Encryption with SESSION_MESSAGE only work with destination private or us' + // ); + // } + // assertUnreachable( + // destination, + // 'Encryption with SESSION_MESSAGE only work with destination private or us' + // ); } - const hexPubFromECKeyPair = PubKey.cast(hexEncryptionKeyPair.publicHex); - - const cipherTextClosedGroup = await MessageEncrypter.encryptUsingSessionProtocol( - hexPubFromECKeyPair, - plainText - ); - - return { - envelopeType: CLOSED_GROUP_MESSAGE, - cipherText: cipherTextClosedGroup, - }; + case CLOSED_GROUP_MESSAGE: { + const groupPk = destination.key; + if (PubKey.isClosedGroupV2(groupPk)) { + return { + envelopeType: CLOSED_GROUP_MESSAGE, + cipherText: await encryptWithLibSession(groupPk, plainTextBuffer), + }; + } + + // if (destination.isLegacyGroup) { + return encryptForLegacyGroup(destination, plainTextPadded); // not padding it again, it is already done by libsession + // } + // if ( + // destination.isBlinded || + // destination.isBlinded || + // destination.isPrivate || + // destination.isUS + // ) { + // throw new PreConditionFailed( + // 'Encryption with CLOSED_GROUP_MESSAGE only work with destination groupv2 or legacy group' + // ); + // } + // assertUnreachable( + // destination, + // 'Encryption with CLOSED_GROUP_MESSAGE only work with destination groupv2 or legacy group' + // ); + } + default: + assertUnreachable(encryptionType, ''); } - const cipherText = await MessageEncrypter.encryptUsingSessionProtocol(device, plainText); - - return { envelopeType: SESSION_MESSAGE, cipherText }; } export async function encryptUsingSessionProtocol( - recipientHexEncodedX25519PublicKey: PubKey, + destinationX25519Pk: PubKey, plaintext: Uint8Array ): Promise { const userED25519KeyPairHex = await UserUtils.getUserED25519KeyPair(); @@ -76,9 +129,9 @@ export async function encryptUsingSessionProtocol( } const sodium = await getSodiumRenderer(); - // window?.log?.info('encryptUsingSessionProtocol for ', recipientHexEncodedX25519PublicKey.key); - - const recipientX25519PublicKey = recipientHexEncodedX25519PublicKey.withoutPrefixToArray(); + const recipientX25519PublicKey = fromHexToArray( + PubKey.removePrefixIfNeeded(destinationX25519Pk.key) + ); const userED25519PubKeyBytes = fromHexToArray(userED25519KeyPairHex.pubKey); const userED25519SecretKeyBytes = fromHexToArray(userED25519KeyPairHex.privKey); diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index e1483e73f..506461ff5 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -60,7 +60,7 @@ export async function initiateClosedGroupUpdate( groupName: string, members: Array ) { - const isGroupV3 = PubKey.isClosedGroupV3(groupId); + const isGroupV3 = PubKey.isClosedGroupV2(groupId); const convo = await getConversationController().getOrCreateAndWait( groupId, isGroupV3 ? ConversationTypeEnum.GROUPV3 : ConversationTypeEnum.GROUP @@ -206,7 +206,7 @@ function buildGroupDiff(convo: ConversationModel, update: GroupInfo): GroupDiff export async function updateOrCreateClosedGroup(details: GroupInfo) { const { id, expireTimer } = details; - const isV3 = PubKey.isClosedGroupV3(id); + const isV3 = PubKey.isClosedGroupV2(id); const conversation = await getConversationController().getOrCreateAndWait( id, diff --git a/ts/session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage.ts b/ts/session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage.ts index 203137ca4..7630323f8 100644 --- a/ts/session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage.ts +++ b/ts/session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage.ts @@ -3,6 +3,9 @@ import { PubKey } from '../../../types'; import { StringUtils } from '../../../utils'; import { VisibleMessage } from './VisibleMessage'; import { ClosedGroupMessage } from '../controlMessage/group/ClosedGroupMessage'; +import { DataMessage } from '../DataMessage'; +import { GroupPubkeyType } from 'libsession_util_nodejs'; +import { SnodeNamespaces } from '../../../apis/snode_api/namespaces'; interface ClosedGroupVisibleMessageParams { identifier?: string; @@ -24,7 +27,7 @@ export class ClosedGroupVisibleMessage extends ClosedGroupMessage { throw new Error('ClosedGroupVisibleMessage: groupId must be set'); } - if (PubKey.isClosedGroupV3(PubKey.cast(params.groupId).key)) { + if (PubKey.isClosedGroupV2(PubKey.cast(params.groupId).key)) { throw new Error('GroupContext should not be used anymore with closed group v3'); } } @@ -45,3 +48,36 @@ export class ClosedGroupVisibleMessage extends ClosedGroupMessage { return dataProto; } } + +type WithDestinationGroupPk = { destination: GroupPubkeyType }; +type WithGroupMessageNamespace = { namespace: SnodeNamespaces.ClosedGroupMessages }; + +export class ClosedGroupV3VisibleMessage extends DataMessage { + private readonly chatMessage: VisibleMessage; + public readonly destination: GroupPubkeyType; + public readonly namespace: SnodeNamespaces.ClosedGroupMessages; + + constructor( + params: Pick & + WithDestinationGroupPk & + WithGroupMessageNamespace + ) { + super({ + timestamp: params.chatMessage.timestamp, + identifier: params.identifier ?? params.chatMessage.identifier, + }); + this.chatMessage = params.chatMessage; + + if (!PubKey.isClosedGroupV2(params.destination)) { + throw new Error('ClosedGroupV3VisibleMessage only work with 03-groups destination'); + } + this.destination = params.destination; + this.namespace = params.namespace; + } + + public dataProto(): SignalService.DataMessage { + // expireTimer is set in the dataProto in this call directly + const dataProto = this.chatMessage.dataProto(); + return dataProto; + } +} diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index d59de9521..e20ad3534 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,28 +1,28 @@ import { AbortController } from 'abort-controller'; -import { PendingMessageCache } from './PendingMessageCache'; -import { JobQueue, MessageUtils, UserUtils } from '../utils'; -import { PubKey, RawMessage } from '../types'; import { MessageSender } from '.'; -import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage'; import { ConfigurationMessage } from '../messages/outgoing/controlMessage/ConfigurationMessage'; +import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage'; import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage'; +import { PubKey, RawMessage } from '../types'; +import { JobQueue, MessageUtils, UserUtils } from '../utils'; +import { PendingMessageCache } from './PendingMessageCache'; -import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; -import { MessageSentHandler } from './MessageSentHandler'; import { ContentMessage } from '../messages/outgoing'; import { ExpirationTimerUpdateMessage } from '../messages/outgoing/controlMessage/ExpirationTimerUpdateMessage'; import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage'; import { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage'; +import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage'; import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; -import { ClosedGroupVisibleMessage } from '../messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; +import { + ClosedGroupV3VisibleMessage, + ClosedGroupVisibleMessage, +} from '../messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; import { SyncMessageType } from '../utils/sync/syncUtils'; +import { MessageSentHandler } from './MessageSentHandler'; import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil'; -import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; -import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage'; -import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2'; import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction'; import { @@ -30,7 +30,10 @@ import { SnodeNamespacesLegacyGroup, SnodeNamespacesUser, } from '../apis/snode_api/namespaces'; +import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; import { SharedConfigMessage } from '../messages/outgoing/controlMessage/SharedConfigMessage'; +import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage'; +import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; type ClosedGroupMessageType = | ClosedGroupVisibleMessage @@ -196,6 +199,26 @@ export class MessageQueue { return this.sendToPubKey(PubKey.cast(destinationPubKey), message, namespace, sentCb, true); } + public async sendToGroupV3({ + message, + sentCb, + }: { + message: ClosedGroupV3VisibleMessage; + sentCb?: (message: RawMessage) => Promise; + }): Promise { + if (!message.destination) { + throw new Error('Invalid group message passed in sendToGroupV3.'); + } + + return this.sendToPubKey( + PubKey.cast(message.destination), + message, + message.namespace, + sentCb, + true + ); + } + public async sendSyncMessage({ namespace, message, diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 2428efab4..5a0644de7 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -7,6 +7,7 @@ import _, { isEmpty, isNil, isString, sample, toNumber } from 'lodash'; import pRetry from 'p-retry'; import { Data } from '../../data/data'; import { SignalService } from '../../protobuf'; +import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil'; import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2'; import { @@ -40,7 +41,6 @@ import { RawMessage } from '../types/RawMessage'; import { UserUtils } from '../utils'; import { fromUInt8ArrayToBase64 } from '../utils/String'; import { EmptySwarmError } from '../utils/errors'; -import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; // ================ SNODE STORE ================ @@ -196,12 +196,15 @@ async function getSignatureParamsFromNamespace(item: StoreOnNodeParamsNoSig, des }); } - if (SnodeNamespace.isGroupConfigNamespace(item.namespace)) { - if (!PubKey.isClosedGroupV3(destination)) { + if ( + SnodeNamespace.isGroupConfigNamespace(item.namespace) || + item.namespace === SnodeNamespaces.ClosedGroupMessages + ) { + if (!PubKey.isClosedGroupV2(destination)) { throw new Error('sendMessagesDataToSnode: groupconfig namespace required a 03 pubkey'); } const group = await UserGroupsWrapperActions.getGroup(destination); - const groupSecretKey = group?.secretKey; + const groupSecretKey = group?.secretKey; // TODO we will need to the user auth at some point if (isNil(groupSecretKey) || isEmpty(groupSecretKey)) { throw new Error(`sendMessagesDataToSnode: failed to find group admin secret key in wrapper`); } @@ -303,7 +306,7 @@ type SharedEncryptAndWrap = { type EncryptAndWrapMessage = { plainTextBuffer: Uint8Array; destination: string; - namespace: number | null; + namespace: number; } & SharedEncryptAndWrap; type EncryptAndWrapMessageResults = { @@ -342,23 +345,11 @@ async function encryptMessageAndWrap( const data = wrapEnvelope(envelope); const data64 = ByteBuffer.wrap(data).toString('base64'); - // override the namespaces if those are unset in the incoming messages - // right when we upgrade from not having namespaces stored in the outgoing cached messages our messages won't have a namespace associated. - // So we need to keep doing the lookup of where they should go if the namespace is not set. - - const overridenNamespace = !isNil(namespace) - ? namespace - : getConversationController() - .get(recipient.key) - ?.isClosedGroup() - ? SnodeNamespaces.LegacyClosedGroup - : SnodeNamespaces.Default; - return { data64, networkTimestamp, data, - namespace: overridenNamespace, + namespace, ttl, identifier, isSyncMessage: syncMessage, diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index dd61aea1e..2ad1912a9 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -26,6 +26,60 @@ export enum KeyPrefixType { groupV3 = '03', } +// export type GroupV2PubKey = { +// key: GroupPubkeyType; // 03 prefix for groups v2 +// isGroupV2: true; +// isLegacyGroup: false; +// isPrivate: false; +// isUS: false; +// isBlinded: false; +// }; + +// export type PrivatePubkey = { +// key: PubkeyType; // 05 prefix for private conversations +// isGroupV2: false; +// isLegacyGroup: false; +// isPrivate: true; +// isUS: false; +// isBlinded: false; +// }; + +// export type UsPubkey = { +// key: PubkeyType; // 05 prefix for note to self +// isGroupV2: false; +// isLegacyGroup: false; +// isPrivate: false; +// isUS: true; +// isBlinded: false; +// }; + +// export type PrivateBlindedPubkey = { +// key: BlindedPubkeyType; // 15 prefix for blinded pubkeys +// isGroupV2: false; +// isLegacyGroup: false; +// isPrivate: true; +// isUS: false; +// isBlinded: true; +// }; + +// export type LegacyGroupPubkey = { +// key: PubkeyType; // 05 prefix for legacy closed group +// isGroupV2: false; +// isLegacyGroup: true; +// isPrivate: false; +// isUS: false; +// isBlinded: false; +// }; + +// export type PubKeyRecord = +// | UsPubkey +// | PrivatePubkey +// | GroupV2PubKey +// | LegacyGroupPubkey +// | PrivateBlindedPubkey; + +// TODO make that Pubkey class more useful, add fields for what types of pubkey it is (group, legacy group, private) + export class PubKey { public static readonly PUBKEY_LEN = 66; public static readonly PUBKEY_LEN_NO_PREFIX = PubKey.PUBKEY_LEN - 2; @@ -233,31 +287,12 @@ export class PubKey { return fromHexToArray(this.key); } - public withoutPrefixToArray(): Uint8Array { - return fromHexToArray(PubKey.removePrefixIfNeeded(this.key)); - } - public static isBlinded(key: string) { return key.startsWith(KeyPrefixType.blinded15) || key.startsWith(KeyPrefixType.blinded25); } - public static isClosedGroupV3(key: string): key is GroupPubkeyType { + public static isClosedGroupV2(key: string): key is GroupPubkeyType { const regex = new RegExp(`^${KeyPrefixType.groupV3}${PubKey.HEX}{64}$`); return regex.test(key); } - - public static isHexOnly(str: string) { - return new RegExp(`^${PubKey.HEX}*$`).test(str); - } - - /** - * - * @returns true if that string is a valid group (as in closed group) pubkey. - * i.e. returns true if length is 66, prefix is 05 only, and it's hex characters only - */ - public static isValidGroupPubkey(pubkey: string): boolean { - return ( - pubkey.length === 66 && pubkey.startsWith(KeyPrefixType.standard) && this.isHexOnly(pubkey) - ); - } } diff --git a/ts/session/types/RawMessage.ts b/ts/session/types/RawMessage.ts index bebabc47c..012dc6535 100644 --- a/ts/session/types/RawMessage.ts +++ b/ts/session/types/RawMessage.ts @@ -7,7 +7,7 @@ export type RawMessage = { device: string; ttl: number; encryption: SignalService.Envelope.Type; - namespace: SnodeNamespaces | null; // allowing null as when we upgrade, we might have messages awaiting sending which won't have a namespace + namespace: SnodeNamespaces; }; // For building RawMessages from JSON diff --git a/ts/session/utils/errors.ts b/ts/session/utils/errors.ts index 0807b10c0..ec8eb5c5d 100644 --- a/ts/session/utils/errors.ts +++ b/ts/session/utils/errors.ts @@ -66,3 +66,17 @@ export class HTTPError extends Error { } } } + +class BaseError extends Error { + public readonly context?: Object; + constructor(message: string, context?: Object) { + super(message); + this.name = this.constructor.name; + this.context = context; + } +} + +export class SigningFailed extends BaseError {} +export class InvalidSigningType extends BaseError {} +export class GroupV2SigningFailed extends SigningFailed {} +export class PreConditionFailed extends BaseError {} diff --git a/ts/session/utils/job_runners/jobs/GroupConfigJob.ts b/ts/session/utils/job_runners/jobs/GroupConfigJob.ts index de9209037..a76f13de0 100644 --- a/ts/session/utils/job_runners/jobs/GroupConfigJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupConfigJob.ts @@ -2,7 +2,7 @@ import { GroupPubkeyType } from 'libsession_util_nodejs'; import { isArray, isEmpty, isNumber, isString } from 'lodash'; import { UserUtils } from '../..'; -import { ConfigDumpData } from '../../../../data/configDump/configDump'; +import { stringify } from '../../../../types/sqlSharedTypes'; import { ReleasedFeatures } from '../../../../util/releaseFeature'; import { isSignInByLinking } from '../../../../util/storage'; import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; @@ -29,7 +29,6 @@ import { PersistedJob, RunJobResult, } from '../PersistedJob'; -import { stringify } from '../../../../types/sqlSharedTypes'; 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; @@ -118,30 +117,7 @@ async function buildAndSaveDumpsToDB( } await MetaGroupWrapperActions.metaConfirmPushed(...toConfirm); - const metaNeedsDump = await MetaGroupWrapperActions.needsDump(groupPk); - // save the concatenated dumps as a single entry in the DB if any of the dumps had a need for dump - if (metaNeedsDump) { - const dump = await MetaGroupWrapperActions.metaDump(groupPk); - await ConfigDumpData.saveConfigDump({ - data: dump, - publicKey: groupPk, - variant: `MetaGroupConfig-${groupPk}`, - }); - } -} - -async function saveDumpsNeededToDB(groupPk: GroupPubkeyType) { - const needsDump = await MetaGroupWrapperActions.needsDump(groupPk); - - if (!needsDump) { - return; - } - const dump = await MetaGroupWrapperActions.metaDump(groupPk); - await ConfigDumpData.saveConfigDump({ - data: dump, - publicKey: groupPk, - variant: `MetaGroupConfig-${groupPk}`, - }); + return LibSessionUtil.saveMetaGroupDumpToDb(groupPk); } class GroupSyncJob extends PersistedJob { @@ -181,12 +157,12 @@ class GroupSyncJob extends PersistedJob { return RunJobResult.PermanentFailure; } - if (!PubKey.isClosedGroupV3(thisJobDestination)) { + if (!PubKey.isClosedGroupV2(thisJobDestination)) { return RunJobResult.PermanentFailure; } // 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 saveDumpsNeededToDB(thisJobDestination); + await LibSessionUtil.saveMetaGroupDumpToDb(thisJobDestination); const newGroupsReleased = await ReleasedFeatures.checkIsNewGroupsReleased(); // if the feature flag is not enabled, we want to keep updating the dumps, but just not sync them. diff --git a/ts/session/utils/libsession/libsession_utils.ts b/ts/session/utils/libsession/libsession_utils.ts index 21391a77f..205fac53d 100644 --- a/ts/session/utils/libsession/libsession_utils.ts +++ b/ts/session/utils/libsession/libsession_utils.ts @@ -227,7 +227,7 @@ async function pendingChangesForGroup( groupPk: GroupPubkeyType ): Promise { const results = new Array(); - if (!PubKey.isClosedGroupV3(groupPk)) { + if (!PubKey.isClosedGroupV2(groupPk)) { throw new Error(`pendingChangesForGroup only works for user or 03 group pubkeys`); } // one of the wrapper behind the metagroup needs a push @@ -319,6 +319,22 @@ async function markAsPushed(variant: ConfigWrapperUser, seqno: number, hash: str return GenericWrapperActions.needsDump(variant); } +/** + * If a dump is needed for that metagroup wrapper, dump it to the Database + */ +async function saveMetaGroupDumpToDb(groupPk: GroupPubkeyType) { + const metaNeedsDump = await MetaGroupWrapperActions.needsDump(groupPk); + // save the concatenated dumps as a single entry in the DB if any of the dumps had a need for dump + if (metaNeedsDump) { + const dump = await MetaGroupWrapperActions.metaDump(groupPk); + await ConfigDumpData.saveConfigDump({ + data: dump, + publicKey: groupPk, + variant: `MetaGroupConfig-${groupPk}`, + }); + } +} + export const LibSessionUtil = { initializeLibSessionUtilWrappers, userVariantToUserKind, @@ -327,4 +343,5 @@ export const LibSessionUtil = { pendingChangesForGroup, userKindToVariant, markAsPushed, + saveMetaGroupDumpToDb, }; diff --git a/ts/session/utils/libsession/libsession_utils_user_groups.ts b/ts/session/utils/libsession/libsession_utils_user_groups.ts index a59dec94b..659b20fd1 100644 --- a/ts/session/utils/libsession/libsession_utils_user_groups.ts +++ b/ts/session/utils/libsession/libsession_utils_user_groups.ts @@ -35,7 +35,7 @@ function isLegacyGroupToStoreInWrapper(convo: ConversationModel): boolean { } function isGroupToStoreInWrapper(convo: ConversationModel): boolean { - return convo.isGroup() && PubKey.isClosedGroupV3(convo.id) && convo.isActive(); // TODO should we filter by left/kicked or they are on the wrapper itself? + return convo.isGroup() && PubKey.isClosedGroupV2(convo.id) && convo.isActive(); // TODO should we filter by left/kicked or they are on the wrapper itself? } /** @@ -78,7 +78,7 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise const convoType: UserGroupsType = isCommunityToStoreInWrapper(foundConvo) ? 'Community' - : PubKey.isClosedGroupV3(convoId) + : PubKey.isClosedGroupV2(convoId) ? 'Group' : 'LegacyGroup';