Skip to content

Commit

Permalink
fix: Add MLS basic devices to the return value of user identities (#5814
Browse files Browse the repository at this point in the history
)
  • Loading branch information
atomrc authored Dec 20, 2023
1 parent 187d0d6 commit 38bce7a
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,30 @@ import {CoreCrypto, WireIdentity} from '@wireapp/core-crypto';
import {E2EIServiceExternal} from './E2EIServiceExternal';

import {ClientService} from '../../../client';
import {getUUID} from '../../../test/PayloadHelper';

function buildE2EIService() {
const coreCrypto = {
getUserIdentities: jest.fn(),
getClientIds: jest.fn().mockResolvedValue([]),
} as unknown as jest.Mocked<CoreCrypto>;

const clientService = {} as jest.Mocked<ClientService>;

return [new E2EIServiceExternal(coreCrypto, clientService), {coreCrypto}] as const;
}

function generateCoreCryptoIdentity({status = 'Valid', deviceId = 'aaaaa'}: {status?: string; deviceId?: string} = {}) {
function generateCoreCryptoIdentity({
userId,
status = 'Valid',
deviceId = getUUID(),
}: {
userId: string;
status?: string;
deviceId?: string;
}) {
return {
client_id: `SKHDsEsOS82TrWTHNEsVNA:${deviceId}@elna.wire.link`,
client_id: `${userId}:${deviceId}@elna.wire.link`,
handle: '[email protected]',
display_name: 'Adrian Weiss 2',
domain: 'elna.wire.link',
Expand All @@ -58,15 +68,41 @@ describe('E2EIServiceExternal', () => {

coreCrypto.getUserIdentities.mockResolvedValue(
new Map([
['48a1c3b0-4b0e-4bcd-93ad-64c7344b1534', [generateCoreCryptoIdentity(), generateCoreCryptoIdentity()]],
['b7d287e4-7bbd-40e0-a550-6b18dcaf5f31', [generateCoreCryptoIdentity()]],
[user1.id, [generateCoreCryptoIdentity({userId: user1.id}), generateCoreCryptoIdentity({userId: user1.id})]],
[user2.id, [generateCoreCryptoIdentity({userId: user2.id})]],
]),
);

const userIdentities = await service.getUsersIdentities(groupId, userIds);

expect(userIdentities.get(user1.id)).toBeDefined();
expect(userIdentities.get(user2.id)).toBeDefined();
expect(userIdentities.get(user1.id)).toHaveLength(2);
expect(userIdentities.get(user2.id)).toHaveLength(1);
});

it('returns MLS basic devices with empty identity', async () => {
const [service, {coreCrypto}] = buildE2EIService();
const user1 = {domain: 'elna.wire.link', id: '48a1c3b0-4b0e-4bcd-93ad-64c7344b1534'};
const user2 = {domain: 'elna.wire.link', id: 'b7d287e4-7bbd-40e0-a550-6b18dcaf5f31'};
const userIds = [user1, user2];

const user1Identities = [
generateCoreCryptoIdentity({userId: user1.id}),
generateCoreCryptoIdentity({userId: user1.id}),
];
const encoder = new TextEncoder();
coreCrypto.getUserIdentities.mockResolvedValue(new Map([[user1.id, user1Identities]]));

const allClients = [
...user1Identities.map(identity => (identity as any).client_id),
`${user1.id}:[email protected]`,
`${user2.id}:[email protected]`,
];
coreCrypto.getClientIds.mockResolvedValue(allClients.map(clientId => encoder.encode(clientId)));

const userIdentities = await service.getUsersIdentities(groupId, userIds);

expect(userIdentities.get(user1.id)).toHaveLength(3);
expect(userIdentities.get(user2.id)).toHaveLength(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import {QualifiedId} from '@wireapp/api-client/lib/user';
import {Decoder} from 'bazinga64';
import logdown from 'logdown';

import {Ciphersuite, CoreCrypto, E2eiConversationState, WireIdentity} from '@wireapp/core-crypto';
import {Ciphersuite, CoreCrypto, E2eiConversationState, WireIdentity, DeviceStatus} from '@wireapp/core-crypto';

import {getE2EIClientId} from './Helper';
import {E2EIStorage} from './Storage/E2EIStorage';

import {ClientService} from '../../../client';
import {parseFullQualifiedClientId} from '../../../util/fullyQualifiedClientIdUtils';

export type DeviceIdentity = Omit<WireIdentity, 'free'> & {deviceId: string};
export type DeviceIdentity = Omit<WireIdentity, 'free' | 'status'> & {status?: DeviceStatus; deviceId: string};

// This export is meant to be accessible from the outside (e.g the Webapp / UI)
export class E2EIServiceExternal {
Expand Down Expand Up @@ -73,20 +73,44 @@ export class E2EIServiceExternal {
}

public async getUsersIdentities(groupId: string, userIds: QualifiedId[]): Promise<Map<string, DeviceIdentity[]>> {
const groupIdBytes = Decoder.fromBase64(groupId).asBytes;
const textDecoder = new TextDecoder();

// we get all the devices that have an identity (either valid, expired or revoked)
const userIdentities = await this.coreCryptoClient.getUserIdentities(
Decoder.fromBase64(groupId).asBytes,
groupIdBytes,
userIds.map(userId => userId.id),
);

const mappedUserIdentities = new Map();
for (const [userId, identities] of userIdentities) {
mappedUserIdentities.set(
userId,
identities.map(identity => ({
...identity,
deviceId: parseFullQualifiedClientId((identity as any).client_id).client,
})),
);
// We get all the devices in the conversation (in order to get devices that have no identity)
const allUsersMLSDevices = (await this.coreCryptoClient.getClientIds(groupIdBytes))
.map(id => textDecoder.decode(id))
.map(fullyQualifiedId => parseFullQualifiedClientId(fullyQualifiedId));

const mappedUserIdentities = new Map<string, DeviceIdentity[]>();
for (const userId of userIds) {
const identities = (userIdentities.get(userId.id) || []).map(identity => ({
...identity,
deviceId: parseFullQualifiedClientId((identity as any).client_id).client,
}));

const basicMLSDevices = allUsersMLSDevices
.filter(({user}) => user === userId.id)
// filtering devices that have a valid identity
.filter(({client}) => !identities.map(identity => identity.deviceId).includes(client))
// map basic MLS devices to "fake" identity object
.map<DeviceIdentity>(id => ({
...id,
deviceId: id.client,
thumbprint: '',
user: '',
certificate: '',
displayName: '',
handle: '',
clientId: id.client,
}));

mappedUserIdentities.set(userId.id, [...identities, ...basicMLSDevices]);
}

return mappedUserIdentities;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/util/fullyQualifiedClientIdUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ type UserId = string;
type ClientId = string;
type Domain = string;
export type ClientIdStringType = `${UserId}:${ClientId}@${Domain}`;
export type ParsedFullyQualifiedId = {user: UserId; client: ClientId; domain: Domain};

export const constructFullyQualifiedClientId = (
userId: UserId,
clientId: ClientId,
domain: Domain,
): ClientIdStringType => `${userId}:${clientId}@${domain}`;

export const parseFullQualifiedClientId = (qualifiedId: string): {user: UserId; client: ClientId; domain: Domain} => {
export const parseFullQualifiedClientId = (qualifiedId: string): ParsedFullyQualifiedId => {
const regexp = /([a-zA-Z0-9\-]+):([a-zA-Z0-9\-]+)@([a-zA-Z0-9\-.]+)/;
const [, user, client, domain] = qualifiedId.match(regexp) ?? [];
if (!user || !client || !domain) {
Expand Down

0 comments on commit 38bce7a

Please sign in to comment.