feat: delete msg on swarm when admin receives member request

pull/3052/head
Audric Ackermann 11 months ago
parent 2ec6c7f29c
commit 0a3d71fe03

@ -24,4 +24,4 @@ export type DeleteAllMessageHashesInConversationMatchingAuthorType = (
author: PubkeyType;
signatureTimestamp: number;
}
) => PrArrayMsgIds;
) => Promise<{ msgIdsDeleted: Array<string>; msgHashesDeleted: Array<string> }>;

@ -126,7 +126,7 @@ async function unsendMessagesForEveryone(
await unsendMessagesForEveryone1o1AndLegacy(conversation, conversation.id, msgsToDelete);
} else if (conversation.isClosedGroupV2()) {
if (!PubKey.is03Pubkey(destinationId)) {
throw new Error('invalid conversation id (03) for unsendMessageForEveryone');
throw new Error('invalid conversation id (03) for unsendMessageForEveryone');
}
await unsendMessagesForEveryoneGroupV2({
groupPk: destinationId,
@ -199,11 +199,10 @@ export async function deleteMessagesFromSwarmOnly(
);
return false;
}
const errorOnAtLeastOneSnode = await SnodeAPI.networkDeleteMessages(
deletionMessageHashes,
pubkey
);
return errorOnAtLeastOneSnode;
if (PubKey.is03Pubkey(pubkey)) {
return await SnodeAPI.networkDeleteMessagesForGroup(deletionMessageHashes, pubkey);
}
return await SnodeAPI.networkDeleteMessageOurSwarm(deletionMessageHashes, pubkey);
} catch (e) {
window.log?.error(
`deleteMessagesFromSwarmOnly: Error deleting message from swarm of ${ed25519Str(pubkey)}, hashes: ${deletionMessageHashes}`,

@ -1134,14 +1134,18 @@ function deleteAllMessageHashesInConversationMatchingAuthor(
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageHashesInConversationMatchingAuthorType> {
if (!groupPk || !author || !messageHashes.length) {
return [];
return { msgHashesDeleted: [], msgIdsDeleted: [] };
}
return assertGlobalInstanceOrInstance(instance)
const results = assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND source = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING id`
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND source = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING id, messageHash;`
)
.all(groupPk, author, signatureTimestamp, ...messageHashes)
.map(m => m.id);
.all(groupPk, author, signatureTimestamp, ...messageHashes);
return {
msgHashesDeleted: results.map(m => m.messageHash),
msgIdsDeleted: results.map(m => m.id),
};
}
function cleanUpExpirationTimerUpdateHistory(

@ -1,6 +1,7 @@
import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isEmpty, isFinite, isNumber } from 'lodash';
import { Data } from '../../data/data';
import { deleteMessagesFromSwarmOnly } from '../../interactions/conversations/unsendingInteractions';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf';
@ -377,16 +378,27 @@ async function handleGroupDeleteMemberContentMessage({
if (isEmpty(change.adminSignature)) {
// this is step 1.
const msgsDeleted = await Data.deleteAllMessageHashesInConversationMatchingAuthor({
author,
groupPk,
messageHashes: change.messageHashes,
signatureTimestamp,
});
const { msgIdsDeleted, msgHashesDeleted } =
await Data.deleteAllMessageHashesInConversationMatchingAuthor({
author,
groupPk,
messageHashes: change.messageHashes,
signatureTimestamp,
});
window.inboxStore.dispatch(
messagesExpired(msgsDeleted.map(m => ({ conversationKey: groupPk, messageId: m })))
messagesExpired(msgIdsDeleted.map(m => ({ conversationKey: groupPk, messageId: m })))
);
if (msgIdsDeleted.length) {
// Note: we `void` it because we don't want to hang while
// processing the handleGroupDeleteMemberContentMessage itself
// (we are running on the receiving pipeline here)
void deleteMessagesFromSwarmOnly(msgHashesDeleted, groupPk).catch(e => {
// we retry a bunch of times already, so if it still fails, there is not much we can do.
window.log.warn('deleteMessagesFromSwarmOnly failed with', e.message);
});
}
convo.updateLastMessage();
return;
}

@ -14,7 +14,7 @@ import { BatchRequests } from './batchRequest';
import { SnodePool } from './snodePool';
import { StringUtils, UserUtils } from '../../utils';
import { ed25519Str, fromBase64ToArray, fromHexToArray } from '../../utils/String';
import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
export const ERROR_CODE_NO_CONNECT = 'ENETUNREACH: No network connection.';
@ -30,124 +30,111 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
async () => {
const snodeToMakeRequestTo = await SnodePool.getNodeFromSwarmOrThrow(usPk);
const builtRequest = await request.buildAndSignParameters(); // we need the timestamp to verify the signature below
return pRetry(
async () => {
const ret = await BatchRequests.doSnodeBatchRequestNoRetries(
[builtRequest],
snodeToMakeRequestTo,
10000,
usPk,
false
);
const ret = await BatchRequests.doSnodeBatchRequestNoRetries(
[builtRequest],
snodeToMakeRequestTo,
10000,
usPk,
false
);
if (!ret || !ret?.[0].body || ret[0].code !== 200) {
throw new Error(
`Empty response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}`
);
}
try {
const firstResultParsedBody = ret[0].body;
const { swarm } = firstResultParsedBody;
if (!swarm) {
throw new Error(
`Invalid JSON swarm response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
}
const swarmAsArray = Object.entries(swarm) as Array<Array<any>>;
if (!swarmAsArray.length) {
throw new Error(
`Invalid JSON swarmAsArray response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
}
// results will only contains the snode pubkeys which returned invalid/empty results
const results: Array<string> = compact(
swarmAsArray.map(snode => {
const snodePubkey = snode[0];
const snodeJson = snode[1];
const isFailed = snodeJson.failed || false;
if (isFailed) {
const reason = snodeJson.reason;
const statusCode = snodeJson.code;
if (reason && statusCode) {
window?.log?.warn(
`Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)} due to error: ${reason}: ${statusCode}`
);
// if we tried to make the delete on a snode not in our swarm, just trigger a pRetry error so the outer block here finds new snodes to make the request to.
if (statusCode === 421) {
throw new pRetry.AbortError(
`421 error on network ${request.method}. Retrying with a new snode`
);
}
} else {
window?.log?.warn(
`Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}`
);
}
return snodePubkey;
}
if (!ret || !ret?.[0].body || ret[0].code !== 200) {
throw new Error(
`Empty response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}`
);
}
const deletedObj = snodeJson.deleted as Record<number, Array<string>>;
const hashes: Array<string> = [];
try {
const firstResultParsedBody = ret[0].body;
const { swarm } = firstResultParsedBody;
for (const key in deletedObj) {
if (deletedObj.hasOwnProperty(key)) {
hashes.push(...deletedObj[key]);
}
}
const sortedHashes = hashes.sort();
const signatureSnode = snodeJson.signature as string;
// The signature format is (with sortedHashes accross all namespaces) ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
const dataToVerify = `${usPk}${builtRequest.params.timestamp}${sortedHashes.join('')}`;
const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8');
const isValid = sodium.crypto_sign_verify_detached(
fromBase64ToArray(signatureSnode),
new Uint8Array(dataToVerifyUtf8),
fromHexToArray(snodePubkey)
if (!swarm) {
throw new Error(
`Invalid JSON swarm response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
}
const swarmAsArray = Object.entries(swarm) as Array<Array<any>>;
if (!swarmAsArray.length) {
throw new Error(
`Invalid JSON swarmAsArray response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
}
// results will only contains the snode pubkeys which returned invalid/empty results
const results: Array<string> = compact(
swarmAsArray.map(snode => {
const snodePubkey = snode[0];
const snodeJson = snode[1];
const isFailed = snodeJson.failed || false;
if (isFailed) {
const reason = snodeJson.reason;
const statusCode = snodeJson.code;
if (reason && statusCode) {
window?.log?.warn(
`Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)} due to error: ${reason}: ${statusCode}`
);
if (!isValid) {
return snodePubkey;
// if we tried to make the delete on a snode not in our swarm, just trigger a pRetry error so the outer block here finds new snodes to make the request to.
if (statusCode === 421) {
throw new pRetry.AbortError(
`421 error on network ${request.method}. Retrying with a new snode`
);
}
return null;
})
);
} else {
window?.log?.warn(
`Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}`
);
}
return snodePubkey;
}
return results;
} catch (e) {
throw new Error(
`Invalid JSON response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${ret}`
);
}
},
{
retries: 3,
minTimeout: SnodeAPI.TEST_getMinTimeout(),
onFailedAttempt: e => {
window?.log?.warn(
`${request.method} INNER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
const deletedObj = snodeJson.deleted as Record<number, Array<string>>;
const hashes: Array<string> = [];
for (const key in deletedObj) {
if (deletedObj.hasOwnProperty(key)) {
hashes.push(...deletedObj[key]);
}
}
const sortedHashes = hashes.sort();
const signatureSnode = snodeJson.signature as string;
// The signature format is (with sortedHashes accross all namespaces) ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
const dataToVerify = `${usPk}${builtRequest.params.timestamp}${sortedHashes.join('')}`;
const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8');
const isValid = sodium.crypto_sign_verify_detached(
fromBase64ToArray(signatureSnode),
new Uint8Array(dataToVerifyUtf8),
fromHexToArray(snodePubkey)
);
},
}
);
if (!isValid) {
return snodePubkey;
}
return null;
})
);
return results;
} catch (e) {
throw new Error(
`Invalid JSON response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${ret}`
);
}
},
{
retries: 3,
retries: 5,
minTimeout: SnodeAPI.TEST_getMinTimeout(),
onFailedAttempt: e => {
window?.log?.warn(
@ -167,141 +154,120 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
const TEST_getMinTimeout = () => 500;
/**
* Locally deletes message and deletes message on the network (all nodes that contain the message)
* Delete the specified message hashes from the our own swarm only.
* Note: legacy group did not support removing messages from the swarm.
*/
const networkDeleteMessages = async (
const networkDeleteMessageOurSwarm = async (
messagesHashes: Array<string>,
pubkey: PubkeyType | GroupPubkeyType
pubkey: PubkeyType
): Promise<boolean> => {
const sodium = await getSodiumRenderer();
if (PubKey.is05Pubkey(pubkey) && pubkey !== UserUtils.getOurPubKeyStrFromCache()) {
throw new Error('networkDeleteMessages with 05 pk can only delete for ourself');
if (!PubKey.is05Pubkey(pubkey) || pubkey !== UserUtils.getOurPubKeyStrFromCache()) {
throw new Error('networkDeleteMessageOurSwarm with 05 pk can only for our own swarm');
}
const request = PubKey.is03Pubkey(pubkey)
? new DeleteHashesFromGroupNodeSubRequest({ messagesHashes, groupPk: pubkey })
: new DeleteHashesFromUserNodeSubRequest({ messagesHashes });
const request = new DeleteHashesFromUserNodeSubRequest({ messagesHashes });
try {
const success = await pRetry(
async () => {
const snodeToMakeRequestTo = await SnodePool.getNodeFromSwarmOrThrow(request.pubkey);
return pRetry(
async () => {
const ret = await BatchRequests.doUnsignedSnodeBatchRequestNoRetries(
[request],
snodeToMakeRequestTo,
10000,
request.pubkey,
false
const ret = await BatchRequests.doUnsignedSnodeBatchRequestNoRetries(
[request],
snodeToMakeRequestTo,
10000,
request.pubkey,
false
);
if (!ret || !ret?.[0].body || ret[0].code !== 200) {
throw new Error(
`networkDeleteMessageOurSwarm: Empty response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)} about pk: ${ed25519Str(request.pubkey)}`
);
}
try {
const firstResultParsedBody = ret[0].body;
const { swarm } = firstResultParsedBody;
if (!swarm) {
throw new Error(
`networkDeleteMessageOurSwarm: Invalid JSON swarm response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
debugger;
}
const swarmAsArray = Object.entries(swarm) as Array<Array<any>>;
if (!swarmAsArray.length) {
throw new Error(
`networkDeleteMessageOurSwarm: Invalid JSON swarmAsArray response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
}
// results will only contains the snode pubkeys which returned invalid/empty results
const results: Array<string> = compact(
swarmAsArray.map(snode => {
const snodePubkey = snode[0];
const snodeJson = snode[1];
if (!ret || !ret?.[0].body || ret[0].code !== 200) {
throw new Error(
`Empty response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)} about pk: ${ed25519Str(request.pubkey)}`
);
}
try {
const firstResultParsedBody = ret[0].body;
const { swarm } = firstResultParsedBody;
if (!swarm) {
throw new Error(
`Invalid JSON swarm response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
}
const swarmAsArray = Object.entries(swarm) as Array<Array<any>>;
if (!swarmAsArray.length) {
throw new Error(
`Invalid JSON swarmAsArray response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${firstResultParsedBody}`
);
}
// results will only contains the snode pubkeys which returned invalid/empty results
const results: Array<string> = compact(
swarmAsArray.map(snode => {
const snodePubkey = snode[0];
const snodeJson = snode[1];
const isFailed = snodeJson.failed || false;
if (isFailed) {
const reason = snodeJson.reason;
const statusCode = snodeJson.code;
if (reason && statusCode) {
window?.log?.warn(
`Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)} due to error: ${reason}: ${statusCode}`
);
// if we tried to make the delete on a snode not in our swarm, just trigger a pRetry error so the outer block here finds new snodes to make the request to.
if (statusCode === 421) {
throw new pRetry.AbortError(
`421 error on network ${request.method}. Retrying with a new snode`
);
}
} else {
window?.log?.warn(
`Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}`
);
}
return snodePubkey;
}
const isFailed = snodeJson.failed || false;
const responseHashes = snodeJson.deleted as Array<string>;
const signatureSnode = snodeJson.signature as string;
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
const dataToVerify = `${request.pubkey}${messagesHashes.join(
''
)}${responseHashes.join('')}`;
const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8');
const isValid = sodium.crypto_sign_verify_detached(
fromBase64ToArray(signatureSnode),
new Uint8Array(dataToVerifyUtf8),
fromHexToArray(snodePubkey)
if (isFailed) {
const reason = snodeJson.reason;
const statusCode = snodeJson.code;
if (reason && statusCode) {
window?.log?.warn(
`networkDeleteMessageOurSwarm: Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)} due to error: ${reason}: ${statusCode}`
);
if (!isValid) {
return snodePubkey;
}
return null;
})
);
} else {
window?.log?.warn(
`networkDeleteMessageOurSwarm: Could not ${request.method} from ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}`
);
}
return snodePubkey;
}
return isEmpty(results);
} catch (e) {
throw new Error(
`Invalid JSON response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${ret}`
);
}
},
{
retries: 3,
minTimeout: SnodeAPI.TEST_getMinTimeout(),
onFailedAttempt: e => {
window?.log?.warn(
`${request.method} INNER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
const responseHashes = snodeJson.deleted as Array<string>;
const signatureSnode = snodeJson.signature as string;
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
const dataToVerify = `${request.pubkey}${messagesHashes.join(
''
)}${responseHashes.join('')}`;
const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8');
const isValid = sodium.crypto_sign_verify_detached(
fromBase64ToArray(signatureSnode),
new Uint8Array(dataToVerifyUtf8),
fromHexToArray(snodePubkey)
);
},
}
);
if (!isValid) {
return snodePubkey;
}
return null;
})
);
return isEmpty(results);
} catch (e) {
throw new Error(
`networkDeleteMessageOurSwarm: Invalid JSON response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)}, ${ret}`
);
}
},
{
retries: 3,
retries: 5,
minTimeout: SnodeAPI.TEST_getMinTimeout(),
onFailedAttempt: e => {
window?.log?.warn(
`${request.method} OUTER request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}`
`networkDeleteMessageOurSwarm: ${request.method} request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}`
);
},
}
@ -309,13 +275,85 @@ const networkDeleteMessages = async (
return success;
} catch (e) {
window?.log?.warn(`failed to ${request.method} message on network:`, e);
window?.log?.warn(
`networkDeleteMessageOurSwarm: failed to ${request.method} message on network:`,
e
);
return false;
}
};
/**
* Delete the specified message hashes from the 03-group's swarm.
* Returns true when the hashes have been removed successufuly.
* Returns false when
* - we don't have the secretKey
* - if one of the hash was already not present in the swarm,
* - if the request failed too many times
*/
const networkDeleteMessagesForGroup = async (
messagesHashes: Array<string>,
groupPk: GroupPubkeyType
): Promise<boolean> => {
if (!PubKey.is03Pubkey(groupPk)) {
throw new Error('networkDeleteMessagesForGroup with 05 pk can only delete for ourself');
}
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group || !group.secretKey || isEmpty(group.secretKey)) {
window.log.warn(
`networkDeleteMessagesForGroup: not deleting from swarm of 03-group ${messagesHashes.length} hashes as we do not the adminKey`
);
return false;
}
try {
const request = new DeleteHashesFromGroupNodeSubRequest({
messagesHashes,
groupPk,
secretKey: group.secretKey,
});
await pRetry(
async () => {
const snodeToMakeRequestTo = await SnodePool.getNodeFromSwarmOrThrow(request.pubkey);
const ret = await BatchRequests.doUnsignedSnodeBatchRequestNoRetries(
[request],
snodeToMakeRequestTo,
10000,
request.pubkey,
false
);
if (!ret || !ret?.[0].body || ret[0].code !== 200) {
throw new Error(
`networkDeleteMessagesForGroup: Empty response got for ${request.method} on snode ${ed25519Str(
snodeToMakeRequestTo.pubkey_ed25519
)} about pk: ${ed25519Str(request.pubkey)}`
);
}
},
{
retries: 5,
minTimeout: SnodeAPI.TEST_getMinTimeout(),
onFailedAttempt: e => {
window?.log?.warn(
`networkDeleteMessagesForGroup: ${request.method} request attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.message}`
);
},
}
);
return true;
} catch (e) {
window?.log?.warn(`networkDeleteMessagesForGroup: failed to delete messages on network:`, e);
return false;
}
};
export const SnodeAPI = {
TEST_getMinTimeout,
networkDeleteMessages,
networkDeleteMessagesForGroup,
networkDeleteMessageOurSwarm,
forceNetworkDeletion,
};

@ -3,10 +3,10 @@ import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_no
import { from_hex } from 'libsodium-wrappers-sumo';
import { isEmpty, isString } from 'lodash';
import { AwaitedReturn, assertUnreachable } from '../../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { concatUInt8Array } from '../../crypto';
import { PubKey } from '../../types';
import { StringUtils, UserUtils } from '../../utils';
import { ed25519Str } from '../../utils/String';
import { GetNetworkTime } from './getNetworkTime';
import {
SnodeNamespace,
@ -25,7 +25,6 @@ import {
WithSignature,
WithTimestamp,
} from './types';
import { ed25519Str } from '../../utils/String';
type WithMaxSize = { max_size?: number };
export type WithShortenOrExtend = { shortenOrExtend: 'shorten' | 'extend' | '' };
@ -559,25 +558,25 @@ export class DeleteHashesFromGroupNodeSubRequest extends SnodeAPISubRequest {
public method = 'delete' as const;
public readonly messageHashes: Array<string>;
public readonly pubkey: GroupPubkeyType;
public readonly secretKey: Uint8Array;
constructor(args: WithMessagesHashes & WithGroupPubkey) {
constructor(args: WithMessagesHashes & WithGroupPubkey & WithSecretKey) {
super();
this.messageHashes = args.messagesHashes;
this.pubkey = args.groupPk;
this.secretKey = args.secretKey;
if (!this.secretKey || isEmpty(this.secretKey)) {
throw new Error('DeleteHashesFromGroupNodeSubRequest needs a secretKey');
}
}
public async buildAndSignParameters() {
const group = await UserGroupsWrapperActions.getGroup(this.pubkey);
if (!group) {
throw new Error('DeleteHashesFromGroupNodeSubRequest no such group found');
}
// This will try to use the adminSecretKey if we have it, or the authData if we have it.
// Otherwise, it will throw
// Note: this request can only be made by an admin and will be denied otherwise, so we make the secretKey mandatory in the constructor.
const signResult = await SnodeGroupSignature.getGroupSignatureByHashesParams({
method: this.method,
messagesHashes: this.messageHashes,
groupPk: this.pubkey,
group,
group: { authData: null, pubkeyHex: this.pubkey, secretKey: this.secretKey },
});
return {
@ -714,8 +713,9 @@ export class StoreGroupConfigOrMessageSubRequest extends SnodeAPISubRequest {
public readonly destination: GroupPubkeyType;
public readonly ttlMs: number;
public readonly encryptedData: Uint8Array;
public readonly dbMessageIdentifier: string | null;
public readonly secretKey: Uint8Array | null;
public readonly authData: Uint8Array | null;
constructor(
args: WithGroupPubkey & {
@ -726,6 +726,8 @@ export class StoreGroupConfigOrMessageSubRequest extends SnodeAPISubRequest {
ttlMs: number;
encryptedData: Uint8Array;
dbMessageIdentifier: string | null;
authData: Uint8Array | null;
secretKey: Uint8Array | null;
}
) {
super();
@ -734,6 +736,8 @@ export class StoreGroupConfigOrMessageSubRequest extends SnodeAPISubRequest {
this.ttlMs = args.ttlMs;
this.encryptedData = args.encryptedData;
this.dbMessageIdentifier = args.dbMessageIdentifier;
this.authData = args.authData;
this.secretKey = args.secretKey;
if (isEmpty(this.encryptedData)) {
throw new Error('this.encryptedData cannot be empty');
@ -743,6 +747,16 @@ export class StoreGroupConfigOrMessageSubRequest extends SnodeAPISubRequest {
'StoreGroupConfigOrMessageSubRequest: groupconfig namespace required a 03 pubkey'
);
}
if (isEmpty(this.secretKey) && isEmpty(this.authData)) {
throw new Error(
'StoreGroupConfigOrMessageSubRequest needs either authData or secretKey to be set'
);
}
if (SnodeNamespace.isGroupConfigNamespace(this.namespace) && isEmpty(this.secretKey)) {
throw new Error(
`StoreGroupConfigOrMessageSubRequest: groupconfig namespace [${this.namespace}] requires an adminSecretKey`
);
}
}
public async buildAndSignParameters(): Promise<{
@ -751,17 +765,11 @@ export class StoreGroupConfigOrMessageSubRequest extends SnodeAPISubRequest {
}> {
const encryptedDataBase64 = ByteBuffer.wrap(this.encryptedData).toString('base64');
const found = await UserGroupsWrapperActions.getGroup(this.destination);
if (SnodeNamespace.isGroupConfigNamespace(this.namespace) && isEmpty(found?.secretKey)) {
throw new Error(
`groupconfig namespace [${this.namespace}] require an adminSecretKey for signature but we found none`
);
}
// this will either sign with our admin key or with the subaccount key if the admin one isn't there
const signDetails = await SnodeGroupSignature.getSnodeGroupSignature({
method: this.method,
namespace: this.namespace,
group: found,
group: { authData: this.authData, pubkeyHex: this.destination, secretKey: this.secretKey },
});
if (!signDetails) {

@ -293,7 +293,6 @@ async function getGroupSignatureByHashesParams({
const signatureTimestamp = GetNetworkTime.now();
const sodium = await getSodiumRenderer();
// N
try {
if (group.secretKey && !isEmpty(group.secretKey)) {
const signature = sodium.crypto_sign_detached(message, group.secretKey);
@ -303,7 +302,7 @@ async function getGroupSignatureByHashesParams({
signature: signatureBase64,
pubkey: group.pubkeyHex,
messages: messagesHashes,
timestamp: signatureTimestamp, // TODO audric is this causing backend signature issues?
timestamp: signatureTimestamp,
};
}

@ -50,7 +50,7 @@ import {
} from '../apis/snode_api/signature/groupSignature';
import { SnodeSignature, SnodeSignatureResult } from '../apis/snode_api/signature/snodeSignatures';
import { SnodePool } from '../apis/snode_api/snodePool';
import { WithMessagesHashes, WithRevokeSubRequest } from '../apis/snode_api/types';
import { WithRevokeSubRequest } from '../apis/snode_api/types';
import { TTL_DEFAULT } from '../constants';
import { ConvoHub } from '../conversations';
import { MessageEncrypter } from '../crypto/MessageEncrypter';
@ -184,6 +184,13 @@ async function sendSingleMessage({
);
}
} else if (PubKey.is03Pubkey(destination)) {
const group = await UserGroupsWrapperActions.getGroup(destination);
if (!group) {
window.log.warn(
`sendSingleMessage: no such group found in wrapper: ${ed25519Str(destination)}`
);
throw new Error('sendSingleMessage: no such group found in wrapper');
}
if (SnodeNamespace.isGroupConfigNamespace(encryptedAndWrapped.namespace)) {
subRequests.push(
new StoreGroupConfigOrMessageSubRequest({
@ -192,6 +199,7 @@ async function sendSingleMessage({
ttlMs: overridenTtl,
groupPk: destination,
dbMessageIdentifier: encryptedAndWrapped.identifier || null,
...group,
})
);
} else if (encryptedAndWrapped.namespace === SnodeNamespaces.ClosedGroupMessages) {
@ -202,6 +210,7 @@ async function sendSingleMessage({
ttlMs: overridenTtl,
groupPk: destination,
dbMessageIdentifier: encryptedAndWrapped.identifier || null,
...group,
})
);
} else {
@ -338,38 +347,33 @@ async function signSubRequests(
return signedRequests;
}
async function sendMessagesDataToSnode(
async function sendMessagesDataToSnode<T extends PubkeyType | GroupPubkeyType>(
storeRequests: Array<
| StoreGroupConfigOrMessageSubRequest
| StoreUserConfigSubRequest
| StoreUserMessageSubRequest
| StoreLegacyGroupMessageSubRequest
>,
asssociatedWith: PubkeyType | GroupPubkeyType,
asssociatedWith: T,
{
messagesHashes: messagesToDelete,
revokeSubRequest,
unrevokeSubRequest,
deleteHashesSubRequest,
deleteAllMessagesSubRequest,
}: WithMessagesHashes &
WithRevokeSubRequest & {
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
},
}: WithRevokeSubRequest & {
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
deleteHashesSubRequest:
| (T extends PubkeyType
? DeleteHashesFromUserNodeSubRequest
: DeleteHashesFromGroupNodeSubRequest)
| null;
},
method: MethodBatchType
): Promise<NotEmptyArrayOfBatchResults> {
if (!asssociatedWith) {
throw new Error('sendMessagesDataToSnode first subrequest pubkey needs to be set');
}
const deleteHashesSubRequest = !messagesToDelete.length
? null
: PubKey.is05Pubkey(asssociatedWith)
? new DeleteHashesFromUserNodeSubRequest({ messagesHashes: messagesToDelete })
: new DeleteHashesFromGroupNodeSubRequest({
messagesHashes: messagesToDelete,
groupPk: asssociatedWith,
});
if (storeRequests.some(m => m.destination !== asssociatedWith)) {
throw new Error(
'sendMessagesDataToSnode tried to send batchrequest containing subrequest not for the right destination'
@ -565,17 +569,21 @@ async function encryptMessagesAndWrap(
* @param destination the pubkey we should deposit those message to
* @returns the hashes of successful deposit
*/
async function sendEncryptedDataToSnode({
async function sendEncryptedDataToSnode<T extends GroupPubkeyType | PubkeyType>({
destination,
storeRequests,
messagesHashesToDelete,
deleteHashesSubRequest,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest,
}: WithRevokeSubRequest & {
storeRequests: Array<StoreGroupConfigOrMessageSubRequest | StoreUserConfigSubRequest>;
destination: GroupPubkeyType | PubkeyType;
messagesHashesToDelete: Set<string> | null;
destination: T;
deleteHashesSubRequest:
| (T extends PubkeyType
? DeleteHashesFromUserNodeSubRequest
: DeleteHashesFromGroupNodeSubRequest)
| null;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
}): Promise<NotEmptyArrayOfBatchResults | null> {
try {
@ -585,7 +593,7 @@ async function sendEncryptedDataToSnode({
storeRequests,
destination,
{
messagesHashes: [...(messagesHashesToDelete || [])],
deleteHashesSubRequest,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest,

@ -139,7 +139,6 @@ class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersisted
return concatUInt8Array(s, StringUtils.stringToUint8Array(`${currentGen}`));
});
debugger;
const multiEncryptedMessage = await MultiEncryptWrapperActions.multiEncrypt({
messages: dataToEncrypt,
recipients: sessionIds,
@ -161,15 +160,17 @@ class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersisted
dbMessageIdentifier: null,
namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
ttlMs: TTL_DEFAULT.CONTENT_MESSAGE,
secretKey: group.secretKey,
authData: null,
});
const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests: [multiEncryptRequest],
destination: groupPk,
messagesHashesToDelete: null,
deleteHashesSubRequest: null,
...revokeUnrevokeParams,
});
console.warn('result', result);
if (result?.length === 2 && result[0].code === 200 && result[1].code === 200) {
// both requests success, remove the members from the group member entirely and sync
await MetaGroupWrapperActions.memberEraseAndRekey(groupPk, sessionIdsHex);

@ -5,9 +5,13 @@ import { UserUtils } from '../..';
import { SignalService } from '../../../../protobuf';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { isSignInByLinking } from '../../../../util/storage';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
DeleteAllFromGroupMsgNodeSubRequest,
DeleteHashesFromGroupNodeSubRequest,
StoreGroupConfigOrMessageSubRequest,
StoreGroupExtraData,
} from '../../../apis/snode_api/SnodeRequestTypes';
@ -85,6 +89,14 @@ async function storeGroupUpdateMessages({
return true;
}
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group) {
window.log.warn(
`storeGroupUpdateMessages for ${ed25519Str(groupPk)}: no group found in wrapper`
);
return false;
}
const updateMessagesToEncrypt: Array<StoreGroupExtraData> = updateMessages.map(updateMessage => {
const wrapped = MessageSender.wrapContentIntoEnvelope(
SignalService.Envelope.Type.SESSION_MESSAGE,
@ -122,13 +134,14 @@ async function storeGroupUpdateMessages({
namespace: m.namespace,
ttlMs: m.ttl,
dbMessageIdentifier: m.dbMessageIdentifier,
...group,
});
});
const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests: [...updateMessagesRequests],
destination: groupPk,
messagesHashesToDelete: null,
deleteHashesSubRequest: null,
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
@ -172,6 +185,13 @@ async function pushChangesToGroupSwarmIfNeeded({
!unrevokeSubRequest &&
!deleteAllMessagesSubRequest
) {
window.log.debug(`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: nothing to push`);
return RunJobResult.Success;
}
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group) {
window.log.debug(`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: group not found`);
return RunJobResult.Success;
}
@ -208,25 +228,75 @@ async function pushChangesToGroupSwarmIfNeeded({
data: keysEncrypted[index],
}));
const pendingConfigRequests = pendingConfigMsgs.map(m => {
return new StoreGroupConfigOrMessageSubRequest({
encryptedData: m.data,
groupPk,
namespace: m.namespace,
ttlMs: m.ttl,
dbMessageIdentifier: null, // those are config messages only, they have no dbMessageIdentifier
let pendingConfigRequests: Array<StoreGroupConfigOrMessageSubRequest> = [];
let keysEncryptedRequests: Array<StoreGroupConfigOrMessageSubRequest> = [];
if (pendingConfigMsgs.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: pendingConfigMsgs not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: pendingConfigMsgs not empty but we do not have the secretKey'
);
}
pendingConfigRequests = pendingConfigMsgs.map(m => {
return new StoreGroupConfigOrMessageSubRequest({
encryptedData: m.data,
groupPk,
namespace: m.namespace,
ttlMs: m.ttl,
dbMessageIdentifier: null, // those are config messages only, they have no dbMessageIdentifier
secretKey: group.secretKey,
authData: null,
});
});
});
}
const keysEncryptedRequests = keysEncryptedmessage.map(m => {
return new StoreGroupConfigOrMessageSubRequest({
encryptedData: m.data,
if (keysEncryptedmessage.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: keysEncryptedmessage not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: keysEncryptedmessage not empty but we do not have the secretKey'
);
}
keysEncryptedRequests = keysEncryptedmessage.map(m => {
return new StoreGroupConfigOrMessageSubRequest({
encryptedData: m.data,
groupPk,
namespace: m.namespace,
ttlMs: m.ttl,
dbMessageIdentifier: null, // those are supplemental keys messages only, they have no dbMessageIdentifier
secretKey: group.secretKey,
authData: null,
});
});
}
let deleteHashesSubRequest: DeleteHashesFromGroupNodeSubRequest | null = null;
const allOldHashesArray = [...allOldHashes];
if (allOldHashesArray.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: allOldHashesArray not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: allOldHashesArray not empty but we do not have the secretKey'
);
}
deleteHashesSubRequest = new DeleteHashesFromGroupNodeSubRequest({
messagesHashes: [...allOldHashes],
groupPk,
namespace: m.namespace,
ttlMs: m.ttl,
dbMessageIdentifier: null, // those are supplemental keys messages only, they have no dbMessageIdentifier
secretKey: group.secretKey,
});
});
}
if (
revokeSubRequest?.revokeTokenHex.length === 0 ||
@ -240,7 +310,7 @@ async function pushChangesToGroupSwarmIfNeeded({
const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests: [...pendingConfigRequests, ...keysEncryptedRequests],
destination: groupPk,
messagesHashesToDelete: allOldHashes,
deleteHashesSubRequest,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest,

@ -8,7 +8,10 @@ import { ConfigDumpData } from '../../../../data/configDump/configDump';
import { UserSyncJobDone } from '../../../../shims/events';
import { isSignInByLinking } from '../../../../util/storage';
import { GenericWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { StoreUserConfigSubRequest } from '../../../apis/snode_api/SnodeRequestTypes';
import {
DeleteHashesFromUserNodeSubRequest,
StoreUserConfigSubRequest,
} from '../../../apis/snode_api/SnodeRequestTypes';
import { TTL_DEFAULT } from '../../../constants';
import { ConvoHub } from '../../../conversations';
import { MessageSender } from '../../../sending/MessageSender';
@ -109,7 +112,9 @@ async function pushChangesToUserSwarmIfNeeded() {
const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests,
destination: us,
messagesHashesToDelete: changesToPush.allOldHashes,
deleteHashesSubRequest: new DeleteHashesFromUserNodeSubRequest({
messagesHashes: [...changesToPush.allOldHashes],
}),
revokeSubRequest: null,
unrevokeSubRequest: null,
});

Loading…
Cancel
Save