add SharedConfig message and signing it when sending them

This commit is contained in:
Audric Ackermann 2023-01-18 11:25:19 +11:00
parent 6d1b406c85
commit 58edbf44ee
12 changed files with 182 additions and 68 deletions

View File

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

View File

@ -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);

View File

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

View File

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

View File

@ -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,

View File

@ -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<string>,
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 },

View File

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

View File

@ -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');
}

View File

@ -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,
});
}
}

View File

@ -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');

View File

@ -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<void> {
} & SendMessageSignatureOpts): Promise<void> {
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;
}

View File

@ -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,