diff --git a/keyserver/src/responders/landing-handler.js b/keyserver/src/responders/landing-handler.js --- a/keyserver/src/responders/landing-handler.js +++ b/keyserver/src/responders/landing-handler.js @@ -13,6 +13,7 @@ isValidPrimaryIdentityPublicKey, isValidSIWENonce, isValidSIWEMessageType, + isValidSIWEIssuedAt, } from 'lib/utils/siwe-utils.js'; import { getMessageForException } from './utils.js'; @@ -153,6 +154,18 @@ } 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(), @@ -215,6 +228,7 @@ siweNonce={siweNonce} siwePrimaryIdentityPublicKey={siwePrimaryIdentityPublicKey} siweMessageType={siweMessageType} + siweMessageIssuedAt={siweMessageIssuedAt} />, ); reactStream.pipe(res, { end: false }); @@ -227,12 +241,16 @@ const siweMessageTypeString = siweMessageType ? `"${siweMessageType}"` : 'null'; + const siweMessageIssuedAtString = siweMessageIssuedAt + ? `"${siweMessageIssuedAt}"` + : 'null'; // prettier-ignore res.end(html` + diff --git a/landing/landing-ssr.react.js b/landing/landing-ssr.react.js --- a/landing/landing-ssr.react.js +++ b/landing/landing-ssr.react.js @@ -14,6 +14,7 @@ +siweNonce: ?string, +siwePrimaryIdentityPublicKey: ?string, +siweMessageType: ?SIWEMessageType, + +siweMessageIssuedAt: ?string, }; function LandingSSR(props: LandingSSRProps): React.Node { const { @@ -22,6 +23,7 @@ siweNonce, siwePrimaryIdentityPublicKey, siweMessageType, + siweMessageIssuedAt, } = props; const siweContextValue = React.useMemo( @@ -29,8 +31,14 @@ siweNonce, siwePrimaryIdentityPublicKey, siweMessageType, + siweMessageIssuedAt, }), - [siweNonce, siwePrimaryIdentityPublicKey, siweMessageType], + [ + siweNonce, + siwePrimaryIdentityPublicKey, + siweMessageType, + siweMessageIssuedAt, + ], ); const routerContext = React.useMemo(() => ({}), []); return ( diff --git a/landing/root.js b/landing/root.js --- a/landing/root.js +++ b/landing/root.js @@ -12,6 +12,7 @@ declare var siweNonce: ?string; declare var siwePrimaryIdentityPublicKey: ?string; declare var siweMessageType: ?SIWEMessageType; +declare var siweMessageIssuedAt: ?string; function RootComponent(): React.Node { const siweContextValue = React.useMemo( @@ -19,6 +20,7 @@ siweNonce, siwePrimaryIdentityPublicKey, siweMessageType, + siweMessageIssuedAt, }), [], ); diff --git a/landing/siwe-context.js b/landing/siwe-context.js --- a/landing/siwe-context.js +++ b/landing/siwe-context.js @@ -8,12 +8,14 @@ +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 --- a/landing/siwe.react.js +++ b/landing/siwe.react.js @@ -13,7 +13,10 @@ 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, @@ -51,9 +54,10 @@ 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', @@ -68,8 +72,13 @@ 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'); @@ -81,13 +90,20 @@ 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(); @@ -141,6 +157,18 @@

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 { diff --git a/lib/types/siwe-types.js b/lib/types/siwe-types.js --- a/lib/types/siwe-types.js +++ b/lib/types/siwe-types.js @@ -141,10 +141,20 @@ 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, diff --git a/lib/utils/siwe-utils.js b/lib/utils/siwe-utils.js --- a/lib/utils/siwe-utils.js +++ b/lib/utils/siwe-utils.js @@ -32,6 +32,12 @@ 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'; @@ -39,6 +45,7 @@ address: string, statement: string, nonce: string, + issuedAt: ?string, ): string { invariant(nonce, 'nonce must be present in createSiweMessage'); const domain = window.location.host; @@ -47,6 +54,7 @@ domain, address, statement, + issuedAt, uri: origin, version: '1', chainId: '1', @@ -134,7 +142,12 @@ `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]: { @@ -153,12 +166,18 @@ showTermsAgreement: false, buttonStatement: siweBackupMessageSigningButtonStatement, }, + [SIWEMessageTypes.MSG_BACKUP_RESTORE]: { + explanationStatement: siweBackupRestoreMessageSigningExplanationStatements, + showTermsAgreement: false, + buttonStatement: siweBackupRestoreMessageSigningButtonStatement, + }, }; export { isValidSIWENonce, isValidEthereumAddress, isValidSIWEMessageType, + isValidSIWEIssuedAt, primaryIdentityPublicKeyRegex, isValidPrimaryIdentityPublicKey, createSIWEMessage,