add SharedConfig message and signing it when sending them
This commit is contained in:
parent
6d1b406c85
commit
58edbf44ee
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 };
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue