diff --git a/lib/components/qr-auth-handler.react.js b/lib/components/qr-auth-handler.react.js --- a/lib/components/qr-auth-handler.react.js +++ b/lib/components/qr-auth-handler.react.js @@ -12,22 +12,35 @@ import { peerToPeerMessageTypes, peerToPeerMessageValidator, + type QRCodeAuthMessage, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; -import { qrCodeAuthMessageTypes } from '../types/tunnelbroker/qr-code-auth-message-types.js'; import { - createQRAuthTunnelbrokerMessage, - parseQRAuthTunnelbrokerMessage, -} from '../utils/qr-code-auth.js'; + qrCodeAuthMessageTypes, + type QRCodeAuthMessagePayload, +} from '../types/tunnelbroker/qr-code-auth-message-types.js'; type QRAuthHandlerProps = { +secondaryDeviceID: ?string, +aesKey: ?string, +performSecondaryDeviceRegistration: (userID: string) => Promise, + +composeMessage: ( + encryptionKey: string, + payload: QRCodeAuthMessagePayload, + ) => Promise, + +processMessage: ( + encryptionKey: string, + message: QRCodeAuthMessage, + ) => Promise, }; function QRAuthHandler(props: QRAuthHandlerProps): React.Node { - const { secondaryDeviceID, aesKey, performSecondaryDeviceRegistration } = - props; + const { + secondaryDeviceID, + aesKey, + processMessage, + composeMessage, + performSecondaryDeviceRegistration, + } = props; const [primaryDeviceID, setPrimaryDeviceID] = React.useState(); const { setUnauthorizedDeviceID, @@ -53,7 +66,7 @@ } void (async () => { - const message = createQRAuthTunnelbrokerMessage(aesKey, { + const message = await composeMessage(aesKey, { type: qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS, }); await sendMessage({ @@ -68,6 +81,7 @@ primaryDeviceID, aesKey, secondaryDeviceID, + composeMessage, ]); const tunnelbrokerMessageListener = React.useCallback( @@ -92,10 +106,7 @@ ) { return; } - const qrCodeAuthMessage = parseQRAuthTunnelbrokerMessage( - aesKey, - innerMessage, - ); + const qrCodeAuthMessage = await processMessage(aesKey, innerMessage); if ( qrCodeAuthMessage?.type === @@ -123,6 +134,7 @@ identityClient, aesKey, performSecondaryDeviceRegistration, + processMessage, ], ); diff --git a/lib/utils/qr-code-auth.js b/lib/utils/qr-code-auth.js deleted file mode 100644 --- a/lib/utils/qr-code-auth.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow - -import { - peerToPeerMessageTypes, - type QRCodeAuthMessage, -} from '../types/tunnelbroker/peer-to-peer-message-types.js'; -import { - qrCodeAuthMessagePayloadValidator, - type QRCodeAuthMessagePayload, -} from '../types/tunnelbroker/qr-code-auth-message-types.js'; - -function createQRAuthTunnelbrokerMessage( - encryptionKey: string, - payload: QRCodeAuthMessagePayload, -): QRCodeAuthMessage { - return { - type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE, - encryptedContent: JSON.stringify(payload), - }; -} - -function parseQRAuthTunnelbrokerMessage( - encryptionKey: string, - message: QRCodeAuthMessage, -): ?QRCodeAuthMessagePayload { - const payload = JSON.parse(message.encryptedContent); - if (!qrCodeAuthMessagePayloadValidator.is(payload)) { - return null; - } - - return payload; -} - -export { createQRAuthTunnelbrokerMessage, parseQRAuthTunnelbrokerMessage }; 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 @@ -20,13 +20,13 @@ 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 { - createQRAuthTunnelbrokerMessage, - parseQRAuthTunnelbrokerMessage, -} from 'lib/utils/qr-code-auth.js'; import type { ProfileNavigationProp } from './profile.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; +import { + composeTunnelbrokerQRAuthMessage, + parseTunnelbrokerQRAuthMessage, +} from '../qr-code/qr-code-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; @@ -145,7 +145,7 @@ return; } - const payload = parseQRAuthTunnelbrokerMessage( + const payload = await parseTunnelbrokerQRAuthMessage( encryptionKey, innerMessage, ); @@ -158,12 +158,15 @@ void broadcastDeviceListUpdate(); - const backupKeyMessage = createQRAuthTunnelbrokerMessage(encryptionKey, { - type: qrCodeAuthMessageTypes.BACKUP_DATA_KEY_MESSAGE, - backupID: 'stub', - backupDataKey: 'stub', - backupLogDataKey: 'stub', - }); + const backupKeyMessage = await composeTunnelbrokerQRAuthMessage( + encryptionKey, + { + type: qrCodeAuthMessageTypes.BACKUP_DATA_KEY_MESSAGE, + backupID: 'stub', + backupDataKey: 'stub', + backupLogDataKey: 'stub', + }, + ); await tunnelbrokerContext.sendMessage({ deviceID: targetDeviceID, payload: JSON.stringify(backupKeyMessage), @@ -228,7 +231,7 @@ throw new Error('missing auth metadata'); } await addDeviceToList(ed25519); - const message = createQRAuthTunnelbrokerMessage(aes256, { + const message = await composeTunnelbrokerQRAuthMessage(aes256, { type: qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS, userID, primaryDeviceID, diff --git a/native/qr-code/qr-code-screen.react.js b/native/qr-code/qr-code-screen.react.js --- a/native/qr-code/qr-code-screen.react.js +++ b/native/qr-code/qr-code-screen.react.js @@ -16,6 +16,10 @@ } from 'lib/types/identity-service-types.js'; import type { QRCodeSignInNavigationProp } from './qr-code-sign-in-navigator.react.js'; +import { + composeTunnelbrokerQRAuthMessage, + parseTunnelbrokerQRAuthMessage, +} from './qr-code-utils.js'; import { commCoreModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; @@ -93,6 +97,8 @@ secondaryDeviceID={qrData?.deviceID} aesKey={qrData?.aesKey} performSecondaryDeviceRegistration={performRegistration} + composeMessage={composeTunnelbrokerQRAuthMessage} + processMessage={parseTunnelbrokerQRAuthMessage} /> Log in to Comm diff --git a/native/qr-code/qr-code-utils.js b/native/qr-code/qr-code-utils.js new file mode 100644 --- /dev/null +++ b/native/qr-code/qr-code-utils.js @@ -0,0 +1,55 @@ +// @flow + +import { hexToUintArray } from 'lib/media/data-utils.js'; +import { + peerToPeerMessageTypes, + type QRCodeAuthMessage, +} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; +import { + qrCodeAuthMessagePayloadValidator, + type QRCodeAuthMessagePayload, +} from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; + +import { + convertBytesToObj, + convertObjToBytes, +} from '../backup/conversion-utils.js'; +import { commUtilsModule } from '../native-modules.js'; +import * as AES from '../utils/aes-crypto-module.js'; + +function composeTunnelbrokerQRAuthMessage( + encryptionKey: string, + obj: QRCodeAuthMessagePayload, +): Promise { + const objBytes = convertObjToBytes(obj); + const keyBytes = hexToUintArray(encryptionKey); + const encryptedBytes = AES.encrypt(keyBytes, objBytes); + const encryptedContent = commUtilsModule.base64EncodeBuffer( + encryptedBytes.buffer, + ); + return Promise.resolve({ + type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE, + encryptedContent, + }); +} + +function parseTunnelbrokerQRAuthMessage( + encryptionKey: string, + message: QRCodeAuthMessage, +): Promise { + const encryptedData = commUtilsModule.base64DecodeBuffer( + message.encryptedContent, + ); + const decryptedData = AES.decrypt( + hexToUintArray(encryptionKey), + new Uint8Array(encryptedData), + ); + const payload = convertBytesToObj(decryptedData); + if (!qrCodeAuthMessagePayloadValidator.is(payload)) { + return Promise.resolve(null); + } + + return Promise.resolve(payload); +} + +export { composeTunnelbrokerQRAuthMessage, parseTunnelbrokerQRAuthMessage }; diff --git a/web/account/qr-code-login.react.js b/web/account/qr-code-login.react.js --- a/web/account/qr-code-login.react.js +++ b/web/account/qr-code-login.react.js @@ -10,7 +10,8 @@ import { QRAuthHandler } from 'lib/components/qr-auth-handler.react.js'; import { qrCodeLinkURL } from 'lib/facts/links.js'; import { generateKeyCommon } from 'lib/media/aes-crypto-utils-common.js'; -import { uintArrayToHexString } from 'lib/media/data-utils.js'; +import * as AES from 'lib/media/aes-crypto-utils-common.js'; +import { hexToUintArray, uintArrayToHexString } from 'lib/media/data-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { CryptoStore, PickledOLMAccount } from 'lib/types/crypto-types.js'; @@ -19,11 +20,27 @@ SignedMessage, } from 'lib/types/identity-service-types.js'; import type { WebAppState } from 'lib/types/redux-types.js'; +import { + peerToPeerMessageTypes, + type QRCodeAuthMessage, +} from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; +import { + qrCodeAuthMessagePayloadValidator, + type QRCodeAuthMessagePayload, +} from 'lib/types/tunnelbroker/qr-code-auth-message-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import css from './qr-code-login.css'; import { initOlm } from '../olm/olm-utils.js'; import { useSelector } from '../redux/redux-utils.js'; +import { + base64DecodeBuffer, + base64EncodeBuffer, +} from '../utils/base64-utils.js'; +import { + convertBytesToObj, + convertObjToBytes, +} from '../utils/conversion-utils.js'; const deviceIDAndPrimaryAccountSelector: (state: WebAppState) => { ed25519Key: ?string, @@ -36,6 +53,38 @@ }), ); +async function composeTunnelbrokerMessage( + encryptionKey: string, + obj: QRCodeAuthMessagePayload, +): Promise { + const objBytes = convertObjToBytes(obj); + const keyBytes = hexToUintArray(encryptionKey); + const encryptedBytes = await AES.encryptCommon(crypto, keyBytes, objBytes); + const encryptedContent = base64EncodeBuffer(encryptedBytes); + return { + type: peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE, + encryptedContent, + }; +} + +async function parseTunnelbrokerMessage( + encryptionKey: string, + message: QRCodeAuthMessage, +): Promise { + const encryptedData = base64DecodeBuffer(message.encryptedContent); + const decryptedData = await AES.decryptCommon( + crypto, + hexToUintArray(encryptionKey), + new Uint8Array(encryptedData), + ); + const payload = convertBytesToObj(decryptedData); + if (!qrCodeAuthMessagePayloadValidator.is(payload)) { + return null; + } + + return payload; +} + function QrCodeLogin(): React.Node { const [qrCodeValue, setQrCodeValue] = React.useState(); const { ed25519Key, primaryAccount } = useSelector( @@ -120,6 +169,8 @@ secondaryDeviceID={deviceKeys?.deviceID} aesKey={deviceKeys?.aesKey} performSecondaryDeviceRegistration={performRegistration} + composeMessage={composeTunnelbrokerMessage} + processMessage={parseTunnelbrokerMessage} />
Log in to Comm
diff --git a/web/jest-setup.js b/web/jest-setup.js --- a/web/jest-setup.js +++ b/web/jest-setup.js @@ -1,8 +1,12 @@ // @flow +/* eslint-disable no-undef -- "global is not defined" */ import crypto from 'crypto'; +import util from 'util'; // crypto.webcrypto was introduced in Node 15.10.0. // It is not defined in Flow so we need a cast -// eslint-disable-next-line no-undef -- "global is not defined" global.crypto = (crypto: any).webcrypto; + +global.TextEncoder = util.TextEncoder; +global.TextDecoder = util.TextDecoder; diff --git a/web/utils/conversion-utils.js b/web/utils/conversion-utils.js new file mode 100644 --- /dev/null +++ b/web/utils/conversion-utils.js @@ -0,0 +1,13 @@ +// @flow + +function convertObjToBytes(obj: T): Uint8Array { + const objStr = JSON.stringify(obj); + return new TextEncoder().encode(objStr ?? ''); +} + +function convertBytesToObj(bytes: Uint8Array): T { + const str = new TextDecoder().decode(bytes.buffer); + return JSON.parse(str); +} + +export { convertObjToBytes, convertBytesToObj }; diff --git a/web/utils/conversion-utils.test.js b/web/utils/conversion-utils.test.js new file mode 100644 --- /dev/null +++ b/web/utils/conversion-utils.test.js @@ -0,0 +1,14 @@ +// @flow + +import { convertBytesToObj, convertObjToBytes } from './conversion-utils.js'; + +describe('convertObjToBytes and convertBytesToObj', () => { + it('should convert object to byte array and back', () => { + const obj = { hello: 'world', foo: 'bar', a: 2, b: false }; + + const bytes = convertObjToBytes(obj); + const restored = convertBytesToObj(bytes); + + expect(restored).toStrictEqual(obj); + }); +});