feat: add support for retrieve signs with admin key of group

pull/2873/head
Audric Ackermann 2 years ago
parent d89ff59560
commit be50aa7a2e

@ -1691,7 +1691,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
try {
const { body, attachments, preview, quote, fileIdsToLink } = await message.uploadData();
const { id } = message;
const destination = this.id;
const destination = this.id as string;
const sentAt = message.get('sent_at');
if (!sentAt) {

@ -1,12 +1,14 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { SharedUserConfigMessage } from '../../messages/outgoing/controlMessage/SharedConfigMessage';
import { SnodeNamespaces } from './namespaces';
import { SnodeNamespaces, SnodeNamespacesGroup } from './namespaces';
export type SwarmForSubRequest = { method: 'get_swarm'; params: { pubkey: string } };
type RetrieveMaxCountSize = { max_count?: number; max_size?: number };
type WithMaxCountSize = { max_count?: number; max_size?: number };
type WithPubkeyAsString = { pubkey: string };
type WithPubkeyAsGroupPubkey = { pubkey: GroupPubkeyType };
type RetrieveAlwaysNeeded = {
pubkey: string;
namespace: number;
last_hash: string;
timestamp?: number;
@ -19,7 +21,8 @@ export type RetrievePubkeySubRequestType = {
pubkey_ed25519: string;
namespace: number;
} & RetrieveAlwaysNeeded &
RetrieveMaxCountSize;
WithMaxCountSize &
WithPubkeyAsString;
};
/** Those namespaces do not require to be authenticated for storing messages.
@ -35,23 +38,24 @@ export type RetrieveLegacyClosedGroupSubRequestType = {
params: {
namespace: SnodeNamespaces.LegacyClosedGroup; // legacy closed groups retrieve are not authenticated because the clients do not have a shared key
} & RetrieveAlwaysNeeded &
RetrieveMaxCountSize;
WithMaxCountSize &
WithPubkeyAsString;
};
export type RetrieveSubKeySubRequestType = {
export type RetrieveGroupAdminSubRequestType = {
method: 'retrieve';
params: {
subkey: string; // 32-byte hex encoded string
signature: string;
namespace: number;
namespace: SnodeNamespacesGroup;
} & RetrieveAlwaysNeeded &
RetrieveMaxCountSize;
WithMaxCountSize &
WithPubkeyAsGroupPubkey;
};
export type RetrieveSubRequestType =
| RetrieveLegacyClosedGroupSubRequestType
| RetrievePubkeySubRequestType
| RetrieveSubKeySubRequestType
| RetrieveGroupAdminSubRequestType
| UpdateExpiryOnNodeSubRequest;
/**

@ -52,15 +52,25 @@ export enum SnodeNamespaces {
ClosedGroupMembers = 14,
}
export type SnodeNamespacesGroup = PickEnum<
export type SnodeNamespacesLegacyGroup = PickEnum<
SnodeNamespaces,
SnodeNamespaces.LegacyClosedGroup
>;
type SnodeNamespacesGroupConfig = PickEnum<
SnodeNamespaces,
| SnodeNamespaces.LegacyClosedGroup
| SnodeNamespaces.ClosedGroupInfo
| SnodeNamespaces.ClosedGroupMembers
| SnodeNamespaces.ClosedGroupKeys
| SnodeNamespaces.Default
>;
/**
* the namespaces to which a 03-group can store/retrieve messages from/to
*/
export type SnodeNamespacesGroup =
| SnodeNamespacesGroupConfig
| PickEnum<SnodeNamespaces, SnodeNamespaces.ClosedGroupMessages>;
export type SnodeNamespacesUser = PickEnum<
SnodeNamespaces,
SnodeNamespaces.UserContacts | SnodeNamespaces.UserProfile | SnodeNamespaces.Default
@ -72,9 +82,6 @@ export type SnodeNamespacesUser = PickEnum<
// eslint-disable-next-line consistent-return
function isUserConfigNamespace(namespace: SnodeNamespaces) {
switch (namespace) {
case SnodeNamespaces.Default:
// user messages is not hosting config based messages
return false;
case SnodeNamespaces.UserContacts:
case SnodeNamespaces.UserProfile:
case SnodeNamespaces.UserGroups:
@ -85,6 +92,8 @@ function isUserConfigNamespace(namespace: SnodeNamespaces) {
case SnodeNamespaces.ClosedGroupMembers:
case SnodeNamespaces.ClosedGroupMessages:
case SnodeNamespaces.LegacyClosedGroup:
case SnodeNamespaces.Default:
// user messages is not hosting config based messages
return false;
default:
@ -97,7 +106,12 @@ function isUserConfigNamespace(namespace: SnodeNamespaces) {
}
}
function isGroupConfigNamespace(namespace: SnodeNamespaces) {
/**
* Returns true if that namespace is one of the namespace used for the 03-group config messages
*/
function isGroupConfigNamespace(
namespace: SnodeNamespaces
): namespace is SnodeNamespacesGroupConfig {
switch (namespace) {
case SnodeNamespaces.Default:
case SnodeNamespaces.UserContacts:
@ -122,8 +136,37 @@ function isGroupConfigNamespace(namespace: SnodeNamespaces) {
}
}
// eslint-disable-next-line consistent-return
function namespacePriority(namespace: SnodeNamespaces): number {
/**
*
* @param namespace the namespace to check
* @returns true if that namespace is a valid namespace for a 03 group (either a config namespace or a message namespace)
*/
function isGroupNamespace(namespace: SnodeNamespaces): namespace is SnodeNamespacesGroup {
if (isGroupConfigNamespace(namespace)) {
return true;
}
if (namespace === SnodeNamespaces.ClosedGroupMessages) {
return true;
}
switch (namespace) {
case SnodeNamespaces.Default:
case SnodeNamespaces.UserContacts:
case SnodeNamespaces.UserProfile:
case SnodeNamespaces.UserGroups:
case SnodeNamespaces.ConvoInfoVolatile:
case SnodeNamespaces.LegacyClosedGroup:
return false;
default:
try {
assertUnreachable(namespace, `isGroupNamespace case not handled: ${namespace}`);
} catch (e) {
window.log.warn(`isGroupNamespace case not handled: ${namespace}: ${e.message}`);
return false;
}
}
}
function namespacePriority(namespace: SnodeNamespaces): 10 | 1 {
switch (namespace) {
case SnodeNamespaces.Default:
case SnodeNamespaces.ClosedGroupMessages:
@ -132,7 +175,6 @@ function namespacePriority(namespace: SnodeNamespaces): number {
case SnodeNamespaces.ConvoInfoVolatile:
case SnodeNamespaces.UserProfile:
case SnodeNamespaces.UserContacts:
return 1;
case SnodeNamespaces.LegacyClosedGroup:
case SnodeNamespaces.ClosedGroupInfo:
case SnodeNamespaces.ClosedGroupMembers:
@ -176,5 +218,6 @@ function maxSizeMap(namespaces: Array<SnodeNamespaces>) {
export const SnodeNamespace = {
isUserConfigNamespace,
isGroupConfigNamespace,
isGroupNamespace,
maxSizeMap,
};

@ -1,13 +1,17 @@
import { omit } from 'lodash';
import { isEmpty, isNil, omit } from 'lodash';
import { Snode } from '../../../data/data';
import { updateIsOnline } from '../../../state/ducks/onion';
import { doSnodeBatchRequest } from './batchRequest';
import { GetNetworkTime } from './getNetworkTime';
import { SnodeNamespace, SnodeNamespaces } from './namespaces';
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { DURATION } from '../../constants';
import { PubKey } from '../../types';
import { UserUtils } from '../../utils';
import {
RetrieveGroupAdminSubRequestType,
RetrieveLegacyClosedGroupSubRequestType,
RetrieveSubRequestType,
UpdateExpiryOnNodeSubRequest,
@ -15,6 +19,112 @@ import {
import { SnodeSignature } from './snodeSignatures';
import { RetrieveMessagesResultsBatched, RetrieveMessagesResultsContent } from './types';
type RetrieveParams = {
pubkey: string;
last_hash: string;
timestamp: number;
max_size: number | undefined;
};
async function retrieveRequestForUs({
namespace,
ourPubkey,
retrieveParam,
}: {
ourPubkey: string;
namespace: SnodeNamespaces;
retrieveParam: RetrieveParams;
}) {
if (!SnodeNamespace.isUserConfigNamespace(namespace) && namespace !== SnodeNamespaces.Default) {
throw new Error(`retrieveRequestForUs not a valid namespace to retrieve as us:${namespace}`);
}
const signatureArgs = { ...retrieveParam, namespace, method: 'retrieve' as const, ourPubkey };
const signatureBuilt = await SnodeSignature.getSnodeSignatureParamsUs(signatureArgs);
const retrieveForUS: RetrieveSubRequestType = {
method: 'retrieve',
params: { ...retrieveParam, namespace, ...signatureBuilt },
};
return retrieveForUS;
}
/**
* Retrieve for legacy groups are not authenticated so no need to sign the request
*/
function retrieveRequestForLegacyGroup({
namespace,
ourPubkey,
pubkey,
retrieveParam,
}: {
pubkey: string;
namespace: SnodeNamespaces.LegacyClosedGroup;
ourPubkey: string;
retrieveParam: RetrieveParams;
}) {
if (pubkey === ourPubkey || !pubkey.startsWith('05')) {
throw new Error(
'namespace -10 can only be used to retrieve messages from a legacy closed group (prefix 05)'
);
}
if (namespace !== SnodeNamespaces.LegacyClosedGroup) {
throw new Error(`retrieveRequestForLegacyGroup namespace can only be -10`);
}
const retrieveLegacyClosedGroup = {
...retrieveParam,
namespace,
};
const retrieveParamsLegacy: RetrieveLegacyClosedGroupSubRequestType = {
method: 'retrieve',
params: omit(retrieveLegacyClosedGroup, 'timestamp'), // if we give a timestamp, a signature will be required by the service node, and we don't want to provide one as this is an unauthenticated namespace
};
return retrieveParamsLegacy;
}
/**
* Retrieve for groups (03-prefixed) are authenticated with the admin key if we have it, or with our subkey auth
*/
async function retrieveRequestForGroup({
namespace,
groupPk,
retrieveParam,
}: {
groupPk: GroupPubkeyType;
namespace: SnodeNamespaces;
retrieveParam: RetrieveParams;
}) {
if (!PubKey.isClosedGroupV3(groupPk)) {
throw new Error('retrieveRequestForGroup: not a 03 group');
}
if (!SnodeNamespace.isGroupNamespace(namespace)) {
throw new Error(`retrieveRequestForGroup: not a groupNamespace: ${namespace}`);
}
const group = await UserGroupsWrapperActions.getGroup(groupPk);
const groupSecretKey = group?.secretKey;
if (isNil(groupSecretKey) || isEmpty(groupSecretKey)) {
throw new Error(`sendMessagesDataToSnode: failed to find group admin secret key in wrapper`);
}
const signatureBuilt = await SnodeSignature.getSnodeGroupSignatureParams({
...retrieveParam,
namespace,
method: 'retrieve' as const,
groupPk,
groupIdentityPrivKey: groupSecretKey,
});
const retrieveGroup = {
...retrieveParam,
...signatureBuilt,
namespace,
};
const retrieveParamsGroup: RetrieveGroupAdminSubRequestType = {
method: 'retrieve',
params: retrieveGroup,
};
return retrieveParamsGroup;
}
async function buildRetrieveRequest(
lastHashes: Array<string>,
pubkey: string,
@ -29,47 +139,25 @@ async function buildRetrieveRequest(
const retrieveParam = {
pubkey,
last_hash: lastHashes.at(index) || '',
namespace,
timestamp: GetNetworkTime.getNowWithNetworkOffset(),
max_size: foundMaxSize,
};
if (namespace === SnodeNamespaces.LegacyClosedGroup) {
if (pubkey === ourPubkey || !pubkey.startsWith('05')) {
throw new Error(
'namespace -10 can only be used to retrieve messages from a legacy closed group (prefix 05)'
);
return retrieveRequestForLegacyGroup({ namespace, ourPubkey, pubkey, retrieveParam });
}
if (PubKey.isClosedGroupV3(pubkey)) {
if (!SnodeNamespace.isGroupNamespace(namespace)) {
// either config or messages namespaces for 03 groups
throw new Error(`tried to poll from a non 03 group namespace ${namespace}`);
}
const retrieveLegacyClosedGroup = {
...retrieveParam,
namespace,
};
const retrieveParamsLegacy: RetrieveLegacyClosedGroupSubRequestType = {
method: 'retrieve',
params: omit(retrieveLegacyClosedGroup, 'timestamp'), // if we give a timestamp, a signature will be required by the service node, and we don't want to provide one as this is an unauthenticated namespace
};
return retrieveParamsLegacy;
return retrieveRequestForGroup({ namespace, groupPk: pubkey, retrieveParam });
}
// all legacy closed group retrieves are unauthenticated and run above.
// if we get here, this can only be a retrieve for our own swarm, which must be authenticated
if (
!SnodeNamespace.isUserConfigNamespace(namespace) &&
namespace !== SnodeNamespaces.Default
) {
throw new Error(`not a legacy closed group. namespace can only be 0 and was ${namespace}`);
}
if (pubkey !== ourPubkey) {
throw new Error('not a legacy closed group. pubkey can only be ours');
}
const signatureArgs = { ...retrieveParam, method: 'retrieve' as const, ourPubkey };
const signatureBuilt = await SnodeSignature.getSnodeSignatureParamsUs(signatureArgs);
const retrieve: RetrieveSubRequestType = {
method: 'retrieve',
params: { ...retrieveParam, ...signatureBuilt },
};
return retrieve;
return retrieveRequestForUs({ namespace, ourPubkey, retrieveParam });
})
);

@ -27,6 +27,8 @@ import { SnodeAPIRetrieve } from './retrieveRequest';
import { SwarmPollingGroupConfig } from './swarm_polling_config/SwarmPollingGroupConfig';
import { SwarmPollingUserConfig } from './swarm_polling_config/SwarmPollingUserConfig';
import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types';
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
export function extractWebSocketContent(
message: string,
@ -62,6 +64,11 @@ export const getSwarmPollingInstance = () => {
return instance;
};
type PollForUs = [pubkey: string, type: ConversationTypeEnum.PRIVATE];
type PollForLegacy = [pubkey: string, type: ConversationTypeEnum.GROUP];
type PollForGroup = [pubkey: GroupPubkeyType, type: ConversationTypeEnum.GROUPV3];
export class SwarmPolling {
private groupPolling: Array<{ pubkey: PubKey; lastPolledTimestamp: number }>;
private readonly lastHashes: Record<string, Record<string, Record<number, string>>>;
@ -166,9 +173,8 @@ export class SwarmPolling {
}
// we always poll as often as possible for our pubkey
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
const userNamespaces = await this.getUserNamespacesPolled();
const directPromise = Promise.all([
this.pollOnceForKey(ourPubkey, ConversationTypeEnum.PRIVATE, userNamespaces),
this.pollOnceForKey([ourPubkey, ConversationTypeEnum.PRIVATE]),
]).then(() => undefined);
const now = Date.now();
@ -176,7 +182,6 @@ export class SwarmPolling {
const convoPollingTimeout = this.getPollingTimeout(group.pubkey);
const diff = now - group.lastPolledTimestamp;
const { key } = group.pubkey;
const isV3 = PubKey.isClosedGroupV3(key);
const loggingId =
getConversationController()
@ -186,17 +191,10 @@ export class SwarmPolling {
window?.log?.debug(
`Polling for ${loggingId}; timeout: ${convoPollingTimeout}; diff: ${diff} `
);
if (isV3) {
return this.pollOnceForKey(key, ConversationTypeEnum.GROUPV3, [
SnodeNamespaces.Default,
SnodeNamespaces.ClosedGroupInfo,
SnodeNamespaces.ClosedGroupMembers,
SnodeNamespaces.ClosedGroupKeys, // keys are fetched last to avoid race conditions when someone deposits them
]);
if (PubKey.isClosedGroupV3(key)) {
return this.pollOnceForKey([key, ConversationTypeEnum.GROUPV3]);
}
return this.pollOnceForKey(key, ConversationTypeEnum.GROUP, [
SnodeNamespaces.LegacyClosedGroup,
]);
return this.pollOnceForKey([key, ConversationTypeEnum.GROUP]);
}
window?.log?.debug(
`Not polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}`
@ -219,10 +217,10 @@ export class SwarmPolling {
* Only exposed as public for testing
*/
public async pollOnceForKey(
pubkey: string,
type: ConversationTypeEnum,
namespaces: Array<SnodeNamespaces>
[pubkey, type]:PollForUs | PollForLegacy | PollForGroup
) {
const namespaces = this.getNamespacesToPollFrom(type);
const swarmSnodes = await snodePool.getSwarmFor(pubkey);
// Select nodes for which we already have lastHashes
@ -469,14 +467,31 @@ export class SwarmPolling {
return newMessages;
}
private async getUserNamespacesPolled() {
private getNamespacesToPollFrom(type: ConversationTypeEnum): Array<SnodeNamespaces> {
if(type === ConversationTypeEnum.PRIVATE) {
return [
SnodeNamespaces.Default,
SnodeNamespaces.UserProfile,
SnodeNamespaces.UserContacts,
SnodeNamespaces.UserGroups,
SnodeNamespaces.ConvoInfoVolatile,
];
] ;
}
if(type === ConversationTypeEnum.GROUP) {
return [
SnodeNamespaces.LegacyClosedGroup
] ;
}
if(type === ConversationTypeEnum.GROUPV3) {
return [
SnodeNamespaces.ClosedGroupMessages,
SnodeNamespaces.ClosedGroupInfo,
SnodeNamespaces.ClosedGroupMembers,
SnodeNamespaces.ClosedGroupKeys, // keys are fetched last to avoid race conditions when someone deposits them
] ;
}
assertUnreachable(type, `getNamespacesToPollFrom case should have been unreachable: type:${type}`)
}
private async updateLastHash({

@ -6,7 +6,7 @@ import { ClosedGroupMessage } from '../controlMessage/group/ClosedGroupMessage';
interface ClosedGroupVisibleMessageParams {
identifier?: string;
groupId: string | PubKey;
groupId: string;
chatMessage: VisibleMessage;
}

@ -63,7 +63,8 @@ const pathFailureThreshold = 3;
// some naming issue here it seems)
export let guardNodes: Array<Snode> = [];
export const ed25519Str = (ed25519Key: string) => `(...${ed25519Key.substr(58)})`;
export const ed25519Str = (ed25519Key: string) =>
`(${ed25519Key.substr(0, 2)}...${ed25519Key.substr(60)})`;
export async function buildNewOnionPathsOneAtATime() {
// this function may be called concurrently make sure we only have one inflight

@ -27,7 +27,7 @@ import { OpenGroupMessageV2 } from '../apis/open_group_api/opengroupV2/OpenGroup
import { sendSogsReactionOnionV4 } from '../apis/open_group_api/sogsv3/sogsV3SendReaction';
import {
SnodeNamespaces,
SnodeNamespacesGroup,
SnodeNamespacesLegacyGroup,
SnodeNamespacesUser,
} from '../apis/snode_api/namespaces';
import { SharedConfigMessage } from '../messages/outgoing/controlMessage/SharedConfigMessage';
@ -179,7 +179,7 @@ export class MessageQueue {
sentCb,
}: {
message: ClosedGroupMessageType;
namespace: SnodeNamespacesGroup;
namespace: SnodeNamespacesLegacyGroup;
sentCb?: (message: RawMessage) => Promise<void>;
groupPubKey?: PubKey;
}): Promise<void> {

@ -9,9 +9,9 @@ import { ClosedGroupVisibleMessage } from '../../../../../session/messages/outgo
import { VisibleMessage } from '../../../../../session/messages/outgoing/visibleMessage/VisibleMessage';
describe('ClosedGroupVisibleMessage', () => {
let groupId: PubKey;
let groupId: string;
beforeEach(() => {
groupId = TestUtils.generateFakePubKey();
groupId = TestUtils.generateFakePubKeyStr();
});
it('can create empty message with timestamp, groupId and chatMessage', () => {
const chatMessage = new VisibleMessage({
@ -28,7 +28,7 @@ describe('ClosedGroupVisibleMessage', () => {
.to.have.property('group')
.to.have.deep.property(
'id',
new Uint8Array(StringUtils.encode(PubKey.PREFIX_GROUP_TEXTSECURE + groupId.key, 'utf8'))
new Uint8Array(StringUtils.encode(PubKey.PREFIX_GROUP_TEXTSECURE + groupId, 'utf8'))
);
expect(decoded.dataMessage)
.to.have.property('group')

@ -97,7 +97,7 @@ describe('Message Utils', () => {
it('should set encryption to ClosedGroup if a ClosedGroupVisibleMessage is passed in', async () => {
const device = TestUtils.generateFakePubKey();
const groupId = TestUtils.generateFakePubKey();
const groupId = TestUtils.generateFakePubKeyStr();
const chatMessage = TestUtils.generateVisibleMessage();
const message = new ClosedGroupVisibleMessage({ chatMessage, groupId });

Loading…
Cancel
Save