diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 053403f38..5093fa2ab 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -162,6 +162,10 @@ message GroupUpdateMessage { message DataMessage { + // 7 = timestamp unused and should not be used + reserved 7; + reserved "timestamp"; + enum Flags { EXPIRATION_TIMER_UPDATE = 2; } @@ -253,7 +257,6 @@ message DataMessage { optional uint32 flags = 4; optional uint32 expireTimer = 5; optional bytes profileKey = 6; - optional uint64 timestamp = 7; optional Quote quote = 8; repeated Preview preview = 10; optional Reaction reaction = 11; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index ea64998e1..727c045f4 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -553,7 +553,7 @@ export class ConversationModel extends Backbone.Model { const chatMessageParams: VisibleMessageParams = { body: '', // we need to use a new timestamp here, otherwise android&iOS will consider this message as a duplicate and drop the synced reaction - timestamp: GetNetworkTime.getNowWithNetworkOffset(), + timestamp: GetNetworkTime.now(), reaction, lokiProfile: UserUtils.getOurProfile(), }; @@ -744,7 +744,7 @@ export class ConversationModel extends Backbone.Model { const { attachments, body, groupInvitation, preview, quote } = msg; this.clearTypingTimers(); const expireTimer = this.get('expireTimer'); - const networkTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + const networkTimestamp = GetNetworkTime.now(); window?.log?.info( 'Sending message to conversation', @@ -2219,9 +2219,9 @@ export class ConversationModel extends Backbone.Model { } const typingParams = { - timestamp: GetNetworkTime.getNowWithNetworkOffset(), + timestamp: GetNetworkTime.now(), isTyping, - typingTimestamp: GetNetworkTime.getNowWithNetworkOffset(), + typingTimestamp: GetNetworkTime.now(), }; const typingMessage = new TypingMessage(typingParams); diff --git a/ts/models/message.ts b/ts/models/message.ts index 2c4b13460..0d1ffd462 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -791,7 +791,7 @@ export class MessageModel extends Backbone.Model { if (conversation.isPublic()) { const openGroupParams: VisibleMessageParams = { identifier: this.id, - timestamp: GetNetworkTime.getNowWithNetworkOffset(), + timestamp: GetNetworkTime.now(), lokiProfile: UserUtils.getOurProfile(), body, attachments, diff --git a/ts/node/hexStrings.ts b/ts/node/hexStrings.ts index 574e19178..782da7a1d 100644 --- a/ts/node/hexStrings.ts +++ b/ts/node/hexStrings.ts @@ -14,7 +14,7 @@ const isHexString = (maybeHex: string) => * * Throws an error if this string is not a hex string. * @param hexString the string to convert from - * @returns the Uint8Arraty + * @returns the Uint8Arrat */ const fromHexString = (hexString: string): Uint8Array => { if (!isHexString(hexString)) { @@ -27,11 +27,24 @@ const fromHexString = (hexString: string): Uint8Array => { return Uint8Array.from(matches.map(byte => parseInt(byte, 16))); }; +/** + * Returns the Uint8Array corresponding to the given string, without a 03/05 prefix when there is a prefix + * Note: this is different than the libsodium.from_hex(). + */ +const fromHexStringNoPrefix = (hexString: string): Uint8Array => { + const asHex = fromHexString(hexString); + if (asHex.length === 33) { + return asHex.slice(1); + } + return asHex; +}; + const toHexString = (bytes: Uint8Array) => bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); export const HexString = { toHexString, fromHexString, + fromHexStringNoPrefix, isHexString, }; diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index 2378bdf1e..0f5d14ddd 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -49,9 +49,7 @@ export async function handleCallMessage( } if (type === SignalService.CallMessage.Type.OFFER) { - if ( - Math.max(sentTimestamp - GetNetworkTime.getNowWithNetworkOffset()) > TTL_DEFAULT.CALL_MESSAGE - ) { + if (Math.max(sentTimestamp - GetNetworkTime.now()) > TTL_DEFAULT.CALL_MESSAGE) { window?.log?.info('Dropping incoming OFFER callMessage sent a while ago: ', sentTimestamp); await IncomingMessageCache.removeFromCache(envelope); diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index f80a69168..7f156cd91 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { isEmpty, isFinite, noop, omit, toNumber } from 'lodash'; +import { isEmpty, noop, omit, toNumber } from 'lodash'; import { SignalService } from '../protobuf'; import { IncomingMessageCache } from './cache'; @@ -100,10 +100,7 @@ export function messageHasVisibleContent(message: SignalService.DataMessage) { ); } -export function cleanIncomingDataMessage( - rawDataMessage: SignalService.DataMessage, - envelope?: EnvelopePlus -) { +export function cleanIncomingDataMessage(rawDataMessage: SignalService.DataMessage) { const FLAGS = SignalService.DataMessage.Flags; // Now that its decrypted, validate the message and clean it up for consumer @@ -134,11 +131,6 @@ export function cleanIncomingDataMessage( } cleanAttachments(rawDataMessage); - // if the decrypted dataMessage timestamp is not set, copy the one from the envelope - if (!isFinite(rawDataMessage?.timestamp) && envelope) { - rawDataMessage.timestamp = envelope.timestamp; - } - return rawDataMessage; } @@ -162,7 +154,7 @@ export async function handleSwarmDataMessage( ): Promise { window.log.info('handleSwarmDataMessage'); - const cleanDataMessage = cleanIncomingDataMessage(rawDataMessage, envelope); + const cleanDataMessage = cleanIncomingDataMessage(rawDataMessage); if (cleanDataMessage.groupUpdateMessage) { await GroupV2Receiver.handleGroupUpdateMessage({ diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts index 71c2f7b33..9a61f585f 100644 --- a/ts/receiver/groupv2/handleGroupV2Message.ts +++ b/ts/receiver/groupv2/handleGroupV2Message.ts @@ -4,10 +4,13 @@ import { ConversationTypeEnum } from '../../models/conversationAttributes'; import { HexString } from '../../node/hexStrings'; import { SignalService } from '../../protobuf'; import { getSwarmPollingInstance } from '../../session/apis/snode_api'; +import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime'; import { ConvoHub } from '../../session/conversations'; import { getSodiumRenderer } from '../../session/crypto'; import { ClosedGroup } from '../../session/group/closed-group'; +import { GroupUpdateInviteResponseMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInviteResponseMessage'; import { ed25519Str } from '../../session/onions/onionPath'; +import { getMessageQueue } from '../../session/sending'; import { PubKey } from '../../session/types'; import { UserUtils } from '../../session/utils'; import { stringToUint8Array } from '../../session/utils/String'; @@ -57,7 +60,7 @@ async function handleGroupInviteMessage({ return; } const sigValid = await verifySig({ - pubKey: HexString.fromHexString(inviteMessage.groupSessionId), + pubKey: HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId), signature: inviteMessage.adminSignature, data: stringToUint8Array(`INVITE${UserUtils.getOurPubKeyStrFromCache()}${envelopeTimestamp}`), }); @@ -108,13 +111,23 @@ async function handleGroupInviteMessage({ groupEd25519Secretkey: null, userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64).buffer, groupEd25519Pubkey: toFixedUint8ArrayOfLength( - HexString.fromHexString(inviteMessage.groupSessionId.slice(2)), + HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId), 32 ).buffer, }); await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache()); await UserSync.queueNewJobIfNeeded(); + // TODO currently sending auto-accept of invite. needs to be removed once we get the Group message request logic + console.warn('currently sending auto accept invite response'); + await getMessageQueue().sendToGroupV2({ + message: new GroupUpdateInviteResponseMessage({ + groupPk: inviteMessage.groupSessionId, + isApproved: true, + timestamp: GetNetworkTime.now(), + }), + }); + // TODO use the pending so we actually don't start polling here unless it is not in the pending state. // once everything is ready, start polling using that authData to get the keys, members, details of that group, and its messages. getSwarmPollingInstance().addGroupId(inviteMessage.groupSessionId); @@ -140,7 +153,7 @@ async function handleGroupInfoChangeMessage({ author, }: GroupUpdateGeneric) { const sigValid = await verifySig({ - pubKey: HexString.fromHexString(groupPk), + pubKey: HexString.fromHexStringNoPrefix(groupPk), signature: change.adminSignature, data: stringToUint8Array(`INFO_CHANGE${change.type}${envelopeTimestamp}`), }); @@ -200,7 +213,7 @@ async function handleGroupMemberChangeMessage({ } const sigValid = await verifySig({ - pubKey: HexString.fromHexString(groupPk), + pubKey: HexString.fromHexStringNoPrefix(groupPk), signature: change.adminSignature, data: stringToUint8Array(`MEMBER_CHANGE${change.type}${envelopeTimestamp}`), }); @@ -281,7 +294,7 @@ async function handleGroupDeleteMemberContentMessage({ } const sigValid = await verifySig({ - pubKey: HexString.fromHexString(groupPk), + pubKey: HexString.fromHexStringNoPrefix(groupPk), signature: change.adminSignature, data: stringToUint8Array( `DELETE_CONTENT${envelopeTimestamp}${change.memberSessionIds.join()}${change.messageHashes.join()}` @@ -312,7 +325,7 @@ async function handleGroupUpdateDeleteMessage({ return; } const sigValid = await verifySig({ - pubKey: HexString.fromHexString(groupPk), + pubKey: HexString.fromHexStringNoPrefix(groupPk), signature: change.adminSignature, data: stringToUint8Array(`DELETE${envelopeTimestamp}${change.memberSessionIds.join()}`), }); @@ -331,20 +344,22 @@ async function handleGroupUpdateDeleteMessage({ async function handleGroupUpdateInviteResponseMessage({ groupPk, - envelopeTimestamp, + change, + author, }: GroupUpdateGeneric) { - // no sig verify for this type of messages + // no sig verify for this type of message const convo = ConvoHub.use().get(groupPk); if (!convo) { return; } - convo.set({ - active_at: envelopeTimestamp, - }); - console.warn('Not implemented'); + if (!change.isApproved) { + window.log.info('got inviteResponse but isApproved is false. Dropping'); + return; + } + + window.inboxStore.dispatch(groupInfoActions.inviteResponseReceived({ groupPk, member: author })); // TODO We should process this message type even if the sender is blocked - throw new Error('Not implemented'); } async function handleGroupUpdatePromoteMessage({ diff --git a/ts/session/apis/open_group_api/opengroupV2/OpenGroupPollingUtils.ts b/ts/session/apis/open_group_api/opengroupV2/OpenGroupPollingUtils.ts index 31559840e..468c84ec1 100644 --- a/ts/session/apis/open_group_api/opengroupV2/OpenGroupPollingUtils.ts +++ b/ts/session/apis/open_group_api/opengroupV2/OpenGroupPollingUtils.ts @@ -1,12 +1,12 @@ import { compact } from 'lodash'; import { OpenGroupData, OpenGroupV2Room } from '../../../../data/opengroups'; -import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; +import { getSodiumRenderer } from '../../../crypto'; import { UserUtils } from '../../../utils'; import { fromHexToArray } from '../../../utils/String'; -import { getSodiumRenderer } from '../../../crypto'; -import { SogsBlinding } from '../sogsv3/sogsBlinding'; import { GetNetworkTime } from '../../snode_api/getNetworkTime'; +import { SogsBlinding } from '../sogsv3/sogsBlinding'; +import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; export type OpenGroupRequestHeaders = { 'X-SOGS-Pubkey': string; @@ -42,7 +42,7 @@ const getOurOpenGroupHeaders = async ( const nonce = (await getSodiumRenderer()).randombytes_buf(16); - const timestamp = Math.floor(GetNetworkTime.getNowWithNetworkOffset() / 1000); + const timestamp = Math.floor(GetNetworkTime.now() / 1000); return SogsBlinding.getOpenGroupHeaders({ signingKeys, serverPK: fromHexToArray(serverPublicKey), diff --git a/ts/session/apis/snode_api/expire.ts b/ts/session/apis/snode_api/expire.ts index 7d1a8e8be..878be242c 100644 --- a/ts/session/apis/snode_api/expire.ts +++ b/ts/session/apis/snode_api/expire.ts @@ -196,7 +196,7 @@ export async function expireMessageOnSnode(props: ExpireMessageOnSnodeProps) { const swarm = await getSwarmFor(ourPubKey); - const expiry = GetNetworkTime.getNowWithNetworkOffset() + expireTimer; + const expiry = GetNetworkTime.now() + expireTimer; const signResult = await SnodeSignature.generateUpdateExpiryOurSignature({ shortenOrExtend, timestamp: expiry, diff --git a/ts/session/apis/snode_api/getNetworkTime.ts b/ts/session/apis/snode_api/getNetworkTime.ts index 086d849b6..819449a8e 100644 --- a/ts/session/apis/snode_api/getNetworkTime.ts +++ b/ts/session/apis/snode_api/getNetworkTime.ts @@ -6,8 +6,8 @@ import { isNumber } from 'lodash'; import { Snode } from '../../../data/data'; -import { doSnodeBatchRequest } from './batchRequest'; import { NetworkTimeSubRequest } from './SnodeRequestTypes'; +import { doSnodeBatchRequest } from './batchRequest'; function getNetworkTimeSubRequests(): Array { const request: NetworkTimeSubRequest = { method: 'info', params: {} }; @@ -43,11 +43,11 @@ let latestTimestampOffset = Number.MAX_SAFE_INTEGER; function handleTimestampOffsetFromNetwork(_request: string, snodeTimestamp: number) { if (snodeTimestamp && isNumber(snodeTimestamp) && snodeTimestamp > 1609419600 * 1000) { // first january 2021. Arbitrary, just want to make sure the return timestamp is somehow valid and not some crazy low value - const now = Date.now(); + const clockTime = Date.now(); if (latestTimestampOffset === Number.MAX_SAFE_INTEGER) { - window?.log?.info(`first timestamp offset received: ${now - snodeTimestamp}ms`); + window?.log?.info(`first timestamp offset received: ${clockTime - snodeTimestamp}ms`); } - latestTimestampOffset = now - snodeTimestamp; + latestTimestampOffset = clockTime - snodeTimestamp; } } @@ -65,7 +65,7 @@ function getLatestTimestampOffset() { return latestTimestampOffset; } -function getNowWithNetworkOffset() { +function now() { // make sure to call exports here, as we stub the exported one for testing. return Date.now() - GetNetworkTime.getLatestTimestampOffset(); } @@ -74,5 +74,5 @@ export const GetNetworkTime = { getNetworkTime, handleTimestampOffsetFromNetwork, getLatestTimestampOffset, - getNowWithNetworkOffset, + now, }; diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 18ad5325e..8fa70c5d1 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -140,7 +140,7 @@ async function buildRetrieveRequest( const retrieveParam = { pubkey, last_hash: lastHashes.at(index) || '', - timestamp: GetNetworkTime.getNowWithNetworkOffset(), + timestamp: GetNetworkTime.now(), max_size: foundMaxSize, }; @@ -163,7 +163,7 @@ async function buildRetrieveRequest( ); if (configHashesToBump?.length) { - const expiry = GetNetworkTime.getNowWithNetworkOffset() + DURATION.DAYS * 30; + const expiry = GetNetworkTime.now() + DURATION.DAYS * 30; if (isUs) { const signResult = await SnodeSignature.generateUpdateExpiryOurSignature({ shortenOrExtend: '', diff --git a/ts/session/apis/snode_api/revokeSubaccount.ts b/ts/session/apis/snode_api/revokeSubaccount.ts index 2062178b2..f8af2e6fc 100644 --- a/ts/session/apis/snode_api/revokeSubaccount.ts +++ b/ts/session/apis/snode_api/revokeSubaccount.ts @@ -29,7 +29,7 @@ async function getRevokeSubaccountRequest({ throw new Error('revokeSubaccountForGroup: not a 03 group'); } - const timestamp = GetNetworkTime.getNowWithNetworkOffset(); + const timestamp = GetNetworkTime.now(); const revokeParams: Array = await Promise.all( diff --git a/ts/session/apis/snode_api/signature/groupSignature.ts b/ts/session/apis/snode_api/signature/groupSignature.ts index a74431f9b..69e2bab48 100644 --- a/ts/session/apis/snode_api/signature/groupSignature.ts +++ b/ts/session/apis/snode_api/signature/groupSignature.ts @@ -13,7 +13,7 @@ import { import { getSodiumRenderer } from '../../../crypto/MessageEncrypter'; import { GroupUpdateInviteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage'; import { StringUtils, UserUtils } from '../../../utils'; -import { fromUInt8ArrayToBase64 } from '../../../utils/String'; +import { fromUInt8ArrayToBase64, stringToUint8Array } from '../../../utils/String'; import { PreConditionFailed } from '../../../utils/errors'; import { GetNetworkTime } from '../getNetworkTime'; import { SnodeNamespacesGroup } from '../namespaces'; @@ -33,15 +33,18 @@ async function getGroupInviteMessage({ groupPk: GroupPubkeyType; }) { const sodium = await getSodiumRenderer(); - const timestamp = GetNetworkTime.getNowWithNetworkOffset(); + const timestamp = GetNetworkTime.now(); if (UserUtils.isUsFromCache(member)) { throw new Error('getGroupInviteMessage: we cannot invite ourselves'); } - const tosign = `INVITE${member}${timestamp}`; + debugger; // Note: as the signature is built with the timestamp here, we cannot override the timestamp later on the sending pipeline - const adminSignature = sodium.crypto_sign_detached(tosign, secretKey); + const adminSignature = sodium.crypto_sign_detached( + stringToUint8Array(`INVITE${member}${timestamp}`), + secretKey + ); const memberAuthData = await MetaGroupWrapperActions.makeSwarmSubAccount(groupPk, member); const invite = new GroupUpdateInviteMessage({ diff --git a/ts/session/apis/snode_api/signature/signatureShared.ts b/ts/session/apis/snode_api/signature/signatureShared.ts index 744461656..85f17f5d3 100644 --- a/ts/session/apis/snode_api/signature/signatureShared.ts +++ b/ts/session/apis/snode_api/signature/signatureShared.ts @@ -27,7 +27,7 @@ export type SnodeSigParamsUs = SnodeSigParamsShared & { }; function getVerificationDataForStoreRetrieve(params: SnodeSigParamsShared) { - const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + const signatureTimestamp = GetNetworkTime.now(); const verificationData = StringUtils.encode( `${params.method}${params.namespace === 0 ? '' : params.namespace}${signatureTimestamp}`, 'utf8' diff --git a/ts/session/apis/snode_api/signature/snodeSignatures.ts b/ts/session/apis/snode_api/signature/snodeSignatures.ts index 0341b9b80..17afe5395 100644 --- a/ts/session/apis/snode_api/signature/snodeSignatures.ts +++ b/ts/session/apis/snode_api/signature/snodeSignatures.ts @@ -84,7 +84,7 @@ function isSigParamsForGroupAdmin( } function getVerificationData(params: SnodeSigParamsShared) { - const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + const signatureTimestamp = GetNetworkTime.now(); const verificationData = StringUtils.encode( `${params.method}${params.namespace === 0 ? '' : params.namespace}${signatureTimestamp}`, 'utf8' diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index d5e2f143a..49f7e9c5d 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -473,7 +473,7 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { await convo.updateGroupAdmins(admins, false); await convo.commit(); - const networkTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + const networkTimestamp = GetNetworkTime.now(); getSwarmPollingInstance().removePubkey(groupId, 'leaveClosedGroup'); diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index 7a2fc7dfb..0d3db8878 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -414,7 +414,7 @@ async function generateAndSendNewEncryptionKeyPair( const keypairsMessage = new ClosedGroupEncryptionPairMessage({ groupId: toHex(groupId), - timestamp: GetNetworkTime.getNowWithNetworkOffset(), + timestamp: GetNetworkTime.now(), encryptedKeyPairs: wrappers, }); diff --git a/ts/session/messages/outgoing/Message.ts b/ts/session/messages/outgoing/Message.ts index 98716b8b8..6930ea6b0 100644 --- a/ts/session/messages/outgoing/Message.ts +++ b/ts/session/messages/outgoing/Message.ts @@ -14,8 +14,9 @@ export abstract class Message { if (identifier && identifier.length === 0) { throw new Error('Cannot set empty identifier'); } - if (!timestamp) { - throw new Error('Cannot set undefined timestamp'); + + if (!timestamp || timestamp <= 0) { + throw new Error('Cannot set undefined timestamp or <=0'); } this.identifier = identifier || uuid(); } diff --git a/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts b/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts index db68a7cdb..552532455 100644 --- a/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts +++ b/ts/session/messages/outgoing/visibleMessage/VisibleMessage.ts @@ -175,8 +175,6 @@ export class VisibleMessage extends DataMessage { }); } - dataMessage.timestamp = this.timestamp; - return dataMessage; } diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 4783f944b..82de56936 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -3,7 +3,7 @@ import { AbortController } from 'abort-controller'; import ByteBuffer from 'bytebuffer'; import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; -import _, { isEmpty, sample, toNumber } from 'lodash'; +import { isEmpty, sample, toNumber } from 'lodash'; import pRetry from 'p-retry'; import { Data } from '../../data/data'; import { SignalService } from '../../protobuf'; @@ -48,7 +48,7 @@ import { EmptySwarmError } from '../utils/errors'; // ================ SNODE STORE ================ function overwriteOutgoingTimestampWithNetworkTimestamp(message: { plainTextBuffer: Uint8Array }) { - const networkTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + const networkTimestamp = GetNetworkTime.now(); const { plainTextBuffer } = message; const contentDecoded = SignalService.Content.decode(plainTextBuffer); @@ -68,7 +68,7 @@ function overwriteOutgoingTimestampWithNetworkTimestamp(message: { plainTextBuff ) { return { overRiddenTimestampBuffer: plainTextBuffer, - networkTimestamp: _.toNumber(dataMessage.timestamp), + networkTimestamp: toNumber(dataMessage.timestamp), }; } dataMessage.timestamp = networkTimestamp; @@ -533,7 +533,7 @@ async function sendToOpenGroupV2( // we agreed to pad message for opengroupv2 const paddedBody = addMessagePadding(rawMessage.plainTextBuffer()); const v2Message = new OpenGroupMessageV2({ - sentTimestamp: GetNetworkTime.getNowWithNetworkOffset(), + sentTimestamp: GetNetworkTime.now(), base64EncodedData: fromUInt8ArrayToBase64(paddedBody), filesToLink, }); @@ -558,7 +558,7 @@ async function sendToOpenGroupV2BlindedRequest( recipientBlindedId: string ): Promise<{ serverId: number; serverTimestamp: number }> { const v2Message = new OpenGroupMessageV2({ - sentTimestamp: GetNetworkTime.getNowWithNetworkOffset(), + sentTimestamp: GetNetworkTime.now(), base64EncodedData: fromUInt8ArrayToBase64(encryptedContent), }); diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index 0b39ec2df..5f9b350a2 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -5,34 +5,34 @@ import { v4 as uuidv4 } from 'uuid'; import { MessageUtils, ToastUtils, UserUtils } from '..'; import { SignalService } from '../../../protobuf'; -import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { + CallStatusEnum, answerCall, callConnected, callReconnecting, - CallStatusEnum, endCall, incomingCall, setFullScreenCall, startingCallWith, } from '../../../state/ducks/call'; +import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { ConvoHub } from '../../conversations'; import { CallMessage } from '../../messages/outgoing/controlMessage/CallMessage'; import { ed25519Str } from '../../onions/onionPath'; import { PubKey } from '../../types'; -import { getIsRinging } from '../RingingManager'; -import { getBlackSilenceMediaStream } from './Silence'; import { getMessageQueue } from '../..'; -import { MessageSender } from '../../sending'; -import { DURATION } from '../../constants'; -import { Data } from '../../../data/data'; import { getCallMediaPermissionsSettings } from '../../../components/settings/SessionSettings'; -import { PnServer } from '../../apis/push_notification_api'; +import { Data } from '../../../data/data'; import { approveConvoAndSendResponse } from '../../../interactions/conversationInteractions'; +import { READ_MESSAGE_STATE } from '../../../models/conversationAttributes'; +import { PnServer } from '../../apis/push_notification_api'; import { GetNetworkTime } from '../../apis/snode_api/getNetworkTime'; import { SnodeNamespaces } from '../../apis/snode_api/namespaces'; -import { READ_MESSAGE_STATE } from '../../../models/conversationAttributes'; +import { DURATION } from '../../constants'; +import { MessageSender } from '../../sending'; +import { getIsRinging } from '../RingingManager'; +import { getBlackSilenceMediaStream } from './Silence'; export type InputItem = { deviceId: string; label: string }; @@ -856,7 +856,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { await peerConnection.addIceCandidate(candicate); } } - const networkTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + const networkTimestamp = GetNetworkTime.now(); const callerConvo = ConvoHub.use().get(fromSender); callerConvo.set('active_at', networkTimestamp); await callerConvo.unhideIfNeeded(false); @@ -1192,14 +1192,14 @@ async function addMissedCallMessage(callerPubkey: string, sentAt: number) { const incomingCallConversation = ConvoHub.use().get(callerPubkey); if (incomingCallConversation.isActive() || incomingCallConversation.isHidden()) { - incomingCallConversation.set('active_at', GetNetworkTime.getNowWithNetworkOffset()); + incomingCallConversation.set('active_at', GetNetworkTime.now()); await incomingCallConversation.unhideIfNeeded(false); } await incomingCallConversation?.addSingleIncomingMessage({ source: callerPubkey, sent_at: sentAt, - received_at: GetNetworkTime.getNowWithNetworkOffset(), + received_at: GetNetworkTime.now(), expireTimer: 0, callNotificationType: 'missed-call', unread: READ_MESSAGE_STATE.unread, diff --git a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts index 8d91f2ce8..4cf02a72b 100644 --- a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts @@ -83,7 +83,7 @@ async function pushChangesToGroupSwarmIfNeeded( return { namespace: item.namespace, pubkey: groupPk, - networkTimestamp: GetNetworkTime.getNowWithNetworkOffset(), + networkTimestamp: GetNetworkTime.now(), ttl: TTL_DEFAULT.TTL_CONFIG, data: item.ciphertext, }; @@ -94,7 +94,7 @@ async function pushChangesToGroupSwarmIfNeeded( namespace: SnodeNamespaces.ClosedGroupKeys, pubkey: groupPk, ttl: TTL_DEFAULT.TTL_CONFIG, - networkTimestamp: GetNetworkTime.getNowWithNetworkOffset(), + networkTimestamp: GetNetworkTime.now(), data: key, }) ); diff --git a/ts/session/utils/job_runners/jobs/UserSyncJob.ts b/ts/session/utils/job_runners/jobs/UserSyncJob.ts index afc3cf203..1d8aeefa2 100644 --- a/ts/session/utils/job_runners/jobs/UserSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/UserSyncJob.ts @@ -90,7 +90,7 @@ async function pushChangesToUserSwarmIfNeeded() { return { namespace: item.namespace, pubkey: us, - networkTimestamp: GetNetworkTime.getNowWithNetworkOffset(), + networkTimestamp: GetNetworkTime.now(), ttl: TTL_DEFAULT.TTL_CONFIG, data: item.ciphertext, }; diff --git a/ts/state/ducks/groups.ts b/ts/state/ducks/groups.ts index c66adedeb..0cad575c4 100644 --- a/ts/state/ducks/groups.ts +++ b/ts/state/ducks/groups.ts @@ -74,6 +74,15 @@ type GroupDetailsUpdate = { members: Array; }; +async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) { + const us = UserUtils.getOurPubKeyStrFromCache(); + const inGroup = await MetaGroupWrapperActions.memberGet(groupPk, us); + const haveAdminkey = await UserGroupsWrapperActions.getGroup(groupPk); + if (!haveAdminkey || inGroup?.promoted) { + throw new Error(`checkWeAreAdminOrThrow failed with ctx: ${context}`); + } +} + /** * Create a brand new group with a 03 prefix. * To be called only when our current logged in user, through the UI, creates a brand new closed group given a name and a list of members. @@ -464,7 +473,7 @@ async function handleRemoveMembers({ } await MetaGroupWrapperActions.memberEraseAndRekey(groupPk, removed); - const timestamp = GetNetworkTime.getNowWithNetworkOffset(); + const timestamp = GetNetworkTime.now(); await Promise.all( removed.map(async m => { const adminSignature = await SnodeGroupSignature.signDataWithAdminSecret( @@ -540,6 +549,8 @@ async function handleMemberChangeFromUIOrNot({ throw new Error('tried to make change to group but we do not have the admin secret key'); } + await checkWeAreAdminOrThrow(groupPk, 'handleMemberChangeFromUIOrNot'); + const { removed, withHistory, withoutHistory, convo, us } = validateMemberChange({ withHistory: addMembersWithHistory, withoutHistory: addMembersWithoutHistory, @@ -622,7 +633,7 @@ async function handleMemberChangeFromUIOrNot({ groupPk, typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED, identifier: msg.id, - timestamp: GetNetworkTime.getNowWithNetworkOffset(), + timestamp: GetNetworkTime.now(), secretKey: group.secretKey, sodium, }), @@ -652,6 +663,8 @@ async function handleNameChangeFromUIOrNot({ throw new PreConditionFailed('nameChange infoGet is empty'); } + await checkWeAreAdminOrThrow(groupPk, 'handleNameChangeFromUIOrNot'); + // this throws if the name is the same, or empty const { newName, convo, us } = validateNameChange({ newName: uncheckedName, @@ -767,6 +780,35 @@ const markUsAsAdmin = createAsyncThunk( } ); +const inviteResponseReceived = createAsyncThunk( + 'group/inviteResponseReceived', + async ( + { + groupPk, + member, + }: { + groupPk: GroupPubkeyType; + member: PubkeyType; + }, + payloadCreator + ): Promise => { + const state = payloadCreator.getState() as StateType; + if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) { + throw new PreConditionFailed('inviteResponseReceived group but not present in redux slice'); + } + await checkWeAreAdminOrThrow(groupPk, 'inviteResponseReceived'); + + await MetaGroupWrapperActions.memberSetAccepted(groupPk, member); + await GroupSync.queueNewJobIfNeeded(groupPk); + + return { + groupPk, + infos: await MetaGroupWrapperActions.infoGet(groupPk), + members: await MetaGroupWrapperActions.memberGetAll(groupPk), + }; + } +); + const currentDeviceGroupNameChange = createAsyncThunk( 'group/currentDeviceGroupNameChange', async ( @@ -783,6 +825,7 @@ const currentDeviceGroupNameChange = createAsyncThunk( if (!state.groups.infos[groupPk] || !state.groups.members[groupPk]) { throw new PreConditionFailed('currentDeviceGroupNameChange group not present in redux slice'); } + await checkWeAreAdminOrThrow(groupPk, 'currentDeviceGroupNameChange'); await handleNameChangeFromUIOrNot({ groupPk, ...args, fromCurrentDevice: true }); @@ -929,6 +972,18 @@ const groupSlice = createSlice({ builder.addCase(markUsAsAdmin.rejected, (_state, action) => { window.log.error('a markUsAsAdmin was rejected', action.error); }); + + builder.addCase(inviteResponseReceived.fulfilled, (state, action) => { + const { infos, members, groupPk } = action.payload; + state.infos[groupPk] = infos; + state.members[groupPk] = members; + + window.log.debug(`groupInfo after inviteResponseReceived: ${stringify(infos)}`); + window.log.debug(`groupMembers after inviteResponseReceived: ${stringify(members)}`); + }); + builder.addCase(inviteResponseReceived.rejected, (_state, action) => { + window.log.error('a inviteResponseReceived was rejected', action.error); + }); }, }); @@ -940,6 +995,7 @@ export const groupInfoActions = { handleUserGroupUpdate, currentDeviceGroupMembersChange, markUsAsAdmin, + inviteResponseReceived, currentDeviceGroupNameChange, ...groupSlice.actions, }; diff --git a/ts/test/session/unit/crypto/SnodeSignatures_test.ts b/ts/test/session/unit/crypto/SnodeSignatures_test.ts index 22dd4e0f5..300d2123c 100644 --- a/ts/test/session/unit/crypto/SnodeSignatures_test.ts +++ b/ts/test/session/unit/crypto/SnodeSignatures_test.ts @@ -50,7 +50,7 @@ describe('SnodeSignature', () => { describe('getSnodeGroupAdminSignatureParams', () => { beforeEach(() => { - Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(hardcodedTimestamp); + Sinon.stub(GetNetworkTime, 'now').returns(hardcodedTimestamp); }); describe('retrieve', () => { @@ -162,7 +162,7 @@ describe('SnodeSignature', () => { // describe('getSnodeGroupSubAccountSignatureParams', () => { // beforeEach(() => { - // Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(hardcodedTimestamp); + // Sinon.stub(GetNetworkTime, 'now').returns(hardcodedTimestamp); // }); // describe('retrieve', () => { diff --git a/ts/test/session/unit/libsession_util/libsession_utils_test.ts b/ts/test/session/unit/libsession_util/libsession_utils_test.ts index f68731b2f..6c4dc3cc7 100644 --- a/ts/test/session/unit/libsession_util/libsession_utils_test.ts +++ b/ts/test/session/unit/libsession_util/libsession_utils_test.ts @@ -164,7 +164,7 @@ describe('LibSessionUtil pendingChangesForGroup', () => { }; Sinon.stub(MetaGroupWrapperActions, 'needsPush').resolves(true); Sinon.stub(MetaGroupWrapperActions, 'push').resolves(pushResults); - Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(1234); + Sinon.stub(GetNetworkTime, 'now').returns(1234); const result = await LibSessionUtil.pendingChangesForGroup(groupPk); expect(result.allOldHashes.size).to.be.equal(4); // check that all of the hashes are there @@ -245,7 +245,7 @@ describe('LibSessionUtil pendingChangesForUs', () => { .withArgs('ConvoInfoVolatileConfig') .resolves(pushResultsConvo); - Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(1234); + Sinon.stub(GetNetworkTime, 'now').returns(1234); const result = await LibSessionUtil.pendingChangesForUs(); expect(needsPush.callCount).to.be.eq(4); expect(needsPush.getCalls().map(m => m.args)).to.be.deep.eq([ @@ -313,7 +313,7 @@ describe('LibSessionUtil pendingChangesForUs', () => { .withArgs('ConvoInfoVolatileConfig') .resolves(pushConvo); - Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(1234); + Sinon.stub(GetNetworkTime, 'now').returns(1234); const result = await LibSessionUtil.pendingChangesForUs(); expect(needsPush.callCount).to.be.eq(4); expect(needsPush.getCalls().map(m => m.args)).to.be.deep.eq([ diff --git a/ts/test/session/unit/utils/job_runner/group_sync_job/GroupSyncJob_test.ts b/ts/test/session/unit/utils/job_runner/group_sync_job/GroupSyncJob_test.ts index a3dfa5bbd..e1e6c8424 100644 --- a/ts/test/session/unit/utils/job_runner/group_sync_job/GroupSyncJob_test.ts +++ b/ts/test/session/unit/utils/job_runner/group_sync_job/GroupSyncJob_test.ts @@ -285,7 +285,7 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => { const member = validMembers(sodium); const networkTimestamp = 4444; const ttl = TTL_DEFAULT.TTL_CONFIG; - Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(networkTimestamp); + Sinon.stub(GetNetworkTime, 'now').returns(networkTimestamp); pendingChangesForGroupStub.resolves({ messages: [info, member], allOldHashes: new Set('123'), diff --git a/ts/test/session/unit/utils/job_runner/user_sync_job/UserSyncJob_test.ts b/ts/test/session/unit/utils/job_runner/user_sync_job/UserSyncJob_test.ts index 6a91e0b47..2a7105fae 100644 --- a/ts/test/session/unit/utils/job_runner/user_sync_job/UserSyncJob_test.ts +++ b/ts/test/session/unit/utils/job_runner/user_sync_job/UserSyncJob_test.ts @@ -281,7 +281,7 @@ describe('UserSyncJob pushChangesToUserSwarmIfNeeded', () => { const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123); const networkTimestamp = 4444; const ttl = TTL_DEFAULT.TTL_CONFIG; - Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(networkTimestamp); + Sinon.stub(GetNetworkTime, 'now').returns(networkTimestamp); pendingChangesForUsStub.resolves({ messages: [profile, contact], diff --git a/ts/util/releaseFeature.ts b/ts/util/releaseFeature.ts index 6f3c4b762..7d122fd23 100644 --- a/ts/util/releaseFeature.ts +++ b/ts/util/releaseFeature.ts @@ -79,10 +79,7 @@ async function checkIsFeatureReleased(featureName: FeatureNameTracked): Promise< const featureAlreadyReleased = await getIsFeatureReleased(featureName); // Is it time to release the feature based on the network timestamp? - if ( - !featureAlreadyReleased && - GetNetworkTime.getNowWithNetworkOffset() >= getFeatureReleaseTimestamp(featureName) - ) { + if (!featureAlreadyReleased && GetNetworkTime.now() >= getFeatureReleaseTimestamp(featureName)) { window.log.info(`[releaseFeature]: It is time to release ${featureName}. Releasing it now`); await Storage.put(featureStorageItemId(featureName), true); setIsFeatureReleasedCached(featureName, true);