feat: add subaccount auth
parent
d7608c42b6
commit
b8876ebbfe
@ -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 };
|
@ -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<GroupMemberGet>;
|
||||||
|
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<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 getSnodeGroupSignatureParams(params: SigParamsAdmin): Promise<SigResultAdmin>;
|
||||||
|
async function getSnodeGroupSignatureParams(
|
||||||
|
params: SigParamsSubaccount
|
||||||
|
): Promise<SigResultSubAccount>;
|
||||||
|
|
||||||
|
async function getSnodeGroupSignatureParams(
|
||||||
|
params: SigParamsAdmin | SigParamsSubaccount
|
||||||
|
): Promise<SigResultSubAccount | SigResultAdmin> {
|
||||||
|
if ('groupIdentityPrivKey' in params) {
|
||||||
|
return getSnodeGroupAdminSignatureParams(params);
|
||||||
|
}
|
||||||
|
return getSnodeGroupSubAccountSignatureParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupDetailsNeededForSignature = Pick<UserGroupsGet, 'pubkeyHex' | 'authData' | 'secretKey'>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
@ -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,
|
||||||
|
};
|
@ -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<GroupMemberGet>;
|
|
||||||
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;
|
|
||||||
}
|
|
Loading…
Reference in New Issue