diff --git a/keyserver/src/responders/landing-handler.js b/keyserver/src/responders/landing-handler.js index 1b7052524..a9f60f6c3 100644 --- a/keyserver/src/responders/landing-handler.js +++ b/keyserver/src/responders/landing-handler.js @@ -1,242 +1,260 @@ // @flow import html from 'common-tags/lib/html/index.js'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; import { promisify } from 'util'; import { type SIWEMessageType } from 'lib/types/siwe-types.js'; import { isValidPrimaryIdentityPublicKey, isValidSIWENonce, isValidSIWEMessageType, + isValidSIWEIssuedAt, } from 'lib/utils/siwe-utils.js'; import { getMessageForException } from './utils.js'; import { type LandingSSRProps } from '../landing/landing-ssr.react.js'; import { waitForStream } from '../utils/json-stream.js'; import { getAndAssertLandingURLFacts } from '../utils/urls.js'; async function landingHandler(req: $Request, res: $Response) { try { await landingResponder(req, res); } catch (e) { console.warn(e); if (!res.headersSent) { res.status(500).send(getMessageForException(e)); } } } const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&family=IBM+Plex+Sans:wght@400;500&display=swap'; const iaDuoFontsURL = 'fonts/duo.css'; const localFontsURL = 'fonts/local-fonts.css'; async function getDevFontURLs(): Promise<$ReadOnlyArray> { try { await access(localFontsURL); return [localFontsURL, iaDuoFontsURL]; } catch { return [googleFontsURL, iaDuoFontsURL]; } } type AssetInfo = { +jsURL: string, +fontURLs: $ReadOnlyArray, +cssInclude: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontURLs = await getDevFontURLs(); assetInfo = ({ jsURL: 'http://localhost:8082/dev.build.js', fontURLs, cssInclude: '', }: AssetInfo); return assetInfo; } try { const manifestString = await readFile( '../landing/dist/manifest.json', 'utf8', ); const manifest = JSON.parse(manifestString); assetInfo = ({ jsURL: `compiled/${manifest['browser.js']}`, fontURLs: [googleFontsURL, iaDuoFontsURL], cssInclude: html` `, }: AssetInfo); return assetInfo; } catch { throw new Error( 'Could not load manifest.json for landing build. ' + 'Did you forget to run `yarn dev` in the landing folder?', ); } } type LandingApp = React.ComponentType; let webpackCompiledRootComponent: ?LandingApp = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe landing/dist doesn't always exist const webpackBuild = await import('landing/dist/landing.build.cjs'); webpackCompiledRootComponent = webpackBuild.landing.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load landing.build.cjs. ' + 'Did you forget to run `yarn dev` in the landing folder?', ); } } // eslint-disable-next-line react/no-deprecated const { renderToNodeStream } = ReactDOMServer; async function landingResponder(req: $Request, res: $Response) { const siweNonce = req.header('siwe-nonce'); if ( siweNonce !== null && siweNonce !== undefined && !isValidSIWENonce(siweNonce) ) { res.status(400).send({ message: 'Invalid nonce in siwe-nonce header.', }); return; } const siwePrimaryIdentityPublicKey = req.header( 'siwe-primary-identity-public-key', ); if ( siwePrimaryIdentityPublicKey !== null && siwePrimaryIdentityPublicKey !== undefined && !isValidPrimaryIdentityPublicKey(siwePrimaryIdentityPublicKey) ) { res.status(400).send({ message: 'Invalid primary identity public key in siwe-primary-identity-public-key header.', }); return; } const siweMessageTypeRawString = req.header('siwe-message-type'); if ( siweMessageTypeRawString !== null && siweMessageTypeRawString !== undefined && !isValidSIWEMessageType(siweMessageTypeRawString) ) { res.status(400).send({ message: 'Invalid siwe message type.', }); return; } const siweMessageType = ((siweMessageTypeRawString: any): SIWEMessageType); + const siweMessageIssuedAt = req.header('siwe-message-issued-at'); + if ( + siweMessageIssuedAt !== null && + siweMessageIssuedAt !== undefined && + !isValidSIWEIssuedAt(siweMessageIssuedAt) + ) { + res.status(400).send({ + message: 'Invalid siwe message issued at.', + }); + return; + } + const [{ jsURL, fontURLs, cssInclude }, LandingSSR] = await Promise.all([ getAssetInfo(), getWebpackCompiledRootComponentForSSR(), ]); const fontsInclude = fontURLs .map(url => ``) .join(''); const urlFacts = getAndAssertLandingURLFacts(); const { basePath } = urlFacts; // prettier-ignore res.write(html` Comm ${fontsInclude} ${cssInclude}
`); // We remove trailing slash for `react-router` const routerBasename = basePath.replace(/\/$/, ''); const clientPath = routerBasename + req.url; const reactStream = renderToNodeStream( , ); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); const siweNonceString = siweNonce ? `"${siweNonce}"` : 'null'; const siwePrimaryIdentityPublicKeyString = siwePrimaryIdentityPublicKey ? `"${siwePrimaryIdentityPublicKey}"` : 'null'; const siweMessageTypeString = siweMessageType ? `"${siweMessageType}"` : 'null'; + const siweMessageIssuedAtString = siweMessageIssuedAt + ? `"${siweMessageIssuedAt}"` + : 'null'; // prettier-ignore res.end(html`
+ `); } export default landingHandler; diff --git a/landing/landing-ssr.react.js b/landing/landing-ssr.react.js index 59f3b0925..8b8a69bbe 100644 --- a/landing/landing-ssr.react.js +++ b/landing/landing-ssr.react.js @@ -1,45 +1,53 @@ // @flow import * as React from 'react'; import { StaticRouter } from 'react-router'; import { type SIWEMessageType } from 'lib/types/siwe-types.js'; import Landing from './landing.react.js'; import { SIWEContext } from './siwe-context.js'; export type LandingSSRProps = { +url: string, +basename: string, +siweNonce: ?string, +siwePrimaryIdentityPublicKey: ?string, +siweMessageType: ?SIWEMessageType, + +siweMessageIssuedAt: ?string, }; function LandingSSR(props: LandingSSRProps): React.Node { const { url, basename, siweNonce, siwePrimaryIdentityPublicKey, siweMessageType, + siweMessageIssuedAt, } = props; const siweContextValue = React.useMemo( () => ({ siweNonce, siwePrimaryIdentityPublicKey, siweMessageType, + siweMessageIssuedAt, }), - [siweNonce, siwePrimaryIdentityPublicKey, siweMessageType], + [ + siweNonce, + siwePrimaryIdentityPublicKey, + siweMessageType, + siweMessageIssuedAt, + ], ); const routerContext = React.useMemo(() => ({}), []); return ( ); } export default LandingSSR; diff --git a/landing/root.js b/landing/root.js index 5d84232d3..c2f404cc0 100644 --- a/landing/root.js +++ b/landing/root.js @@ -1,34 +1,36 @@ // @flow import * as React from 'react'; import { BrowserRouter } from 'react-router-dom'; import { type SIWEMessageType } from 'lib/types/siwe-types.js'; import Landing from './landing.react.js'; import { SIWEContext } from './siwe-context.js'; declare var routerBasename: string; declare var siweNonce: ?string; declare var siwePrimaryIdentityPublicKey: ?string; declare var siweMessageType: ?SIWEMessageType; +declare var siweMessageIssuedAt: ?string; function RootComponent(): React.Node { const siweContextValue = React.useMemo( () => ({ siweNonce, siwePrimaryIdentityPublicKey, siweMessageType, + siweMessageIssuedAt, }), [], ); return ( ); } export default RootComponent; diff --git a/landing/siwe-context.js b/landing/siwe-context.js index 96d25a561..40d7a4a1e 100644 --- a/landing/siwe-context.js +++ b/landing/siwe-context.js @@ -1,19 +1,21 @@ // @flow import * as React from 'react'; import { type SIWEMessageType } from 'lib/types/siwe-types.js'; export type SIWEContextType = { +siweNonce: ?string, +siwePrimaryIdentityPublicKey: ?string, +siweMessageType: ?SIWEMessageType, + +siweMessageIssuedAt: ?string, }; const SIWEContext: React.Context = React.createContext({ siweNonce: null, siwePrimaryIdentityPublicKey: null, siweMessageType: null, + siweMessageIssuedAt: null, }); export { SIWEContext }; diff --git a/landing/siwe.react.js b/landing/siwe.react.js index b0c8fcb2c..94e879608 100644 --- a/landing/siwe.react.js +++ b/landing/siwe.react.js @@ -1,204 +1,232 @@ // @flow import { useConnectModal, RainbowKitProvider, darkTheme, } from '@rainbow-me/rainbowkit'; import '@rainbow-me/rainbowkit/styles.css'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import invariant from 'invariant'; import _merge from 'lodash/fp/merge.js'; import * as React from 'react'; import { useAccount, useWalletClient, WagmiProvider } from 'wagmi'; import ConnectedWalletInfo from 'lib/components/connected-wallet-info.react.js'; -import { type SIWEWebViewMessage } from 'lib/types/siwe-types.js'; +import { + type SIWEWebViewMessage, + SIWEMessageTypes, +} from 'lib/types/siwe-types.js'; import { getSIWEStatementForPublicKey, userTextsForSIWEMessageTypes, createSIWEMessage, } from 'lib/utils/siwe-utils.js'; import { AlchemyENSCacheProvider, getWagmiConfig, } from 'lib/utils/wagmi-utils.js'; import { SIWEContext } from './siwe-context.js'; import css from './siwe.css'; import { useMonitorForWalletConnectModal, type WalletConnectModalUpdate, } from './walletconnect-hooks.js'; function postMessageToNativeWebView(message: SIWEWebViewMessage) { window.ReactNativeWebView?.postMessage?.(JSON.stringify(message)); } const wagmiConfig = getWagmiConfig([ 'rainbow', 'metamask', 'coinbase', 'walletconnect', ]); type Signer = { +signMessage: ({ +message: string, ... }) => Promise, ... }; async function signInWithEthereum( address: string, signer: Signer, nonce: string, statement: string, + issuedAt: ?string, ) { invariant(nonce, 'nonce must be present in signInWithEthereum'); - const message = createSIWEMessage(address, statement, nonce); + const message = createSIWEMessage(address, statement, nonce, issuedAt); const signature = await signer.signMessage({ message }); postMessageToNativeWebView({ type: 'siwe_success', address, message, signature, }); } const queryClient = new QueryClient(); function SIWE(): React.Node { const { address } = useAccount(); const { data: signer } = useWalletClient(); - const { siweNonce, siwePrimaryIdentityPublicKey, siweMessageType } = - React.useContext(SIWEContext); + const { + siweNonce, + siwePrimaryIdentityPublicKey, + siweMessageType, + siweMessageIssuedAt, + } = React.useContext(SIWEContext); + const onClick = React.useCallback(() => { invariant(siweNonce, 'nonce must be present during SIWE attempt'); invariant(siweMessageType, 'message type must be set during SIWE attempt'); invariant( siwePrimaryIdentityPublicKey, 'primaryIdentityPublicKey must be present during SIWE attempt', ); const statement = getSIWEStatementForPublicKey( siwePrimaryIdentityPublicKey, siweMessageType, ); - void signInWithEthereum(address, signer, siweNonce, statement); + void signInWithEthereum( + address, + signer, + siweNonce, + statement, + siweMessageIssuedAt, + ); }, [ address, signer, siweNonce, siwePrimaryIdentityPublicKey, siweMessageType, + siweMessageIssuedAt, ]); const { openConnectModal, connectModalOpen } = useConnectModal(); const hasNonce = siweNonce !== null && siweNonce !== undefined; React.useEffect(() => { if (hasNonce && openConnectModal) { openConnectModal(); } }, [hasNonce, openConnectModal]); const [wcModalOpen, setWCModalOpen] = React.useState(false); const prevModalOpen = React.useRef(false); const modalOpen = connectModalOpen || wcModalOpen; React.useEffect(() => { if (!modalOpen && prevModalOpen.current && !signer) { postMessageToNativeWebView({ type: 'siwe_closed' }); } prevModalOpen.current = modalOpen; }, [modalOpen, signer]); const onWalletConnectModalUpdate = React.useCallback( (update: WalletConnectModalUpdate) => { if (update.state === 'closed') { setWCModalOpen(false); postMessageToNativeWebView({ type: 'walletconnect_modal_update', ...update, }); } else { setWCModalOpen(true); postMessageToNativeWebView({ type: 'walletconnect_modal_update', ...update, }); } }, [], ); useMonitorForWalletConnectModal(onWalletConnectModalUpdate); if (!siweMessageType) { return (

Unable to proceed: message type not found

); } if (!hasNonce) { return (

Unable to proceed: nonce not found.

); + } + if ( + siweMessageType === SIWEMessageTypes.MSG_BACKUP_RESTORE && + !siweMessageIssuedAt + ) { + return ( +
+

+ Unable to proceed: issuedAt type not found for msg_backup_restore +

+
+ ); } else if (!signer) { return null; } else { const { explanationStatement, buttonStatement, showTermsAgreement } = userTextsForSIWEMessageTypes[siweMessageType]; let termsOfUseAndPolicyInfo = null; if (showTermsAgreement) { termsOfUseAndPolicyInfo = (

By signing up, you agree to our{' '} Terms of Use &{' '} Privacy Policy.

); } return (

Wallet Connected

{explanationStatement}

{termsOfUseAndPolicyInfo}
{buttonStatement}
); } } function SIWEWrapper(): React.Node { const theme = React.useMemo(() => { return _merge(darkTheme())({ radii: { modal: 0, modalMobile: 0, }, colors: { modalBackdrop: '#242529', }, }); }, []); return ( ); } export default SIWEWrapper; diff --git a/lib/types/siwe-types.js b/lib/types/siwe-types.js index bc8e04edd..7dae8f300 100644 --- a/lib/types/siwe-types.js +++ b/lib/types/siwe-types.js @@ -1,154 +1,164 @@ // @flow import type { LegacyLogInExtraInfo } from './account-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { type DeviceTokenUpdateRequest, type PlatformDetails, } from './device-types.js'; import { type CalendarQuery } from './entry-types.js'; export type SIWENonceResponse = { +nonce: string, }; export type SIWEAuthRequest = { +message: string, +signature: string, +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, +doNotRegister?: boolean, }; export type LegacySIWEAuthServerCall = { +message: string, +signature: string, +doNotRegister?: boolean, ...LegacyLogInExtraInfo, }; export type SIWESocialProof = { +siweMessage: string, +siweMessageSignature: string, }; // This is a message that the rendered webpage (landing/siwe.react.js) uses to // communicate back to the React Native WebView that is rendering it // (native/account/siwe-panel.react.js) export type SIWEWebViewMessage = | { +type: 'siwe_success', +address: string, +message: string, +signature: string, } | { +type: 'siwe_closed', } | { +type: 'walletconnect_modal_update', +state: 'open', +height: number, } | { +type: 'walletconnect_modal_update', +state: 'closed', }; export type SIWEMessage = { // RFC 4501 dns authority that is requesting the signing. +domain: string, // Ethereum address performing the signing conformant to capitalization // encoded checksum specified in EIP-55 where applicable. +address: string, // Human-readable ASCII assertion that the user will sign, and it must not // contain `\n`. +statement?: string, // RFC 3986 URI referring to the resource that is the subject of the signing // (as in the __subject__ of a claim). +uri: string, // Current version of the message. +version: string, // EIP-155 Chain ID to which the session is bound, and the network where // Contract Accounts must be resolved. +chainId: number, // Randomized token used to prevent replay attacks, at least 8 alphanumeric // characters. +nonce: string, // ISO 8601 datetime string of the current time. +issuedAt: string, // ISO 8601 datetime string that, if present, indicates when the signed // authentication message is no longer valid. +expirationTime?: string, // ISO 8601 datetime string that, if present, indicates when the signed // authentication message will become valid. +notBefore?: string, // System-specific identifier that may be used to uniquely refer to the // sign-in request. +requestId?: string, // List of information or references to information the user wishes to have // resolved as part of authentication by the relying party. They are // expressed as RFC 3986 URIs separated by `\n- `. +resources?: $ReadOnlyArray, // @deprecated // Signature of the message signed by the wallet. // // This field will be removed in future releases, an additional parameter // was added to the validate function were the signature goes to validate // the message. +signature?: string, // @deprecated // Type of sign message to be generated. // // This field will be removed in future releases and will rely on the // message version. +type?: 'Personal signature', +verify: ({ +signature: string, ... }) => Promise, +toMessage: () => string, }; export type SIWEResult = { +address: string, +message: string, +signature: string, +nonceTimestamp: number, }; export type IdentityWalletRegisterInput = { +address: string, +message: string, +signature: string, +fid?: ?string, }; export const SIWEMessageTypes = Object.freeze({ MSG_AUTH: 'msg_auth', MSG_BACKUP: 'msg_backup', + MSG_BACKUP_RESTORE: 'msg_backup_restore', }); export type SIWEMessageType = $Values; +export type SIWESignatureRequestData = + | { +messageType: SIWEMessageType } + | { + +messageType: 'msg_backup_restore', + +siweNonce: string, + +siweStatement: string, + +siweIssuedAt: string, + }; + export type SIWEBackupSecrets = { +message: string, +signature: string, }; export const legacyKeyserverSIWENonceLifetime = 30 * 60 * 1000; // 30 minutes export const identitySIWENonceLifetime = 2 * 60 * 1000; // 2 minutes diff --git a/lib/utils/siwe-utils.js b/lib/utils/siwe-utils.js index d1a3797f7..dbe3d423f 100644 --- a/lib/utils/siwe-utils.js +++ b/lib/utils/siwe-utils.js @@ -1,174 +1,193 @@ // @flow import invariant from 'invariant'; import { SiweMessage } from 'siwe'; import t, { type TEnums } from 'tcomb'; import { isDev } from './dev-utils.js'; import { type SIWEMessage, SIWEMessageTypes, type SIWEMessageType, } from '../types/siwe-types.js'; import { values } from '../utils/objects.js'; const siweNonceRegex: RegExp = /^[a-zA-Z0-9]{17}$/; function isValidSIWENonce(candidate: string): boolean { return siweNonceRegex.test(candidate); } const ethereumAddressRegex: RegExp = /^0x[a-fA-F0-9]{40}$/; function isValidEthereumAddress(candidate: string): boolean { return ethereumAddressRegex.test(candidate); } const primaryIdentityPublicKeyRegex: RegExp = /^[a-zA-Z0-9+/]{43}$/; function isValidPrimaryIdentityPublicKey(candidate: string): boolean { return primaryIdentityPublicKeyRegex.test(candidate); } const siweMessageTypeValidator: TEnums = t.enums.of(values(SIWEMessageTypes)); function isValidSIWEMessageType(candidate: string): boolean { return siweMessageTypeValidator.is(candidate); } +const siweMessageIssuedAtRegex: RegExp = + /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)((-(\d{2}):(\d{2})|Z)?)$/; +function isValidSIWEIssuedAt(candidate: string): boolean { + return siweMessageIssuedAtRegex.test(candidate); +} + const siweStatementLegalAgreement: string = 'By continuing, I accept the Comm Terms of Service: https://comm.app/terms'; function createSIWEMessage( address: string, statement: string, nonce: string, + issuedAt: ?string, ): string { invariant(nonce, 'nonce must be present in createSiweMessage'); const domain = window.location.host; const origin = window.location.origin; const message = new SiweMessage({ domain, address, statement, + issuedAt, uri: origin, version: '1', chainId: '1', nonce, }); return message.prepareMessage(); } function isValidSIWEDomain(candidate: string): boolean { return isDev ? candidate === 'localhost:3000' : candidate === 'comm.app' || candidate === 'web.comm.app'; } function isValidSIWEURI(candidate: string): boolean { return isDev ? candidate === 'http://localhost:3000' : candidate === 'https://comm.app' || candidate === 'https://web.comm.app'; } // Verify that the SIWEMessage is a well formed Comm SIWE Auth message. function isValidSIWEMessage(candidate: SIWEMessage): boolean { return ( candidate.statement !== null && candidate.statement !== undefined && isValidSIWEStatementWithPublicKey(candidate.statement) && candidate.version === '1' && candidate.chainId === 1 && isValidSIWEDomain(candidate.domain) && isValidSIWEURI(candidate.uri) && isValidSIWENonce(candidate.nonce) && isValidEthereumAddress(candidate.address) ); } function getSIWEStatementForPublicKey( publicKey: string, messageType: SIWEMessageType, ): string { invariant( isValidPrimaryIdentityPublicKey(publicKey), 'publicKey must be well formed in getSIWEStatementForPublicKey', ); if (messageType === SIWEMessageTypes.MSG_AUTH) { return `Primary device IdPubKey: ${publicKey} ${siweStatementLegalAgreement}`; } return ( `Backup message for primary device IdPubKey: ` + `${publicKey} ${siweStatementLegalAgreement}` ); } const siweStatementWithPublicKeyRegex = /^(Primary device|Backup message for primary device) IdPubKey: [a-zA-Z0-9+/]{43} By continuing, I accept the Comm Terms of Service: https:\/\/comm.app\/terms$/; function isValidSIWEStatementWithPublicKey(candidate: string): boolean { return siweStatementWithPublicKeyRegex.test(candidate); } const publicKeyFromSIWEStatementRegex: RegExp = /[a-zA-Z0-9+/]{43}/; function getPublicKeyFromSIWEStatement(statement: string): string { invariant( isValidSIWEStatementWithPublicKey(statement), 'candidate must be well formed SIWE statement with public key', ); const publicKeyMatchArray = statement.match(publicKeyFromSIWEStatementRegex); invariant( publicKeyMatchArray !== null && publicKeyMatchArray !== undefined && publicKeyMatchArray.length === 1, 'publicKeyMatchArray should have one and only one element', ); return publicKeyMatchArray[0]; } // These are shown in the `SIWE` components on both `landing` and `web`. const siweMessageSigningExplanationStatements: string = `To complete the login process, you’ll now be ` + `asked to sign a message using your wallet. ` + `This signature will attest that your Ethereum ` + `identity is represented by your new Comm identity.`; const siweMessageSigningButtonStatement = 'Sign in using this wallet'; const siweBackupMessageSigningExplanationStatements: string = `Your signature on this message will be used to derive ` + `a secret key that will encrypt your Comm backup.`; +const siweBackupRestoreMessageSigningExplanationStatements: string = + `Your signature on this message will be used to derive ` + + `a secret key that will decrypt your Comm backup.`; + const siweBackupMessageSigningButtonStatement = 'Create a backup key'; +const siweBackupRestoreMessageSigningButtonStatement = 'Retrieve a backup key'; const userTextsForSIWEMessageTypes: { +[signatureRequestType: string]: { +explanationStatement: string, +showTermsAgreement: boolean, +buttonStatement: string, }, } = { [SIWEMessageTypes.MSG_AUTH]: { explanationStatement: siweMessageSigningExplanationStatements, showTermsAgreement: true, buttonStatement: siweMessageSigningButtonStatement, }, [SIWEMessageTypes.MSG_BACKUP]: { explanationStatement: siweBackupMessageSigningExplanationStatements, showTermsAgreement: false, buttonStatement: siweBackupMessageSigningButtonStatement, }, + [SIWEMessageTypes.MSG_BACKUP_RESTORE]: { + explanationStatement: siweBackupRestoreMessageSigningExplanationStatements, + showTermsAgreement: false, + buttonStatement: siweBackupRestoreMessageSigningButtonStatement, + }, }; export { isValidSIWENonce, isValidEthereumAddress, isValidSIWEMessageType, + isValidSIWEIssuedAt, primaryIdentityPublicKeyRegex, isValidPrimaryIdentityPublicKey, createSIWEMessage, isValidSIWEMessage, getSIWEStatementForPublicKey, isValidSIWEStatementWithPublicKey, getPublicKeyFromSIWEStatement, siweMessageSigningExplanationStatements, siweBackupMessageSigningExplanationStatements, siweBackupMessageSigningButtonStatement, siweMessageSigningButtonStatement, userTextsForSIWEMessageTypes, };