Skip to content

Commit

Permalink
feat: migrate aws kms flow to v5 (#626)
Browse files Browse the repository at this point in the history
* feat: migrate aws kms flow to v5

* chore: revert hardcoded test backend wallet address

* Shared signature logic + move function defintions inside body

* remove twAddress override

* feat: GCP KMS + refactor

- use multiple wallet types simultaneously
- resourcePath or ARN is source of truth: treat config AWS/GCP details as "default"
- credentials override

* add unit tests for kms accounts with prool

* fix .env file ordering for tests

* bugfix: split awn arn correctly

* ignore engines globally (temp fix)

* fix yarnrc

* Address CR Comments

* tests for AWS ARN and GCP KMS

* always store kms credentials in WalletDetails

* modify healthCheck function to include HETEROGENEOUS_WALLET_TYPES feature

* remove bad comment

* refactor invalid check

---------

Signed-off-by: Prithvish Baidya <[email protected]>
  • Loading branch information
d4mr authored Oct 4, 2024
1 parent 3015e63 commit 4e8cc30
Show file tree
Hide file tree
Showing 42 changed files with 2,988 additions and 500 deletions.
11 changes: 10 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@ ENCRYPTION_PASSWORD="test"
ENABLE_KEYPAIR_AUTH="true"
ENABLE_HTTPS="true"
REDIS_URL="redis://127.0.0.1:6379/0"
THIRDWEB_API_SECRET_KEY="my-thirdweb-secret-key"
THIRDWEB_API_SECRET_KEY="my-thirdweb-secret-key"

TEST_AWS_KMS_KEY_ID=""
TEST_AWS_KMS_ACCESS_KEY_ID=""
TEST_AWS_KMS_SECRET_ACCESS_KEY=""
TEST_AWS_KMS_REGION=""

TEST_GCP_KMS_RESOURCE_PATH=""
TEST_GCP_KMS_EMAIL=""
TEST_GCP_KMS_PK=""
1 change: 1 addition & 0 deletions .yarnrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--ignore-engines true
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"dependencies": {
"@aws-sdk/client-kms": "^3.398.0",
"@bull-board/fastify": "^5.21.1",
"@cloud-cryptographic-wallet/cloud-kms-signer": "^0.1.2",
"@cloud-cryptographic-wallet/signer": "^0.0.5",
"@fastify/basic-auth": "^5.1.1",
"@fastify/cookie": "^8.3.0",
"@fastify/express": "^2.3.0",
Expand All @@ -44,6 +46,7 @@
"@thirdweb-dev/sdk": "^4.0.89",
"@thirdweb-dev/service-utils": "^0.4.28",
"@types/base-64": "^1.0.2",
"aws-kms-signer": "^0.5.3",
"base-64": "^1.0.0",
"body-parser": "^1.20.2",
"bullmq": "^5.11.0",
Expand All @@ -69,6 +72,7 @@
"pg": "^8.11.3",
"prisma": "^5.14.0",
"prom-client": "^15.1.3",
"prool": "^0.0.16",
"superjson": "^2.2.1",
"thirdweb": "^5.58.4",
"uuid": "^9.0.1",
Expand Down
92 changes: 53 additions & 39 deletions src/db/configuration/getConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Configuration } from "@prisma/client";
import { Static } from "@sinclair/typebox";
import type { Configuration } from "@prisma/client";
import type { Static } from "@sinclair/typebox";
import { LocalWallet } from "@thirdweb-dev/wallets";
import { ethers } from "ethers";
import { Chain } from "thirdweb";
import { ParsedConfig } from "../../schema/config";
import type { Chain } from "thirdweb";
import type {
AwsWalletConfiguration,
GcpWalletConfiguration,
ParsedConfig,
} from "../../schema/config";
import { WalletType } from "../../schema/wallet";
import { mandatoryAllowedCorsUrls } from "../../server/utils/cors-urls";
import { networkResponseSchema } from "../../utils/cache/getSdk";
import type { networkResponseSchema } from "../../utils/cache/getSdk";
import { decrypt } from "../../utils/crypto";
import { env } from "../../utils/env";
import { logger } from "../../utils/logger";
Expand Down Expand Up @@ -53,6 +57,18 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
}
}

// LEGACY COMPATIBILITY
// legacy behaviour was to check for these in order:
// 1. AWS KMS Configuration - if found, wallet type is AWS KMS
// 2. GCP KMS Configuration - if found, wallet type is GCP KMS
// 3. If neither are found, wallet type is Local
// to maintain compatibility where users expect to call create new backend wallet endpoint without an explicit wallet type
// we need to preserve the wallet type in the configuration but only as the "default" wallet type
let legacyWalletType_removeInNextBreakingChange: WalletType =
WalletType.local;

let awsWalletConfiguration: AwsWalletConfiguration | null = null;

// TODO: Remove backwards compatibility with next breaking change
if (awsAccessKeyId && awsSecretAccessKey && awsRegion) {
// First try to load the aws secret using the encryption password
Expand All @@ -73,7 +89,8 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
logger({
service: "worker",
level: "info",
message: `[Encryption] Updating awsSecretAccessKey to use ENCRYPTION_PASSWORD`,
message:
"[Encryption] Updating awsSecretAccessKey to use ENCRYPTION_PASSWORD",
});

await updateConfiguration({
Expand All @@ -85,28 +102,18 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
// Renaming contractSubscriptionsRetryDelaySeconds
// to contractSubscriptionsRequeryDelaySeconds to reflect its purpose
// as we are requerying (& not retrying) with different delays
return {
...restConfig,
contractSubscriptionsRequeryDelaySeconds:
contractSubscriptionsRetryDelaySeconds,
chainOverridesParsed,
walletConfiguration: {
type: WalletType.awsKms,
awsRegion,
awsAccessKeyId,
awsSecretAccessKey: decryptedSecretAccessKey,
},
awsWalletConfiguration = {
awsAccessKeyId,
awsSecretAccessKey: decryptedSecretAccessKey,
defaultAwsRegion: awsRegion,
};

legacyWalletType_removeInNextBreakingChange = WalletType.awsKms;
}

let gcpWalletConfiguration: GcpWalletConfiguration | null = null;
// TODO: Remove backwards compatibility with next breaking change
if (
gcpApplicationProjectId &&
gcpKmsLocationId &&
gcpKmsKeyRingId &&
gcpApplicationCredentialEmail &&
gcpApplicationCredentialPrivateKey
) {
if (gcpApplicationCredentialEmail && gcpApplicationCredentialPrivateKey) {
// First try to load the gcp secret using the encryption password
let decryptedGcpKey = decrypt(
gcpApplicationCredentialPrivateKey,
Expand All @@ -125,7 +132,8 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
logger({
service: "worker",
level: "info",
message: `[Encryption] Updating gcpApplicationCredentialPrivateKey to use ENCRYPTION_PASSWORD`,
message:
"[Encryption] Updating gcpApplicationCredentialPrivateKey to use ENCRYPTION_PASSWORD",
});

await updateConfiguration({
Expand All @@ -134,20 +142,24 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
}
}

return {
...restConfig,
contractSubscriptionsRequeryDelaySeconds:
contractSubscriptionsRetryDelaySeconds,
chainOverridesParsed,
walletConfiguration: {
type: WalletType.gcpKms,
gcpApplicationProjectId,
gcpKmsLocationId,
gcpKmsKeyRingId,
gcpApplicationCredentialEmail,
gcpApplicationCredentialPrivateKey: decryptedGcpKey,
},
if (!gcpKmsLocationId || !gcpKmsKeyRingId || !gcpApplicationProjectId) {
throw new Error(
"GCP KMS location ID, project ID, and key ring ID are required configuration for this wallet type",
);
}

gcpWalletConfiguration = {
gcpApplicationCredentialEmail,
gcpApplicationCredentialPrivateKey: decryptedGcpKey,

// TODO: Remove these with the next breaking change
// These are used because import endpoint does not yet support GCP KMS resource path
defaultGcpKmsLocationId: gcpKmsLocationId,
defaultGcpKmsKeyRingId: gcpKmsKeyRingId,
defaultGcpApplicationProjectId: gcpApplicationProjectId,
};

legacyWalletType_removeInNextBreakingChange = WalletType.gcpKms;
}

return {
Expand All @@ -156,7 +168,9 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
contractSubscriptionsRetryDelaySeconds,
chainOverridesParsed,
walletConfiguration: {
type: WalletType.local,
aws: awsWalletConfiguration,
gcp: gcpWalletConfiguration,
legacyWalletType_removeInNextBreakingChange,
},
};
};
Expand Down
68 changes: 50 additions & 18 deletions src/db/wallets/createWalletDetails.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { PrismaTransaction } from "../../schema/prisma";
import type { WalletType } from "../../schema/wallet";
import type { PrismaTransaction } from "../../schema/prisma";
import { encrypt } from "../../utils/crypto";
import { getPrismaWithPostgresTx } from "../client";

// TODO: Case on types by wallet type
interface CreateWalletDetailsParams {
type CreateWalletDetailsParams = {
pgtx?: PrismaTransaction;
address: string;
type: WalletType;
label?: string;
awsKmsKeyId?: string;
awsKmsArn?: string;
gcpKmsKeyRingId?: string;
gcpKmsKeyId?: string;
gcpKmsKeyVersionId?: string;
gcpKmsLocationId?: string;
gcpKmsResourcePath?: string;
}
} & (
| {
type: "aws-kms";
awsKmsKeyId?: string; // depcrecated and unused, todo: remove with next breaking change
awsKmsArn: string;

awsKmsSecretAccessKey?: string; // will be encrypted and stored, pass plaintext to this function
awsKmsAccessKeyId?: string;
}
| {
type: "gcp-kms";
gcpKmsResourcePath: string;
gcpKmsKeyRingId?: string; // depcrecated and unused, todo: remove with next breaking change
gcpKmsKeyId?: string; // depcrecated and unused, todo: remove with next breaking change
gcpKmsKeyVersionId?: string; // depcrecated and unused, todo: remove with next breaking change
gcpKmsLocationId?: string; // depcrecated and unused, todo: remove with next breaking change

gcpApplicationCredentialPrivateKey?: string; // encrypted
gcpApplicationCredentialEmail?: string;
}
);

export const createWalletDetails = async ({
pgtx,
Expand All @@ -35,10 +47,30 @@ export const createWalletDetails = async ({
);
}

return prisma.walletDetails.create({
data: {
...walletDetails,
address: walletDetails.address.toLowerCase(),
},
});
if (walletDetails.type === "aws-kms") {
return prisma.walletDetails.create({
data: {
...walletDetails,
address: walletDetails.address.toLowerCase(),

awsKmsSecretAccessKey: walletDetails.awsKmsSecretAccessKey
? encrypt(walletDetails.awsKmsSecretAccessKey)
: undefined,
},
});
}

if (walletDetails.type === "gcp-kms") {
return prisma.walletDetails.create({
data: {
...walletDetails,
address: walletDetails.address.toLowerCase(),

gcpApplicationCredentialPrivateKey:
walletDetails.gcpApplicationCredentialPrivateKey
? encrypt(walletDetails.gcpApplicationCredentialPrivateKey)
: undefined,
},
});
}
};
2 changes: 1 addition & 1 deletion src/db/wallets/getWalletDetails.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaTransaction } from "../../schema/prisma";
import type { PrismaTransaction } from "../../schema/prisma";
import { getPrismaWithPostgresTx } from "../client";

interface GetWalletDetailsParams {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "wallet_details" ADD COLUMN "awsKmsAccessKeyId" TEXT,
ADD COLUMN "awsKmsSecretAccessKey" TEXT,
ADD COLUMN "gcpApplicationCredentialEmail" TEXT,
ADD COLUMN "gcpApplicationCredentialPrivateKey" TEXT;
42 changes: 23 additions & 19 deletions src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ model Configuration {
contractSubscriptionsRetryDelaySeconds String @default("10") @map("contractSubscriptionsRetryDelaySeconds")
// AWS
awsAccessKeyId String? @map("awsAccessKeyId")
awsSecretAccessKey String? @map("awsSecretAccessKey")
awsRegion String? @map("awsRegion")
awsAccessKeyId String? @map("awsAccessKeyId") /// global config, precedence goes to WalletDetails
awsSecretAccessKey String? @map("awsSecretAccessKey") /// global config, precedence goes to WalletDetails
awsRegion String? @map("awsRegion") /// global config, treat as "default", store in WalletDetails.awsKmsArn
// GCP
gcpApplicationProjectId String? @map("gcpApplicationProjectId")
gcpKmsLocationId String? @map("gcpKmsLocationId")
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId")
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail")
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey")
gcpApplicationProjectId String? @map("gcpApplicationProjectId") /// global config, treat as "default", store in WalletDetails.gcpKmsResourcePath
gcpKmsLocationId String? @map("gcpKmsLocationId") /// global config, treat as "default", store in WalletDetails.gcpKmsResourcePath
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId") /// global config, treat as "default", store in WalletDetails.gcpKmsResourcePath
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") /// global config, precedence goes to WalletDetails
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") /// global config, precedence goes to WalletDetails
// Auth
authDomain String @default("") @map("authDomain") // TODO: Remove defaults on major
authWalletEncryptedJson String @default("") @map("authWalletEncryptedJson") // TODO: Remove defaults on major
Expand Down Expand Up @@ -76,20 +76,24 @@ model Tokens {
}

model WalletDetails {
address String @id @map("address")
type String @map("type")
label String? @map("label")
address String @id @map("address")
type String @map("type")
label String? @map("label")
// Local
encryptedJson String? @map("encryptedJson")
encryptedJson String? @map("encryptedJson")
// KMS
awsKmsKeyId String? @map("awsKmsKeyId")
awsKmsArn String? @map("awsKmsArn")
awsKmsKeyId String? @map("awsKmsKeyId") /// deprecated and unused, todo: remove with next breaking change. Use awsKmsArn
awsKmsArn String? @map("awsKmsArn")
awsKmsSecretAccessKey String? @map("awsKmsSecretAccessKey") /// if not available, default to: Configuration.awsSecretAccessKey
awsKmsAccessKeyId String? @map("awsKmsAccessKeyId") /// if not available, default to: Configuration.awsAccessKeyId
// GCP
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId") @db.VarChar(50)
gcpKmsKeyId String? @map("gcpKmsKeyId") @db.VarChar(50)
gcpKmsKeyVersionId String? @map("gcpKmsKeyVersionId") @db.VarChar(20)
gcpKmsLocationId String? @map("gcpKmsLocationId") @db.VarChar(20)
gcpKmsResourcePath String? @map("gcpKmsResourcePath") @db.Text
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId") @db.VarChar(50) /// deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
gcpKmsKeyId String? @map("gcpKmsKeyId") @db.VarChar(50) /// deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
gcpKmsKeyVersionId String? @map("gcpKmsKeyVersionId") @db.VarChar(20) /// deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
gcpKmsLocationId String? @map("gcpKmsLocationId") @db.VarChar(20) /// deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
gcpKmsResourcePath String? @map("gcpKmsResourcePath") @db.Text
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") /// if not available, default to: Configuration.gcpApplicationCredentialEmail
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") /// if not available, default to: Configuration.gcpApplicationCredentialPrivateKey
@@map("wallet_details")
}
Expand Down
Loading

0 comments on commit 4e8cc30

Please sign in to comment.