fix: add sigtimestamp to all content message

pull/3281/head
Audric Ackermann 2 months ago
parent 1e74158be7
commit f26133a504
No known key found for this signature in database

@ -92,7 +92,7 @@
"fs-extra": "9.0.0",
"glob": "10.3.10",
"image-type": "^4.1.0",
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.16/libsession_util_nodejs-v0.4.16.tar.gz",
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.17/libsession_util_nodejs-v0.4.17.tar.gz",
"libsodium-wrappers-sumo": "^0.7.9",
"linkify-it": "^4.0.1",
"lodash": "^4.17.21",

@ -60,6 +60,7 @@ message Content {
optional MessageRequestResponse messageRequestResponse = 10;
optional ExpirationType expirationType = 12;
optional uint32 expirationTimer = 13;
optional uint64 sigTimestamp = 14;
}
message KeyPair {

@ -1,5 +1,7 @@
import { toNumber } from 'lodash';
import { EnvelopePlus } from './types';
import type { SignalService } from '../protobuf';
import { DURATION } from '../session/constants';
export function getEnvelopeId(envelope: EnvelopePlus) {
if (envelope.source) {
@ -8,3 +10,22 @@ export function getEnvelopeId(envelope: EnvelopePlus) {
return envelope.id;
}
export function shouldProcessContentMessage(
envelope: Pick<EnvelopePlus, 'timestamp'>,
content: Pick<SignalService.Content, 'sigTimestamp'>,
isCommunity: boolean
) {
// FIXME: drop this case once the change has been out in the wild long enough
if (!content.sigTimestamp || !toNumber(content.sigTimestamp)) {
// legacy client
return true;
}
const envelopeTimestamp = toNumber(envelope.timestamp);
const contentTimestamp = toNumber(content.sigTimestamp);
if (!isCommunity) {
return envelopeTimestamp === contentTimestamp;
}
// we want to process a community message and allow a window of 6 hours
return Math.abs(envelopeTimestamp - contentTimestamp) <= 6 * DURATION.HOURS;
}

@ -36,6 +36,7 @@ import { handleCallMessage } from './callMessage';
import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGroups';
import { ECKeyPair } from './keypairs';
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../models/types';
import { shouldProcessContentMessage } from './common';
export async function handleSwarmContentMessage(
envelope: EnvelopePlus,
@ -480,6 +481,11 @@ export async function innerHandleSwarmContentMessage({
window.log.info('innerHandleSwarmContentMessage');
const content = SignalService.Content.decode(new Uint8Array(contentDecrypted));
if (!shouldProcessContentMessage(envelope, content, false)) {
window.log.info('innerHandleSwarmContentMessage: dropping invalid content message');
await IncomingMessageCache.removeFromCache(envelope);
return;
}
/**
* senderIdentity is set ONLY if that message is a closed group message.

@ -14,6 +14,7 @@ import { fromBase64ToArray } from '../session/utils/String';
import { cleanIncomingDataMessage, messageHasVisibleContent } from './dataMessage';
import { handleMessageJob, toRegularMessage } from './queuedJob';
import { OpenGroupRequestCommonType } from '../data/types';
import { shouldProcessContentMessage } from './common';
export const handleOpenGroupV4Message = async (
message: OpenGroupMessageV4,
@ -52,6 +53,14 @@ const handleOpenGroupMessage = async (
const decodedContent = SignalService.Content.decode(dataUint);
if (!shouldProcessContentMessage({ timestamp: sentTimestamp }, decodedContent, true)) {
window?.log?.info(
'sogs message: shouldProcessContentMessage is false for message sentAt:',
sentTimestamp
);
return;
}
const conversationId = getOpenGroupV2ConversationId(serverUrl, roomId);
if (!conversationId) {
window?.log?.error('We cannot handle a message without a conversationId');

@ -42,6 +42,7 @@ import { sogsRollingDeletions } from './sogsRollingDeletions';
import { processMessagesUsingCache } from './sogsV3MutationCache';
import { OpenGroupRequestCommonType } from '../../../../data/types';
import { ConversationTypeEnum } from '../../../../models/types';
import { shouldProcessContentMessage } from '../../../../receiver/common';
/**
* Get the convo matching those criteria and make sure it is an opengroup convo, or return null.
@ -405,7 +406,13 @@ async function handleInboxOutboxMessages(
id: v4(),
type: SignalService.Envelope.Type.SESSION_MESSAGE, // this is not right, but we forward an already decrypted envelope so we don't care
};
const contentDecrypted = SignalService.Content.decode(content);
if (!shouldProcessContentMessage(builtEnvelope, contentDecrypted, true)) {
window.log.warn(
`received inbox/outbox message that did not pass the shouldProcessContentMessage test envelopeTs: ${builtEnvelope.timestamp}`
);
continue;
}
if (isOutbox) {
/**
* Handling outbox messages needs to skip some of the pipeline.
@ -414,7 +421,6 @@ async function handleInboxOutboxMessages(
* We will need this to send new message to that user from our second device.
*/
const recipient = inboxOutboxItem.recipient;
const contentDecrypted = SignalService.Content.decode(content);
// if we already know this user's unblinded pubkey, store the blinded message we sent to that blinded recipient under
// the unblinded conversation instead (as we would have merge the blinded one with the other )

@ -2,14 +2,31 @@ import { SignalService } from '../../../protobuf';
import { TTL_DEFAULT } from '../../constants';
import { Message } from './Message';
type InstanceKeys<T> = {
// eslint-disable-next-line @typescript-eslint/ban-types
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
type ContentFields = Partial<Omit<InstanceKeys<SignalService.Content>, 'sigTimestamp'>>;
export abstract class ContentMessage extends Message {
public plainTextBuffer(): Uint8Array {
return SignalService.Content.encode(this.contentProto()).finish();
const contentProto = this.contentProto();
if (!contentProto.sigTimestamp) {
throw new Error('trying to build a ContentMessage without a sig timestamp is unsupported');
}
return SignalService.Content.encode(contentProto).finish();
}
public ttl(): number {
return TTL_DEFAULT.CONTENT_MESSAGE;
}
public makeContentProto<T extends ContentFields>(extra: T) {
return new SignalService.Content({
sigTimestamp: this.createAtNetworkTimestamp,
...extra,
});
}
public abstract contentProto(): SignalService.Content;
}

@ -24,7 +24,7 @@ export class ExpirableMessage extends ContentMessage {
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
return super.makeContentProto({
// TODO legacy messages support will be removed in a future release
expirationType:
this.expirationType === 'deleteAfterSend'

@ -30,13 +30,6 @@ export class ExpirationTimerUpdateMessage extends DataMessage {
this.syncTarget = params.syncTarget ? PubKey.cast(params.syncTarget).key : undefined;
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
...super.contentProto(),
dataMessage: this.dataProto(),
});
}
public dataProto(): SignalService.DataMessage {
const data = new SignalService.DataMessage({});

@ -26,9 +26,7 @@ export class MessageRequestResponse extends ContentMessage {
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
messageRequestResponse: this.messageRequestResponseProto(),
});
return super.makeContentProto({ messageRequestResponse: this.messageRequestResponseProto() });
}
public messageRequestResponseProto(): SignalService.MessageRequestResponse {

@ -23,9 +23,7 @@ export class TypingMessage extends ContentMessage {
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
typingMessage: this.typingProto(),
});
return super.makeContentProto({ typingMessage: this.typingProto() });
}
protected typingProto(): SignalService.TypingMessage {

@ -18,9 +18,7 @@ export class UnsendMessage extends ContentMessage {
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
unsendMessage: this.unsendProto(),
});
return super.makeContentProto({ unsendMessage: this.unsendProto() });
}
public unsendProto(): SignalService.Unsend {

@ -14,9 +14,7 @@ export class ReadReceiptMessage extends ContentMessage {
}
public contentProto(): SignalService.Content {
return new SignalService.Content({
receiptMessage: this.receiptProto(),
});
return super.makeContentProto({ receiptMessage: this.receiptProto() });
}
protected receiptProto(): SignalService.ReceiptMessage {

@ -127,7 +127,7 @@ async function handleSwarmMessageSentSuccess(
}
} catch (e) {
window.log.info(
'failed to decode content (excpected except if message was for a 1o1 as we need it to send the sync message'
'failed to decode content (expected except if message was for a 1o1 as we need it to send the sync message'
);
}
} else if (shouldMarkMessageAsSynced) {
@ -185,7 +185,7 @@ async function handleSwarmMessageSentFailure(
expirationStartTimestamp: undefined,
});
window.log.warn(
`[handleSwarmMessageSentFailure] Stopping a message from disppearing until we retry the send operation. messageId: ${fetchedMessage.get(
`[handleSwarmMessageSentFailure] Stopping a message from disappearing until we retry the send operation. messageId: ${fetchedMessage.get(
'id'
)}`
);

@ -0,0 +1,130 @@
import { describe } from 'mocha';
import Sinon from 'sinon';
import Long from 'long';
import { expect } from 'chai';
import { TestUtils } from '../../../../test-utils';
import { shouldProcessContentMessage } from '../../../../../receiver/common';
import { DURATION } from '../../../../../session/constants';
describe('shouldProcessContentMessage', () => {
let envelopeTs: number;
beforeEach(() => {
TestUtils.stubWindowLog();
envelopeTs = Math.floor(Date.now() + Math.random() * 1000000);
});
afterEach(() => {
Sinon.restore();
});
describe('not a community', () => {
const isCommunity = false;
describe('with sig timestamp', () => {
it('if timestamps match: return true', async () => {
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: envelopeTs },
isCommunity
)
).to.eq(true);
});
it('if timestamps do not match: return false', async () => {
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: envelopeTs + 2 },
isCommunity
)
).to.eq(false);
});
});
describe('without sig timestamp', () => {
it('if timestamps match or not: return true', async () => {
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: undefined as any },
isCommunity
)
).to.eq(true);
expect(
shouldProcessContentMessage({ timestamp: envelopeTs }, { sigTimestamp: 0 }, isCommunity)
).to.eq(true);
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: Long.fromNumber(0) as any },
isCommunity
)
).to.eq(true);
});
});
});
describe('a community', () => {
const isCommunity = true;
describe('with sig timestamp', () => {
it('if timestamps roughly match: return true', async () => {
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: envelopeTs }, // exact match
isCommunity
)
).to.eq(true);
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: envelopeTs + 6 * DURATION.HOURS - 1 }, // just below 6h of diff (positive)
isCommunity
)
).to.eq(true);
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: envelopeTs - 6 * DURATION.HOURS + 1 }, // just below 6h of diff (negative)
isCommunity
)
).to.eq(true);
});
it('if timestamps do not roughly match: return false', async () => {
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: envelopeTs + 6 * DURATION.HOURS + 1 }, // just above 6h of diff
isCommunity
)
).to.eq(false);
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: envelopeTs - 6 * DURATION.HOURS - 1 }, // just above 6h of diff
isCommunity
)
).to.eq(false);
});
});
describe('without sig timestamp', () => {
it('if timestamps match or not: return true', async () => {
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: undefined as any },
isCommunity
)
).to.eq(true);
expect(
shouldProcessContentMessage({ timestamp: envelopeTs }, { sigTimestamp: 0 }, isCommunity)
).to.eq(true);
expect(
shouldProcessContentMessage(
{ timestamp: envelopeTs },
{ sigTimestamp: Long.fromNumber(0) as any },
isCommunity
)
).to.eq(true);
});
});
});
});

@ -4944,9 +4944,9 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
"libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.16/libsession_util_nodejs-v0.4.16.tar.gz":
version "0.4.16"
resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.16/libsession_util_nodejs-v0.4.16.tar.gz#253d4d02388b5bfb41f24c88fae5061b137ca615"
"libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.17/libsession_util_nodejs-v0.4.17.tar.gz":
version "0.4.17"
resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.17/libsession_util_nodejs-v0.4.17.tar.gz#d31d7d2e1d1534c872dc64e0040d6ce533f11ffb"
dependencies:
cmake-js "7.2.1"
node-addon-api "^6.1.0"

Loading…
Cancel
Save