You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/session/apis/snode_api/signature/groupSignature.ts

334 lines
10 KiB
TypeScript

import {
GroupPubkeyType,
PubkeyType,
Uint8ArrayLen100,
Uint8ArrayLen64,
UserGroupsGet,
WithGroupPubkey,
} from 'libsession_util_nodejs';
import { isEmpty, isString } from 'lodash';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { GroupUpdateInviteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage';
import { GroupUpdatePromoteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdatePromoteMessage';
import { StringUtils, UserUtils } from '../../../utils';
import { fromUInt8ArrayToBase64, stringToUint8Array } from '../../../utils/String';
import { PreConditionFailed } from '../../../utils/errors';
import { SnodeNamespacesGroup } from '../namespaces';
import { SignedGroupHashesParams } from '../types';
import { WithMessagesHashes, WithShortenOrExtend } from '../../../types/with';
import { SignatureShared } from './signatureShared';
import { SnodeSignatureResult } from './snodeSignatures';
import { getSodiumRenderer } from '../../../crypto';
import { NetworkTime } from '../../../../util/NetworkTime';
async function getGroupInviteMessage({
groupName,
member,
secretKey,
groupPk,
}: {
member: PubkeyType;
groupName: string;
/**
* secretKey, length of 64 bytes
*/
secretKey: Uint8ArrayLen64;
groupPk: GroupPubkeyType;
}) {
const sodium = await getSodiumRenderer();
const createAtNetworkTimestamp = NetworkTime.now();
if (UserUtils.isUsFromCache(member)) {
throw new Error('getGroupInviteMessage: we cannot invite ourselves');
}
// Note: as the signature is built with the timestamp here, we cannot override the timestamp later on the sending pipeline
const adminSignature = sodium.crypto_sign_detached(
stringToUint8Array(`INVITE${member}${createAtNetworkTimestamp}`),
secretKey
);
const memberAuthData = await MetaGroupWrapperActions.makeSwarmSubAccount(groupPk, member);
const invite = new GroupUpdateInviteMessage({
groupName,
groupPk,
createAtNetworkTimestamp,
adminSignature,
memberAuthData,
expirationType: 'unknown', // an invite is not expiring
expireTimer: 0,
});
return invite;
}
async function getGroupPromoteMessage({
member,
secretKey,
groupPk,
groupName,
}: {
member: PubkeyType;
/**
* secretKey, length of 64 bytes
*/
secretKey: Uint8ArrayLen64;
groupPk: GroupPubkeyType;
groupName: string;
}) {
const createAtNetworkTimestamp = NetworkTime.now();
if (UserUtils.isUsFromCache(member)) {
throw new Error('getGroupPromoteMessage: we cannot promote ourselves');
}
const msg = new GroupUpdatePromoteMessage({
groupPk,
createAtNetworkTimestamp,
groupIdentitySeed: secretKey.slice(0, 32), // the seed is the first 32 bytes of the secretkey
expirationType: 'unknown', // a promote message is not expiring
expireTimer: 0,
groupName,
});
return msg;
}
type ParamsShared = {
groupPk: GroupPubkeyType;
namespace: SnodeNamespacesGroup;
method: 'retrieve' | 'store' | 'delete_all';
};
type SigParamsAdmin = ParamsShared & {
groupIdentityPrivKey: Uint8ArrayLen64;
};
type SigParamsSubaccount = ParamsShared & {
authData: Uint8ArrayLen100;
};
export type SigResultAdmin = Pick<SnodeSignatureResult, 'signature' | 'timestamp'> & {
pubkey: GroupPubkeyType; // this is the 03 pubkey of the corresponding group
};
export type SigResultSubAccount = SigResultAdmin & {
subaccount: string;
subaccount_sig: string;
};
async function getSnodeGroupSubAccountSignatureParams(
params: SigParamsSubaccount
): Promise<SigResultSubAccount> {
const { signatureTimestamp, toSign } =
SignatureShared.getVerificationDataForStoreRetrieve(params);
const sigResult = await MetaGroupWrapperActions.swarmSubaccountSign(
params.groupPk,
toSign,
params.authData
);
return {
...sigResult,
timestamp: signatureTimestamp,
pubkey: params.groupPk,
};
}
async function getSnodeGroupAdminSignatureParams(params: SigParamsAdmin): Promise<SigResultAdmin> {
const sigData = await SignatureShared.getSnodeSignatureShared({
pubKey: params.groupPk,
method: params.method,
namespace: params.namespace,
privKey: params.groupIdentityPrivKey,
});
return { ...sigData, pubkey: params.groupPk };
}
export type GroupDetailsNeededForSignature = Pick<
UserGroupsGet,
'pubkeyHex' | 'authData' | 'secretKey'
>;
type StoreOrRetrieve = { method: 'store' | 'retrieve'; namespace: SnodeNamespacesGroup };
type DeleteHashes = { method: 'delete'; hashes: Array<string> };
type DeleteAllNonConfigs = { method: 'delete_all'; namespace: SnodeNamespacesGroup };
async function getSnodeGroupSignature({
group,
...args
}: {
group: GroupDetailsNeededForSignature | null;
} & (StoreOrRetrieve | DeleteHashes | DeleteAllNonConfigs)): Promise<
SigResultSubAccount | SigResultAdmin
> {
if (!group) {
throw new Error(`getSnodeGroupSignature: we need GroupDetailsNeededForSignature`);
}
const { pubkeyHex: groupPk, secretKey, authData } = group;
const groupSecretKey = secretKey && !isEmpty(secretKey) ? secretKey : null;
const groupAuthData = authData && !isEmpty(authData) ? authData : null;
if (args.method === 'delete_all' && isEmpty(secretKey)) {
throw new Error('getSnodeGroupSignature: delete_all needs an adminSecretKey');
}
if (groupSecretKey) {
if (args.method === 'delete') {
return getGroupSignatureByHashesParams({
groupPk,
method: args.method,
messagesHashes: args.hashes,
group,
});
}
return getSnodeGroupAdminSignatureParams({
method: args.method,
namespace: args.namespace,
groupPk,
groupIdentityPrivKey: groupSecretKey,
});
}
if (groupAuthData) {
if (args.method === 'delete') {
return getGroupSignatureByHashesParams({
groupPk,
method: args.method,
messagesHashes: args.hashes,
group,
});
}
return getSnodeGroupSubAccountSignatureParams({
groupPk,
method: args.method,
namespace: args.namespace,
authData: groupAuthData,
});
}
throw new Error(`getSnodeGroupSignature: needs either groupSecretKey or authData`);
}
async function signDataWithAdminSecret(
verificationString: string | Uint8Array,
group: Pick<GroupDetailsNeededForSignature, 'secretKey'>
) {
const verificationData = isString(verificationString)
? StringUtils.encode(verificationString, 'utf8')
: verificationString;
const message = new Uint8Array(verificationData);
if (!group) {
throw new Error('signDataWithAdminSecret group was not found');
}
const { secretKey } = group;
const groupSecretKey = secretKey && !isEmpty(secretKey) ? secretKey : null;
if (!groupSecretKey) {
throw new Error('groupSecretKey is empty');
}
const sodium = await getSodiumRenderer();
return {
signature: fromUInt8ArrayToBase64(sodium.crypto_sign_detached(message, groupSecretKey)),
};
}
// this is kind of duplicated with `generateUpdateExpirySignature`, but needs to use the authData when secretKey is not available
async function generateUpdateExpiryGroupSignature({
shortenOrExtend,
expiryMs,
messagesHashes,
group,
}: WithMessagesHashes &
WithShortenOrExtend & {
group: GroupDetailsNeededForSignature | null;
expiryMs: number;
}) {
if (!group || isEmpty(group.pubkeyHex)) {
throw new PreConditionFailed('generateUpdateExpiryGroupSignature groupPk is empty');
}
// "expire" || ShortenOrExtend || expiry || messages[0] || ... || messages[N]
const verificationString = `expire${shortenOrExtend}${expiryMs}${messagesHashes.join('')}`;
const verificationData = StringUtils.encode(verificationString, 'utf8');
const message = new Uint8Array(verificationData);
if (!group) {
throw new Error('generateUpdateExpiryGroupSignature group was not found');
}
const { pubkeyHex: groupPk, secretKey, authData } = group;
const groupSecretKey = secretKey && !isEmpty(secretKey) ? secretKey : null;
const groupAuthData = authData && !isEmpty(authData) ? authData : null;
if (!groupSecretKey && !groupAuthData) {
throw new Error(`retrieveRequestForGroup: needs either groupSecretKey or authData`);
}
const sodium = await getSodiumRenderer();
const shared = { expiry: expiryMs, pubkey: groupPk }; // expiry and the other fields come from what the expire endpoint expects
if (groupSecretKey) {
return {
signature: fromUInt8ArrayToBase64(sodium.crypto_sign_detached(message, groupSecretKey)),
...shared,
};
}
if (!groupAuthData) {
// typescript should see this already but doesn't, so let's enforce it.
throw new Error(
`retrieveRequestForGroup: needs either groupSecretKey or authData but both are empty`
);
}
const subaccountSign = await MetaGroupWrapperActions.swarmSubaccountSign(
groupPk,
message,
groupAuthData
);
return {
...subaccountSign,
...shared,
};
}
async function getGroupSignatureByHashesParams({
messagesHashes,
method,
group,
}: WithMessagesHashes &
WithGroupPubkey & {
method: 'delete';
group: GroupDetailsNeededForSignature;
}): Promise<SignedGroupHashesParams> {
const verificationString = `${method}${messagesHashes.join('')}`;
const message = new Uint8Array(StringUtils.encode(verificationString, 'utf8'));
const signatureTimestamp = NetworkTime.now();
const sodium = await getSodiumRenderer();
try {
if (group.secretKey && !isEmpty(group.secretKey)) {
const signature = sodium.crypto_sign_detached(message, group.secretKey);
const signatureBase64 = fromUInt8ArrayToBase64(signature);
return {
signature: signatureBase64,
pubkey: group.pubkeyHex,
messages: messagesHashes,
timestamp: signatureTimestamp,
};
}
throw new Error('getSnodeGroupSignatureByHashesParams needs admin secretKey set');
} catch (e) {
window.log.warn('getSnodeGroupSignatureByHashesParams failed with: ', e.message);
throw e;
}
}
export const SnodeGroupSignature = {
generateUpdateExpiryGroupSignature,
getGroupInviteMessage,
getGroupPromoteMessage,
getSnodeGroupSignature,
getGroupSignatureByHashesParams,
signDataWithAdminSecret,
};