feat: add sending and receiving of libsession encrypted msgs

still a wip though
pull/2873/head
Audric Ackermann 2 years ago
parent 90f4dd761c
commit 08164f2fd0

@ -21,7 +21,10 @@ import { from_hex } from 'libsodium-wrappers-sumo';
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { getMessageQueue } from '../session'; import { getMessageQueue } from '../session';
import { getConversationController } from '../session/conversations'; 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 { PubKey } from '../session/types';
import { ToastUtils, UserUtils } from '../session/utils'; import { ToastUtils, UserUtils } from '../session/utils';
import { BlockedNumberController } from '../util'; import { BlockedNumberController } from '../util';
@ -221,7 +224,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
public isClosedGroupV3(): boolean { 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() { public isPrivate() {
@ -1701,6 +1706,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// we are trying to send a message to someone. Make sure this convo is not hidden // we are trying to send a message to someone. Make sure this convo is not hidden
await this.unhideIfNeeded(true); 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 // an OpenGroupV2 message is just a visible message
const chatMessageParams: VisibleMessageParams = { const chatMessageParams: VisibleMessageParams = {
body, body,
@ -1761,8 +1767,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (this.isPrivate()) { if (this.isPrivate()) {
if (this.isMe()) { if (this.isMe()) {
chatMessageParams.syncTarget = this.id; const chatMessageMe = new VisibleMessage({ ...chatMessageParams, syncTarget: this.id });
const chatMessageMe = new VisibleMessage(chatMessageParams);
await getMessageQueue().sendSyncMessage({ await getMessageQueue().sendSyncMessage({
namespace: SnodeNamespaces.Default, namespace: SnodeNamespaces.Default,
@ -1798,6 +1803,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return; 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()) { if (this.isClosedGroup()) {
const chatMessageMediumGroup = new VisibleMessage(chatMessageParams); const chatMessageMediumGroup = new VisibleMessage(chatMessageParams);
const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({ const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({
@ -1819,6 +1830,20 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
} }
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) { private async sendBlindedMessageRequest(messageParams: VisibleMessageParams) {
const ourSignKeyBytes = await UserUtils.getUserED25519KeyPairBytes(); const ourSignKeyBytes = await UserUtils.getUserED25519KeyPairBytes();
const groupUrl = this.getSogsOriginMessage(); const groupUrl = this.getSogsOriginMessage();

@ -87,7 +87,7 @@ export async function handleClosedGroupControlMessage(
}` }`
); );
if (PubKey.isClosedGroupV3(envelope.source)) { if (PubKey.isClosedGroupV2(envelope.source)) {
window?.log?.warn( window?.log?.warn(
'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage ' 'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage '
); );
@ -168,7 +168,7 @@ function sanityCheckNewGroup(
return false; return false;
} }
if (PubKey.isClosedGroupV3(hexGroupPublicKey)) { if (PubKey.isClosedGroupV2(hexGroupPublicKey)) {
window?.log?.warn('sanityCheckNewGroup: got a v3 new group as a ClosedGroupControlMessage. '); window?.log?.warn('sanityCheckNewGroup: got a v3 new group as a ClosedGroupControlMessage. ');
return false; return false;
} }
@ -520,7 +520,7 @@ async function performIfValid(
const groupPublicKey = envelope.source; const groupPublicKey = envelope.source;
const sender = envelope.senderIdentity; const sender = envelope.senderIdentity;
if (PubKey.isClosedGroupV3(groupPublicKey)) { if (PubKey.isClosedGroupV2(groupPublicKey)) {
window?.log?.warn( window?.log?.warn(
'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage ' 'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage '
); );

@ -33,7 +33,11 @@ import { Storage } from '../util/storage';
import { handleCallMessage } from './callMessage'; import { handleCallMessage } from './callMessage';
import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGroups'; import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGroups';
import { ECKeyPair } from './keypairs'; 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) { export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try { 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) { async function decryptForClosedGroup(envelope: EnvelopePlus) {
// case .closedGroupCiphertext: for ios // case .closedGroupCiphertext: for ios
window?.log?.info('received closed group message'); window?.log?.info('received closed group message');
@ -266,7 +282,11 @@ async function decrypt(envelope: EnvelopePlus): Promise<any> {
plaintext = await decryptEnvelopeWithOurKey(envelope); plaintext = await decryptEnvelopeWithOurKey(envelope);
break; break;
case SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE: case SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE:
if (PubKey.isClosedGroupV2(envelope.source)) {
plaintext = await decryptForGroupV2(envelope);
} else {
plaintext = await decryptForClosedGroup(envelope); plaintext = await decryptForClosedGroup(envelope);
}
break; break;
default: default:
assertUnreachable(envelope.type, `Unknown message type:${envelope.type}`); assertUnreachable(envelope.type, `Unknown message type:${envelope.type}`);

@ -195,7 +195,7 @@ export async function handleSwarmDataMessage(
); );
const isGroupMessage = !!envelope.senderIdentity; const isGroupMessage = !!envelope.senderIdentity;
const isGroupV3Message = isGroupMessage && PubKey.isClosedGroupV3(envelope.source); const isGroupV3Message = isGroupMessage && PubKey.isClosedGroupV2(envelope.source);
let typeOfConvo = ConversationTypeEnum.PRIVATE; let typeOfConvo = ConversationTypeEnum.PRIVATE;
if (isGroupV3Message) { if (isGroupV3Message) {
typeOfConvo = ConversationTypeEnum.GROUPV3; typeOfConvo = ConversationTypeEnum.GROUPV3;

@ -4,6 +4,12 @@ import { processOnionRequestErrorAtDestination, SnodeResponse } from './onions';
import { snodeRpc } from './sessionRpc'; import { snodeRpc } from './sessionRpc';
import { NotEmptyArrayOfBatchResults, SnodeApiSubRequests } from './SnodeRequestTypes'; import { NotEmptyArrayOfBatchResults, SnodeApiSubRequests } from './SnodeRequestTypes';
function logSubRequests(requests: Array<SnodeApiSubRequests>) {
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. * 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. * If the global status code is not 200, an exception is thrown.
@ -23,7 +29,7 @@ export async function doSnodeBatchRequest(
): Promise<NotEmptyArrayOfBatchResults> { ): Promise<NotEmptyArrayOfBatchResults> {
console.warn( console.warn(
`doSnodeBatchRequest "${method}":`, `doSnodeBatchRequest "${method}":`,
JSON.stringify(subRequests.map(m => m.method)) JSON.stringify(logSubRequests(subRequests))
// subRequests // subRequests
); );
const result = await snodeRpc({ const result = await snodeRpc({

@ -93,7 +93,7 @@ async function retrieveRequestForGroup({
namespace: SnodeNamespaces; namespace: SnodeNamespaces;
retrieveParam: RetrieveParams; retrieveParam: RetrieveParams;
}) { }) {
if (!PubKey.isClosedGroupV3(groupPk)) { if (!PubKey.isClosedGroupV2(groupPk)) {
throw new Error('retrieveRequestForGroup: not a 03 group'); throw new Error('retrieveRequestForGroup: not a 03 group');
} }
if (!SnodeNamespace.isGroupNamespace(namespace)) { if (!SnodeNamespace.isGroupNamespace(namespace)) {
@ -147,7 +147,7 @@ async function buildRetrieveRequest(
return retrieveRequestForLegacyGroup({ namespace, ourPubkey, pubkey, retrieveParam }); return retrieveRequestForLegacyGroup({ namespace, ourPubkey, pubkey, retrieveParam });
} }
if (PubKey.isClosedGroupV3(pubkey)) { if (PubKey.isClosedGroupV2(pubkey)) {
if (!SnodeNamespace.isGroupNamespace(namespace)) { if (!SnodeNamespace.isGroupNamespace(namespace)) {
// either config or messages namespaces for 03 groups // either config or messages namespaces for 03 groups
throw new Error(`tried to poll from a non 03 group namespace ${namespace}`); throw new Error(`tried to poll from a non 03 group namespace ${namespace}`);

@ -77,7 +77,7 @@ function isSigParamsForGroupAdmin(
sigParams: SnodeSigParamsAdminGroup | SnodeSigParamsUs sigParams: SnodeSigParamsAdminGroup | SnodeSigParamsUs
): sigParams is SnodeSigParamsAdminGroup { ): sigParams is SnodeSigParamsAdminGroup {
const asGr = sigParams as 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) { async function getSnodeShared(params: SnodeSigParamsAdminGroup | SnodeSigParamsUs) {

@ -191,7 +191,7 @@ export class SwarmPolling {
window?.log?.debug( window?.log?.debug(
`Polling for ${loggingId}; timeout: ${convoPollingTimeout}; diff: ${diff} ` `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.GROUPV3]);
} }
return this.pollOnceForKey([key, ConversationTypeEnum.GROUP]); return this.pollOnceForKey([key, ConversationTypeEnum.GROUP]);
@ -263,7 +263,7 @@ export class SwarmPolling {
} else if ( } else if (
type === ConversationTypeEnum.GROUPV3 && type === ConversationTypeEnum.GROUPV3 &&
confMessages?.length && confMessages?.length &&
PubKey.isClosedGroupV3(pubkey) PubKey.isClosedGroupV2(pubkey)
) { ) {
await SwarmPollingGroupConfig.handleGroupSharedConfigMessages(confMessages, pubkey); await SwarmPollingGroupConfig.handleGroupSharedConfigMessages(confMessages, pubkey);
} }

@ -96,7 +96,7 @@ export class ConversationController {
throw new TypeError(`'type' must be 'private' or 'group' or 'groupv3' but got: '${type}'`); 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( throw new Error(
'required v3 closed group` ` but the pubkey does not match the 03 prefix for them' '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. // 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. // Just delete everything related to it, not trying to add update message or send a left message.
await this.removeGroupOrCommunityFromDBAndRedux(groupId); await this.removeGroupOrCommunityFromDBAndRedux(groupId);
if (PubKey.isClosedGroupV3(groupId)) { if (PubKey.isClosedGroupV2(groupId)) {
await remove03GroupFromWrappers(groupId); await remove03GroupFromWrappers(groupId);
} else { } else {
await removeLegacyGroupFromWrappers(groupId); await removeLegacyGroupFromWrappers(groupId);

@ -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 { SignalService } from '../../protobuf';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { MetaGroupWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { PubKey } from '../types'; import { PubKey } from '../types';
import { concatUInt8Array, getSodiumRenderer, MessageEncrypter } from '.';
import { fromHexToArray } from '../utils/String';
import { Data } from '../../data/data';
import { UserUtils } from '../utils'; import { UserUtils } from '../utils';
import { fromHexToArray } from '../utils/String';
import { SigningFailed } from '../utils/errors';
import { addMessagePadding } from './BufferPadding'; import { addMessagePadding } from './BufferPadding';
export { concatUInt8Array, getSodiumRenderer }; export { concatUInt8Array, getSodiumRenderer };
@ -13,57 +17,106 @@ type EncryptResult = {
cipherText: Uint8Array; 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 plainTextBuffer The unpadded plaintext buffer. It will be padded
* @param encryptionType The type of encryption. * @param encryptionType The type of encryption.
* @returns The envelope type and the base64 encoded cipher text * @returns The envelope type and the base64 encoded cipher text
*/ */
export async function encrypt( export async function encrypt(
device: PubKey, destination: PubKey,
plainTextBuffer: Uint8Array, plainTextBuffer: Uint8Array,
encryptionType: SignalService.Envelope.Type encryptionType: SignalService.Envelope.Type
): Promise<EncryptResult> { ): Promise<EncryptResult> {
const { CLOSED_GROUP_MESSAGE, SESSION_MESSAGE } = SignalService.Envelope.Type; const { CLOSED_GROUP_MESSAGE, SESSION_MESSAGE } = SignalService.Envelope.Type;
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 (encryptionType !== CLOSED_GROUP_MESSAGE && encryptionType !== SESSION_MESSAGE) { // if (destination.isGroupV2 || destination.isLegacyGroup) {
throw new Error(`Invalid encryption type:${encryptionType}`); // throw new PreConditionFailed(
} // 'Encryption with SESSION_MESSAGE only work with destination private or us'
// );
const encryptForClosedGroup = encryptionType === CLOSED_GROUP_MESSAGE; // }
const plainText = addMessagePadding(plainTextBuffer); // assertUnreachable(
// destination,
if (encryptForClosedGroup) { // 'Encryption with SESSION_MESSAGE only work with destination private or us'
// 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 hexPubFromECKeyPair = PubKey.cast(hexEncryptionKeyPair.publicHex); case CLOSED_GROUP_MESSAGE: {
const groupPk = destination.key;
const cipherTextClosedGroup = await MessageEncrypter.encryptUsingSessionProtocol( if (PubKey.isClosedGroupV2(groupPk)) {
hexPubFromECKeyPair,
plainText
);
return { return {
envelopeType: CLOSED_GROUP_MESSAGE, envelopeType: CLOSED_GROUP_MESSAGE,
cipherText: cipherTextClosedGroup, cipherText: await encryptWithLibSession(groupPk, plainTextBuffer),
}; };
} }
const cipherText = await MessageEncrypter.encryptUsingSessionProtocol(device, plainText);
return { envelopeType: SESSION_MESSAGE, cipherText }; // 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, '');
}
} }
export async function encryptUsingSessionProtocol( export async function encryptUsingSessionProtocol(
recipientHexEncodedX25519PublicKey: PubKey, destinationX25519Pk: PubKey,
plaintext: Uint8Array plaintext: Uint8Array
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const userED25519KeyPairHex = await UserUtils.getUserED25519KeyPair(); const userED25519KeyPairHex = await UserUtils.getUserED25519KeyPair();
@ -76,9 +129,9 @@ export async function encryptUsingSessionProtocol(
} }
const sodium = await getSodiumRenderer(); const sodium = await getSodiumRenderer();
// window?.log?.info('encryptUsingSessionProtocol for ', recipientHexEncodedX25519PublicKey.key); const recipientX25519PublicKey = fromHexToArray(
PubKey.removePrefixIfNeeded(destinationX25519Pk.key)
const recipientX25519PublicKey = recipientHexEncodedX25519PublicKey.withoutPrefixToArray(); );
const userED25519PubKeyBytes = fromHexToArray(userED25519KeyPairHex.pubKey); const userED25519PubKeyBytes = fromHexToArray(userED25519KeyPairHex.pubKey);
const userED25519SecretKeyBytes = fromHexToArray(userED25519KeyPairHex.privKey); const userED25519SecretKeyBytes = fromHexToArray(userED25519KeyPairHex.privKey);

@ -60,7 +60,7 @@ export async function initiateClosedGroupUpdate(
groupName: string, groupName: string,
members: Array<string> members: Array<string>
) { ) {
const isGroupV3 = PubKey.isClosedGroupV3(groupId); const isGroupV3 = PubKey.isClosedGroupV2(groupId);
const convo = await getConversationController().getOrCreateAndWait( const convo = await getConversationController().getOrCreateAndWait(
groupId, groupId,
isGroupV3 ? ConversationTypeEnum.GROUPV3 : ConversationTypeEnum.GROUP isGroupV3 ? ConversationTypeEnum.GROUPV3 : ConversationTypeEnum.GROUP
@ -206,7 +206,7 @@ function buildGroupDiff(convo: ConversationModel, update: GroupInfo): GroupDiff
export async function updateOrCreateClosedGroup(details: GroupInfo) { export async function updateOrCreateClosedGroup(details: GroupInfo) {
const { id, expireTimer } = details; const { id, expireTimer } = details;
const isV3 = PubKey.isClosedGroupV3(id); const isV3 = PubKey.isClosedGroupV2(id);
const conversation = await getConversationController().getOrCreateAndWait( const conversation = await getConversationController().getOrCreateAndWait(
id, id,

@ -3,6 +3,9 @@ import { PubKey } from '../../../types';
import { StringUtils } from '../../../utils'; import { StringUtils } from '../../../utils';
import { VisibleMessage } from './VisibleMessage'; import { VisibleMessage } from './VisibleMessage';
import { ClosedGroupMessage } from '../controlMessage/group/ClosedGroupMessage'; 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 { interface ClosedGroupVisibleMessageParams {
identifier?: string; identifier?: string;
@ -24,7 +27,7 @@ export class ClosedGroupVisibleMessage extends ClosedGroupMessage {
throw new Error('ClosedGroupVisibleMessage: groupId must be set'); 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'); throw new Error('GroupContext should not be used anymore with closed group v3');
} }
} }
@ -45,3 +48,36 @@ export class ClosedGroupVisibleMessage extends ClosedGroupMessage {
return dataProto; 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<ClosedGroupVisibleMessageParams, 'chatMessage' | 'identifier'> &
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;
}
}

@ -1,28 +1,28 @@
import { AbortController } from 'abort-controller'; import { AbortController } from 'abort-controller';
import { PendingMessageCache } from './PendingMessageCache';
import { JobQueue, MessageUtils, UserUtils } from '../utils';
import { PubKey, RawMessage } from '../types';
import { MessageSender } from '.'; import { MessageSender } from '.';
import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage';
import { ConfigurationMessage } from '../messages/outgoing/controlMessage/ConfigurationMessage'; import { ConfigurationMessage } from '../messages/outgoing/controlMessage/ConfigurationMessage';
import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage';
import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage'; 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 { ContentMessage } from '../messages/outgoing';
import { ExpirationTimerUpdateMessage } from '../messages/outgoing/controlMessage/ExpirationTimerUpdateMessage'; import { ExpirationTimerUpdateMessage } from '../messages/outgoing/controlMessage/ExpirationTimerUpdateMessage';
import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage'; import { ClosedGroupAddedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage';
import { ClosedGroupEncryptionPairMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage'; 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 { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage';
import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; 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 { SyncMessageType } from '../utils/sync/syncUtils';
import { MessageSentHandler } from './MessageSentHandler';
import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil'; 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 { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2';
import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction'; import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction';
import { import {
@ -30,7 +30,10 @@ import {
SnodeNamespacesLegacyGroup, SnodeNamespacesLegacyGroup,
SnodeNamespacesUser, SnodeNamespacesUser,
} from '../apis/snode_api/namespaces'; } from '../apis/snode_api/namespaces';
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { SharedConfigMessage } from '../messages/outgoing/controlMessage/SharedConfigMessage'; import { SharedConfigMessage } from '../messages/outgoing/controlMessage/SharedConfigMessage';
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
type ClosedGroupMessageType = type ClosedGroupMessageType =
| ClosedGroupVisibleMessage | ClosedGroupVisibleMessage
@ -196,6 +199,26 @@ export class MessageQueue {
return this.sendToPubKey(PubKey.cast(destinationPubKey), message, namespace, sentCb, true); return this.sendToPubKey(PubKey.cast(destinationPubKey), message, namespace, sentCb, true);
} }
public async sendToGroupV3({
message,
sentCb,
}: {
message: ClosedGroupV3VisibleMessage;
sentCb?: (message: RawMessage) => Promise<void>;
}): Promise<void> {
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({ public async sendSyncMessage({
namespace, namespace,
message, message,

@ -7,6 +7,7 @@ import _, { isEmpty, isNil, isString, sample, toNumber } from 'lodash';
import pRetry from 'p-retry'; import pRetry from 'p-retry';
import { Data } from '../../data/data'; import { Data } from '../../data/data';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil'; import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/ApiUtil';
import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2'; import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroupMessageV2';
import { import {
@ -40,7 +41,6 @@ import { RawMessage } from '../types/RawMessage';
import { UserUtils } from '../utils'; import { UserUtils } from '../utils';
import { fromUInt8ArrayToBase64 } from '../utils/String'; import { fromUInt8ArrayToBase64 } from '../utils/String';
import { EmptySwarmError } from '../utils/errors'; import { EmptySwarmError } from '../utils/errors';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
// ================ SNODE STORE ================ // ================ SNODE STORE ================
@ -196,12 +196,15 @@ async function getSignatureParamsFromNamespace(item: StoreOnNodeParamsNoSig, des
}); });
} }
if (SnodeNamespace.isGroupConfigNamespace(item.namespace)) { if (
if (!PubKey.isClosedGroupV3(destination)) { SnodeNamespace.isGroupConfigNamespace(item.namespace) ||
item.namespace === SnodeNamespaces.ClosedGroupMessages
) {
if (!PubKey.isClosedGroupV2(destination)) {
throw new Error('sendMessagesDataToSnode: groupconfig namespace required a 03 pubkey'); throw new Error('sendMessagesDataToSnode: groupconfig namespace required a 03 pubkey');
} }
const group = await UserGroupsWrapperActions.getGroup(destination); 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)) { if (isNil(groupSecretKey) || isEmpty(groupSecretKey)) {
throw new Error(`sendMessagesDataToSnode: failed to find group admin secret key in wrapper`); throw new Error(`sendMessagesDataToSnode: failed to find group admin secret key in wrapper`);
} }
@ -303,7 +306,7 @@ type SharedEncryptAndWrap = {
type EncryptAndWrapMessage = { type EncryptAndWrapMessage = {
plainTextBuffer: Uint8Array; plainTextBuffer: Uint8Array;
destination: string; destination: string;
namespace: number | null; namespace: number;
} & SharedEncryptAndWrap; } & SharedEncryptAndWrap;
type EncryptAndWrapMessageResults = { type EncryptAndWrapMessageResults = {
@ -342,23 +345,11 @@ async function encryptMessageAndWrap(
const data = wrapEnvelope(envelope); const data = wrapEnvelope(envelope);
const data64 = ByteBuffer.wrap(data).toString('base64'); 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 { return {
data64, data64,
networkTimestamp, networkTimestamp,
data, data,
namespace: overridenNamespace, namespace,
ttl, ttl,
identifier, identifier,
isSyncMessage: syncMessage, isSyncMessage: syncMessage,

@ -26,6 +26,60 @@ export enum KeyPrefixType {
groupV3 = '03', 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 { export class PubKey {
public static readonly PUBKEY_LEN = 66; public static readonly PUBKEY_LEN = 66;
public static readonly PUBKEY_LEN_NO_PREFIX = PubKey.PUBKEY_LEN - 2; public static readonly PUBKEY_LEN_NO_PREFIX = PubKey.PUBKEY_LEN - 2;
@ -233,31 +287,12 @@ export class PubKey {
return fromHexToArray(this.key); return fromHexToArray(this.key);
} }
public withoutPrefixToArray(): Uint8Array {
return fromHexToArray(PubKey.removePrefixIfNeeded(this.key));
}
public static isBlinded(key: string) { public static isBlinded(key: string) {
return key.startsWith(KeyPrefixType.blinded15) || key.startsWith(KeyPrefixType.blinded25); 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}$`); const regex = new RegExp(`^${KeyPrefixType.groupV3}${PubKey.HEX}{64}$`);
return regex.test(key); 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)
);
}
} }

@ -7,7 +7,7 @@ export type RawMessage = {
device: string; device: string;
ttl: number; ttl: number;
encryption: SignalService.Envelope.Type; 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 // For building RawMessages from JSON

@ -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 {}

@ -2,7 +2,7 @@
import { GroupPubkeyType } from 'libsession_util_nodejs'; import { GroupPubkeyType } from 'libsession_util_nodejs';
import { isArray, isEmpty, isNumber, isString } from 'lodash'; import { isArray, isEmpty, isNumber, isString } from 'lodash';
import { UserUtils } from '../..'; import { UserUtils } from '../..';
import { ConfigDumpData } from '../../../../data/configDump/configDump'; import { stringify } from '../../../../types/sqlSharedTypes';
import { ReleasedFeatures } from '../../../../util/releaseFeature'; import { ReleasedFeatures } from '../../../../util/releaseFeature';
import { isSignInByLinking } from '../../../../util/storage'; import { isSignInByLinking } from '../../../../util/storage';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
@ -29,7 +29,6 @@ import {
PersistedJob, PersistedJob,
RunJobResult, RunJobResult,
} from '../PersistedJob'; } 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 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; const defaultMaxAttempts = 2;
@ -118,30 +117,7 @@ async function buildAndSaveDumpsToDB(
} }
await MetaGroupWrapperActions.metaConfirmPushed(...toConfirm); await MetaGroupWrapperActions.metaConfirmPushed(...toConfirm);
const metaNeedsDump = await MetaGroupWrapperActions.needsDump(groupPk); return LibSessionUtil.saveMetaGroupDumpToDb(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}`,
});
} }
class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> { class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
@ -181,12 +157,12 @@ class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
return RunJobResult.PermanentFailure; return RunJobResult.PermanentFailure;
} }
if (!PubKey.isClosedGroupV3(thisJobDestination)) { if (!PubKey.isClosedGroupV2(thisJobDestination)) {
return RunJobResult.PermanentFailure; 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 // 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(); const newGroupsReleased = await ReleasedFeatures.checkIsNewGroupsReleased();
// if the feature flag is not enabled, we want to keep updating the dumps, but just not sync them. // if the feature flag is not enabled, we want to keep updating the dumps, but just not sync them.

@ -227,7 +227,7 @@ async function pendingChangesForGroup(
groupPk: GroupPubkeyType groupPk: GroupPubkeyType
): Promise<GroupSingleDestinationChanges> { ): Promise<GroupSingleDestinationChanges> {
const results = new Array<PendingChangesForGroup>(); const results = new Array<PendingChangesForGroup>();
if (!PubKey.isClosedGroupV3(groupPk)) { if (!PubKey.isClosedGroupV2(groupPk)) {
throw new Error(`pendingChangesForGroup only works for user or 03 group pubkeys`); throw new Error(`pendingChangesForGroup only works for user or 03 group pubkeys`);
} }
// one of the wrapper behind the metagroup needs a push // 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); 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 = { export const LibSessionUtil = {
initializeLibSessionUtilWrappers, initializeLibSessionUtilWrappers,
userVariantToUserKind, userVariantToUserKind,
@ -327,4 +343,5 @@ export const LibSessionUtil = {
pendingChangesForGroup, pendingChangesForGroup,
userKindToVariant, userKindToVariant,
markAsPushed, markAsPushed,
saveMetaGroupDumpToDb,
}; };

@ -35,7 +35,7 @@ function isLegacyGroupToStoreInWrapper(convo: ConversationModel): boolean {
} }
function isGroupToStoreInWrapper(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) const convoType: UserGroupsType = isCommunityToStoreInWrapper(foundConvo)
? 'Community' ? 'Community'
: PubKey.isClosedGroupV3(convoId) : PubKey.isClosedGroupV2(convoId)
? 'Group' ? 'Group'
: 'LegacyGroup'; : 'LegacyGroup';

Loading…
Cancel
Save