From 58edbf44eeffa23095a675c3db21abef81af8eea Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 18 Jan 2023 11:25:19 +1100 Subject: [PATCH] add SharedConfig message and signing it when sending them --- protos/SignalService.proto | 22 +++++-- ts/components/leftpane/ActionsPanel.tsx | 5 ++ .../apis/snode_api/SnodeRequestTypes.ts | 2 + ts/session/apis/snode_api/batchRequest.ts | 14 ++++- ts/session/apis/snode_api/onions.ts | 5 +- ts/session/apis/snode_api/retrieveRequest.ts | 53 +---------------- ts/session/apis/snode_api/snodeSignatures.ts | 59 +++++++++++++++++++ ts/session/apis/snode_api/storeMessage.ts | 2 +- .../controlMessage/SharedConfigMessage.ts | 39 ++++++++++++ ts/session/sending/MessageQueue.ts | 3 + ts/session/sending/MessageSender.ts | 42 ++++++++++--- ts/session/utils/syncUtils.ts | 4 +- 12 files changed, 182 insertions(+), 68 deletions(-) create mode 100644 ts/session/apis/snode_api/snodeSignatures.ts create mode 100644 ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 105041ced..e4c36fe68 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -22,27 +22,38 @@ message TypingMessage { STARTED = 0; STOPPED = 1; } - // @required required uint64 timestamp = 1; - // @required required Action action = 2; } message Unsend { - // @required required uint64 timestamp = 1; - // @required required string author = 2; } message MessageRequestResponse { - // @required required bool isApproved = 1; optional bytes profileKey = 2; optional DataMessage.LokiProfile profile = 3; } +message SharedConfigMessage { + enum Kind { + USER_PROFILE = 1; + CONTACTS = 2; + CONVERSATION_INFO = 3; + LEGACY_CLOSED_GROUPS = 4; + CLOSED_GROUP_INFO = 5; + CLOSED_GROUP_MEMBERS = 6; + ENCRYPTION_KEYS = 7; + } + + required Kind kind = 1; + required int64 seqno = 2; + required bytes data = 3; +} + message Content { optional DataMessage dataMessage = 1; optional CallMessage callMessage = 3; @@ -52,6 +63,7 @@ message Content { optional DataExtractionNotification dataExtractionNotification = 8; optional Unsend unsendMessage = 9; optional MessageRequestResponse messageRequestResponse = 10; + optional SharedConfigMessage sharedConfigMessage = 11; } message KeyPair { diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index 22873200f..5950b451d 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -48,6 +48,11 @@ import { ThemeStateType } from '../../themes/constants/colors'; import { isDarkTheme } from '../../state/selectors/theme'; import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool'; import { callLibSessionWorker } from '../../webworker/workers/browser/libsession_worker_interface'; +import { SharedConfigMessage } from '../../session/messages/outgoing/controlMessage/SharedConfigMessage'; +import { SignalService } from '../../protobuf'; +import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime'; +import Long from 'long'; +import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces'; const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); diff --git a/ts/session/apis/snode_api/SnodeRequestTypes.ts b/ts/session/apis/snode_api/SnodeRequestTypes.ts index 7b8665d35..5388098c3 100644 --- a/ts/session/apis/snode_api/SnodeRequestTypes.ts +++ b/ts/session/apis/snode_api/SnodeRequestTypes.ts @@ -111,6 +111,8 @@ export type StoreOnNodeParams = { timestamp: string; data: string; namespace: number; + signature?: string; + pubkey_ed25519?: string; }; export type StoreOnNodeSubRequest = { method: 'store'; params: StoreOnNodeParams }; diff --git a/ts/session/apis/snode_api/batchRequest.ts b/ts/session/apis/snode_api/batchRequest.ts index 01f239bff..dfade5612 100644 --- a/ts/session/apis/snode_api/batchRequest.ts +++ b/ts/session/apis/snode_api/batchRequest.ts @@ -1,6 +1,6 @@ import { isArray } from 'lodash'; import { Snode } from '../../../data/data'; -import { SnodeResponse } from './onions'; +import { processOnionRequestErrorAtDestination, SnodeResponse } from './onions'; import { snodeRpc } from './sessionRpc'; import { NotEmptyArrayOfBatchResults, SnodeApiSubRequests } from './SnodeRequestTypes'; @@ -36,6 +36,18 @@ export async function doSnodeBatchRequest( } const decoded = decodeBatchRequest(result); + if (decoded?.length) { + for (let index = 0; index < decoded.length; index++) { + const resultRow = decoded[index]; + await processOnionRequestErrorAtDestination({ + statusCode: resultRow.code, + body: JSON.stringify(resultRow.body), + associatedWith, + destinationSnodeEd25519: targetNode.pubkey_ed25519, + }); + } + } + return decoded; } diff --git a/ts/session/apis/snode_api/onions.ts b/ts/session/apis/snode_api/onions.ts index 20f929240..31e9ced62 100644 --- a/ts/session/apis/snode_api/onions.ts +++ b/ts/session/apis/snode_api/onions.ts @@ -282,7 +282,7 @@ async function process421Error( * * If destinationEd25519 is set, we will increment the failure count of the specified snode */ -async function processOnionRequestErrorAtDestination({ +export async function processOnionRequestErrorAtDestination({ statusCode, body, destinationSnodeEd25519, @@ -299,10 +299,9 @@ async function processOnionRequestErrorAtDestination({ window?.log?.info( `processOnionRequestErrorAtDestination. statusCode nok: ${statusCode}: "${body}"` ); - process406Or425Error(statusCode); - await process421Error(statusCode, body, associatedWith, destinationSnodeEd25519); processOxenServerError(statusCode, body); + await process421Error(statusCode, body, associatedWith, destinationSnodeEd25519); if (destinationSnodeEd25519) { await processAnyOtherErrorAtDestination( statusCode, diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 47c6361a0..262a54a2d 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -1,8 +1,5 @@ import { Snode } from '../../../data/data'; import { updateIsOnline } from '../../../state/ducks/onion'; -import { getSodiumRenderer } from '../../crypto'; -import { StringUtils, UserUtils } from '../../utils'; -import { fromHexToArray, fromUInt8ArrayToBase64 } from '../../utils/String'; import { doSnodeBatchRequest } from './batchRequest'; import { GetNetworkTime } from './getNetworkTime'; import { SnodeNamespaces } from './namespaces'; @@ -11,53 +8,9 @@ import { RetrieveLegacyClosedGroupSubRequestType, RetrieveSubRequestType, } from './SnodeRequestTypes'; +import { SnodeSignature } from './snodeSignatures'; import { RetrieveMessagesResultsBatched, RetrieveMessagesResultsContent } from './types'; -async function getRetrieveSignatureParams(params: { - pubkey: string; - namespace: number; - ourPubkey: string; -}): Promise<{ - timestamp: number; - signature: string; - pubkey_ed25519: string; - namespace: number; -}> { - const ourEd25519Key = await UserUtils.getUserED25519KeyPair(); - - if (!ourEd25519Key) { - window.log.warn('getRetrieveSignatureParams: User has no getUserED25519KeyPair()'); - throw new Error('getRetrieveSignatureParams: User has no getUserED25519KeyPair()'); - } - const namespace = params.namespace || 0; - const edKeyPrivBytes = fromHexToArray(ourEd25519Key?.privKey); - - const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset(); - - const verificationData = - namespace === 0 - ? StringUtils.encode(`retrieve${signatureTimestamp}`, 'utf8') - : StringUtils.encode(`retrieve${namespace}${signatureTimestamp}`, 'utf8'); - - const message = new Uint8Array(verificationData); - - const sodium = await getSodiumRenderer(); - try { - const signature = sodium.crypto_sign_detached(message, edKeyPrivBytes); - const signatureBase64 = fromUInt8ArrayToBase64(signature); - - return { - timestamp: signatureTimestamp, - signature: signatureBase64, - pubkey_ed25519: ourEd25519Key.pubKey, - namespace, - }; - } catch (e) { - window.log.warn('getSignatureParams failed with: ', e.message); - throw e; - } -} - async function buildRetrieveRequest( lastHashes: Array, pubkey: string, @@ -103,8 +56,8 @@ async function buildRetrieveRequest( if (pubkey !== ourPubkey) { throw new Error('not a legacy closed group. pubkey can only be ours'); } - const signatureArgs = { ...retrieveParam, ourPubkey }; - const signatureBuilt = await getRetrieveSignatureParams(signatureArgs); + const signatureArgs = { ...retrieveParam, method: 'retrieve' as 'retrieve', ourPubkey }; + const signatureBuilt = await SnodeSignature.getSnodeSignatureParams(signatureArgs); const retrieve: RetrieveSubRequestType = { method: 'retrieve', params: { ...retrieveParam, ...signatureBuilt }, diff --git a/ts/session/apis/snode_api/snodeSignatures.ts b/ts/session/apis/snode_api/snodeSignatures.ts new file mode 100644 index 000000000..8ab5eda2e --- /dev/null +++ b/ts/session/apis/snode_api/snodeSignatures.ts @@ -0,0 +1,59 @@ +import { to_string } from 'libsodium-wrappers-sumo'; +import { getSodiumRenderer } from '../../crypto'; +import { UserUtils, StringUtils } from '../../utils'; +import { fromHexToArray, fromUInt8ArrayToBase64 } from '../../utils/String'; +import { GetNetworkTime } from './getNetworkTime'; + +export type SnodeSignatureResult = { + timestamp: number; + signature: string; + pubkey_ed25519: string; + namespace: number; +}; + +async function getSnodeSignatureParams(params: { + pubkey: string; + namespace: number; + ourPubkey: string; + method: 'retrieve' | 'store'; +}): Promise { + const ourEd25519Key = await UserUtils.getUserED25519KeyPair(); + + if (!ourEd25519Key) { + const err = `getSnodeSignatureParams "${params.method}": User has no getUserED25519KeyPair()`; + window.log.warn(err); + throw new Error(err); + } + const namespace = params.namespace || 0; + const edKeyPrivBytes = fromHexToArray(ourEd25519Key?.privKey); + + const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + + const verificationData = + namespace === 0 + ? StringUtils.encode(`${params.method}${signatureTimestamp}`, 'utf8') + : StringUtils.encode(`${params.method}${namespace}${signatureTimestamp}`, 'utf8'); + + const message = new Uint8Array(verificationData); + + const sodium = await getSodiumRenderer(); + try { + const signature = sodium.crypto_sign_detached(message, edKeyPrivBytes); + const signatureBase64 = fromUInt8ArrayToBase64(signature); + console.warn( + `signing: "${to_string(new Uint8Array(verificationData))}" signature:"${signatureBase64}"` + ); + + return { + timestamp: signatureTimestamp, + signature: signatureBase64, + pubkey_ed25519: ourEd25519Key.pubKey, + namespace, + }; + } catch (e) { + window.log.warn('getSnodeSignatureParams failed with: ', e.message); + throw e; + } +} + +export const SnodeSignature = { getSnodeSignatureParams }; diff --git a/ts/session/apis/snode_api/storeMessage.ts b/ts/session/apis/snode_api/storeMessage.ts index 98bd5f8dd..90c7eee81 100644 --- a/ts/session/apis/snode_api/storeMessage.ts +++ b/ts/session/apis/snode_api/storeMessage.ts @@ -30,7 +30,7 @@ async function storeOnNode( const firstResult = result[0]; if (firstResult.code !== 200) { - window?.log?.warn('Status is not 200 for storeOnNode but: ', firstResult.code); + window?.log?.warn('first result status is not 200 for storeOnNode but: ', firstResult.code); throw new Error('storeOnNode: Invalid status code'); } diff --git a/ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts b/ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts new file mode 100644 index 000000000..b7e915804 --- /dev/null +++ b/ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts @@ -0,0 +1,39 @@ +// this is not a very good name, but a configuration message is a message sent to our other devices so sync our current public and closed groups + +import { SignalService } from '../../../../protobuf'; +import { MessageParams } from '../Message'; +import { ContentMessage } from '..'; +import Long from 'long'; + +interface SharedConfigParams extends MessageParams { + seqno: Long; + kind: SignalService.SharedConfigMessage.Kind; + data: Uint8Array; +} + +export class SharedConfigMessage extends ContentMessage { + public readonly seqno: Long; + public readonly kind: SignalService.SharedConfigMessage.Kind; + public readonly data: Uint8Array; + + constructor(params: SharedConfigParams) { + super({ timestamp: params.timestamp, identifier: params.identifier }); + this.data = params.data; + this.kind = params.kind; + this.seqno = params.seqno; + } + + public contentProto(): SignalService.Content { + return new SignalService.Content({ + sharedConfigMessage: this.sharedConfigProto(), + }); + } + + protected sharedConfigProto(): SignalService.SharedConfigMessage { + return new SignalService.SharedConfigMessage({ + data: this.data, + kind: this.kind, + seqno: this.seqno, + }); + } +} diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index ab4050991..b063b3b99 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -29,6 +29,7 @@ import { SnodeNamespacesGroup, SnodeNamespacesUser, } from '../apis/snode_api/namespaces'; +import { SharedConfigMessage } from '../messages/outgoing/controlMessage/SharedConfigMessage'; type ClosedGroupMessageType = | ClosedGroupVisibleMessage @@ -208,6 +209,7 @@ export class MessageQueue { if ( !(message instanceof ConfigurationMessage) && !(message instanceof UnsendMessage) && + !(message instanceof SharedConfigMessage) && !(message as any)?.syncTarget ) { throw new Error('Invalid message given to sendSyncMessage'); @@ -328,6 +330,7 @@ export class MessageQueue { message instanceof ConfigurationMessage || message instanceof ClosedGroupNewMessage || message instanceof UnsendMessage || + message instanceof SharedConfigMessage || (message as any).syncTarget?.length > 0 ) { window?.log?.warn('Processing sync message'); diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 78e944817..682bf6cd1 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -26,7 +26,9 @@ import { AbortController } from 'abort-controller'; import { SnodeAPIStore } from '../apis/snode_api/storeMessage'; import { StoreOnNodeParams } from '../apis/snode_api/SnodeRequestTypes'; import { GetNetworkTime } from '../apis/snode_api/getNetworkTime'; -import { SnodeNamespaces } from '../apis/snode_api/namespaces'; +import { SnodeNamespace, SnodeNamespaces } from '../apis/snode_api/namespaces'; +import { SnodeSignature, SnodeSignatureResult } from '../apis/snode_api/snodeSignatures'; +import { UserUtils } from '../utils'; // ================ SNODE STORE ================ @@ -122,14 +124,26 @@ export async function send( ? SnodeNamespaces.ClosedGroupMessage : SnodeNamespaces.UserMessages; } + let timestamp = networkTimestamp; + // the user config namespacesm requires a signature to be added + let signOpts: SnodeSignatureResult | undefined; + if (SnodeNamespace.isUserConfigNamespace(namespace)) { + signOpts = await SnodeSignature.getSnodeSignatureParams({ + method: 'store' as 'store', + namespace, + ourPubkey: UserUtils.getOurPubKeyStrFromCache(), + pubkey: recipient.key, + }); + } await MessageSender.sendMessageToSnode({ pubKey: recipient.key, data, ttl, - timestamp: networkTimestamp, + timestamp, isSyncMessage, messageId: message.identifier, namespace, + ...signOpts, }); return { wrappedEnvelope: data, effectiveTimestamp: networkTimestamp }; }, @@ -141,6 +155,13 @@ export async function send( ); } +export type SendMessageSignatureOpts = { + signature?: string; // needed for some namespaces + namespace: SnodeNamespaces; + pubkey_ed25519?: string; + timestamp: number; +}; + // tslint:disable-next-line: function-name export async function sendMessageToSnode({ data, @@ -149,16 +170,17 @@ export async function sendMessageToSnode({ timestamp, ttl, isSyncMessage, + signature, messageId, + pubkey_ed25519, }: { pubKey: string; data: Uint8Array; ttl: number; - timestamp: number; namespace: SnodeNamespaces; isSyncMessage?: boolean; messageId?: string; -}): Promise { +} & SendMessageSignatureOpts): Promise { const data64 = ByteBuffer.wrap(data).toString('base64'); const swarm = await getSwarmFor(pubKey); @@ -174,6 +196,11 @@ export async function sendMessageToSnode({ namespace, }; + if (signature && pubkey_ed25519) { + params.signature = signature; + params.pubkey_ed25519 = pubkey_ed25519; + } + const usedNodes = _.slice(swarm, 0, 1); if (!usedNodes || usedNodes.length === 0) { throw new EmptySwarmError(pubKey, 'Ran out of swarm nodes to query'); @@ -182,8 +209,9 @@ export async function sendMessageToSnode({ let successfulSendHash: string | undefined; let snode: Snode | undefined; + const snodeTried = usedNodes[0]; + try { - const snodeTried = usedNodes[0]; // No pRetry here as if this is a bad path it will be handled and retried in lokiOnionFetch. // the only case we could care about a retry would be when the usedNode is not correct, // but considering we trigger this request with a few snode in //, this should be fine. @@ -196,9 +224,9 @@ export async function sendMessageToSnode({ snode = snodeTried; } } catch (e) { - const snodeStr = snode ? `${snode.ip}:${snode.port}` : 'null'; + const snodeStr = snodeTried ? `${snodeTried.ip}:${snodeTried.port}` : 'null'; window?.log?.warn( - `loki_message:::sendMessage - ${e.code} ${e.message} to ${pubKey} via snode:${snodeStr}` + `loki_message:::sendMessage - "${e.code}:${e.message}" to ${pubKey} via snode:${snodeStr}` ); throw e; } diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 62b18628b..cfbf99f62 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -27,6 +27,7 @@ import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage import { MessageRequestResponse } from '../messages/outgoing/controlMessage/MessageRequestResponse'; import { PubKey } from '../types'; import { SnodeNamespaces } from '../apis/snode_api/namespaces'; +import { SharedConfigMessage } from '../messages/outgoing/controlMessage/SharedConfigMessage'; const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp'; @@ -321,7 +322,8 @@ export type SyncMessageType = | ExpirationTimerUpdateMessage | ConfigurationMessage | MessageRequestResponse - | UnsendMessage; + | UnsendMessage + | SharedConfigMessage; export const buildSyncMessage = ( identifier: string,