diff --git a/lib/components/qr-auth-provider.react.js b/lib/components/qr-auth-provider.react.js new file mode 100644 --- /dev/null +++ b/lib/components/qr-auth-provider.react.js @@ -0,0 +1,124 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import { useSecondaryDeviceLogIn } from '../hooks/login-hooks.js'; +import { useQRAuth } from '../hooks/qr-auth.js'; +import { uintArrayToHexString } from '../media/data-utils.js'; +import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; +import type { BackupKeys } from '../types/backup-types.js'; +import type { QRCodeAuthMessage } from '../types/tunnelbroker/peer-to-peer-message-types.js'; +import type { QRCodeAuthMessagePayload } from '../types/tunnelbroker/qr-code-auth-message-types.js'; +import { getContentSigningKey } from '../utils/crypto-utils.js'; + +type Props = { + +children: React.Node, + +onLoginError: (error: mixed) => void, + +generateAESKey: () => Promise, + +composeTunnelbrokerMessage: ( + encryptionKey: string, + payload: QRCodeAuthMessagePayload, + ) => Promise, + +processTunnelbrokerMessage: ( + encryptionKey: string, + message: QRCodeAuthMessage, + ) => Promise, + +performBackupRestore?: (backupKeys: BackupKeys) => Promise, +}; + +type QRData = ?{ +deviceID: string, +aesKey: string }; + +type QRAuthContextType = { + +qrData: QRData, + +generateQRCode: () => Promise, +}; + +const QRAuthContext: React.Context = + React.createContext({ + qrData: null, + generateQRCode: async () => {}, + }); + +function QRAuthProvider(props: Props): React.Node { + const { + children, + onLoginError, + generateAESKey, + composeTunnelbrokerMessage, + processTunnelbrokerMessage, + performBackupRestore, + } = props; + + const [qrData, setQRData] = React.useState(); + + const { setUnauthorizedDeviceID } = useTunnelbroker(); + + const generateQRCode = React.useCallback(async () => { + try { + const [ed25519, rawAESKey] = await Promise.all([ + getContentSigningKey(), + generateAESKey(), + ]); + const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); + + setUnauthorizedDeviceID(ed25519); + setQRData({ deviceID: ed25519, aesKey: aesKeyAsHexString }); + } catch (err) { + console.error('Failed to generate QR Code:', err); + } + }, [generateAESKey, setUnauthorizedDeviceID]); + + const logInSecondaryDevice = useSecondaryDeviceLogIn(); + const performLogIn = React.useCallback( + async (userID: string) => { + try { + await logInSecondaryDevice(userID); + } catch (err) { + onLoginError(err); + void generateQRCode(); + } + }, + [logInSecondaryDevice, onLoginError, generateQRCode], + ); + + const qrAuthInput = React.useMemo( + () => ({ + secondaryDeviceID: qrData?.deviceID, + aesKey: qrData?.aesKey, + performSecondaryDeviceRegistration: performLogIn, + composeMessage: composeTunnelbrokerMessage, + processMessage: processTunnelbrokerMessage, + performBackupRestore, + }), + [ + qrData, + performLogIn, + composeTunnelbrokerMessage, + processTunnelbrokerMessage, + performBackupRestore, + ], + ); + useQRAuth(qrAuthInput); + + const value = React.useMemo( + () => ({ + qrData, + generateQRCode, + }), + [qrData, generateQRCode], + ); + + return ( + {children} + ); +} + +function useQRAuthContext(): QRAuthContextType { + const context = React.useContext(QRAuthContext); + invariant(context, 'QRAuthContext not found'); + + return context; +} + +export { QRAuthProvider, useQRAuthContext };