diff --git a/ts/session/apis/snode_api/SnodeRequestTypes.ts b/ts/session/apis/snode_api/SnodeRequestTypes.ts index 5d51352fd..8f67cfdea 100644 --- a/ts/session/apis/snode_api/SnodeRequestTypes.ts +++ b/ts/session/apis/snode_api/SnodeRequestTypes.ts @@ -192,10 +192,6 @@ export type UpdateExpireNodeUserParams = WithPubkeyAsString & export type UpdateExpireNodeGroupParams = WithPubkeyAsGroupPubkey & UpdateExpireAlwaysNeeded; -type UpdateExpiryOnNodeSubRequest = - | UpdateExpiryOnNodeUserSubRequest - | UpdateExpiryOnNodeGroupSubRequest; - export type UpdateExpiryOnNodeUserSubRequest = { method: 'expire'; params: UpdateExpireNodeUserParams; @@ -206,6 +202,20 @@ export type UpdateExpiryOnNodeGroupSubRequest = { params: UpdateExpireNodeGroupParams; }; +type UpdateExpiryOnNodeSubRequest = + | UpdateExpiryOnNodeUserSubRequest + | UpdateExpiryOnNodeGroupSubRequest; + +export type RevokeSubaccountParams = { + pubkey: GroupPubkeyType; + revoke: string; // the subaccount token to revoke in hex + signature: string; +}; +export type RevokeSubaccountSubRequest = { + method: 'revoke_subaccount' | 'unrevoke_subaccount'; + params: RevokeSubaccountParams; +}; + export type OxendSubRequest = OnsResolveSubRequest | GetServiceNodesSubRequest; export type SnodeApiSubRequests = @@ -216,7 +226,8 @@ export type SnodeApiSubRequests = | NetworkTimeSubRequest | DeleteFromNodeSubRequest | DeleteAllFromNodeSubRequest - | UpdateExpiryOnNodeSubRequest; + | UpdateExpiryOnNodeSubRequest + | RevokeSubaccountSubRequest; // eslint-disable-next-line @typescript-eslint/array-type export type NonEmptyArray = [T, ...T[]]; diff --git a/ts/session/apis/snode_api/revokeSubaccount.ts b/ts/session/apis/snode_api/revokeSubaccount.ts new file mode 100644 index 000000000..8cd77cef4 --- /dev/null +++ b/ts/session/apis/snode_api/revokeSubaccount.ts @@ -0,0 +1,84 @@ +import { GroupPubkeyType } from 'libsession_util_nodejs'; +import _, { isEmpty } from 'lodash'; +import { doSnodeBatchRequest } from './batchRequest'; + +import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; +import { PubKey } from '../../types'; +import { stringToUint8Array } from '../../utils/String'; +import { RevokeSubaccountSubRequest } from './SnodeRequestTypes'; +import { SnodeGroupSignature } from './signature/groupSignature'; +import { getSwarmFor } from './snodePool'; + +type Change = { + action: 'revoke_subaccount' | 'unrevoke_subaccount'; + tokenToRevoke: string; +}; + +type ArrayOfChange = Array; +async function getRevokeSubaccountRequest({ + groupPk, + actions, +}: { + groupPk: GroupPubkeyType; + actions: ArrayOfChange; +}): Promise> { + if (!PubKey.isClosedGroupV2(groupPk)) { + throw new Error('revokeSubaccountForGroup: not a 03 group'); + } + + const group = await UserGroupsWrapperActions.getGroup(groupPk); + + if (!group || isEmpty(group?.secretKey)) { + throw new Error(`revokeSubaccountForGroup ${groupPk} needs admin secretkey`); + } + + const revokeParams: Array = await Promise.all( + actions.map(async action => { + const verificationString = `${action}${stringToUint8Array(action.tokenToRevoke)}`; + const sigResult = await SnodeGroupSignature.signDataWithAdminSecret( + verificationString, + group + ); + + return { + method: action.action, + params: { + revoke: action.tokenToRevoke, + ...sigResult, + pubkey: groupPk, + }, + }; + }) + ); + + return revokeParams; +} + +async function revokeSubAccounts( + groupPk: GroupPubkeyType, + actions: ArrayOfChange +): Promise { + try { + const swarm = await getSwarmFor(groupPk); + const snode = _.sample(swarm); + if (!snode) { + throw new Error('revoke subaccounts empty swarm'); + } + const revokeParams = await getRevokeSubaccountRequest({ + groupPk, + actions, + }); + + const results = await doSnodeBatchRequest(revokeParams, snode, 4000, null); + + if (!results || !results.length) { + throw new Error(`_revokeSubAccounts could not talk to ${snode.ip}:${snode.port}`); + } + return true; + } catch (e) { + window?.log?.warn(`_revokeSubAccounts failed with ${e.message}`); + return false; + } +} + +export const SnodeAPIRetrieve = { revokeSubAccounts }; diff --git a/ts/session/apis/snode_api/signature/groupSignature.ts b/ts/session/apis/snode_api/signature/groupSignature.ts index 568bf6e39..1b1b87210 100644 --- a/ts/session/apis/snode_api/signature/groupSignature.ts +++ b/ts/session/apis/snode_api/signature/groupSignature.ts @@ -154,6 +154,29 @@ async function getSnodeGroupSignature({ throw new Error(`getSnodeGroupSignature: needs either groupSecretKey or authData`); } +async function signDataWithAdminSecret( + verificationString: string, + group: Pick +) { + const verificationData = StringUtils.encode(verificationString, 'utf8'); + 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, @@ -214,4 +237,5 @@ export const SnodeGroupSignature = { generateUpdateExpiryGroupSignature, getGroupInviteMessage, getSnodeGroupSignature, + signDataWithAdminSecret, };