diff --git a/backend/CHANGELOG.md b/backend/CHANGELOG.md index 1fa9055..af2ad08 100644 --- a/backend/CHANGELOG.md +++ b/backend/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog + +## [4.2.4] - 2026-08-14 + +- Added support for OpenBao vault as an additional secrets manager. + ## [4.2.3] - 2026-04-05 - Added endpoints to withdraw stake and deposit. diff --git a/backend/package.json b/backend/package.json index f4132ce..f8efc6b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "arka", - "version": "4.2.3", + "version": "4.2.4", "description": "ARKA - (Albanian for Cashier's case) is the first open source Paymaster as a service software", "type": "module", "directories": { diff --git a/backend/src/plugins/config.ts b/backend/src/plugins/config.ts index 2ab112f..2d9b1ba 100644 --- a/backend/src/plugins/config.ts +++ b/backend/src/plugins/config.ts @@ -43,6 +43,8 @@ const ConfigSchema = Type.Strict( MTP_PVGL: Type.String() || undefined, MTP_PPGL: Type.String() || undefined, ENFORCE_LEGACY_TRANSACTIONS_CHAINS: Type.Array(Type.String()) || undefined, + OPENBAO_ADDR: Type.String(), + OPENBAO_TOKEN: Type.String(), }) ); @@ -56,6 +58,22 @@ const ajv = new Ajv({ export type ArkaConfig = Static; +const REDACTED_CONFIG_KEYS = new Set([ + "DATABASE_URL", + "DEFAULT_BUNDLER_API_KEY", + "HMAC_SECRET", + "OPENBAO_TOKEN", +]); + +function redactConfigForLogging(config: Record): Record { + return Object.fromEntries( + Object.entries(config).map(([key, value]) => [ + key, + REDACTED_CONFIG_KEYS.has(key) && value ? "[redacted]" : value, + ]), + ); +} + const configPlugin: FastifyPluginAsync = async (server) => { const validate = ajv.compile(ConfigSchema); server.log.info("Validating .env file on config"); @@ -89,7 +107,9 @@ const configPlugin: FastifyPluginAsync = async (server) => { EP8_PVGL: process.env.EP8_PVGL ?? '30000', MTP_PVGL: process.env.MTP_PVGL ?? '50000', MTP_PPGL: process.env.MTP_PPGL ?? '70000', - ENFORCE_LEGACY_TRANSACTIONS_CHAINS: process.env.ENFORCE_LEGACY_TRANSACTIONS_CHAINS?.split(',') ?? [] + ENFORCE_LEGACY_TRANSACTIONS_CHAINS: process.env.ENFORCE_LEGACY_TRANSACTIONS_CHAINS?.split(',') ?? [], + OPENBAO_ADDR: process.env.OPENBAO_ADDR ?? '', + OPENBAO_TOKEN: process.env.OPENBAO_TOKEN ?? '', } const valid = validate(envVar); @@ -132,10 +152,12 @@ const configPlugin: FastifyPluginAsync = async (server) => { EP8_PVGL: process.env.EP8_PVGL ?? '30000', MTP_PVGL: process.env.MTP_PVGL ?? '50000', MTP_PPGL: process.env.MTP_PPGL ?? '70000', - ENFORCE_LEGACY_TRANSACTIONS_CHAINS: process.env.ENFORCE_LEGACY_TRANSACTIONS_CHAINS?.split(',') ?? [] + ENFORCE_LEGACY_TRANSACTIONS_CHAINS: process.env.ENFORCE_LEGACY_TRANSACTIONS_CHAINS?.split(',') ?? [], + OPENBAO_ADDR: process.env.OPENBAO_ADDR ?? '', + OPENBAO_TOKEN: process.env.OPENBAO_TOKEN ?? '', } - server.log.info(config, "config:"); + server.log.info(redactConfigForLogging(config), "config:"); server.decorate("config", config); }; diff --git a/backend/src/routes/admin-routes.ts b/backend/src/routes/admin-routes.ts index d303722..09dac43 100644 --- a/backend/src/routes/admin-routes.ts +++ b/backend/src/routes/admin-routes.ts @@ -17,9 +17,8 @@ import ReturnCode from "../constants/ReturnCode.js"; import { encode, decode, verifySignature } from "../utils/crypto.js"; import SupportedNetworks from "../../config.json"; import { APIKey } from "../models/api-key.js"; -import { ArkaConfigUpdateData } from "../types/arka-config-dto.js"; +import { ArkaConfigUpdateData, SecretManagerRoutesOpts } from "../types/arka-config-dto.js"; import { ApiKeyDto } from "../types/apikey-dto.js"; -import { CreateSecretCommand, DeleteSecretCommand, GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import EtherspotAbi from "../abi/EtherspotAbi.js"; import { AuthDto } from "../types/auth-dto.js"; import { IncomingHttpHeaders } from "http"; @@ -27,7 +26,8 @@ import { EPVersions } from "../types/sponsorship-policy-dto.js"; import { getNetworkConfig, getViemChainDef } from "../utils/common.js"; import { Paymaster } from "../paymaster/index.js"; -const adminRoutes: FastifyPluginAsync = async (server) => { +const adminRoutes: FastifyPluginAsync = async (server, options) => { + const { secretManager } = options; const paymaster = new Paymaster({ feeMarkUp: server.config.FEE_MARKUP, multiTokenMarkUp: server.config.MULTI_TOKEN_MARKUP, @@ -44,13 +44,9 @@ const adminRoutes: FastifyPluginAsync = async (server) => { const prefixSecretId = 'arka_'; - let client: SecretsManagerClient; - const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; - if (!unsafeMode) { - client = new SecretsManagerClient(); - } + const getApiKeySecret = (apiKey: string) => secretManager.getSecret>(prefixSecretId + apiKey); const SUPPORTED_ENTRYPOINTS = { EPV_06: server.config.EPV_06, @@ -147,26 +143,20 @@ const adminRoutes: FastifyPluginAsync = async (server) => { if(!verifySignature(signature, request.body as string, timestamp, server.config.HMAC_SECRET)) return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.INVALID_SIGNATURE_OR_TIMESTAMP }); - const command = new GetSecretValueCommand({SecretId: prefixSecretId + body.apiKey}) - const secrets = await client.send(command).catch((err) => err); + const secrets = await getApiKeySecret(body.apiKey).catch((err) => err); if(!(secrets instanceof Error)) { request.log.error('Duplicate record found'); return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.DUPLICATE_RECORD }); } - const createCommand = new CreateSecretCommand({ - Name: prefixSecretId + body.apiKey, - SecretString: JSON.stringify({ - PRIVATE_KEY: privateKey, - PUBLIC_ADDRESS: publicAddress, - MNEMONIC: mnemonic, - BUNDLER_API_KEY: server.config.DEFAULT_BUNDLER_API_KEY - }), + await secretManager.createSecret(prefixSecretId + body.apiKey, { + PRIVATE_KEY: privateKey, + PUBLIC_ADDRESS: publicAddress, + MNEMONIC: mnemonic, + BUNDLER_API_KEY: server.config.DEFAULT_BUNDLER_API_KEY }); - await client.send(createCommand); - await server.apiKeyRepository.create({ apiKey: body.apiKey, walletAddress: publicAddress, @@ -281,12 +271,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.INVALID_SIGNATURE_OR_TIMESTAMP }); if(!verifySignature(signature, request.body as string, timestamp, server.config.HMAC_SECRET)) return reply.code(ReturnCode.NOT_AUTHORIZED).send({ error: ErrorMessage.INVALID_SIGNATURE_OR_TIMESTAMP }); - const getSecretCommand = new GetSecretValueCommand({SecretId: prefixSecretId + body.apiKey}); - const secretValue = await client.send(getSecretCommand) - if(secretValue instanceof Error) - return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.RECORD_NOT_FOUND }); - - const secrets = JSON.parse(secretValue.SecretString ?? '{}'); + const secrets = await getApiKeySecret(body.apiKey); if (!secrets['PRIVATE_KEY']) { server.log.info("Invalid Api Key provided") @@ -353,12 +338,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { return reply.code(400).send({error: ErrorMessage.BALANCE_EXCEEDS_THRESHOLD }); } - const deleteCommand = new DeleteSecretCommand({ - SecretId: prefixSecretId + body.apiKey, - RecoveryWindowInDays: server.config.DELETE_KEY_RECOVER_WINDOW, - }); - - await client.send(deleteCommand); + await secretManager.destroySecret(prefixSecretId + body.apiKey, server.config.DELETE_KEY_RECOVER_WINDOW); await server.apiKeyRepository.delete(body.apiKey); } else { @@ -442,12 +422,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { let supportedNetworks; if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + apiKey, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(apiKey); if (!secrets['PRIVATE_KEY']) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); } @@ -556,12 +531,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { let supportedNetworks; if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + apiKey, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(apiKey); if (!secrets['PRIVATE_KEY']) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); } @@ -659,12 +629,7 @@ const adminRoutes: FastifyPluginAsync = async (server) => { let supportedNetworks; if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + apiKey, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(apiKey); if (!secrets['PRIVATE_KEY']) { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }); } diff --git a/backend/src/routes/deposit-route.ts b/backend/src/routes/deposit-route.ts index 4a26aaa..a40673e 100644 --- a/backend/src/routes/deposit-route.ts +++ b/backend/src/routes/deposit-route.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Type } from "@sinclair/typebox"; import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify"; -import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { isAddress } from "viem"; import { Paymaster } from "../paymaster/index.js"; import SupportedNetworks from "../../config.json"; @@ -11,8 +10,10 @@ import { decode } from "../utils/crypto.js"; import { printRequest, getNetworkConfig } from "../utils/common.js"; import { APIKey } from "../models/api-key.js"; import { EPVersions } from "../types/sponsorship-policy-dto.js"; +import { SecretManagerRoutesOpts } from "../types/arka-config-dto.js"; -const depositRoutes: FastifyPluginAsync = async (server) => { +const depositRoutes: FastifyPluginAsync = async (server, options) => { + const { secretManager } = options; const paymaster = new Paymaster({ feeMarkUp: server.config.FEE_MARKUP, multiTokenMarkUp: server.config.MULTI_TOKEN_MARKUP, @@ -35,13 +36,9 @@ const depositRoutes: FastifyPluginAsync = async (server) => { const prefixSecretId = 'arka_'; - let client: SecretsManagerClient; - const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; - if (!unsafeMode) { - client = new SecretsManagerClient(); - } + const getApiKeySecret = (apiKey: string) => secretManager.getSecret>(prefixSecretId + apiKey); const ResponseSchema = { schema: { @@ -75,12 +72,7 @@ const depositRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -172,12 +164,7 @@ const depositRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { diff --git a/backend/src/routes/metadata-routes.ts b/backend/src/routes/metadata-routes.ts index 59ca89c..d1c532e 100644 --- a/backend/src/routes/metadata-routes.ts +++ b/backend/src/routes/metadata-routes.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { FastifyPluginAsync } from "fastify"; import { createPublicClient, http, getContract, getAddress, Address } from "viem"; import { privateKeyToAccount } from "viem/accounts"; @@ -13,8 +12,10 @@ import * as EtherspotAbi from "../abi/EtherspotAbi.js"; import {abi as verifyingPaymasterAbi} from "../abi/VerifyingPaymasterAbi.js"; import {abi as verifyingPaymasterV2Abi} from "../abi/VerifyingPaymasterAbiV2.js"; import {abi as verifyingPaymastersV3Abi} from "../abi/VerifyingPaymasterAbiV3.js"; +import { SecretManagerRoutesOpts } from "../types/arka-config-dto.js"; -const metadataRoutes: FastifyPluginAsync = async (server) => { +const metadataRoutes: FastifyPluginAsync = async (server, options) => { + const { secretManager } = options; const prefixSecretId = 'arka_'; @@ -24,13 +25,9 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { EPV_08: server.config.EPV_08 } - let client: SecretsManagerClient; - const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; - if (!unsafeMode) { - client = new SecretsManagerClient(); - } + const getApiKeySecret = (apiKey: string) => secretManager.getSecret>(prefixSecretId + apiKey); server.get('/metadata', async function (request, reply) { try { @@ -52,12 +49,7 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) } if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) { server.log.info("Invalid Api Key provided") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) @@ -157,12 +149,7 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) } if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) { server.log.info("Invalid Api Key provided") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) @@ -260,12 +247,7 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) } if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) { server.log.info("Invalid Api Key provided") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) @@ -334,4 +316,4 @@ const metadataRoutes: FastifyPluginAsync = async (server) => { }) } -export default metadataRoutes; \ No newline at end of file +export default metadataRoutes; diff --git a/backend/src/routes/paymaster-routes.ts b/backend/src/routes/paymaster-routes.ts index 060e08c..1defe82 100644 --- a/backend/src/routes/paymaster-routes.ts +++ b/backend/src/routes/paymaster-routes.ts @@ -4,7 +4,6 @@ import { createPublicClient, http, getAddress, isAddress } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { ethers } from "ethers"; import { gql, request as GLRequest } from "graphql-request"; -import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import SupportedNetworks from "../../config.json"; import ErrorMessage, { generateErrorMessage } from "../constants/ErrorMessage.js"; import ReturnCode from "../constants/ReturnCode.js"; @@ -17,7 +16,7 @@ import { PaymasterRoutesOpts } from "../types/arka-config-dto.js"; const paymasterRoutes: FastifyPluginAsync = async (server, options: PaymasterRoutesOpts) => { - const { paymaster } = options; + const { paymaster, secretManager } = options; const SUPPORTED_ENTRYPOINTS = { EPV_06: server.config.EPV_06, @@ -27,13 +26,9 @@ const paymasterRoutes: FastifyPluginAsync = async (server, const prefixSecretId = 'arka_'; - let client: SecretsManagerClient; - const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; - if (!unsafeMode) { - client = new SecretsManagerClient(); - } + const getApiKeySecret = (apiKey: string) => secretManager.getSecret>(prefixSecretId + apiKey); server.post("/", async function (request, reply) { @@ -113,12 +108,7 @@ const paymasterRoutes: FastifyPluginAsync = async (server, } if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) { server.log.info("Invalid Api Key provided") return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) @@ -577,4 +567,4 @@ const paymasterRoutes: FastifyPluginAsync = async (server, } */ }; -export default paymasterRoutes; \ No newline at end of file +export default paymasterRoutes; diff --git a/backend/src/routes/whitelist-routes.ts b/backend/src/routes/whitelist-routes.ts index a8679f9..b54f246 100644 --- a/backend/src/routes/whitelist-routes.ts +++ b/backend/src/routes/whitelist-routes.ts @@ -2,7 +2,6 @@ import { FastifyPluginAsync } from "fastify"; import { isAddress } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { Paymaster } from "../paymaster/index.js"; import SupportedNetworks from "../../config.json"; import ErrorMessage from "../constants/ErrorMessage.js"; @@ -12,8 +11,10 @@ import { printRequest, getNetworkConfig } from "../utils/common.js"; import { APIKey } from "../models/api-key.js"; import { ContractWhitelistDto } from "../types/contractWhitelist-dto.js"; import { EPVersions } from "../types/sponsorship-policy-dto.js"; +import { SecretManagerRoutesOpts } from "../types/arka-config-dto.js"; -const whitelistRoutes: FastifyPluginAsync = async (server) => { +const whitelistRoutes: FastifyPluginAsync = async (server, options) => { + const { secretManager } = options; const paymaster = new Paymaster({ feeMarkUp: server.config.FEE_MARKUP, multiTokenMarkUp: server.config.MULTI_TOKEN_MARKUP, @@ -36,13 +37,9 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const prefixSecretId = 'arka_'; - let client: SecretsManagerClient; - const unsafeMode: boolean = process.env.UNSAFE_MODE == "true" ? true : false; - if (!unsafeMode) { - client = new SecretsManagerClient(); - } + const getApiKeySecret = (apiKey: string) => secretManager.getSecret>(prefixSecretId + apiKey); server.post("/whitelist", async function (request, reply) { @@ -72,12 +69,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -181,12 +173,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (secrets['BUNDLER_API_KEY']) bundlerApiKey = secrets['BUNDLER_API_KEY']; privateKey = secrets['PRIVATE_KEY']; @@ -297,12 +284,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (secrets['BUNDLER_API_KEY']) bundlerApiKey = secrets['BUNDLER_API_KEY']; privateKey = secrets['PRIVATE_KEY']; @@ -378,12 +360,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -471,12 +448,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -571,12 +543,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -636,12 +603,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -702,12 +664,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -796,12 +753,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -896,12 +848,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -961,12 +908,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -1019,12 +961,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -1082,12 +1019,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -1144,12 +1076,7 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { const apiKeyEntity: APIKey | null = await server.apiKeyRepository.findOneByApiKey(api_key); if (!apiKeyEntity) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) if (!unsafeMode) { - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await getApiKeySecret(api_key); if (!secrets['PRIVATE_KEY']) return reply.code(ReturnCode.FAILURE).send({ error: ErrorMessage.INVALID_API_KEY }) privateKey = secrets['PRIVATE_KEY']; } else { @@ -1189,4 +1116,4 @@ const whitelistRoutes: FastifyPluginAsync = async (server) => { ) }; -export default whitelistRoutes; \ No newline at end of file +export default whitelistRoutes; diff --git a/backend/src/server.ts b/backend/src/server.ts index c410b2a..e0bc575 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -13,7 +13,6 @@ import { encodeFunctionData, } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; import fetch from 'node-fetch'; import sequelizePlugin from './plugins/sequelizePlugin.js'; import config from './plugins/config.js'; @@ -38,6 +37,7 @@ import { Paymaster } from './paymaster/index.js'; import { NativeOracles } from './constants/ChainlinkOracles.js'; import { MultiTokenPaymaster } from './models/multiTokenPaymaster.js'; import { MULTI_TOKEN_ORACLES, MULTI_TOKEN_PAYMASTERS } from './constants/MultiTokenPaymasterCronJob.js'; +import { getSecretManager } from './services/secrets-manager/index.js'; let server: FastifyInstance; const defaultThresholdValue = '0.001'; // in ETH @@ -86,17 +86,18 @@ const initializeServer = async (): Promise => { ep8Pvgl: server.config.EP8_PVGL, skipType2Txns: server.config.ENFORCE_LEGACY_TRANSACTIONS_CHAINS }); + const secretManager = getSecretManager(server.config.OPENBAO_ADDR, server.config.OPENBAO_TOKEN); // Synchronize all models await server.sequelize.sync(); server.log.info('registered sequelizePlugin...') - await server.register(paymasterRoutes, { paymaster }); + await server.register(paymasterRoutes, { paymaster, secretManager }); - await server.register(adminRoutes); + await server.register(adminRoutes, { secretManager }); - await server.register(metadataRoutes); + await server.register(metadataRoutes, { secretManager }); const coingeckoRepo = new CoingeckoTokensRepository(server.sequelize); @@ -127,11 +128,11 @@ const initializeServer = async (): Promise => { server.log.error('Error caught on getAndSetCoingeckoPrice: ', err); } - await server.register(depositRoutes); + await server.register(depositRoutes, { secretManager }); await server.register(tokenRoutes); - await server.register(whitelistRoutes); + await server.register(whitelistRoutes, { secretManager }); await server.register(sponsorshipPolicyRoutes); @@ -153,15 +154,9 @@ const initializeServer = async (): Promise => { if (process.env.CRON_PRIVATE_KEY) { const unsafeMode = process.env.UNSAFE_MODE === "true" ? true : false; if (!unsafeMode) { - const client = new SecretsManagerClient(); const api_key = process.env.DEFAULT_API_KEY; const prefixSecretId = "arka_"; - const AWSresponse = await client.send( - new GetSecretValueCommand({ - SecretId: prefixSecretId + api_key, - }) - ); - const secrets = JSON.parse(AWSresponse.SecretString ?? '{}'); + const secrets = await secretManager.getSecret>(prefixSecretId + api_key); configData = { coingeckoApiUrl: secrets["COINGECKO_API_URL"], coingeckoIds: secrets["COINGECKO_IDS"], @@ -172,7 +167,6 @@ const initializeServer = async (): Promise => { pythTestnetChainIds: secrets["PYTH_TESTNET_CHAINIDS"], pythTestnetUrl: secrets["PYTH_TESTNET_URL"] } - client.destroy(); } else { const configDatas = await arkaConfigRepository.findAll(); configData = configDatas.length > 0 ? configDatas[0] : null; diff --git a/backend/src/services/secrets-manager/aws.ts b/backend/src/services/secrets-manager/aws.ts new file mode 100644 index 0000000..d6d55a5 --- /dev/null +++ b/backend/src/services/secrets-manager/aws.ts @@ -0,0 +1,55 @@ +import { + CreateSecretCommand, + DeleteSecretCommand, + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; +import type { SecretManager, JsonObject } from "./interface.js"; + +export class AwsSecretManager implements SecretManager { + private readonly client = new SecretsManagerClient(); + + async getSecret(secretName: string): Promise { + const response = await this.client.send( + new GetSecretValueCommand({ + SecretId: secretName, + }), + ); + + const secretString = + response.SecretString ?? + (response.SecretBinary + ? Buffer.from(response.SecretBinary).toString("utf-8") + : undefined); + + return JSON.parse(secretString ?? "{}") as T; + } + + async createSecret( + secretName: string, + secretData: JsonObject, + ): Promise { + await this.client.send( + new CreateSecretCommand({ + Name: secretName, + SecretString: JSON.stringify(secretData), + }), + ); + + return true; + } + + async destroySecret( + secretName: string, + recoveryWindowInDays?: number, + ): Promise { + await this.client.send( + new DeleteSecretCommand({ + SecretId: secretName, + ...(recoveryWindowInDays === undefined ? {} : { RecoveryWindowInDays: recoveryWindowInDays }), + }), + ); + + return true; + } +} diff --git a/backend/src/services/secrets-manager/index.ts b/backend/src/services/secrets-manager/index.ts new file mode 100644 index 0000000..631e323 --- /dev/null +++ b/backend/src/services/secrets-manager/index.ts @@ -0,0 +1,10 @@ +import { AwsSecretManager } from "./aws.js"; +import { OpenBaoSecretManager } from "./openbao.js"; +import type { SecretManager } from "./interface.js"; + +export function getSecretManager(openbaoAddr = "", openbaoToken = ""): SecretManager { + if (openbaoAddr && openbaoToken) { + return new OpenBaoSecretManager(openbaoAddr, openbaoToken); + } + return new AwsSecretManager(); +} diff --git a/backend/src/services/secrets-manager/interface.ts b/backend/src/services/secrets-manager/interface.ts new file mode 100644 index 0000000..8807b06 --- /dev/null +++ b/backend/src/services/secrets-manager/interface.ts @@ -0,0 +1,13 @@ +export type JsonObject = Record; + +export interface SecretManager { + getSecret(secretName: string): Promise; + createSecret( + secretName: string, + secretData: JsonObject, + ): Promise; + destroySecret( + secretName: string, + recoveryWindowInDays?: number, + ): Promise; +} diff --git a/backend/src/services/secrets-manager/openbao.ts b/backend/src/services/secrets-manager/openbao.ts new file mode 100644 index 0000000..d1186d5 --- /dev/null +++ b/backend/src/services/secrets-manager/openbao.ts @@ -0,0 +1,139 @@ +import fetch from "node-fetch"; +import { SecretManager, JsonObject } from "./interface.js"; + +type OpenBaoKvV2Response = { + request_id: string; + lease_id: string; + lease_duration: number; + renewable: boolean; + data: { + data: T; + metadata: { + created_time: string; + custom_metadata: Record | null; + deletion_time: string; + destroyed: boolean; + version: number; + }; + }; + warnings: string[] | null; +}; + +export type OpenBaoDestroyResponse = Record; + +export type OpenBaoWriteResponse = { + request_id: string; + lease_id: string; + lease_duration: number; + renewable: boolean; + data?: { + created_time: string; + custom_metadata: Record | null; + deletion_time: string; + destroyed: boolean; + version: number; + }; + warnings: string[] | null; +}; + +export class OpenBaoSecretManager implements SecretManager { + constructor( + private readonly openbaoAddr: string, + private readonly openbaoToken: string, + ) {} + + async getSecret(secretName: string, mount = "arka"): Promise { + const url = `${this.openbaoAddr.replace(/\/$/, "")}/v1/${mount}/data/${this.encodeSecretPath( + secretName, + )}`; + + const response = await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + "X-Vault-Token": this.openbaoToken, + }, + }); + + const body = await response.text(); + + if (!response.ok) { + throw new Error(`OpenBao error ${response.status}: ${body}`); + } + + const json = JSON.parse(body) as OpenBaoKvV2Response; + + return json.data.data; + } + + async createSecret( + secretName: string, + secretData: JsonObject, + mount = "arka" + ): Promise { + + const url = `${this.openbaoAddr.replace(/\/+$/, "")}/v1/${this.encodeSecretPath( + mount, + )}/data/${this.encodeSecretPath(secretName)}`; + + const response = await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-Vault-Token": this.openbaoToken, + }, + body: JSON.stringify({ + options: { + cas: 0, + }, + data: secretData, + }), + }); + + const body = await response.text(); + + if (!response.ok) { + throw new Error(`OpenBao error ${response.status}: ${body}`); + } + + return true; + } + + async destroySecret( + secretName: string, + _recoveryWindowInDays?: number, + mount = "arka" + ): Promise { + // KV v2 metadata delete removes the secret and all versions immediately. + // OpenBao does not support AWS-style recovery windows here. + const url = `${this.openbaoAddr.replace(/\/+$/, "")}/v1/${this.encodeSecretPath( + mount, + )}/metadata/${this.encodeSecretPath(secretName)}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Accept: "application/json", + "X-Vault-Token": this.openbaoToken, + }, + }); + + const body = await response.text(); + + if (!response.ok) { + throw new Error(`OpenBao error ${response.status}: ${body}`); + } + + return true; + } + + private encodeSecretPath(path: string): string { + return path + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean) + .map(encodeURIComponent) + .join("/"); + } +} diff --git a/backend/src/types/arka-config-dto.ts b/backend/src/types/arka-config-dto.ts index 1cd5860..7b192e1 100644 --- a/backend/src/types/arka-config-dto.ts +++ b/backend/src/types/arka-config-dto.ts @@ -1,4 +1,5 @@ import { Paymaster } from "../paymaster/index.js"; +import type { SecretManager } from "../services/secrets-manager/interface.js"; export interface ArkaConfigUpdateData { deployedErc20Paymasters: string; @@ -12,6 +13,10 @@ export interface ArkaConfigUpdateData { coingeckoApiUrl: string; } -export interface PaymasterRoutesOpts { +export interface SecretManagerRoutesOpts { + secretManager: SecretManager; +} + +export interface PaymasterRoutesOpts extends SecretManagerRoutesOpts { paymaster: Paymaster; -} \ No newline at end of file +}