Skip to content

Commit b6852bc

Browse files
committed
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
1 parent f8fa400 commit b6852bc

27 files changed

+1164
-468
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"dependencies": {
2929
"@aws-sdk/client-kms": "^3.398.0",
3030
"@bull-board/fastify": "^5.21.1",
31+
"@cloud-cryptographic-wallet/cloud-kms-signer": "^0.1.2",
32+
"@cloud-cryptographic-wallet/signer": "^0.0.5",
3133
"@fastify/basic-auth": "^5.1.1",
3234
"@fastify/cookie": "^8.3.0",
3335
"@fastify/express": "^2.3.0",

src/db/configuration/getConfiguration.ts

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { Configuration } from "@prisma/client";
2-
import { Static } from "@sinclair/typebox";
1+
import type { Configuration } from "@prisma/client";
2+
import type { Static } from "@sinclair/typebox";
33
import { LocalWallet } from "@thirdweb-dev/wallets";
44
import { ethers } from "ethers";
5-
import { Chain } from "thirdweb";
6-
import { ParsedConfig } from "../../schema/config";
5+
import type { Chain } from "thirdweb";
6+
import type {
7+
AwsWalletConfiguration,
8+
GcpWalletConfiguration,
9+
ParsedConfig,
10+
} from "../../schema/config";
711
import { WalletType } from "../../schema/wallet";
812
import { mandatoryAllowedCorsUrls } from "../../server/utils/cors-urls";
9-
import { networkResponseSchema } from "../../utils/cache/getSdk";
13+
import type { networkResponseSchema } from "../../utils/cache/getSdk";
1014
import { decrypt } from "../../utils/crypto";
1115
import { env } from "../../utils/env";
1216
import { logger } from "../../utils/logger";
@@ -53,6 +57,18 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
5357
}
5458
}
5559

60+
// LEGACY COMPATIBILITY
61+
// legacy behaviour was to check for these in order:
62+
// 1. AWS KMS Configuration - if found, wallet type is AWS KMS
63+
// 2. GCP KMS Configuration - if found, wallet type is GCP KMS
64+
// 3. If neither are found, wallet type is Local
65+
// to maintain compatibility where users expect to call create new backend wallet endpoint without an explicit wallet type
66+
// we need to preserve the wallet type in the configuration but only as the "default" wallet type
67+
let legacyWalletType_removeInNextBreakingChange: WalletType =
68+
WalletType.local;
69+
70+
let awsWalletConfiguration: AwsWalletConfiguration | null = null;
71+
5672
// TODO: Remove backwards compatibility with next breaking change
5773
if (awsAccessKeyId && awsSecretAccessKey && awsRegion) {
5874
// First try to load the aws secret using the encryption password
@@ -73,7 +89,8 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
7389
logger({
7490
service: "worker",
7591
level: "info",
76-
message: `[Encryption] Updating awsSecretAccessKey to use ENCRYPTION_PASSWORD`,
92+
message:
93+
"[Encryption] Updating awsSecretAccessKey to use ENCRYPTION_PASSWORD",
7794
});
7895

7996
await updateConfiguration({
@@ -85,28 +102,18 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
85102
// Renaming contractSubscriptionsRetryDelaySeconds
86103
// to contractSubscriptionsRequeryDelaySeconds to reflect its purpose
87104
// as we are requerying (& not retrying) with different delays
88-
return {
89-
...restConfig,
90-
contractSubscriptionsRequeryDelaySeconds:
91-
contractSubscriptionsRetryDelaySeconds,
92-
chainOverridesParsed,
93-
walletConfiguration: {
94-
type: WalletType.awsKms,
95-
awsRegion,
96-
awsAccessKeyId,
97-
awsSecretAccessKey: decryptedSecretAccessKey,
98-
},
105+
awsWalletConfiguration = {
106+
awsAccessKeyId,
107+
awsSecretAccessKey: decryptedSecretAccessKey,
108+
defaultAwsRegion: awsRegion,
99109
};
110+
111+
legacyWalletType_removeInNextBreakingChange = WalletType.awsKms;
100112
}
101113

114+
let gcpWalletConfiguration: GcpWalletConfiguration | null = null;
102115
// TODO: Remove backwards compatibility with next breaking change
103-
if (
104-
gcpApplicationProjectId &&
105-
gcpKmsLocationId &&
106-
gcpKmsKeyRingId &&
107-
gcpApplicationCredentialEmail &&
108-
gcpApplicationCredentialPrivateKey
109-
) {
116+
if (gcpApplicationCredentialEmail && gcpApplicationCredentialPrivateKey) {
110117
// First try to load the gcp secret using the encryption password
111118
let decryptedGcpKey = decrypt(
112119
gcpApplicationCredentialPrivateKey,
@@ -125,7 +132,8 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
125132
logger({
126133
service: "worker",
127134
level: "info",
128-
message: `[Encryption] Updating gcpApplicationCredentialPrivateKey to use ENCRYPTION_PASSWORD`,
135+
message:
136+
"[Encryption] Updating gcpApplicationCredentialPrivateKey to use ENCRYPTION_PASSWORD",
129137
});
130138

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

137-
return {
138-
...restConfig,
139-
contractSubscriptionsRequeryDelaySeconds:
140-
contractSubscriptionsRetryDelaySeconds,
141-
chainOverridesParsed,
142-
walletConfiguration: {
143-
type: WalletType.gcpKms,
144-
gcpApplicationProjectId,
145-
gcpKmsLocationId,
146-
gcpKmsKeyRingId,
147-
gcpApplicationCredentialEmail,
148-
gcpApplicationCredentialPrivateKey: decryptedGcpKey,
149-
},
145+
if (!gcpKmsLocationId || !gcpKmsKeyRingId || !gcpApplicationProjectId) {
146+
throw new Error(
147+
"GCP KMS location ID, project ID, and key ring ID are required configuration for this wallet type",
148+
);
149+
}
150+
151+
gcpWalletConfiguration = {
152+
gcpApplicationCredentialEmail,
153+
gcpApplicationCredentialPrivateKey: decryptedGcpKey,
154+
155+
// TODO: Remove these with the next breaking change
156+
// These are used because import endpoint does not yet support GCP KMS resource path
157+
defaultGcpKmsLocationId: gcpKmsLocationId,
158+
defaultGcpKmsKeyRingId: gcpKmsKeyRingId,
159+
defaultGcpApplicationProjectId: gcpApplicationProjectId,
150160
};
161+
162+
legacyWalletType_removeInNextBreakingChange = WalletType.gcpKms;
151163
}
152164

153165
return {
@@ -156,7 +168,9 @@ const toParsedConfig = async (config: Configuration): Promise<ParsedConfig> => {
156168
contractSubscriptionsRetryDelaySeconds,
157169
chainOverridesParsed,
158170
walletConfiguration: {
159-
type: WalletType.local,
171+
aws: awsWalletConfiguration,
172+
gcp: gcpWalletConfiguration,
173+
legacyWalletType_removeInNextBreakingChange,
160174
},
161175
};
162176
};

src/db/wallets/createWalletDetails.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { PrismaTransaction } from "../../schema/prisma";
1+
import type { PrismaTransaction } from "../../schema/prisma";
22
import type { WalletType } from "../../schema/wallet";
3+
import { encrypt } from "../../utils/crypto";
34
import { getPrismaWithPostgresTx } from "../client";
45

56
// TODO: Case on types by wallet type
@@ -8,13 +9,23 @@ interface CreateWalletDetailsParams {
89
address: string;
910
type: WalletType;
1011
label?: string;
11-
awsKmsKeyId?: string;
12+
13+
// AWS KMS
14+
awsKmsKeyId?: string; // depcrecated and unused, todo: remove with next breaking change
1215
awsKmsArn?: string;
13-
gcpKmsKeyRingId?: string;
14-
gcpKmsKeyId?: string;
15-
gcpKmsKeyVersionId?: string;
16-
gcpKmsLocationId?: string;
16+
17+
awsKmsSecretAccessKey?: string; // encrypted
18+
awsKmsAccessKeyId?: string;
19+
20+
// GCP KMS
1721
gcpKmsResourcePath?: string;
22+
gcpKmsKeyRingId?: string; // depcrecated and unused, todo: remove with next breaking change
23+
gcpKmsKeyId?: string; // depcrecated and unused, todo: remove with next breaking change
24+
gcpKmsKeyVersionId?: string; // depcrecated and unused, todo: remove with next breaking change
25+
gcpKmsLocationId?: string; // depcrecated and unused, todo: remove with next breaking change
26+
27+
gcpApplicationCredentialPrivateKey?: string; // encrypted
28+
gcpApplicationCredentialEmail?: string;
1829
}
1930

2031
export const createWalletDetails = async ({
@@ -39,6 +50,15 @@ export const createWalletDetails = async ({
3950
data: {
4051
...walletDetails,
4152
address: walletDetails.address.toLowerCase(),
53+
54+
awsKmsSecretAccessKey: walletDetails.awsKmsSecretAccessKey
55+
? encrypt(walletDetails.awsKmsSecretAccessKey)
56+
: undefined,
57+
58+
gcpApplicationCredentialPrivateKey:
59+
walletDetails.gcpApplicationCredentialPrivateKey
60+
? encrypt(walletDetails.gcpApplicationCredentialPrivateKey)
61+
: undefined,
4262
},
4363
});
4464
};

src/db/wallets/getWalletDetails.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PrismaTransaction } from "../../schema/prisma";
1+
import type { PrismaTransaction } from "../../schema/prisma";
22
import { getPrismaWithPostgresTx } from "../client";
33

44
interface GetWalletDetailsParams {

src/prisma/schema.prisma

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ model Configuration {
3030
contractSubscriptionsRetryDelaySeconds String @default("10") @map("contractSubscriptionsRetryDelaySeconds")
3131
3232
// AWS
33-
awsAccessKeyId String? @map("awsAccessKeyId")
34-
awsSecretAccessKey String? @map("awsSecretAccessKey")
35-
awsRegion String? @map("awsRegion")
33+
awsAccessKeyId String? @map("awsAccessKeyId") // global config, precendence goes to WalletDetails
34+
awsSecretAccessKey String? @map("awsSecretAccessKey") // global config, precendence goes to WalletDetails
35+
awsRegion String? @map("awsRegion") // // global config, treat as "default", store in WalletDetails.awsKmsArn
3636
// GCP
37-
gcpApplicationProjectId String? @map("gcpApplicationProjectId")
38-
gcpKmsLocationId String? @map("gcpKmsLocationId")
39-
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId")
40-
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail")
41-
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey")
37+
gcpApplicationProjectId String? @map("gcpApplicationProjectId") // global config, treat as "defult", store in WalletDetails.gcpKmsResourcePath
38+
gcpKmsLocationId String? @map("gcpKmsLocationId") // global config, treat as "defult", store in WalletDetails.gcpKmsResourcePath
39+
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId") // global config, treat as "defult", store in WalletDetails.gcpKmsResourcePath
40+
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") // global config, precendence goes to WalletDetails
41+
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") // global config, precendence goes to WalletDetails
4242
// Auth
4343
authDomain String @default("") @map("authDomain") // TODO: Remove defaults on major
4444
authWalletEncryptedJson String @default("") @map("authWalletEncryptedJson") // TODO: Remove defaults on major
@@ -76,20 +76,24 @@ model Tokens {
7676
}
7777

7878
model WalletDetails {
79-
address String @id @map("address")
80-
type String @map("type")
81-
label String? @map("label")
79+
address String @id @map("address")
80+
type String @map("type")
81+
label String? @map("label")
8282
// Local
83-
encryptedJson String? @map("encryptedJson")
83+
encryptedJson String? @map("encryptedJson")
8484
// KMS
85-
awsKmsKeyId String? @map("awsKmsKeyId")
86-
awsKmsArn String? @map("awsKmsArn")
85+
awsKmsKeyId String? @map("awsKmsKeyId") // deprecated and unused, todo: remove with next breaking change. Use awsKmsArn
86+
awsKmsArn String? @map("awsKmsArn")
87+
awsKmsSecretAccessKey String? @map("awsKmsSecretAccessKey") // if not available, default to: Configuration.awsSecretAccessKey
88+
awsKmsAccessKeyId String? @map("awsKmsAccessKeyId") // if not available, default to: Configuration.awsAccessKeyId
8789
// GCP
88-
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId") @db.VarChar(50)
89-
gcpKmsKeyId String? @map("gcpKmsKeyId") @db.VarChar(50)
90-
gcpKmsKeyVersionId String? @map("gcpKmsKeyVersionId") @db.VarChar(20)
91-
gcpKmsLocationId String? @map("gcpKmsLocationId") @db.VarChar(20)
92-
gcpKmsResourcePath String? @map("gcpKmsResourcePath") @db.Text
90+
gcpKmsKeyRingId String? @map("gcpKmsKeyRingId") @db.VarChar(50) // deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
91+
gcpKmsKeyId String? @map("gcpKmsKeyId") @db.VarChar(50) // deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
92+
gcpKmsKeyVersionId String? @map("gcpKmsKeyVersionId") @db.VarChar(20) // deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
93+
gcpKmsLocationId String? @map("gcpKmsLocationId") @db.VarChar(20) // deprecated and unused. Use gcpKmsResourcePath instead, todo: remove with next breaking change
94+
gcpKmsResourcePath String? @map("gcpKmsResourcePath") @db.Text
95+
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") // if not available, default to: Configuration.gcpApplicationCredentialEmail
96+
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") // if not available, default to: Configuration.gcpApplicationCredentialPrivateKey
9397
9498
@@map("wallet_details")
9599
}

src/schema/config.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
1-
import { Configuration } from "@prisma/client";
2-
import { Chain } from "thirdweb";
3-
import { WalletType } from "./wallet";
1+
import type { Configuration } from "@prisma/client";
2+
import type { Chain } from "thirdweb";
3+
import type { WalletType } from "./wallet";
4+
5+
export type AwsWalletConfiguration = {
6+
awsAccessKeyId: string;
7+
awsSecretAccessKey: string;
8+
9+
defaultAwsRegion: string;
10+
};
11+
12+
export type GcpWalletConfiguration = {
13+
gcpApplicationCredentialEmail: string;
14+
gcpApplicationCredentialPrivateKey: string;
15+
16+
// these values are used as default so users don't need to specify them every time to the create wallet endpoint
17+
// for fetching a wallet, always trust the resource path in the wallet details
18+
// only use these values for creating a new wallet, when the resource path is not known
19+
defaultGcpKmsLocationId: string;
20+
defaultGcpKmsKeyRingId: string;
21+
defaultGcpApplicationProjectId: string;
22+
};
423

524
export interface ParsedConfig
625
extends Omit<
@@ -15,24 +34,11 @@ export interface ParsedConfig
1534
| "gcpApplicationCredentialPrivateKey"
1635
| "contractSubscriptionsRetryDelaySeconds"
1736
> {
18-
walletConfiguration:
19-
| {
20-
type: WalletType.local;
21-
}
22-
| {
23-
type: WalletType.awsKms;
24-
awsAccessKeyId: string;
25-
awsSecretAccessKey: string;
26-
awsRegion: string;
27-
}
28-
| {
29-
type: WalletType.gcpKms;
30-
gcpApplicationProjectId: string;
31-
gcpKmsLocationId: string;
32-
gcpKmsKeyRingId: string;
33-
gcpApplicationCredentialEmail: string;
34-
gcpApplicationCredentialPrivateKey: string;
35-
};
37+
walletConfiguration: {
38+
aws: AwsWalletConfiguration | null;
39+
gcp: GcpWalletConfiguration | null;
40+
legacyWalletType_removeInNextBreakingChange: WalletType;
41+
};
3642
contractSubscriptionsRequeryDelaySeconds: string;
3743
chainOverridesParsed: Chain[];
3844
}

src/server/routes/backend-wallet/create.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import { createLocalWallet } from "../../utils/wallets/createLocalWallet";
1111

1212
const requestBodySchema = Type.Object({
1313
label: Type.Optional(Type.String()),
14+
type: Type.Optional(
15+
Type.Enum(WalletType, {
16+
description:
17+
"Optional wallet type. If not provided, the default wallet type will be used.",
18+
}),
19+
),
1420
});
1521

1622
const responseSchema = Type.Object({
@@ -28,7 +34,7 @@ responseSchema.example = {
2834
};
2935

3036
export const createBackendWallet = async (fastify: FastifyInstance) => {
31-
fastify.route<{
37+
fastify.withTypeProvider().route<{
3238
Body: Static<typeof requestBodySchema>;
3339
Reply: Static<typeof responseSchema>;
3440
}>({
@@ -50,7 +56,12 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
5056

5157
let walletAddress: string;
5258
const config = await getConfig();
53-
switch (config.walletConfiguration.type) {
59+
60+
const walletType =
61+
req.body.type ??
62+
config.walletConfiguration.legacyWalletType_removeInNextBreakingChange;
63+
64+
switch (walletType) {
5465
case WalletType.local:
5566
walletAddress = await createLocalWallet({ label });
5667
break;

0 commit comments

Comments
 (0)