Skip to content

Commit

Permalink
feat: store info+members+keys on right namespaces for groups
Browse files Browse the repository at this point in the history
make the signature work with the admin key, fetching it from the
usergroups wrapper
  • Loading branch information
Bilb committed Sep 13, 2023
1 parent f86b368 commit d89ff59
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 108 deletions.
17 changes: 7 additions & 10 deletions ts/session/apis/snode_api/SNodeAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ export const ERROR_CODE_NO_CONNECT = 'ENETUNREACH: No network connection.';
// TODOLATER we should merge those two functions together as they are almost exactly the same
const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
const sodium = await getSodiumRenderer();
const userX25519PublicKey = UserUtils.getOurPubKeyStrFromCache();
const usPk = UserUtils.getOurPubKeyStrFromCache();

const userED25519KeyPair = await UserUtils.getUserED25519KeyPair();
const usED25519KeyPair = await UserUtils.getUserED25519KeyPairBytes();

if (!userED25519KeyPair) {
if (!usED25519KeyPair) {
window?.log?.warn('Cannot forceNetworkDeletion, did not find user ed25519 key.');
return null;
}
Expand All @@ -30,7 +30,7 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
try {
const maliciousSnodes = await pRetry(
async () => {
const userSwarm = await getSwarmFor(userX25519PublicKey);
const userSwarm = await getSwarmFor(usPk);
const snodeToMakeRequestTo: Snode | undefined = sample(userSwarm);

if (!snodeToMakeRequestTo) {
Expand All @@ -40,17 +40,16 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {

return pRetry(
async () => {
const signOpts = await SnodeSignature.getSnodeSignatureParams({
const signOpts = await SnodeSignature.getSnodeSignatureParamsUs({
method,
namespace,
pubkey: userX25519PublicKey,
});

const ret = await doSnodeBatchRequest(
[{ method, params: { ...signOpts, namespace } }],
snodeToMakeRequestTo,
10000,
userX25519PublicKey
usPk
);

if (!ret || !ret?.[0].body || ret[0].code !== 200) {
Expand Down Expand Up @@ -124,9 +123,7 @@ const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
const sortedHashes = hashes.sort();
const signatureSnode = snodeJson.signature as string;
// The signature format is (with sortedHashes accross all namespaces) ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
const dataToVerify = `${userX25519PublicKey}${
signOpts.timestamp
}${sortedHashes.join('')}`;
const dataToVerify = `${usPk}${signOpts.timestamp}${sortedHashes.join('')}`;

const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8');
const isValid = sodium.crypto_sign_verify_detached(
Expand Down
10 changes: 5 additions & 5 deletions ts/session/apis/snode_api/batchRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export async function doSnodeBatchRequest(
associatedWith: string | null,
method: 'batch' | 'sequence' = 'batch'
): Promise<NotEmptyArrayOfBatchResults> {
// console.warn(
// `doSnodeBatchRequest "${method}":`,
// subRequests.map(m => m.method),
// subRequests
// );
console.warn(
`doSnodeBatchRequest "${method}":`,
JSON.stringify(subRequests.map(m => m.method)),
subRequests
);
const result = await snodeRpc({
method,
params: { requests: subRequests },
Expand Down
1 change: 0 additions & 1 deletion ts/session/apis/snode_api/onions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,6 @@ async function sendOnionRequestSnodeDest(
onionPath: Array<Snode>,
targetNode: Snode,
headers: Record<string, any>,

plaintext: string | null,
associatedWith?: string
) {
Expand Down
2 changes: 1 addition & 1 deletion ts/session/apis/snode_api/retrieveRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function buildRetrieveRequest(
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.getSnodeSignatureParams(signatureArgs);
const signatureBuilt = await SnodeSignature.getSnodeSignatureParamsUs(signatureArgs);
const retrieve: RetrieveSubRequestType = {
method: 'retrieve',
params: { ...retrieveParam, ...signatureBuilt },
Expand Down
124 changes: 94 additions & 30 deletions ts/session/apis/snode_api/snodeSignatures.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { FixedSizeUint8Array, GroupPubkeyType } from 'libsession_util_nodejs';
import { getSodiumRenderer } from '../../crypto';
import { StringUtils, UserUtils } from '../../utils';
import { fromHexToArray, fromUInt8ArrayToBase64 } from '../../utils/String';
import { GetNetworkTime } from './getNetworkTime';
import { SnodeNamespaces } from './namespaces';
import { PubKey } from '../../types';
import { toFixedUint8ArrayOfLength } from '../../../types/sqlSharedTypes';

export type SnodeSignatureResult = {
timestamp: number;
// sig_timestamp: number;
signature: string;
pubkey_ed25519: string;
pubkey: string; // this is the x25519 key of the pubkey we are doing the request to (ourself for our swarm usually)
};

export type SnodeGroupSignatureResult = Pick<SnodeSignatureResult, 'signature' | 'timestamp'> & {
pubkey: GroupPubkeyType; // this is the 03 pubkey of the corresponding group
};

async function getSnodeSignatureByHashesParams({
messages,
method,
Expand Down Expand Up @@ -52,50 +59,106 @@ async function getSnodeSignatureByHashesParams({
}
}

async function getSnodeSignatureParams(params: {
pubkey: string;
type SnodeSigParamsShared = {
namespace: number | null | 'all'; // 'all' can be used to clear all namespaces (during account deletion)
method: 'retrieve' | 'store' | 'delete_all';
}): 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 withoutNamespace = `${params.method}${signatureTimestamp}`;
const withNamespace = `${params.method}${namespace}${signatureTimestamp}`;
const verificationData =
namespace === 0
? StringUtils.encode(withoutNamespace, 'utf8')
: StringUtils.encode(withNamespace, 'utf8');
type SnodeSigParamsAdminGroup = SnodeSigParamsShared & {
groupPk: GroupPubkeyType;
privKey: Uint8Array; // our ed25519 key when we are signing with our pubkey
};
type SnodeSigParamsUs = SnodeSigParamsShared & {
pubKey: string;
privKey: FixedSizeUint8Array<64>;
};

const message = new Uint8Array(verificationData);
function isSigParamsForGroupAdmin(
sigParams: SnodeSigParamsAdminGroup | SnodeSigParamsUs
): sigParams is SnodeSigParamsAdminGroup {
const asGr = sigParams as SnodeSigParamsAdminGroup;
return PubKey.isClosedGroupV3(asGr.groupPk) && !!asGr.privKey;
}

const sodium = await getSodiumRenderer();
async function getSnodeShared(params: SnodeSigParamsAdminGroup | SnodeSigParamsUs) {
const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset();
const verificationData = StringUtils.encode(
`${params.method}${params.namespace === 0 ? '' : params.namespace}${signatureTimestamp}`,
'utf8'
);
try {
const signature = sodium.crypto_sign_detached(message, edKeyPrivBytes);
const message = new Uint8Array(verificationData);
const sodium = await getSodiumRenderer();
const signature = sodium.crypto_sign_detached(message, params.privKey as Uint8Array);
const signatureBase64 = fromUInt8ArrayToBase64(signature);

if (isSigParamsForGroupAdmin(params)) {
return {
timestamp: signatureTimestamp,
signature: signatureBase64,
pubkey: params.groupPk,
};
}
return {
// sig_timestamp: signatureTimestamp,
timestamp: signatureTimestamp,
signature: signatureBase64,
pubkey_ed25519: ourEd25519Key.pubKey,
pubkey: params.pubkey,
};
} catch (e) {
window.log.warn('getSnodeSignatureParams failed with: ', e.message);
window.log.warn('getSnodeShared failed with: ', e.message);
throw e;
}
}

async function getSnodeSignatureParamsUs({
method,
namespace = 0,
}: Pick<SnodeSigParamsUs, 'method' | 'namespace'>): Promise<SnodeSignatureResult> {
const ourEd25519Key = await UserUtils.getUserED25519KeyPairBytes();
const ourEd25519PubKey = await UserUtils.getUserED25519KeyPair();

if (!ourEd25519Key || !ourEd25519PubKey) {
const err = `getSnodeSignatureParams "${method}": User has no getUserED25519KeyPairBytes()`;
window.log.warn(err);
throw new Error(err);
}

const edKeyPrivBytes = ourEd25519Key.privKeyBytes;

const lengthCheckedPrivKey = toFixedUint8ArrayOfLength(edKeyPrivBytes, 64);
const sigData = await getSnodeShared({
pubKey: UserUtils.getOurPubKeyStrFromCache(),
method,
namespace,
privKey: lengthCheckedPrivKey,
});

const us = UserUtils.getOurPubKeyStrFromCache();
return {
...sigData,
pubkey_ed25519: ourEd25519PubKey.pubKey,
pubkey: us,
};
}

async function getSnodeGroupSignatureParams({
groupIdentityPrivKey,
groupPk,
method,
namespace = 0,
}: {
groupPk: GroupPubkeyType;
groupIdentityPrivKey: FixedSizeUint8Array<64>;
namespace: SnodeNamespaces;
method: 'retrieve' | 'store';
}): Promise<SnodeGroupSignatureResult> {
const sigData = await getSnodeShared({
pubKey: groupPk,
method,
namespace,
privKey: groupIdentityPrivKey,
});
return { ...sigData, pubkey: groupPk };
}

async function generateUpdateExpirySignature({
shortenOrExtend,
timestamp,
Expand Down Expand Up @@ -136,7 +199,8 @@ async function generateUpdateExpirySignature({
}

export const SnodeSignature = {
getSnodeSignatureParams,
getSnodeSignatureParamsUs,
getSnodeGroupSignatureParams,
getSnodeSignatureByHashesParams,
generateUpdateExpirySignature,
};
5 changes: 3 additions & 2 deletions ts/session/apis/snode_api/storeMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function buildDeleteByHashesSubRequest(
async function storeOnNode(
targetNode: Snode,
params: Array<StoreOnNodeParams>,
toDeleteOnSequence: DeleteByHashesFromNodeParams | null
toDeleteOnSequence: DeleteByHashesFromNodeParams | null,
method: 'batch' | 'sequence'
): Promise<NotEmptyArrayOfBatchResults> {
try {
const subRequests = buildStoreRequests(params, toDeleteOnSequence);
Expand All @@ -56,7 +57,7 @@ async function storeOnNode(
targetNode,
4000,
params[0].pubkey,
toDeleteOnSequence ? 'sequence' : 'batch'
method
);

if (!result || !result.length) {
Expand Down
34 changes: 23 additions & 11 deletions ts/session/conversations/ConversationController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable more/no-then */
import { ConvoVolatileType } from 'libsession_util_nodejs';
import { ConvoVolatileType, GroupPubkeyType } from 'libsession_util_nodejs';
import { isEmpty, isNil } from 'lodash';

import { Data } from '../../data/data';
Expand All @@ -15,24 +15,24 @@ import { getOpenGroupManager } from '../apis/open_group_api/opengroupV2/OpenGrou
import { getSwarmFor } from '../apis/snode_api/snodePool';
import { PubKey } from '../types';

import { getMessageQueue } from '..';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes';
import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups';
import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { OpenGroupUtils } from '../apis/open_group_api/utils';
import { getSwarmPollingInstance } from '../apis/snode_api';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { UserUtils } from '../utils';
import { ConfigurationSync } from '../utils/job_runners/jobs/ConfigurationSyncJob';
import { LibSessionUtil } from '../utils/libsession/libsession_utils';
import { SessionUtilContact } from '../utils/libsession/libsession_utils_contacts';
import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_utils_convo_info_volatile';
import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { getMessageQueue } from '..';
import { getSwarmPollingInstance } from '../apis/snode_api';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { UserUtils } from '../utils';
import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations';
import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups';
import { OpenGroupUtils } from '../apis/open_group_api/utils';

let instance: ConversationController | null;

Expand Down Expand Up @@ -226,7 +226,11 @@ export class ConversationController {
// if we were kicked or sent our left message, we have nothing to do more with that group.
// Just delete everything related to it, not trying to add update message or send a left message.
await this.removeGroupOrCommunityFromDBAndRedux(groupId);
await removeLegacyGroupFromWrappers(groupId);
if (PubKey.isClosedGroupV3(groupId)) {
await remove03GroupFromWrappers(groupId);
} else {
await removeLegacyGroupFromWrappers(groupId);
}

if (!options.fromSyncMessage) {
await ConfigurationSync.queueNewJobIfNeeded();
Expand Down Expand Up @@ -528,6 +532,14 @@ async function removeLegacyGroupFromWrappers(groupId: string) {
await removeAllClosedGroupEncryptionKeyPairs(groupId);
}

async function remove03GroupFromWrappers(groupId: GroupPubkeyType) {
getSwarmPollingInstance().removePubkey(groupId);

await UserGroupsWrapperActions.eraseGroup(groupId);
await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupId);
window.log.warn('remove 03 from metagroup wrapper');
}

async function removeCommunityFromWrappers(conversationId: string) {
if (!conversationId || !OpenGroupUtils.isOpenGroupV2(conversationId)) {
return;
Expand Down

0 comments on commit d89ff59

Please sign in to comment.