From 7e3af2d4c92981e5ed0960f26f139b137b1c4712 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jan 2025 04:00:07 +0200 Subject: [PATCH] TW-1612: Temple Tap AirDrop. + SigAuth middleware --- src/index.ts | 4 +- src/magic-square.ts | 2 +- src/sig-auth.ts | 82 ++++++++++++++++++++++++++++++++++++++ src/utils/signing-nonce.ts | 32 --------------- 4 files changed, 85 insertions(+), 35 deletions(-) create mode 100644 src/sig-auth.ts delete mode 100644 src/utils/signing-nonce.ts diff --git a/src/index.ts b/src/index.ts index 54c5675..4055186 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { redisClient } from './redis'; import { evmRouter } from './routers/evm'; import { adRulesRouter } from './routers/slise-ad-rules'; import { templeWalletAdsRouter } from './routers/temple-wallet-ads'; +import { getSigningNonce, tezosSigAuthMiddleware } from './sig-auth'; import { getTkeyStats } from './tkey-stats'; import { getABData } from './utils/ab-test'; import { cancelAliceBobOrder } from './utils/alice-bob/cancel-alice-bob-order'; @@ -39,7 +40,6 @@ 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 } from './utils/signing-nonce'; import SingleQueryDataProvider from './utils/SingleQueryDataProvider'; import { getExchangeRates } from './utils/tokens'; @@ -398,7 +398,7 @@ app.get('/api/signing-nonce', (req, res) => { } }); -app.post('/api/temple-tap/confirm-airdrop-username', async (req, res) => { +app.post('/api/temple-tap/confirm-airdrop-username', tezosSigAuthMiddleware, async (req, res) => { try { const response = await fetch(new URL('v1/confirm-airdrop-address', EnvVars.TEMPLE_TAP_API_URL + '/'), { method: 'POST', diff --git a/src/magic-square.ts b/src/magic-square.ts index 28f3a2e..6976fd9 100644 --- a/src/magic-square.ts +++ b/src/magic-square.ts @@ -6,9 +6,9 @@ import { verifySignature, getPkhfromPk } from '@taquito/utils'; import { StatusCodes } from 'http-status-codes'; import { objectStorageMethodsFactory } from './redis'; +import { getSigningNonce, removeSigningNonce } from './sig-auth'; import { CodedError } from './utils/errors'; import { safeCheck } from './utils/helpers'; -import { getSigningNonce, removeSigningNonce } from './utils/signing-nonce'; interface Participant { pkh: string; diff --git a/src/sig-auth.ts b/src/sig-auth.ts new file mode 100644 index 0000000..d0c891c --- /dev/null +++ b/src/sig-auth.ts @@ -0,0 +1,82 @@ +import { randomStringForEntropy } from '@stablelib/random'; +import { getPkhfromPk, validateAddress, ValidationResult, verifySignature } from '@taquito/utils'; +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import memoizee from 'memoizee'; +import * as yup from 'yup'; + +import { CodedError } from './utils/errors'; + +const SIGNING_NONCE_TTL = 5 * 60_000; + +export const getSigningNonce = memoizee( + (pkh: string) => { + if (validateAddress(pkh) !== ValidationResult.VALID) throw new CodedError(400, 'Invalid address'); + + return buildNonce(); + }, + { + max: 1_000_000, + maxAge: SIGNING_NONCE_TTL + } +); + +export function removeSigningNonce(pkh: string) { + getSigningNonce.delete(pkh); +} + +export async function tezosSigAuthMiddleware(req: Request, res: Response, next: NextFunction) { + const sigHeaders = await sigAuthHeadersSchema.validate(req.headers).catch(() => null); + + if (!sigHeaders) return void res.status(StatusCodes.UNAUTHORIZED).send(); + + const { 'tw-tez-sig-pk': publicKey, 'tw-tez-sig-msg': messageBytes, 'tw-tez-sig-sig': signature } = sigHeaders; + + let pkh: string; + try { + pkh = getPkhfromPk(publicKey); + } catch (err) { + console.error(err); + + return void res.status(StatusCodes.BAD_REQUEST).send({ message: 'Invalid public key' }); + } + + // Nonce + const { value: nonce } = getSigningNonce(pkh); + const nonceBytes = Buffer.from(nonce, 'utf-8').toString('hex'); + + console.log('messageBytes=', messageBytes, '|', nonce); + + if (!messageBytes.includes(nonceBytes)) + return void res.status(StatusCodes.UNAUTHORIZED).send({ code: 'INVALID_NONCE', message: 'Invalid message nonce' }); + + // Signature + try { + verifySignature(messageBytes, publicKey, signature); + } catch (error) { + console.error(error); + + return void res + .status(StatusCodes.UNAUTHORIZED) + .send({ code: 'INVALID_SIG', message: 'Invalid signature or message' }); + } + + removeSigningNonce(pkh); + + next(); +} + +const sigAuthHeadersSchema = yup.object({ + 'tw-tez-sig-pk': yup.string().required(), + 'tw-tez-sig-msg': yup.string().required(), + 'tw-tez-sig-sig': yup.string().required() +}); + +function buildNonce() { + // 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/src/utils/signing-nonce.ts b/src/utils/signing-nonce.ts deleted file mode 100644 index c793348..0000000 --- a/src/utils/signing-nonce.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { randomStringForEntropy } from '@stablelib/random'; -import { validateAddress, ValidationResult } from '@taquito/utils'; -import memoizee from 'memoizee'; - -import { CodedError } from './errors'; - -const SIGNING_NONCE_TTL = 5 * 60_000; - -export const getSigningNonce = memoizee( - (pkh: string) => { - if (validateAddress(pkh) !== ValidationResult.VALID) throw new CodedError(400, 'Invalid address'); - - return buildNonce(); - }, - { - max: 1_000_000, - maxAge: SIGNING_NONCE_TTL - } -); - -export function removeSigningNonce(pkh: string) { - getSigningNonce.delete(pkh); -} - -function buildNonce() { - // Same as in in SIWE.generateNonce() - const value = randomStringForEntropy(96); - - const expiresAt = new Date(Date.now() + SIGNING_NONCE_TTL).toISOString(); - - return { value, expiresAt }; -}