Completed kms

This commit is contained in:
Matthew Bessette 2024-12-20 21:18:23 -05:00
parent c34ea76e4e
commit 1dc45267ac
24 changed files with 2062 additions and 71 deletions

1268
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-kms": "^3.716.0",
"@nestjs/common": "^10.4.15", "@nestjs/common": "^10.4.15",
"@nestjs/config": "^3.3.0", "@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.4.15", "@nestjs/core": "^10.4.15",

Binary file not shown.

View File

@ -0,0 +1,25 @@
/*
Warnings:
- Added the required column `updatedAt` to the `KmsAlias` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_KmsAlias" (
"name" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"kmsKeyId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
PRIMARY KEY ("accountId", "region", "name"),
CONSTRAINT "KmsAlias_kmsKeyId_fkey" FOREIGN KEY ("kmsKeyId") REFERENCES "KmsKey" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_KmsAlias" ("accountId", "kmsKeyId", "name", "region") SELECT "accountId", "kmsKeyId", "name", "region" FROM "KmsAlias";
DROP TABLE "KmsAlias";
ALTER TABLE "new_KmsAlias" RENAME TO "KmsAlias";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,36 @@
/*
Warnings:
- You are about to alter the column `key` on the `KmsKey` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`.
- Added the required column `enabled` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `keyState` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `multiRegion` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `origin` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `policy` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_KmsKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"enabled" BOOLEAN NOT NULL,
"usage" TEXT NOT NULL,
"description" TEXT NOT NULL,
"keySpec" TEXT NOT NULL,
"keyState" TEXT NOT NULL,
"origin" TEXT NOT NULL,
"multiRegion" BOOLEAN NOT NULL,
"policy" TEXT NOT NULL,
"key" BLOB NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_KmsKey" ("accountId", "createdAt", "description", "id", "key", "keySpec", "region", "usage") SELECT "accountId", "createdAt", "description", "id", "key", "keySpec", "region", "usage" FROM "KmsKey";
DROP TABLE "KmsKey";
ALTER TABLE "new_KmsKey" RENAME TO "KmsKey";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "KmsKey" ADD COLUMN "nextRotation" DATETIME;
ALTER TABLE "KmsKey" ADD COLUMN "rotationPeriod" INTEGER;

View File

@ -29,19 +29,33 @@ model KmsAlias {
accountId String accountId String
region String region String
kmsKeyId String kmsKeyId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
kmsKey KmsKey @relation(fields: [kmsKeyId], references: [id])
@@id([accountId, region, name]) @@id([accountId, region, name])
} }
model KmsKey { model KmsKey {
id String @id id String @id
enabled Boolean
usage String usage String
description String description String
keySpec String keySpec String
key String keyState String
origin String
multiRegion Boolean
policy String
key Bytes
rotationPeriod Int?
nextRotation DateTime?
accountId String accountId String
region String region String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
aliases KmsAlias[]
} }
model Secret { model Secret {

View File

@ -6,7 +6,7 @@ import { Format } from "../abstract-action.handler";
export interface RequestContext { export interface RequestContext {
action?: Action; action?: Action;
format?: Format; format?: Format;
requestId: string; readonly requestId: string;
} }
export interface IRequest extends Request { export interface IRequest extends Request {

View File

@ -154,3 +154,12 @@ export class InvalidArnException extends AwsException {
) )
} }
} }
export class UnsupportedOperationException extends AwsException {
constructor(message: string) {
super(
message,
UnsupportedOperationException.name,
HttpStatus.BAD_REQUEST,
)
}
}

View File

@ -20,7 +20,7 @@ export class TagsService {
} }
async createMany(arn: string, records: { key: string, value: string }[]): Promise<void> { async createMany(arn: string, records: { key: string, value: string }[]): Promise<void> {
await this.prismaService.attribute.createMany({ await this.prismaService.tag.createMany({
data: records.map(r => ({ data: records.map(r => ({
name: r.key, name: r.key,
value: r.value, value: r.value,

View File

@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
AliasName: string;
TargetKeyId: string;
}
@Injectable()
export class CreateAliasHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsCreateAlias;
validator = Joi.object<QueryParams, true>({
TargetKeyId: Joi.string().required(),
AliasName: Joi.string().min(1).max(256).regex(new RegExp(`^alias/[a-zA-Z0-9/_-]+$`)).required(),
});
protected async handle({ TargetKeyId, AliasName }: QueryParams, awsProperties: AwsProperties) {
const keyRecord = await this.kmsService.findOneByRef(TargetKeyId, awsProperties);
if (!keyRecord) {
throw new NotFoundException();
}
await this.kmsService.createAlias({
accountId: awsProperties.accountId,
region: awsProperties.region,
name: AliasName,
kmsKey: {
connect: {
id: keyRecord.id,
},
},
});
}
}

View File

@ -0,0 +1,184 @@
import { CustomerMasterKeySpec, KeySpec, KeyState, KeyUsageType, OriginType, Tag } from '@aws-sdk/client-kms';
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { KmsService } from './kms.service';
import * as crypto from 'crypto';
import { keySpecToUsageType } from './kms-key.entity';
import { UnsupportedOperationException } from '../aws-shared-entities/aws-exceptions';
import { TagsService } from '../aws-shared-entities/tags.service';
type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
type QueryParams = {
BypassPolicyLockoutSafetyCheck: boolean;
CustomerMasterKeySpec: CustomerMasterKeySpec;
CustomKeyStoreId: string;
Description: string;
KeySpec: KeySpec;
KeyUsage: KeyUsageType;
MultiRegion: boolean;
Origin: OriginType;
Policy: string;
Tags: NoUndefinedField<Tag>[];
XksKeyId: string;
}
const generateDefaultPolicy = (accountId: string) => JSON.stringify({
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": `arn:aws:iam::${accountId}:root`
},
"Action": "kms:*",
"Resource": "*"
})
@Injectable()
export class CreateKeyHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
private readonly tagsService: TagsService,
) {
super();
}
format = Format.Json;
action = Action.KmsCreateKey;
validator = Joi.object<QueryParams, true>({
BypassPolicyLockoutSafetyCheck: Joi.boolean().default(false),
CustomerMasterKeySpec: Joi.string().allow(...Object.values(CustomerMasterKeySpec)),
CustomKeyStoreId: Joi.string().min(1).max(64),
Description: Joi.string().min(0).max(8192).default(''),
KeySpec: Joi.string().allow(...Object.values(KeySpec)).default(KeySpec.SYMMETRIC_DEFAULT),
KeyUsage: Joi.string().allow(...Object.values(KeyUsageType)).default(KeyUsageType.ENCRYPT_DECRYPT),
MultiRegion: Joi.boolean().default(false),
Origin: Joi.string().allow(...Object.values(OriginType)).default(OriginType.AWS_KMS),
Policy: Joi.string().min(1).max(32768),
Tags: Joi.array().items(
Joi.object<Tag, true>({
TagKey: Joi.string().min(1).max(128).required(),
TagValue: Joi.string().min(0).max(256).required(),
})
),
XksKeyId: Joi.when('Origin', {
is: OriginType.EXTERNAL_KEY_STORE,
then: Joi.string().min(1).max(128),
otherwise: Joi.forbidden(),
}) as unknown as Joi.StringSchema,
});
protected async handle({ KeyUsage, Description, KeySpec, Origin, MultiRegion, Policy, Tags, CustomerMasterKeySpec }: QueryParams, awsProperties: AwsProperties) {
const keySpec = CustomerMasterKeySpec ?? KeySpec;
if (!keySpecToUsageType[keySpec].includes(KeyUsage)) {
throw new UnsupportedOperationException(`KeySpec ${KeySpec} is not valid for KeyUsage ${KeyUsage}`);
}
const key = this.keyGeneratorMap[keySpec]();
const createdKey = await this.kmsService.createKmsKey({
id: crypto.randomUUID(),
enabled: true,
usage: KeyUsage,
description: Description,
keySpec: keySpec,
keyState: KeyState.Enabled,
origin: Origin,
multiRegion: MultiRegion,
policy: Policy ?? generateDefaultPolicy(awsProperties.accountId),
key,
accountId: awsProperties.accountId,
region: awsProperties.region,
});
await this.tagsService.createMany(createdKey.arn, Tags.map(({ TagKey, TagValue }) => ({ key: TagKey, value: TagValue })));
return {
KeyMetadata: createdKey.metadata,
}
}
private keyGeneratorMap: Record<KeySpec, () => Buffer> = {
ECC_NIST_P256: function (): Buffer {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'X9_62_prime256v1' });
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
},
ECC_NIST_P384: function (): Buffer {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'secp384r1' });
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
},
ECC_NIST_P521: function (): Buffer {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'secp521r1' });
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
},
ECC_SECG_P256K1: function (): Buffer {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' });
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
},
HMAC_224: function (): Buffer {
return crypto.randomBytes(32);
},
HMAC_256: function (): Buffer {
return crypto.randomBytes(32);
},
HMAC_384: function (): Buffer {
return crypto.randomBytes(32);
},
HMAC_512: function (): Buffer {
return crypto.randomBytes(32);
},
RSA_2048: function (): Buffer {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
},
RSA_3072: function (): Buffer {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 3072,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
},
RSA_4096: function (): Buffer {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
},
SM2: function (): Buffer {
throw new Error('Function not implemented.');
},
SYMMETRIC_DEFAULT: function (): Buffer {
return crypto.randomBytes(32);
}
}
}

View File

@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { breakdownArn } from '../util/breakdown-arn';
import { KmsService } from './kms.service'; import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
@ -29,20 +28,7 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) { protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
const searchable = KeyId.startsWith('arn') ? breakdownArn(KeyId) : { const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
service: 'kms',
region: awsProperties.region,
accountId: awsProperties.accountId,
identifier: KeyId,
};
const [ type, pk ] = searchable.identifier.split('/');
const keyId = await (type === 'key' ? Promise.resolve(pk) : this.kmsService.findKeyIdFromAlias(pk, searchable));
if (!keyId) {
throw new NotFoundException();
}
const keyRecord = await this.kmsService.findOneById(keyId);
if (!keyRecord) { if (!keyRecord) {
throw new NotFoundException(); throw new NotFoundException();

View File

@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
KeyId: string;
RotationPeriodInDays: number;
}
@Injectable()
export class EnableKeyRotationHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsEnableKeyRotation;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
RotationPeriodInDays: Joi.number().min(90).max(2560).default(365),
});
protected async handle({ KeyId, RotationPeriodInDays }: QueryParams, awsProperties: AwsProperties) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
if (!keyRecord) {
throw new NotFoundException();
}
const next = new Date();
next.setDate(next.getDate() + RotationPeriodInDays);
await this.kmsService.updateKmsKey(keyRecord.id, {
rotationPeriod: RotationPeriodInDays,
nextRotation: next,
});
}
}

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
PolicyName: string;
KeyId: string;
}
@Injectable()
export class GetKeyPolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsGetKeyPolicy;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
PolicyName: Joi.string().min(1).max(128).default('default'),
});
protected async handle({ KeyId, PolicyName }: QueryParams, awsProperties: AwsProperties) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
if (!keyRecord) {
throw new NotFoundException();
}
return {
PolicyName,
Policy: keyRecord.policy,
}
}
}

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
KeyId: string;
}
@Injectable()
export class GetKeyRotationStatusHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsGetKeyRotationStatus;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
});
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
if (!keyRecord) {
throw new NotFoundException();
}
return {
KeyId: keyRecord.id,
KeyRotationEnabled: !!keyRecord.rotationPeriod,
NextRotationDate: keyRecord.nextRotation,
RotationPeriodInDays: keyRecord.rotationPeriod,
}
}
}

View File

@ -0,0 +1,34 @@
import { KmsAlias as PrismaKeyAlias } from "@prisma/client"
export class KmsAlias implements PrismaKeyAlias {
name: string
accountId: string
region: string
kmsKeyId: string
createdAt: Date;
updatedAt: Date;
constructor(p: PrismaKeyAlias) {
this.name = p.name;
this.accountId = p.accountId;
this.region = p.region;
this.kmsKeyId = p.kmsKeyId;
this.createdAt = p.createdAt;
this.updatedAt = p.updatedAt;
}
get arn() {
return `arn:aws:kms:${this.region}:${this.accountId}:${this.name}`;
}
toAws() {
return {
AliasArn: this.arn,
AliasName: this.name,
CreationDate: this.createdAt.getAwsTime(),
LastUpdatedDate: this.updatedAt.getAwsTime(),
TargetKeyId: this.kmsKeyId,
}
}
}

View File

@ -1,11 +0,0 @@
export class KmsKeyAlias {
// name: string;
// targetKeyId: string;
// accountId: string;
// region: string;
// get arn() {
// return `arn:aws:kms:${this.region}:${this.accountId}:alias/${this.name}`;
// }
}

View File

@ -1,28 +1,58 @@
import { KeySpec, KeyUsageType, KeyState, AlgorithmSpec, OriginType, ExpirationModelType, KeyAgreementAlgorithmSpec, MacAlgorithmSpec, MultiRegionKeyType, SigningAlgorithmSpec } from '@aws-sdk/client-kms';
import { KmsKey as PrismaKmsKey } from '@prisma/client'; import { KmsKey as PrismaKmsKey } from '@prisma/client';
export type KeySpec = 'RSA_2048' | 'RSA_3072' | 'RSA_4096' | 'ECC_NIST_P256' | 'ECC_NIST_P384' | 'ECC_NIST_P521' | 'ECC_SECG_P256K1' | 'SYMMETRIC_DEFAULT' | 'HMAC_224' | 'HMAC_256' | 'HMAC_384' | 'HMAC_512' | 'SM2'; export const keySpecToUsageType: Record<KeySpec, KeyUsageType[]> = {
export type KeyUsage = 'SIGN_VERIFY' | 'ENCRYPT_DECRYPT' | 'GENERATE_VERIFY_MAC'; ECC_NIST_P256: [KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
ECC_NIST_P384: [KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
ECC_NIST_P521: [KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
ECC_SECG_P256K1: [KeyUsageType.SIGN_VERIFY],
HMAC_224: [KeyUsageType.GENERATE_VERIFY_MAC],
HMAC_256: [KeyUsageType.GENERATE_VERIFY_MAC],
HMAC_384: [KeyUsageType.GENERATE_VERIFY_MAC],
HMAC_512: [KeyUsageType.GENERATE_VERIFY_MAC],
RSA_2048: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY],
RSA_3072: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY],
RSA_4096: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY],
SM2: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
SYMMETRIC_DEFAULT: [KeyUsageType.ENCRYPT_DECRYPT]
}
export class KmsKey implements PrismaKmsKey { export class KmsKey implements PrismaKmsKey {
id: string; id: string;
usage: KeyUsage; enabled: boolean;
usage: KeyUsageType;
description: string; description: string;
keySpec: KeySpec; keySpec: KeySpec;
key: string; keyState: KeyState;
origin: OriginType;
multiRegion: boolean;
policy: string;
key: Buffer;
nextRotation: Date | null;
rotationPeriod: number | null;
accountId: string; accountId: string;
region: string; region: string;
createdAt: Date; createdAt: Date;
updatedAt: Date;
constructor(p: PrismaKmsKey) { constructor(p: PrismaKmsKey) {
this.id = p.id; this.id = p.id;
this.usage = p.usage as KeyUsage; this.enabled = p.enabled;
this.usage = p.usage as KeyUsageType;
this.description = p.description; this.description = p.description;
this.keySpec = p.keySpec as KeySpec; this.keySpec = p.keySpec as KeySpec;
this.key = p.key; this.keyState = p.keyState as KeyState;
this.origin = p.origin as OriginType;
this.multiRegion = p.multiRegion;
this.policy = p.policy;
this.key = Buffer.from(p.key);
this.nextRotation = p.nextRotation;
this.rotationPeriod = p.rotationPeriod;
this.accountId = p.accountId; this.accountId = p.accountId;
this.region = p.region; this.region = p.region;
this.createdAt = p.createdAt; this.createdAt = p.createdAt;
this.updatedAt = p.updatedAt;
} }
get arn() { get arn() {
@ -30,27 +60,58 @@ export class KmsKey implements PrismaKmsKey {
} }
get metadata() { get metadata() {
const dynamicContent: Record<string, any> = {};
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.ENCRYPT_DECRYPT)) {
dynamicContent.EncryptionAlgorithms = Object.values(AlgorithmSpec);
}
if (this.origin === OriginType.EXTERNAL) {
dynamicContent.ExpirationModel = ExpirationModelType.KEY_MATERIAL_DOES_NOT_EXPIRE;
}
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.KEY_AGREEMENT)) {
dynamicContent.KeyAgreementAlgorithms = Object.values(KeyAgreementAlgorithmSpec);
}
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.GENERATE_VERIFY_MAC)) {
dynamicContent.MacAlgorithms = Object.values(MacAlgorithmSpec);
}
if (this.multiRegion) {
dynamicContent.MultiRegionConfiguration = {
MultiRegionKeyType: MultiRegionKeyType.PRIMARY,
PrimaryKey: {
Arn: this.arn,
Region: this.region,
},
ReplicaKeys: [],
}
}
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.SIGN_VERIFY)) {
dynamicContent.SigningAlgorithms = Object.values(SigningAlgorithmSpec);
}
return { return {
AWSAccountId: this.accountId, AWSAccountId: this.accountId,
KeyId: this.id,
Arn: this.arn, Arn: this.arn,
CreationDate: new Date(this.createdAt).toISOString(), CreationDate: this.createdAt.getAwsTime(),
Enabled: true,
Description: this.description,
KeyUsage: this.usage,
KeyState: 'Enabled',
KeyManager: "CUSTOMER",
CustomerMasterKeySpec: this.keySpec, CustomerMasterKeySpec: this.keySpec,
Description: this.description,
Enabled: true,
KeyId: this.id,
KeyManager: undefined,
KeySpec: this.keySpec, KeySpec: this.keySpec,
DeletionDate: null, KeyState: this.keyState,
SigningAlgorithms: [ KeyUsage: this.usage,
"RSASSA_PSS_SHA_256", MultiRegion: this.multiRegion,
"RSASSA_PSS_SHA_384", Origin: this.origin,
"RSASSA_PSS_SHA_512", PendingDeletionWindowInDays: undefined,
"RSASSA_PKCS1_V1_5_SHA_256", ValidTo: undefined,
"RSASSA_PKCS1_V1_5_SHA_384", XksKeyConfiguration: undefined,
"RSASSA_PKCS1_V1_5_SHA_512" ...dynamicContent,
]
} }
} }
} }

View File

@ -9,9 +9,23 @@ import { KmsService } from './kms.service';
import { KMSHandlers } from './kms.constants'; import { KMSHandlers } from './kms.constants';
import { DescribeKeyHandler } from './describe-key.handler'; import { DescribeKeyHandler } from './describe-key.handler';
import { PrismaModule } from '../_prisma/prisma.module'; import { PrismaModule } from '../_prisma/prisma.module';
import { ListAliasesHandler } from './list-aliases.handler';
import { CreateKeyHandler } from './create-key.handler';
import { EnableKeyRotationHandler } from './enable-key-rotation.handler';
import { GetKeyRotationStatusHandler } from './get-key-rotation-status.handler';
import { GetKeyPolicyHandler } from './get-key-policy.handler';
import { ListResourceTagsHandler } from './list-resource-tags.handler';
import { CreateAliasHandler } from './create-alias.handler';
const handlers = [ const handlers = [
CreateAliasHandler,
CreateKeyHandler,
DescribeKeyHandler, DescribeKeyHandler,
EnableKeyRotationHandler,
GetKeyPolicyHandler,
GetKeyRotationStatusHandler,
ListAliasesHandler,
ListResourceTagsHandler,
] ]
const actions = [ const actions = [

View File

@ -1,8 +1,12 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../_prisma/prisma.service'; import { PrismaService } from '../_prisma/prisma.service';
import { ArnParts } from '../util/breakdown-arn'; import { breakdownArn } from '../util/breakdown-arn';
import { KmsKey } from './kms-key.entity'; import { KmsKey } from './kms-key.entity';
import { KmsAlias } from './kms-alias.entity';
import { AwsProperties } from '../abstract-action.handler';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
@Injectable() @Injectable()
export class KmsService { export class KmsService {
@ -10,21 +14,103 @@ export class KmsService {
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
) {} ) {}
async findOneById(id: string): Promise<KmsKey | null> { async findOneByRef(ref: string, awsProperties: AwsProperties): Promise<KmsKey> {
const pRecord = await this.prismaService.kmsKey.findFirst({ if (ref.startsWith('arn')) {
where: { id } return await this.findOneByArn(ref);
}); }
return pRecord ? new KmsKey(pRecord) : null; return await this.findOneById(awsProperties.accountId, awsProperties.region, ref);
} }
async findKeyIdFromAlias(alias: string, arn: ArnParts): Promise<string | null> { async findOneByArn(arn: string): Promise<KmsKey> {
const record = await this.prismaService.kmsAlias.findFirst({ const parts = breakdownArn(arn);
where: { return await this.findOneById(parts.accountId, parts.region, parts.identifier.split('/')[1]);
name: alias,
accountId: arn.accountId,
region: arn.region,
} }
async findOneById(accountId: string, region: string, ref: string): Promise<KmsKey> {
const [alias, record] = await Promise.all([
this.prismaService.kmsAlias.findFirst({
include: {
kmsKey: true
},
where: {
accountId,
region,
name: ref,
}
}),
this.prismaService.kmsKey.findFirst({
where: {
accountId,
region,
id: ref,
}
})
]);
if (!alias?.kmsKey && !record) {
throw new NotFoundException();
}
return record ? new KmsKey(record) : new KmsKey(alias!.kmsKey);
}
async findAndCountAliasesByKeyId(accountId: string, region: string, limit: number, kmsKeyId: string, marker = ''): Promise<KmsAlias[]> {
const take = limit + 1;
const records = await this.prismaService.kmsAlias.findMany({
where: {
accountId,
region,
kmsKeyId,
name: {
gte: marker,
}
},
take,
orderBy: {
name: 'desc',
},
});
return records.map(r => new KmsAlias(r));
}
async findAndCountAliases(accountId: string, region: string, limit: number, marker = ''): Promise<KmsAlias[]> {
const take = limit + 1;
const records = await this.prismaService.kmsAlias.findMany({
where: {
accountId,
region,
name: {
gte: marker,
}
},
take,
orderBy: {
name: 'desc',
},
});
return records.map(r => new KmsAlias(r));
}
async createKmsKey(data: Prisma.KmsKeyCreateInput): Promise<KmsKey> {
const record = await this.prismaService.kmsKey.create({
data
});
return new KmsKey(record);
}
async updateKmsKey(id: string, data: Prisma.KmsKeyUpdateInput): Promise<void> {
await this.prismaService.kmsKey.update({
where: { id },
data,
});
}
async createAlias(data: Prisma.KmsAliasCreateInput) {
await this.prismaService.kmsAlias.create({
data
}); });
return record?.kmsKeyId ?? null;
} }
} }

View File

@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { KmsService } from './kms.service';
type QueryParams = {
KeyId?: string;
Limit: number;
Marker?: string;
}
@Injectable()
export class ListAliasesHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsListAliases;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string(),
Limit: Joi.number().min(1).max(100).default(50),
Marker: Joi.string(),
});
protected async handle({ KeyId, Limit, Marker }: QueryParams, awsProperties: AwsProperties) {
const records = await (KeyId
? this.kmsService.findAndCountAliasesByKeyId(awsProperties.accountId, awsProperties.region, Limit, KeyId, Marker)
: this.kmsService.findAndCountAliases(awsProperties.accountId, awsProperties.region, Limit, Marker)
)
const nextMarker = records.length > Limit ? records.pop() : null;
return {
Aliases: records.map(r => r.toAws()),
NextMarker: nextMarker?.name,
Truncated: !!nextMarker,
}
}
}

View File

@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { TagsService } from '../aws-shared-entities/tags.service';
type QueryParams = {
KeyId: string;
Limit: number;
Marker: string;
}
@Injectable()
export class ListResourceTagsHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
private readonly tagsService: TagsService,
) {
super();
}
format = Format.Json;
action = Action.KmsListResourceTags;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
Limit: Joi.number().min(1).max(100).default(50),
Marker: Joi.string(),
});
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
if (!keyRecord) {
throw new NotFoundException();
}
const tags = await this.tagsService.getByArn(keyRecord.arn);
return {
Tags: tags.map(({ name, value }) => ({ TagKey: name, TagValue: value })),
Truncated: false,
}
}
}

View File

@ -8,6 +8,16 @@ import { AwsExceptionFilter } from './_context/exception.filter';
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
declare global {
interface Date {
getAwsTime(): number;
}
}
Date.prototype.getAwsTime = function (this: Date) {
return Math.floor(this.getTime() / 1000);
};
(async () => { (async () => {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);