diff --git a/lib/shared/device-list-utils.js b/lib/shared/device-list-utils.js --- a/lib/shared/device-list-utils.js +++ b/lib/shared/device-list-utils.js @@ -6,7 +6,10 @@ SignedDeviceList, } from '../types/identity-service-types.js'; import { getConfig } from '../utils/config.js'; -import { rawDeviceListFromSignedList } from '../utils/device-list-utils.js'; +import { + composeRawDeviceList, + rawDeviceListFromSignedList, +} from '../utils/device-list-utils.js'; export type DeviceListVerificationResult = | { +valid: true, +deviceList: RawDeviceList } @@ -118,4 +121,17 @@ return { valid: true, deviceList }; } -export { verifyAndGetDeviceList }; +async function createAndSignInitialDeviceList( + primaryDeviceID: string, +): Promise { + const initialDeviceList = composeRawDeviceList([primaryDeviceID]); + const rawDeviceList = JSON.stringify(initialDeviceList); + const { olmAPI } = getConfig(); + const curPrimarySignature = await olmAPI.signMessage(rawDeviceList); + return { + rawDeviceList, + curPrimarySignature, + }; +} + +export { verifyAndGetDeviceList, createAndSignInitialDeviceList }; diff --git a/lib/shared/device-list-utils.test.js b/lib/shared/device-list-utils.test.js --- a/lib/shared/device-list-utils.test.js +++ b/lib/shared/device-list-utils.test.js @@ -1,12 +1,16 @@ // @flow -import { verifyAndGetDeviceList } from './device-list-utils.js'; +import { + createAndSignInitialDeviceList, + verifyAndGetDeviceList, +} from './device-list-utils.js'; import type { RawDeviceList, SignedDeviceList, IdentityServiceClient, } from '../types/identity-service-types.js'; import * as config from '../utils/config.js'; +import { rawDeviceListFromSignedList } from '../utils/device-list-utils.js'; // mockOlmAPIVerification replaces OlmAPI with a mock // save original to avoid affecting other test suites @@ -146,6 +150,21 @@ }); }); +describe(createAndSignInitialDeviceList, () => { + it('creates initial device list', async () => { + const signMessage = jest + .fn<[string], string>() + .mockResolvedValue('mock_signature'); + mockOlmAPISign(signMessage); + + const payload = await createAndSignInitialDeviceList('device1'); + expect(payload.curPrimarySignature).toStrictEqual('mock_signature'); + + const raw = rawDeviceListFromSignedList(payload); + expect(raw.devices).toStrictEqual(['device1']); + }); +}); + function createDeviceList( rawList: RawDeviceList, curPrimarySignature?: string, @@ -176,3 +195,11 @@ const cfg: any = { olmAPI }; config.registerConfig(cfg); } + +function mockOlmAPISign(func: JestMockFn<[string], Promise>) { + const olmAPI: any = { + signMessage: func, + }; + const cfg: any = { olmAPI }; + config.registerConfig(cfg); +} diff --git a/lib/utils/device-list-utils.js b/lib/utils/device-list-utils.js --- a/lib/utils/device-list-utils.js +++ b/lib/utils/device-list-utils.js @@ -9,6 +9,13 @@ } from '../types/identity-service-types.js'; import { rawDeviceListValidator } from '../types/identity-service-types.js'; +function composeRawDeviceList(devices: $ReadOnlyArray): RawDeviceList { + return { + devices, + timestamp: Date.now(), + }; +} + function convertSignedDeviceListsToRawDeviceLists( signedDeviceLists: UsersSignedDeviceLists, ): UsersRawDeviceLists { @@ -34,4 +41,5 @@ export { convertSignedDeviceListsToRawDeviceLists, rawDeviceListFromSignedList, + composeRawDeviceList, }; diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -3,6 +3,7 @@ import * as React from 'react'; import { getOneTimeKeyValues } from 'lib/shared/crypto-utils.js'; +import { createAndSignInitialDeviceList } from 'lib/shared/device-list-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { type IdentityKeysBlob, @@ -325,6 +326,9 @@ commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); + const initialDeviceList = await createAndSignInitialDeviceList( + primaryIdentityPublicKeys.ed25519, + ); const registrationResult = await commRustModule.registerPasswordUser( username, password, @@ -337,7 +341,7 @@ getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), fid ?? '', - '', // initialDeviceList + JSON.stringify(initialDeviceList), ); const { userID, accessToken: token } = JSON.parse(registrationResult); const identityAuthResult = { accessToken: token, userID, username }; @@ -404,6 +408,9 @@ commCoreModule.getOneTimeKeys(ONE_TIME_KEYS_NUMBER), commCoreModule.validateAndGetPrekeys(), ]); + const initialDeviceList = await createAndSignInitialDeviceList( + primaryIdentityPublicKeys.ed25519, + ); const registrationResult = await commRustModule.registerWalletUser( siweMessage, siweSignature, @@ -416,7 +423,7 @@ getOneTimeKeyValues(contentOneTimeKeys), getOneTimeKeyValues(notificationsOneTimeKeys), fid ?? '', - '', // initialDeviceList + JSON.stringify(initialDeviceList), ); const { userID, accessToken: token } = JSON.parse(registrationResult); const identityAuthResult = { diff --git a/native/profile/secondary-device-qr-code-scanner.react.js b/native/profile/secondary-device-qr-code-scanner.react.js --- a/native/profile/secondary-device-qr-code-scanner.react.js +++ b/native/profile/secondary-device-qr-code-scanner.react.js @@ -13,7 +13,6 @@ backupKeysValidator, type BackupKeys, } from 'lib/types/backup-types.js'; -import type { RawDeviceList } from 'lib/types/identity-service-types.js'; import { tunnelbrokerMessageTypes, type TunnelbrokerMessage, @@ -24,6 +23,10 @@ type PeerToPeerMessage, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import { qrCodeAuthMessageTypes } from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; +import { + composeRawDeviceList, + rawDeviceListFromSignedList, +} from 'lib/utils/device-list-utils.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; @@ -72,9 +75,7 @@ invariant(deviceLists.length > 0, 'received empty device list history'); const lastSignedDeviceList = deviceLists[deviceLists.length - 1]; - const deviceList: RawDeviceList = JSON.parse( - lastSignedDeviceList.rawDeviceList, - ); + const deviceList = rawDeviceListFromSignedList(lastSignedDeviceList); const promises = deviceList.devices.map(recipient => tunnelbrokerContext.sendMessage({ @@ -109,19 +110,14 @@ invariant(deviceLists.length > 0, 'received empty device list history'); const lastSignedDeviceList = deviceLists[deviceLists.length - 1]; - const deviceList: RawDeviceList = JSON.parse( - lastSignedDeviceList.rawDeviceList, - ); + const deviceList = rawDeviceListFromSignedList(lastSignedDeviceList); const { devices } = deviceList; if (devices.includes(newDeviceID)) { return; } - const newDeviceList: RawDeviceList = { - devices: [...devices, newDeviceID], - timestamp: Date.now(), - }; + const newDeviceList = composeRawDeviceList([...devices, newDeviceID]); const signedDeviceList = await signDeviceListUpdate(newDeviceList); await updateDeviceList(signedDeviceList); },