From b7ae20f0b09f9cedf30e3563b0e04d403130d518 Mon Sep 17 00:00:00 2001 From: JP Angelle Date: Fri, 20 Oct 2023 14:00:50 -0500 Subject: [PATCH] fix: safe auth --- .../src/components/OnboardingAuthProvider.tsx | 86 ++++++++++++++++-- .../controllers/auth/authenticateWallet.ts | 91 ++++++++++++++++++- .../src/utils/networks/networkSwitch.ts | 2 +- 3 files changed, 167 insertions(+), 12 deletions(-) diff --git a/centrifuge-app/src/components/OnboardingAuthProvider.tsx b/centrifuge-app/src/components/OnboardingAuthProvider.tsx index 53cfb23100..92fd8a8a1b 100644 --- a/centrifuge-app/src/components/OnboardingAuthProvider.tsx +++ b/centrifuge-app/src/components/OnboardingAuthProvider.tsx @@ -2,6 +2,8 @@ import Centrifuge from '@centrifuge/centrifuge-js' import { useCentrifuge, useCentrifugeUtils, useEvmProvider, useWallet } from '@centrifuge/centrifuge-react' import { encodeAddress } from '@polkadot/util-crypto' import { Wallet } from '@subwallet/wallet-connect/types' +import { BigNumber, ethers } from 'ethers' +import { hashMessage } from 'ethers/lib/utils' import * as React from 'react' import { useMutation, useQuery } from 'react-query' @@ -222,20 +224,41 @@ Nonce: ${nonce} Issued At: ${new Date().toISOString()}` const signedMessage = await signer?.signMessage(message) - const tokenRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/authenticateWallet`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ + + let body + + if (signedMessage === '0x') { + const messageHash = hashMessage(message) + + const isValid = await isValidSignature(signer, address, messageHash, evmChainId || 1) + + if (isValid) { + body = JSON.stringify({ + safeAddress: address, + messageHash, + evmChainId, + }) + } else { + throw new Error('Invalid signature') + } + } else { + body = JSON.stringify({ message, signature: signedMessage, address, nonce, network: isEvmOnSubstrate ? 'evmOnSubstrate' : 'evm', chainId: evmChainId || 1, - }), + }) + } + + const tokenRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/authenticateWallet`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body, }) if (tokenRes.status !== 200) { throw new Error('Failed to authenticate wallet') @@ -249,3 +272,50 @@ Issued At: ${new Date().toISOString()}` ) } } + +const isValidSignature = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => { + const MAGIC_VALUE_BYTES = '0x20c13b0b' + + const safeContract = new ethers.Contract( + safeAddress, + [ + 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', + 'function getMessageHash(bytes memory message) public view returns (bytes32)', + 'function getThreshold() public view returns (uint256)', + ], + provider + ) + + const safeMessageHash = await safeContract.getMessageHash(messageHash) + + const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) + + if (!safeMessage) { + throw new Error('Unable to fetch SafeMessage') + } + + const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() + + if (!threshold || threshold > safeMessage.confirmations.length) { + throw new Error('Threshold has not been met') + } + + const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) + + return response === MAGIC_VALUE_BYTES +} + +const TX_SERVICE_URLS: Record = { + '1': 'https://safe-transaction-mainnet.safe.global/api', + '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', +} + +const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { + const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] + + const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { + headers: { 'Content-Type': 'application/json' }, + }) + + return response.json() +} diff --git a/onboarding-api/src/controllers/auth/authenticateWallet.ts b/onboarding-api/src/controllers/auth/authenticateWallet.ts index 34ec8c1169..ccb77ad9a8 100644 --- a/onboarding-api/src/controllers/auth/authenticateWallet.ts +++ b/onboarding-api/src/controllers/auth/authenticateWallet.ts @@ -1,6 +1,9 @@ +import { InfuraProvider } from '@ethersproject/providers' import { isAddress } from '@polkadot/util-crypto' +import { BigNumber, ethers } from 'ethers' import { Request, Response } from 'express' import * as jwt from 'jsonwebtoken' +import fetch from 'node-fetch' import { InferType, number, object, string, StringSchema } from 'yup' import { SupportedNetworks } from '../../database' import { reportHttpError } from '../../utils/httpError' @@ -33,13 +36,36 @@ const verifyWalletInput = object({ chainId: number().required(), }) +const verifySafeInput = object({ + messageHash: string().required(), + safeAddress: string() + .required() + .test({ + name: 'is-address', + test(value, ctx) { + if (isAddress(value)) return true + return ctx.createError({ message: 'Invalid address', path: ctx.path }) + }, + }), + evmChainId: number().required(), +}) + export const authenticateWalletController = async ( - req: Request<{}, {}, InferType>, + req: Request<{}, {}, InferType>, res: Response ) => { try { - await validateInput(req.body, verifyWalletInput) - const payload = await new NetworkSwitch(req.body.network).verifiyWallet(req, res) + let payload + + if ('safeAddress' in req.body) { + await validateInput(req.body, verifySafeInput) + + const provider = new InfuraProvider(req.body.evmChainId, process.env.INFURA_KEY) + payload = await verifySafeWallet(provider, req.body.safeAddress, req.body.messageHash, req.body.evmChainId) + } else { + await validateInput(req.body, verifyWalletInput) + payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res) + } const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '8h', @@ -51,3 +77,62 @@ export const authenticateWalletController = async ( return res.status(error.code).send({ error: error.message, e }) } } + +const verifySafeWallet = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => { + try { + const MAGIC_VALUE_BYTES = '0x20c13b0b' + + const safeContract = new ethers.Contract( + safeAddress, + [ + 'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)', + 'function getMessageHash(bytes memory message) public view returns (bytes32)', + 'function getThreshold() public view returns (uint256)', + ], + provider + ) + + const safeMessageHash = await safeContract.getMessageHash(messageHash) + + const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId) + + if (!safeMessage) { + throw new Error('Unable to fetch SafeMessage') + } + + const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber() + + if (!threshold || threshold > safeMessage.confirmations.length) { + throw new Error('Threshold has not been met') + } + + const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature) + + if (response === MAGIC_VALUE_BYTES) { + return { + address: safeAddress, + chainId: evmChainId, + network: 'evm', + } + } + + throw new Error('Invalid signature') + } catch { + throw new Error('Something went wrong') + } +} + +const TX_SERVICE_URLS: Record = { + '1': 'https://safe-transaction-mainnet.safe.global/api', + '5': 'https://safe-transaction-goerli.staging.5afe.dev/api', +} + +const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => { + const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()] + + const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, { + headers: { 'Content-Type': 'application/json' }, + }) + + return response.json() +} diff --git a/onboarding-api/src/utils/networks/networkSwitch.ts b/onboarding-api/src/utils/networks/networkSwitch.ts index 73a9d14976..19fcf6e146 100644 --- a/onboarding-api/src/utils/networks/networkSwitch.ts +++ b/onboarding-api/src/utils/networks/networkSwitch.ts @@ -18,7 +18,7 @@ export class NetworkSwitch { this.network = network } - verifiyWallet = (req: Request, res: Response) => { + verifyWallet = (req: Request, res: Response) => { if (this.network === 'substrate') { return verifySubstrateWallet(req, res) } else if (this.network === 'evm' || this.network === 'evmOnSubstrate') {