From b8876ebbfe669b73f64b754c16cfab9e40bf3d23 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 26 Oct 2023 14:29:38 +1100 Subject: [PATCH] feat: add subaccount auth --- .eslintrc.js | 3 + .../message-content/MessageContent.tsx | 1 - .../leftpane/overlay/OverlayClosedGroup.tsx | 1 + ts/receiver/closedGroups.ts | 2 +- ts/receiver/dataMessage.ts | 21 +- ts/receiver/groupv2/handleGroupV2Message.ts | 90 +++++++ ts/session/apis/snode_api/SNodeAPI.ts | 2 +- .../apis/snode_api/SnodeRequestTypes.ts | 33 ++- ts/session/apis/snode_api/expire.ts | 2 +- ts/session/apis/snode_api/retrieveRequest.ts | 53 ++--- .../snode_api/signature/groupSignature.ts | 225 ++++++++++++++++++ .../snode_api/signature/signatureShared.ts | 75 ++++++ .../{ => signature}/snodeSignatures.ts | 106 +++------ ts/session/apis/snode_api/types.ts | 5 + ts/session/crypto/group/groupSignature.ts | 51 ---- ts/session/sending/MessageSender.ts | 39 +-- ts/session/utils/TaskWithTimeout.ts | 1 - ts/session/utils/User.ts | 4 +- ts/state/ducks/groups.ts | 8 +- .../unit/crypto/SnodeSignatures_test.ts | 187 ++++++++++++--- .../browser/libsession_worker_interface.ts | 12 + 21 files changed, 692 insertions(+), 229 deletions(-) create mode 100644 ts/receiver/groupv2/handleGroupV2Message.ts create mode 100644 ts/session/apis/snode_api/signature/groupSignature.ts create mode 100644 ts/session/apis/snode_api/signature/signatureShared.ts rename ts/session/apis/snode_api/{ => signature}/snodeSignatures.ts (70%) delete mode 100644 ts/session/crypto/group/groupSignature.ts diff --git a/.eslintrc.js b/.eslintrc.js index ff58cdd07..84ad7ccef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -56,6 +56,9 @@ module.exports = { // it helps readability to put public API at top, 'no-use-before-define': 'off', + // we need them with code in WIP sometimes, and it doesn't do any harm + 'no-useless-return': 'off', + // useful for unused or internal fields 'no-underscore-dangle': 'off', diff --git a/ts/components/conversation/message/message-content/MessageContent.tsx b/ts/components/conversation/message/message-content/MessageContent.tsx index 61724294b..c89495bc4 100644 --- a/ts/components/conversation/message/message-content/MessageContent.tsx +++ b/ts/components/conversation/message/message-content/MessageContent.tsx @@ -40,7 +40,6 @@ function onClickOnMessageInnerContainer(event: React.MouseEvent) // User clicked on message body const target = event.target as HTMLDivElement; if (target.className === 'text-selectable' || window.contextMenuShown) { - // eslint-disable-next-line no-useless-return return; } } diff --git a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx index 35c5ae8e7..f9c54fdbd 100644 --- a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx +++ b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx @@ -34,6 +34,7 @@ const StyledMemberListNoContacts = styled.div` const StyledGroupMemberListContainer = styled.div` padding: 2px 0px; width: 100%; + min-height: 40px; max-height: 400px; overflow-y: auto; border-top: 1px solid var(--border-color); diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 2ec248783..d94ca7799 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -75,7 +75,7 @@ export async function removeAllClosedGroupEncryptionKeyPairs(groupPubKey: string await Data.removeAllClosedGroupEncryptionKeyPairs(groupPubKey); } -export async function handleClosedGroupControlMessage( +export async function handleLegacyClosedGroupControlMessage( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage ) { diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 835494636..a64291a67 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -11,7 +11,7 @@ import { ConversationModel } from '../models/conversation'; import { ConvoHub } from '../session/conversations'; import { PubKey } from '../session/types'; import { StringUtils, UserUtils } from '../session/utils'; -import { handleClosedGroupControlMessage } from './closedGroups'; +import { handleLegacyClosedGroupControlMessage } from './closedGroups'; import { handleMessageJob, toRegularMessage } from './queuedJob'; import { ConversationTypeEnum } from '../models/conversationAttributes'; @@ -25,6 +25,7 @@ import { isUsFromCache } from '../session/utils/User'; import { Action, Reaction } from '../types/Reaction'; import { toLogFormat } from '../types/attachments/Errors'; import { Reactions } from '../util/reactions'; +import { GroupV2Receiver } from './groupv2/handleGroupV2Message'; function cleanAttachment(attachment: any) { return { @@ -43,7 +44,7 @@ function cleanAttachments(decrypted: SignalService.DataMessage) { // Here we go from binary to string/base64 in all AttachmentPointer digest/key fields - // we do not care about group on Session + // we do not care about group on Session Desktop decrypted.group = null; @@ -150,7 +151,6 @@ export function cleanIncomingDataMessage( * * envelope.source is our pubkey (our other device has the same pubkey as us) * * dataMessage.syncTarget is either the group public key OR the private conversation this message is about. */ - export async function handleSwarmDataMessage( envelope: EnvelopePlus, sentAtTimestamp: number, @@ -161,9 +161,20 @@ export async function handleSwarmDataMessage( window.log.info('handleSwarmDataMessage'); const cleanDataMessage = cleanIncomingDataMessage(rawDataMessage, envelope); - // we handle group updates from our other devices in handleClosedGroupControlMessage() + + if (cleanDataMessage.groupUpdateMessage) { + await GroupV2Receiver.handleGroupUpdateMessage({ + envelopeTimestamp: sentAtTimestamp, + updateMessage: rawDataMessage.groupUpdateMessage as SignalService.GroupUpdateMessage, + }); + // Groups update should always be able to be decrypted as we get the keys before trying to decrypt them. + // If decryption failed once, it will keep failing, so no need to keep it in the cache. + await removeFromCache({ id: envelope.id }); + return; + } + // we handle legacy group updates from our other devices in handleLegacyClosedGroupControlMessage() if (cleanDataMessage.closedGroupControlMessage) { - await handleClosedGroupControlMessage( + await handleLegacyClosedGroupControlMessage( envelope, cleanDataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage ); diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts new file mode 100644 index 000000000..20c4fb03f --- /dev/null +++ b/ts/receiver/groupv2/handleGroupV2Message.ts @@ -0,0 +1,90 @@ +import { isEmpty } from 'lodash'; +import { ConversationTypeEnum } from '../../models/conversationAttributes'; +import { HexString } from '../../node/hexStrings'; +import { SignalService } from '../../protobuf'; +import { getSwarmPollingInstance } from '../../session/apis/snode_api'; +import { ConvoHub } from '../../session/conversations'; +import { PubKey } from '../../session/types'; +import { UserUtils } from '../../session/utils'; +import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils'; +import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; +import { + MetaGroupWrapperActions, + UserGroupsWrapperActions, +} from '../../webworker/workers/browser/libsession_worker_interface'; + +type WithEnvelopeTimestamp = { envelopeTimestamp: number }; + +type GroupInviteDetails = { + inviteMessage: SignalService.GroupUpdateInviteMessage; +} & WithEnvelopeTimestamp; + +type GroupUpdateDetails = { + updateMessage: SignalService.GroupUpdateMessage; +} & WithEnvelopeTimestamp; + +async function handleGroupInviteMessage({ inviteMessage, envelopeTimestamp }: GroupInviteDetails) { + if (!PubKey.isClosedGroupV2(inviteMessage.groupSessionId)) { + // invite to a group which has not a 03 prefix, we can just drop it. + return; + } + // TODO verify sig invite adminSignature + const convo = await ConvoHub.use().getOrCreateAndWait( + inviteMessage.groupSessionId, + ConversationTypeEnum.GROUPV2 + ); + convo.set({ + active_at: envelopeTimestamp, + didApproveMe: true, + }); + + if (inviteMessage.name && isEmpty(convo.getRealSessionUsername())) { + convo.set({ + displayNameInProfile: inviteMessage.name, + }); + } + await convo.commit(); + + let found = await UserGroupsWrapperActions.getGroup(inviteMessage.groupSessionId); + if (!found) { + found = { + authData: null, + joinedAtSeconds: Date.now(), + name: inviteMessage.name, + priority: 0, + pubkeyHex: inviteMessage.groupSessionId, + secretKey: null, + }; + } + // not sure if we should drop it, or set it again? They should be the same anyway + found.authData = inviteMessage.memberAuthData; + + const userEd25519Secretkey = (await UserUtils.getUserED25519KeyPairBytes()).privKeyBytes; + await UserGroupsWrapperActions.setGroup(found); + await MetaGroupWrapperActions.init(inviteMessage.groupSessionId, { + metaDumped: null, + groupEd25519Secretkey: null, + userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64).buffer, + groupEd25519Pubkey: toFixedUint8ArrayOfLength( + HexString.fromHexString(inviteMessage.groupSessionId.slice(2)), + 32 + ).buffer, + }); + await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache()); + + // TODO use the pending so we actually don't start polling here unless it is not in the pending state. + // once everything is ready, start polling using that authData to get the keys, members, details of that group, and its messages. + getSwarmPollingInstance().addGroupId(inviteMessage.groupSessionId); +} + +async function handleGroupUpdateMessage(args: GroupUpdateDetails) { + if (args.updateMessage.inviteMessage) { + await handleGroupInviteMessage({ + inviteMessage: args.updateMessage.inviteMessage as SignalService.GroupUpdateInviteMessage, + ...args, + }); + return; + } +} + +export const GroupV2Receiver = { handleGroupUpdateMessage }; diff --git a/ts/session/apis/snode_api/SNodeAPI.ts b/ts/session/apis/snode_api/SNodeAPI.ts index e3628221a..6d54ca08e 100644 --- a/ts/session/apis/snode_api/SNodeAPI.ts +++ b/ts/session/apis/snode_api/SNodeAPI.ts @@ -8,8 +8,8 @@ import { ed25519Str } from '../../onions/onionPath'; import { StringUtils, UserUtils } from '../../utils'; import { fromBase64ToArray, fromHexToArray } from '../../utils/String'; import { doSnodeBatchRequest } from './batchRequest'; +import { SnodeSignature } from './signature/snodeSignatures'; import { getSwarmFor } from './snodePool'; -import { SnodeSignature } from './snodeSignatures'; export const ERROR_CODE_NO_CONNECT = 'ENETUNREACH: No network connection.'; diff --git a/ts/session/apis/snode_api/SnodeRequestTypes.ts b/ts/session/apis/snode_api/SnodeRequestTypes.ts index df1aba59a..5d51352fd 100644 --- a/ts/session/apis/snode_api/SnodeRequestTypes.ts +++ b/ts/session/apis/snode_api/SnodeRequestTypes.ts @@ -49,6 +49,16 @@ export type RetrieveGroupAdminSubRequestType = WithRetrieveMethod & { params: { signature: string; namespace: SnodeNamespacesGroup; + } & RetrieveAlwaysNeeded & + WithMaxCountSize; +}; + +export type RetrieveGroupSubAccountSubRequestType = WithRetrieveMethod & { + params: { + namespace: SnodeNamespacesGroup; + signature: string; + subaccount: string; + subaccount_sig: string; } & RetrieveAlwaysNeeded & WithMaxCountSize & WithPubkeyAsGroupPubkey; @@ -59,7 +69,8 @@ export type RetrieveSubRequestType = | RetrievePubkeySubRequestType | RetrieveGroupAdminSubRequestType | UpdateExpiryOnNodeUserSubRequest - | UpdateExpiryOnNodeGroupSubRequest; + | UpdateExpiryOnNodeGroupSubRequest + | RetrieveGroupSubAccountSubRequestType; /** * OXEND_REQUESTS @@ -91,7 +102,7 @@ export type GetServiceNodesSubRequest = { }; }; -export type StoreOnNodeParams = { +type StoreOnNodeNormalParams = { pubkey: string; ttl: number; timestamp: number; @@ -102,6 +113,19 @@ export type StoreOnNodeParams = { pubkey_ed25519?: string; }; +type StoreOnNodeSubAccountParams = Pick< + StoreOnNodeNormalParams, + 'data' | 'namespace' | 'ttl' | 'timestamp' +> & { + pubkey: GroupPubkeyType; + subaccount: string; + subaccount_sig: string; + namespace: SnodeNamespaces.ClosedGroupMessages; // this can only be this one, subaccounts holder can not post to something else atm + signature: string; // signature is mandatory for subaccount +}; + +export type StoreOnNodeParams = StoreOnNodeNormalParams | StoreOnNodeSubAccountParams; + export type StoreOnNodeParamsNoSig = Pick< StoreOnNodeParams, 'pubkey' | 'ttl' | 'timestamp' | 'ttl' | 'namespace' @@ -131,7 +155,10 @@ type StoreOnNodeUserConfig = StoreOnNodeShared & { export type StoreOnNodeData = StoreOnNodeGroupConfig | StoreOnNodeUserConfig; -export type StoreOnNodeSubRequest = { method: 'store'; params: StoreOnNodeParams }; +export type StoreOnNodeSubRequest = { + method: 'store'; + params: StoreOnNodeParams | StoreOnNodeSubAccountParams; +}; export type NetworkTimeSubRequest = { method: 'info'; params: object }; type DeleteSigParameters = { diff --git a/ts/session/apis/snode_api/expire.ts b/ts/session/apis/snode_api/expire.ts index ffdd062a2..7d1a8e8be 100644 --- a/ts/session/apis/snode_api/expire.ts +++ b/ts/session/apis/snode_api/expire.ts @@ -8,8 +8,8 @@ import { EmptySwarmError } from '../../utils/errors'; import { UpdateExpireNodeUserParams } from './SnodeRequestTypes'; import { doSnodeBatchRequest } from './batchRequest'; import { GetNetworkTime } from './getNetworkTime'; +import { SnodeSignature } from './signature/snodeSignatures'; import { getSwarmFor } from './snodePool'; -import { SnodeSignature } from './snodeSignatures'; async function verifySignature({ pubkey, diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 832eecae8..5d3f61f4a 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -1,5 +1,5 @@ -import { isEmpty, isNil, omit } from 'lodash'; import { GroupPubkeyType } from 'libsession_util_nodejs'; +import { omit } from 'lodash'; import { Snode } from '../../../data/data'; import { updateIsOnline } from '../../../state/ducks/onion'; import { doSnodeBatchRequest } from './batchRequest'; @@ -12,14 +12,15 @@ import { PubKey } from '../../types'; import { UserUtils } from '../../utils'; import { RetrieveGroupAdminSubRequestType, + RetrieveGroupSubAccountSubRequestType, RetrieveLegacyClosedGroupSubRequestType, RetrieveSubRequestType, UpdateExpiryOnNodeGroupSubRequest, UpdateExpiryOnNodeUserSubRequest, } from './SnodeRequestTypes'; -import { SnodeSignature } from './snodeSignatures'; +import { SnodeGroupSignature } from './signature/groupSignature'; +import { SnodeSignature } from './signature/snodeSignatures'; import { RetrieveMessagesResultsBatched, RetrieveMessagesResultsContent } from './types'; -import { PreConditionFailed } from '../../utils/errors'; type RetrieveParams = { pubkey: string; @@ -102,28 +103,23 @@ async function retrieveRequestForGroup({ 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 PreConditionFailed( - `retrieveRequestForGroup: failed to find group admin secret key in wrapper` - ); - } - const signatureBuilt = await SnodeSignature.getSnodeGroupSignatureParams({ - ...retrieveParam, + + const sigResult = await SnodeGroupSignature.getSnodeGroupSignature({ + method: 'retrieve', namespace, - method: 'retrieve' as const, - groupPk, - groupIdentityPrivKey: groupSecretKey, + group, }); - const retrieveGroup = { - ...retrieveParam, - ...signatureBuilt, - namespace, - }; - const retrieveParamsGroup: RetrieveGroupAdminSubRequestType = { - method: 'retrieve' as const, - params: retrieveGroup, + const retrieveParamsGroup: + | RetrieveGroupSubAccountSubRequestType + | RetrieveGroupAdminSubRequestType = { + method: 'retrieve', + params: { + ...retrieveParam, + ...sigResult, + + namespace, + }, }; return retrieveParamsGroup; @@ -188,17 +184,12 @@ async function buildRetrieveRequest( retrieveRequestsParams.push(expireParams); } else if (PubKey.isClosedGroupV2(pubkey)) { const group = await UserGroupsWrapperActions.getGroup(pubkey); - if (!group || !group.secretKey || isEmpty(group.secretKey)) { - throw new PreConditionFailed( - 'generateUpdateExpiryGroupSignature only handles when the group is in the userwrapper currently and we have the adminkey' - ); - } - const signResult = await SnodeSignature.generateUpdateExpiryGroupSignature({ + + const signResult = await SnodeGroupSignature.generateUpdateExpiryGroupSignature({ shortenOrExtend: '', timestamp: expiry, messagesHashes: configHashesToBump, - groupPk: pubkey, - groupPrivKey: group.secretKey, + group, }); const expireParams: UpdateExpiryOnNodeGroupSubRequest = { @@ -206,7 +197,7 @@ async function buildRetrieveRequest( params: { messages: configHashesToBump, expiry, - signature: signResult.signature, + ...signResult, pubkey, }, }; diff --git a/ts/session/apis/snode_api/signature/groupSignature.ts b/ts/session/apis/snode_api/signature/groupSignature.ts new file mode 100644 index 000000000..933e8b082 --- /dev/null +++ b/ts/session/apis/snode_api/signature/groupSignature.ts @@ -0,0 +1,225 @@ +import { + GroupMemberGet, + GroupPubkeyType, + Uint8ArrayLen100, + Uint8ArrayLen64, + UserGroupsGet, +} from 'libsession_util_nodejs'; +import { compact, isEmpty } from 'lodash'; +import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; +import { getSodiumRenderer } from '../../../crypto/MessageEncrypter'; +import { GroupUpdateInviteMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage'; +import { StringUtils, UserUtils } from '../../../utils'; +import { fromUInt8ArrayToBase64 } from '../../../utils/String'; +import { PreConditionFailed } from '../../../utils/errors'; +import { GetNetworkTime } from '../getNetworkTime'; +import { SnodeNamespacesGroup } from '../namespaces'; +import { WithMessagesHashes, WithShortenOrExtend, WithTimestamp } from '../types'; +import { SignatureShared } from './signatureShared'; +import { SnodeSignatureResult } from './snodeSignatures'; + +async function getGroupInvitesMessages({ + groupName, + membersFromWrapper, + secretKey, + groupPk, +}: { + membersFromWrapper: Array; + groupName: string; + secretKey: Uint8ArrayLen64; // len 64 + groupPk: GroupPubkeyType; +}) { + const sodium = await getSodiumRenderer(); + const timestamp = GetNetworkTime.getNowWithNetworkOffset(); + + const inviteDetails = compact( + await Promise.all( + membersFromWrapper.map(async ({ pubkeyHex: member }) => { + if (UserUtils.isUsFromCache(member)) { + return null; + } + const tosign = `INVITE${member}${timestamp}`; + + // 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(tosign, secretKey); + const memberAuthData = await MetaGroupWrapperActions.makeSwarmSubAccount(groupPk, member); + + const invite = new GroupUpdateInviteMessage({ + groupName, + groupPk, + timestamp, + adminSignature, + memberAuthData, + }); + + return { member, invite }; + }) + ) + ); + return inviteDetails; +} + +type ParamsShared = { + groupPk: GroupPubkeyType; + namespace: SnodeNamespacesGroup; + method: 'retrieve' | 'store'; +}; + +type SigParamsAdmin = ParamsShared & { + groupIdentityPrivKey: Uint8ArrayLen64; +}; + +type SigParamsSubaccount = ParamsShared & { + authData: Uint8ArrayLen100; +}; + +export type SigResultAdmin = Pick & { + pubkey: GroupPubkeyType; // this is the 03 pubkey of the corresponding group +}; + +export type SigResultSubAccount = SigResultAdmin & { + subaccount: string; + subaccount_sig: string; +}; + +async function getSnodeGroupSignatureParams(params: SigParamsAdmin): Promise; +async function getSnodeGroupSignatureParams( + params: SigParamsSubaccount +): Promise; + +async function getSnodeGroupSignatureParams( + params: SigParamsAdmin | SigParamsSubaccount +): Promise { + if ('groupIdentityPrivKey' in params) { + return getSnodeGroupAdminSignatureParams(params); + } + return getSnodeGroupSubAccountSignatureParams(params); +} + +async function getSnodeGroupSubAccountSignatureParams( + params: SigParamsSubaccount +): Promise { + 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 { + const sigData = await SignatureShared.getSnodeSignatureShared({ + pubKey: params.groupPk, + method: params.method, + namespace: params.namespace, + privKey: params.groupIdentityPrivKey, + }); + return { ...sigData, pubkey: params.groupPk }; +} + +type GroupDetailsNeededForSignature = Pick; + +async function getSnodeGroupSignature({ + group, + method, + namespace, +}: { + group: GroupDetailsNeededForSignature | null; + method: 'store' | 'retrieve'; + namespace: SnodeNamespacesGroup; +}) { + if (!group) { + throw new Error(`getSnodeGroupSignature: did not find group in wrapper`); + } + const { pubkeyHex: groupPk, secretKey, authData } = group; + + const groupSecretKey = secretKey && !isEmpty(secretKey) ? secretKey : null; + const groupAuthData = authData && !isEmpty(authData) ? authData : null; + + if (groupSecretKey) { + return getSnodeGroupSignatureParams({ + method, + namespace, + groupPk, + groupIdentityPrivKey: groupSecretKey, + }); + } + if (groupAuthData) { + const subAccountSign = await getSnodeGroupSignatureParams({ + groupPk, + method, + namespace, + authData: groupAuthData, + }); + return subAccountSign; + } + throw new Error(`getSnodeGroupSignature: needs either groupSecretKey or authData`); +} + +// this is kind of duplicated with `generateUpdateExpirySignature`, but needs to use the authData when secretKey is not available +async function generateUpdateExpiryGroupSignature({ + shortenOrExtend, + timestamp, + messagesHashes, + group, +}: WithMessagesHashes & + WithShortenOrExtend & + WithTimestamp & { + group: GroupDetailsNeededForSignature | null; + }) { + if (!group || isEmpty(group.pubkeyHex)) { + throw new PreConditionFailed('generateUpdateExpiryGroupSignature groupPk is empty'); + } + + // "expire" || ShortenOrExtend || expiry || messages[0] || ... || messages[N] + const verificationString = `expire${shortenOrExtend}${timestamp}${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 = { timestamp, pubkey: groupPk }; + + if (groupSecretKey) { + return { + signature: fromUInt8ArrayToBase64(sodium.crypto_sign_detached(message, groupSecretKey)), + ...shared, + }; + } + + if (groupAuthData) { + const subaccountSign = await MetaGroupWrapperActions.swarmSubaccountSign( + groupPk, + message, + groupAuthData + ); + return { + ...subaccountSign, + ...shared, + }; + } + + throw new Error(`generateUpdateExpiryGroupSignature: needs either groupSecretKey or authData`); +} + +export const SnodeGroupSignature = { + generateUpdateExpiryGroupSignature, + getGroupInvitesMessages, + getSnodeGroupSignature, +}; diff --git a/ts/session/apis/snode_api/signature/signatureShared.ts b/ts/session/apis/snode_api/signature/signatureShared.ts new file mode 100644 index 000000000..51ae73a50 --- /dev/null +++ b/ts/session/apis/snode_api/signature/signatureShared.ts @@ -0,0 +1,75 @@ +import { GroupPubkeyType, Uint8ArrayLen100, Uint8ArrayLen64 } from 'libsession_util_nodejs'; +import { isEmpty } from 'lodash'; +import { getSodiumRenderer } from '../../../crypto'; +import { PubKey } from '../../../types'; +import { StringUtils } from '../../../utils'; +import { fromUInt8ArrayToBase64 } from '../../../utils/String'; +import { GetNetworkTime } from '../getNetworkTime'; + +export type SnodeSigParamsShared = { + namespace: number | null | 'all'; // 'all' can be used to clear all namespaces (during account deletion) + method: 'retrieve' | 'store' | 'delete_all'; +}; + +export type SnodeSigParamsAdminGroup = SnodeSigParamsShared & { + groupPk: GroupPubkeyType; + privKey: Uint8ArrayLen64; // len 64 +}; + +export type SnodeSigParamsSubAccount = SnodeSigParamsShared & { + groupPk: GroupPubkeyType; + authData: Uint8ArrayLen100; // len 100 +}; + +export type SnodeSigParamsUs = SnodeSigParamsShared & { + pubKey: string; + privKey: Uint8ArrayLen64; // len 64 +}; + +function getVerificationDataForStoreRetrieve(params: SnodeSigParamsShared) { + const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset(); + const verificationData = StringUtils.encode( + `${params.method}${params.namespace === 0 ? '' : params.namespace}${signatureTimestamp}`, + 'utf8' + ); + return { + toSign: new Uint8Array(verificationData), + signatureTimestamp, + }; +} + +function isSigParamsForGroupAdmin( + sigParams: SnodeSigParamsAdminGroup | SnodeSigParamsUs | SnodeSigParamsSubAccount +): sigParams is SnodeSigParamsAdminGroup { + const asGr = sigParams as SnodeSigParamsAdminGroup; + return PubKey.isClosedGroupV2(asGr.groupPk) && !isEmpty(asGr.privKey); +} + +async function getSnodeSignatureShared(params: SnodeSigParamsAdminGroup | SnodeSigParamsUs) { + const { signatureTimestamp, toSign } = getVerificationDataForStoreRetrieve(params); + + try { + const sodium = await getSodiumRenderer(); + const signature = sodium.crypto_sign_detached(toSign, params.privKey); + const signatureBase64 = fromUInt8ArrayToBase64(signature); + if (isSigParamsForGroupAdmin(params)) { + return { + timestamp: signatureTimestamp, + signature: signatureBase64, + pubkey: params.groupPk, + }; + } + return { + timestamp: signatureTimestamp, + signature: signatureBase64, + }; + } catch (e) { + window.log.warn('getSnodeShared failed with: ', e.message); + throw e; + } +} + +export const SignatureShared = { + getSnodeSignatureShared, + getVerificationDataForStoreRetrieve, +}; diff --git a/ts/session/apis/snode_api/snodeSignatures.ts b/ts/session/apis/snode_api/signature/snodeSignatures.ts similarity index 70% rename from ts/session/apis/snode_api/snodeSignatures.ts rename to ts/session/apis/snode_api/signature/snodeSignatures.ts index 64ae7028f..5c139d8f1 100644 --- a/ts/session/apis/snode_api/snodeSignatures.ts +++ b/ts/session/apis/snode_api/signature/snodeSignatures.ts @@ -1,15 +1,13 @@ -import { GroupPubkeyType } from 'libsession_util_nodejs'; +import { GroupPubkeyType, Uint8ArrayLen100, Uint8ArrayLen64 } from 'libsession_util_nodejs'; import { isEmpty } from 'lodash'; -import { toFixedUint8ArrayOfLength } from '../../../types/sqlSharedTypes'; -import { getSodiumRenderer } from '../../crypto'; -import { PubKey } from '../../types'; -import { StringUtils, UserUtils } from '../../utils'; -import { fromHexToArray, fromUInt8ArrayToBase64 } from '../../utils/String'; -import { PreConditionFailed } from '../../utils/errors'; -import { GetNetworkTime } from './getNetworkTime'; -import { SnodeNamespacesGroup } from './namespaces'; - -type WithTimestamp = { timestamp: number }; +import { toFixedUint8ArrayOfLength } from '../../../../types/sqlSharedTypes'; +import { getSodiumRenderer } from '../../../crypto'; +import { PubKey } from '../../../types'; +import { StringUtils, UserUtils } from '../../../utils'; +import { fromHexToArray, fromUInt8ArrayToBase64 } from '../../../utils/String'; +import { PreConditionFailed } from '../../../utils/errors'; +import { GetNetworkTime } from '../getNetworkTime'; +import { WithMessagesHashes, WithShortenOrExtend, WithTimestamp } from '../types'; export type SnodeSignatureResult = WithTimestamp & { signature: string; @@ -17,14 +15,6 @@ export type SnodeSignatureResult = WithTimestamp & { pubkey: string; // this is the x25519 key of the pubkey we are doing the request to (ourself for our swarm usually) }; -type ShortenOrExtend = 'extend' | 'shorten' | ''; -type WithShortenOrExtend = { shortenOrExtend: ShortenOrExtend }; -type WithMessagesHashes = { messagesHashes: Array }; - -export type SnodeGroupSignatureResult = Pick & { - pubkey: GroupPubkeyType; // this is the 03 pubkey of the corresponding group -}; - async function getSnodeSignatureByHashesParams({ messagesHashes, method, @@ -72,30 +62,44 @@ type SnodeSigParamsShared = { type SnodeSigParamsAdminGroup = SnodeSigParamsShared & { groupPk: GroupPubkeyType; - privKey: Uint8Array; // len 64 + privKey: Uint8ArrayLen64; // len 64 +}; + +type SnodeSigParamsSubAccount = SnodeSigParamsShared & { + groupPk: GroupPubkeyType; + authData: Uint8ArrayLen100; // len 100 }; + type SnodeSigParamsUs = SnodeSigParamsShared & { pubKey: string; - privKey: Uint8Array; // len 64 + privKey: Uint8ArrayLen64; // len 64 }; function isSigParamsForGroupAdmin( - sigParams: SnodeSigParamsAdminGroup | SnodeSigParamsUs + sigParams: SnodeSigParamsAdminGroup | SnodeSigParamsUs | SnodeSigParamsSubAccount ): sigParams is SnodeSigParamsAdminGroup { const asGr = sigParams as SnodeSigParamsAdminGroup; - return PubKey.isClosedGroupV2(asGr.groupPk) && !!asGr.privKey; + return PubKey.isClosedGroupV2(asGr.groupPk) && !isEmpty(asGr.privKey); } -async function getSnodeSignatureShared(params: SnodeSigParamsAdminGroup | SnodeSigParamsUs) { +function getVerificationData(params: SnodeSigParamsShared) { const signatureTimestamp = GetNetworkTime.getNowWithNetworkOffset(); const verificationData = StringUtils.encode( `${params.method}${params.namespace === 0 ? '' : params.namespace}${signatureTimestamp}`, 'utf8' ); + return { + toSign: new Uint8Array(verificationData), + signatureTimestamp, + }; +} + +async function getSnodeSignatureShared(params: SnodeSigParamsAdminGroup | SnodeSigParamsUs) { + const { signatureTimestamp, toSign } = getVerificationData(params); + try { - const message = new Uint8Array(verificationData); const sodium = await getSodiumRenderer(); - const signature = sodium.crypto_sign_detached(message, params.privKey); + const signature = sodium.crypto_sign_detached(toSign, params.privKey); const signatureBase64 = fromUInt8ArrayToBase64(signature); if (isSigParamsForGroupAdmin(params)) { return { @@ -145,26 +149,6 @@ async function getSnodeSignatureParamsUs({ }; } -async function getSnodeGroupSignatureParams({ - groupIdentityPrivKey, - groupPk, - method, - namespace, -}: { - groupPk: GroupPubkeyType; - groupIdentityPrivKey: Uint8Array; // len 64 - namespace: SnodeNamespacesGroup; - method: 'retrieve' | 'store'; -}): Promise { - const sigData = await getSnodeSignatureShared({ - pubKey: groupPk, - method, - namespace, - privKey: groupIdentityPrivKey, - }); - return { ...sigData, pubkey: groupPk }; -} - async function generateUpdateExpirySignature({ shortenOrExtend, timestamp, @@ -220,38 +204,8 @@ async function generateUpdateExpiryOurSignature({ ed25519Pubkey: ourEd25519Key.pubKey, }); } - -async function generateUpdateExpiryGroupSignature({ - shortenOrExtend, - timestamp, - messagesHashes, - groupPrivKey, - groupPk, -}: WithMessagesHashes & - WithShortenOrExtend & - WithTimestamp & { - groupPk: GroupPubkeyType; - groupPrivKey: Uint8Array; // len 64 - }) { - if (isEmpty(groupPrivKey) || isEmpty(groupPk)) { - throw new PreConditionFailed( - 'generateUpdateExpiryGroupSignature groupPrivKey or groupPk is empty' - ); - } - - return generateUpdateExpirySignature({ - messagesHashes, - shortenOrExtend, - timestamp, - ed25519Privkey: groupPrivKey, - ed25519Pubkey: groupPk, - }); -} - export const SnodeSignature = { getSnodeSignatureParamsUs, - getSnodeGroupSignatureParams, getSnodeSignatureByHashesParams, generateUpdateExpiryOurSignature, - generateUpdateExpiryGroupSignature, }; diff --git a/ts/session/apis/snode_api/types.ts b/ts/session/apis/snode_api/types.ts index 9e267cb2a..d7fd39145 100644 --- a/ts/session/apis/snode_api/types.ts +++ b/ts/session/apis/snode_api/types.ts @@ -25,3 +25,8 @@ export type RetrieveRequestResult = { }; export type RetrieveMessagesResultsBatched = Array; + +export type WithTimestamp = { timestamp: number }; +export type ShortenOrExtend = 'extend' | 'shorten' | ''; +export type WithShortenOrExtend = { shortenOrExtend: ShortenOrExtend }; +export type WithMessagesHashes = { messagesHashes: Array }; diff --git a/ts/session/crypto/group/groupSignature.ts b/ts/session/crypto/group/groupSignature.ts deleted file mode 100644 index 76c0d3502..000000000 --- a/ts/session/crypto/group/groupSignature.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { GroupMemberGet, GroupPubkeyType, Uint8ArrayLen64 } from 'libsession_util_nodejs'; -import { compact } from 'lodash'; -import { MetaGroupWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; -import { GetNetworkTime } from '../../apis/snode_api/getNetworkTime'; -import { GroupUpdateInviteMessage } from '../../messages/outgoing/controlMessage/group_v2/to_user/GroupUpdateInviteMessage'; -import { UserUtils } from '../../utils'; -import { getSodiumRenderer } from '../MessageEncrypter'; - -export async function getGroupInvitesMessages({ - groupName, - membersFromWrapper, - secretKey, - groupPk, -}: { - membersFromWrapper: Array; - groupName: string; - secretKey: Uint8ArrayLen64; // len 64 - groupPk: GroupPubkeyType; -}) { - const sodium = await getSodiumRenderer(); - const timestamp = GetNetworkTime.getNowWithNetworkOffset(); - - const inviteDetails = compact( - await Promise.all( - membersFromWrapper.map(async ({ pubkeyHex: member }) => { - if (UserUtils.isUsFromCache(member)) { - return null; - } - const tosign = `INVITE${member}${timestamp}`; - - // 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(tosign, secretKey); - console.info(`before makeSwarmSubAccount ${groupPk}:${member}`); - const memberAuthData = await MetaGroupWrapperActions.makeSwarmSubAccount(groupPk, member); - debugger; - console.info(`after makeSwarmSubAccount ${groupPk}:${member}`); - - const invite = new GroupUpdateInviteMessage({ - groupName, - groupPk, - timestamp, - adminSignature, - memberAuthData, - }); - - return { member, invite }; - }) - ) - ); - return inviteDetails; -} diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 8942fe7cd..189700447 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -3,7 +3,7 @@ import { AbortController } from 'abort-controller'; import ByteBuffer from 'bytebuffer'; import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; -import _, { isEmpty, isNil, sample, toNumber } from 'lodash'; +import _, { isEmpty, sample, toNumber } from 'lodash'; import pRetry from 'p-retry'; import { Data } from '../../data/data'; import { SignalService } from '../../protobuf'; @@ -22,8 +22,13 @@ import { } from '../apis/snode_api/SnodeRequestTypes'; import { GetNetworkTime } from '../apis/snode_api/getNetworkTime'; import { SnodeNamespace, SnodeNamespaces } from '../apis/snode_api/namespaces'; +import { + SigResultAdmin, + SigResultSubAccount, + SnodeGroupSignature, +} from '../apis/snode_api/signature/groupSignature'; +import { SnodeSignature, SnodeSignatureResult } from '../apis/snode_api/signature/snodeSignatures'; import { getSwarmFor } from '../apis/snode_api/snodePool'; -import { SnodeSignature } from '../apis/snode_api/snodeSignatures'; import { SnodeAPIStore } from '../apis/snode_api/storeMessage'; import { ConvoHub } from '../conversations'; import { MessageEncrypter } from '../crypto'; @@ -184,14 +189,20 @@ async function send( ); } -async function getSignatureParamsFromNamespace(item: StoreOnNodeParamsNoSig, destination: string) { +async function getSignatureParamsFromNamespace( + item: StoreOnNodeParamsNoSig, + destination: string +): Promise { + const store = 'store' as const; if (SnodeNamespace.isUserConfigNamespace(item.namespace)) { const ourPrivKey = (await UserUtils.getUserED25519KeyPairBytes())?.privKeyBytes; if (!ourPrivKey) { - throw new Error('sendMessagesDataToSnode UserUtils.getUserED25519KeyPairBytes is empty'); + throw new Error( + 'getSignatureParamsFromNamespace UserUtils.getUserED25519KeyPairBytes is empty' + ); } return SnodeSignature.getSnodeSignatureParamsUs({ - method: 'store' as const, + method: store, namespace: item.namespace, }); } @@ -201,20 +212,18 @@ async function getSignatureParamsFromNamespace(item: StoreOnNodeParamsNoSig, des item.namespace === SnodeNamespaces.ClosedGroupMessages ) { if (!PubKey.isClosedGroupV2(destination)) { - throw new Error('sendMessagesDataToSnode: groupconfig namespace required a 03 pubkey'); - } - const group = await UserGroupsWrapperActions.getGroup(destination); - const groupSecretKey = group?.secretKey; // TODO we will need to the user auth at some point - if (isNil(groupSecretKey) || isEmpty(groupSecretKey)) { - throw new Error(`sendMessagesDataToSnode: failed to find group admin secret key in wrapper`); + throw new Error( + 'getSignatureParamsFromNamespace: groupconfig namespace required a 03 pubkey' + ); } - return SnodeSignature.getSnodeGroupSignatureParams({ - method: 'store' as const, + const found = await UserGroupsWrapperActions.getGroup(destination); + return SnodeGroupSignature.getSnodeGroupSignature({ + method: store, namespace: item.namespace, - groupPk: destination, - groupIdentityPrivKey: groupSecretKey, + group: found, }); } + // no signature required for this namespace/pubkey combo return {}; } diff --git a/ts/session/utils/TaskWithTimeout.ts b/ts/session/utils/TaskWithTimeout.ts index c9f0ef591..1e77199e3 100644 --- a/ts/session/utils/TaskWithTimeout.ts +++ b/ts/session/utils/TaskWithTimeout.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-useless-return */ /* eslint-disable consistent-return */ /* eslint-disable no-promise-executor-return */ diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index b4479c321..208316c22 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -83,7 +83,7 @@ export async function getUserED25519KeyPair(): Promise { return undefined; } -export const getUserED25519KeyPairBytes = async (): Promise => { +export const getUserED25519KeyPairBytes = async (): Promise => { // 'identityKey' keeps the ed25519KeyPair under a ed25519KeyPair field. // it is only set if the user migrated to the ed25519 way of generating a key const item = await UserUtils.getIdentityKeyPair(); @@ -96,7 +96,7 @@ export const getUserED25519KeyPairBytes = async (): Promise { Sinon.restore(); }); - describe('getSnodeGroupSignatureParams', () => { + describe('getSnodeGroupAdminSignatureParams', () => { beforeEach(() => { Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(hardcodedTimestamp); }); describe('retrieve', () => { it('retrieve namespace ClosedGroupInfo', async () => { - const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + const ret = await SnodeGroupSignature.getSnodeGroupSignature({ method: 'retrieve', namespace: SnodeNamespaces.ClosedGroupInfo, - groupIdentityPrivKey: privKeyUint, - groupPk: validGroupPk, + group: { + authData: null, + pubkeyHex: validGroupPk, + secretKey: privKeyUint, + }, }); expect(ret.pubkey).to.be.eq(validGroupPk); @@ -68,11 +72,14 @@ describe('SnodeSignature', () => { }); it('retrieve namespace ClosedGroupKeys', async () => { - const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + const ret = await SnodeGroupSignature.getSnodeGroupSignature({ method: 'retrieve', namespace: SnodeNamespaces.ClosedGroupKeys, - groupIdentityPrivKey: privKeyUint, - groupPk: validGroupPk, + group: { + authData: null, + pubkeyHex: validGroupPk, + secretKey: privKeyUint, + }, }); expect(ret.pubkey).to.be.eq(validGroupPk); @@ -83,11 +90,14 @@ describe('SnodeSignature', () => { }); it('retrieve namespace ClosedGroupMessages', async () => { - const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + const ret = await SnodeGroupSignature.getSnodeGroupSignature({ method: 'retrieve', namespace: SnodeNamespaces.ClosedGroupMessages, - groupIdentityPrivKey: privKeyUint, - groupPk: validGroupPk, + group: { + authData: null, + pubkeyHex: validGroupPk, + secretKey: privKeyUint, + }, }); expect(ret.pubkey).to.be.eq(validGroupPk); @@ -99,11 +109,14 @@ describe('SnodeSignature', () => { describe('store', () => { it('store namespace ClosedGroupInfo', async () => { - const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + const ret = await SnodeGroupSignature.getSnodeGroupSignature({ method: 'store', namespace: SnodeNamespaces.ClosedGroupInfo, - groupIdentityPrivKey: privKeyUint, - groupPk: validGroupPk, + group: { + authData: null, + pubkeyHex: validGroupPk, + secretKey: privKeyUint, + }, }); expect(ret.pubkey).to.be.eq(validGroupPk); expect(ret.timestamp).to.be.eq(hardcodedTimestamp); @@ -113,11 +126,14 @@ describe('SnodeSignature', () => { }); it('store namespace ClosedGroupKeys', async () => { - const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + const ret = await SnodeGroupSignature.getSnodeGroupSignature({ method: 'store', namespace: SnodeNamespaces.ClosedGroupKeys, - groupIdentityPrivKey: privKeyUint, - groupPk: validGroupPk, + group: { + authData: null, + pubkeyHex: validGroupPk, + secretKey: privKeyUint, + }, }); expect(ret.pubkey).to.be.eq(validGroupPk); @@ -127,11 +143,14 @@ describe('SnodeSignature', () => { }); it('store namespace ClosedGroupMessages', async () => { - const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + const ret = await SnodeGroupSignature.getSnodeGroupSignature({ method: 'store', namespace: SnodeNamespaces.ClosedGroupMessages, - groupIdentityPrivKey: privKeyUint, - groupPk: validGroupPk, + group: { + authData: null, + pubkeyHex: validGroupPk, + secretKey: privKeyUint, + }, }); expect(ret.pubkey).to.be.eq(validGroupPk); expect(ret.timestamp).to.be.eq(hardcodedTimestamp); @@ -141,12 +160,104 @@ describe('SnodeSignature', () => { }); }); + // describe('getSnodeGroupSubAccountSignatureParams', () => { + // beforeEach(() => { + // Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(hardcodedTimestamp); + // }); + + // describe('retrieve', () => { + // it('retrieve namespace ClosedGroupInfo', async () => { + // const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + // method: 'retrieve', + // namespace: SnodeNamespaces.ClosedGroupInfo, + // groupPk: validGroupPk, + // groupIdentityPrivKey: privKeyUint, + // }); + // expect(ret.pubkey).to.be.eq(validGroupPk); + + // expect(ret.timestamp).to.be.eq(hardcodedTimestamp); + // const verificationData = `retrieve${SnodeNamespaces.ClosedGroupInfo}${hardcodedTimestamp}`; + // await verifySig(ret, verificationData); + // }); + + // it('retrieve namespace ClosedGroupKeys', async () => { + // const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + // method: 'retrieve', + // namespace: SnodeNamespaces.ClosedGroupKeys, + // groupIdentityPrivKey: privKeyUint, + // groupPk: validGroupPk, + // }); + // expect(ret.pubkey).to.be.eq(validGroupPk); + + // expect(ret.timestamp).to.be.eq(hardcodedTimestamp); + // const verificationData = `retrieve${SnodeNamespaces.ClosedGroupKeys}${hardcodedTimestamp}`; + + // await verifySig(ret, verificationData); + // }); + + // it('retrieve namespace ClosedGroupMessages', async () => { + // const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + // method: 'retrieve', + // namespace: SnodeNamespaces.ClosedGroupMessages, + // groupIdentityPrivKey: privKeyUint, + // groupPk: validGroupPk, + // }); + // expect(ret.pubkey).to.be.eq(validGroupPk); + + // expect(ret.timestamp).to.be.eq(hardcodedTimestamp); + // const verificationData = `retrieve${SnodeNamespaces.ClosedGroupMessages}${hardcodedTimestamp}`; + // await verifySig(ret, verificationData); + // }); + // }); + + // describe('store', () => { + // it('store namespace ClosedGroupInfo', async () => { + // const ret = await SnodeSignature.getSnodeGroupSignatureParams({ + // method: 'store', + // namespace: SnodeNamespaces.ClosedGroupInfo, + // groupIdentityPrivKey: privKeyUint, + // groupPk: validGroupPk, + // }); + // expect(ret.pubkey).to.be.eq(validGroupPk); + // expect(ret.timestamp).to.be.eq(hardcodedTimestamp); + + // const verificationData = `store${SnodeNamespaces.ClosedGroupInfo}${hardcodedTimestamp}`; + // await verifySig(ret, verificationData); + // }); + + // it('store namespace ClosedGroupKeys', async () => { + // const ret = await SnodeSignature.getSnodeGroupSubAccountSignatureParams({ + // method: 'store', + // namespace: SnodeNamespaces.ClosedGroupKeys, + // groupIdentityPrivKey: privKeyUint, + // groupPk: validGroupPk, + // }); + // expect(ret.pubkey).to.be.eq(validGroupPk); + + // expect(ret.timestamp).to.be.eq(hardcodedTimestamp); + // const verificationData = `store${SnodeNamespaces.ClosedGroupKeys}${hardcodedTimestamp}`; + // await verifySig(ret, verificationData); + // }); + + // it('store namespace ClosedGroupMessages', async () => { + // const ret = await SnodeSignature.getSnodeGroupSubAccountSignatureParams({ + // method: 'store', + // namespace: SnodeNamespaces.ClosedGroupMessages, + // groupPk: validGroupPk, + // }); + // expect(ret.groupPk).to.be.eq(validGroupPk); + // expect(ret.timestamp).to.be.eq(hardcodedTimestamp); + // const verificationData = `store${SnodeNamespaces.ClosedGroupMessages}${hardcodedTimestamp}`; + // await verifySig(ret, verificationData); + // }); + // }); + // }); + describe('generateUpdateExpiryGroupSignature', () => { it('throws if groupPk not given', async () => { const func = async () => { - return SnodeSignature.generateUpdateExpiryGroupSignature({ - groupPk: null as any, - groupPrivKey: privKeyUint, + return SnodeGroupSignature.generateUpdateExpiryGroupSignature({ + group: { pubkeyHex: null as any, secretKey: privKeyUint, authData: null }, messagesHashes: ['[;p['], shortenOrExtend: '', timestamp: hardcodedTimestamp, @@ -159,9 +270,13 @@ describe('SnodeSignature', () => { it('throws if groupPrivKey is empty', async () => { const func = async () => { - return SnodeSignature.generateUpdateExpiryGroupSignature({ - groupPk: validGroupPk, - groupPrivKey: new Uint8Array() as any, + return SnodeGroupSignature.generateUpdateExpiryGroupSignature({ + group: { + pubkeyHex: validGroupPk as any, + secretKey: new Uint8Array() as any, + authData: null, + }, + messagesHashes: ['[;p['], shortenOrExtend: '', timestamp: hardcodedTimestamp, @@ -176,9 +291,8 @@ describe('SnodeSignature', () => { const hashes = ['hash4321', 'hash4221']; const timestamp = hardcodedTimestamp; const shortenOrExtend = ''; - const ret = await SnodeSignature.generateUpdateExpiryGroupSignature({ - groupPk: validGroupPk, - groupPrivKey: privKeyUint, + const ret = await SnodeGroupSignature.generateUpdateExpiryGroupSignature({ + group: { pubkeyHex: validGroupPk, secretKey: privKeyUint, authData: null }, messagesHashes: hashes, shortenOrExtend: '', timestamp, @@ -194,9 +308,8 @@ describe('SnodeSignature', () => { const hashes = ['hash4321', 'hash4221']; const timestamp = hardcodedTimestamp; const shortenOrExtend = ''; - const ret = await SnodeSignature.generateUpdateExpiryGroupSignature({ - groupPk: validGroupPk, - groupPrivKey: privKeyUint, + const ret = await SnodeGroupSignature.generateUpdateExpiryGroupSignature({ + group: { pubkeyHex: validGroupPk, secretKey: privKeyUint, authData: null }, messagesHashes: hashes, shortenOrExtend: '', timestamp, @@ -213,9 +326,8 @@ describe('SnodeSignature', () => { const hashes = ['hash4321', 'hash4221']; const timestamp = hardcodedTimestamp; const shortenOrExtend = ''; - const ret = await SnodeSignature.generateUpdateExpiryGroupSignature({ - groupPk: validGroupPk, - groupPrivKey: privKeyUint, + const ret = await SnodeGroupSignature.generateUpdateExpiryGroupSignature({ + group: { pubkeyHex: validGroupPk, secretKey: privKeyUint, authData: null }, messagesHashes: hashes, shortenOrExtend: '', timestamp, @@ -234,9 +346,8 @@ describe('SnodeSignature', () => { const hashes = ['hash4321', 'hash4221']; const timestamp = hardcodedTimestamp; const shortenOrExtend = ''; - const ret = await SnodeSignature.generateUpdateExpiryGroupSignature({ - groupPk: validGroupPk, - groupPrivKey: privKeyUint, + const ret = await SnodeGroupSignature.generateUpdateExpiryGroupSignature({ + group: { pubkeyHex: validGroupPk, secretKey: privKeyUint, authData: null }, messagesHashes: hashes, shortenOrExtend: '', timestamp, diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index 1ca5fb7f7..70afb91a2 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -13,6 +13,7 @@ import { MetaGroupWrapperActionsCalls, ProfilePicture, PubkeyType, + Uint8ArrayLen100, UserConfigWrapperActionsCalls, UserGroupsSet, UserGroupsWrapperActionsCalls, @@ -505,6 +506,17 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = { 'makeSwarmSubAccount', memberPubkeyHex, ]) as Promise>, + swarmSubaccountSign: async ( + groupPk: GroupPubkeyType, + message: Uint8Array, + authData: Uint8ArrayLen100 + ) => + callLibSessionWorker([ + `MetaGroupConfig-${groupPk}`, + 'swarmSubaccountSign', + message, + authData, + ]) as Promise>, }; export const callLibSessionWorker = async (