From 6c5abb78556f1e29ee2a639fb396f2b622c759eb Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 13 Jan 2024 22:28:35 +0200 Subject: [PATCH] TW-1240: Magic Square campaign. + Msg nonce --- package.json | 1 + src/index.ts | 8 +++----- src/magic-square.ts | 27 ++++++++++++++++++--------- src/utils/errors.ts | 6 +++++- src/utils/signing-nonce.ts | 31 ++++++++++++++++++++----------- yarn.lock | 5 +++++ 6 files changed, 52 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index f446d1d..d44a9c9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dotenv": "^9.0.2", "express": "^4.18.2", "firebase-admin": "^10.0.2", + "http-status-codes": "^2.3.0", "ioredis": "^5.3.2", "lodash": "^4.17.21", "memoizee": "^0.4.15", diff --git a/src/index.ts b/src/index.ts index 8d86be7..623101d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ import { coinGeckoTokens } from './utils/gecko-tokens'; import { getExternalApiErrorPayload, isDefined, isNonEmptyString } from './utils/helpers'; import logger from './utils/logger'; import { getSignedMoonPayUrl } from './utils/moonpay/get-signed-moonpay-url'; -import { getSigningNonce, SIGNING_NONCE_TTL } from './utils/signing-nonce'; +import { getSigningNonce } from './utils/signing-nonce'; import SingleQueryDataProvider from './utils/SingleQueryDataProvider'; import { tezExchangeRateProvider } from './utils/tezos'; import { getExchangeRatesFromDB } from './utils/tokens'; @@ -360,14 +360,12 @@ app.get('/api/magic-square-quest/participants', basicAuth, async (req, res) => { } }); -app.get('/api/auth-nonce', async (req, res) => { +app.get('/api/signing-nonce', async (req, res) => { try { const pkh = req.query.pkh; if (!pkh || typeof pkh !== 'string') throw new Error('PKH is not a string'); - const nonce = getSigningNonce(pkh); - - res.status(200).send({ nonce, ttl: SIGNING_NONCE_TTL }); + res.status(200).send(getSigningNonce(pkh)); } catch (error: any) { console.error(error); diff --git a/src/magic-square.ts b/src/magic-square.ts index 0d880ee..0576e21 100644 --- a/src/magic-square.ts +++ b/src/magic-square.ts @@ -2,13 +2,13 @@ import * as ethersAddressUtils from '@ethersproject/address'; import * as ethersHashUtils from '@ethersproject/hash'; import * as ethersStringsUtils from '@ethersproject/strings'; import * as ethersTxUtils from '@ethersproject/transactions'; -import { STATUS_CODE } from '@taquito/http-utils'; import { validateAddress, ValidationResult, verifySignature, getPkhfromPk } from '@taquito/utils'; +import { StatusCodes } from 'http-status-codes'; import { redisClient } from './redis'; import { CodedError } from './utils/errors'; import { safeCheck } from './utils/helpers'; -import { getSigningNonce } from './utils/signing-nonce'; +import { getSigningNonce, removeSigningNonce } from './utils/signing-nonce'; const REDIS_DB_KEY = 'magic_square_quest'; @@ -32,7 +32,7 @@ export async function startMagicSquareQuest({ pkh, publicKey, messageBytes, sign // Public Key Hashes if (!safeCheck(() => validateAddress(pkh) === ValidationResult.VALID && getPkhfromPk(publicKey) === pkh)) - throw new CodedError(STATUS_CODE.BAD_REQUEST, 'Invalid Tezos public key (hash)'); + throw new CodedError(StatusCodes.BAD_REQUEST, 'Invalid Tezos public key (hash)'); let evmPkh: string; try { @@ -40,13 +40,23 @@ export async function startMagicSquareQuest({ pkh, publicKey, messageBytes, sign evmPkh = ethersAddressUtils.getAddress(evm.pkh); } catch (err) { console.error(err); - throw new CodedError(STATUS_CODE.BAD_REQUEST, 'Invalid EVM public key hash'); + throw new CodedError(StatusCodes.BAD_REQUEST, 'Invalid EVM public key hash'); } + // Nonce + const { value: nonce } = getSigningNonce(pkh); + const nonceBytes = Buffer.from(nonce, 'utf-8').toString('hex'); + + if (!messageBytes.includes(nonceBytes)) + throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid Tezos message nonce', 'INVALID_NONCE_TEZ'); + + if (!evm.messageBytes.includes(nonceBytes)) + throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid EVM message nonce', 'INVALID_NONCE_EVM'); + // Signatures if (!safeCheck(() => verifySignature(messageBytes, publicKey, signature))) - throw new CodedError(STATUS_CODE.UNAUTHORIZED, 'Invalid Tezos signature or message'); + throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid Tezos signature or message'); if ( !safeCheck(() => { @@ -56,7 +66,7 @@ export async function startMagicSquareQuest({ pkh, publicKey, messageBytes, sign return ethersTxUtils.recoverAddress(messageHash, evm.signature) === evmPkh; }) ) - throw new CodedError(STATUS_CODE.UNAUTHORIZED, 'Invalid EVM signature or message'); + throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid EVM signature or message'); // Presence check @@ -64,11 +74,10 @@ export async function startMagicSquareQuest({ pkh, publicKey, messageBytes, sign .lrange(REDIS_DB_KEY, 0, -1) .then(items => items.some(item => item.includes(pkh) && item.includes(evmPkh))); - if (exists) - throw new CodedError(STATUS_CODE.CONFLICT, 'Quest already started for the given credentials', 'QUEST_STARTED'); + if (exists) throw new CodedError(StatusCodes.CONFLICT, 'Your quest was already started before', 'QUEST_IS_STARTED'); // Auth nonce - getSigningNonce.delete(pkh); + removeSigningNonce(pkh); // Registering diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 0181cca..1877d31 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,10 +1,14 @@ +import type { StatusCodes } from 'http-status-codes'; + interface CodedErrorForResponse { message: string; code?: string; } +type StatusCodeNumber = (typeof StatusCodes)[keyof typeof StatusCodes]; + export class CodedError extends Error { - constructor(public code: number, message: string, public errorCode?: string) { + constructor(public code: StatusCodeNumber, message: string, public errorCode?: string) { super(message); } diff --git a/src/utils/signing-nonce.ts b/src/utils/signing-nonce.ts index edeb0bf..3933b7d 100644 --- a/src/utils/signing-nonce.ts +++ b/src/utils/signing-nonce.ts @@ -4,20 +4,29 @@ import memoizee from 'memoizee'; import { CodedError } from './errors'; -export const SIGNING_NONCE_TTL = 5 * 60_000; +const SIGNING_NONCE_TTL = 5 * 60_000; -const MEMOIZE_OPTIONS = { - max: 500, - maxAge: SIGNING_NONCE_TTL -}; +export const getSigningNonce = memoizee( + (pkh: string) => { + if (validateAddress(pkh) !== ValidationResult.VALID) throw new CodedError(400, 'Invalid address'); -export const getSigningNonce = memoizee((pkh: string) => { - if (validateAddress(pkh) !== ValidationResult.VALID) throw new CodedError(400, 'Invalid address'); + return buildNonce(); + }, + { + max: 500, + maxAge: SIGNING_NONCE_TTL + } +); - return buildNonce(); -}, MEMOIZE_OPTIONS); +export function removeSigningNonce(pkh: string) { + getSigningNonce.delete(pkh); +} function buildNonce() { - // The way it is done in SIWE.generateNonce() - return randomStringForEntropy(96); + // Same as in in SIWE.generateNonce() + const value = randomStringForEntropy(96); + + const expiresAt = new Date(Date.now() + SIGNING_NONCE_TTL).toISOString(); + + return { value, expiresAt }; } diff --git a/yarn.lock b/yarn.lock index 87912e5..4649e53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2674,6 +2674,11 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-status-codes@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc" + integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== + https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"