Completed kms
This commit is contained in:
parent
c34ea76e4e
commit
1dc45267ac
File diff suppressed because it is too large
Load Diff
|
|
@ -10,6 +10,7 @@
|
|||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-kms": "^3.716.0",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "KmsKey" ADD COLUMN "nextRotation" DATETIME;
|
||||
ALTER TABLE "KmsKey" ADD COLUMN "rotationPeriod" INTEGER;
|
||||
|
|
@ -29,19 +29,33 @@ model KmsAlias {
|
|||
accountId String
|
||||
region String
|
||||
kmsKeyId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
kmsKey KmsKey @relation(fields: [kmsKeyId], references: [id])
|
||||
|
||||
@@id([accountId, region, name])
|
||||
}
|
||||
|
||||
model KmsKey {
|
||||
id String @id
|
||||
enabled Boolean
|
||||
usage String
|
||||
description String
|
||||
keySpec String
|
||||
key String
|
||||
keyState String
|
||||
origin String
|
||||
multiRegion Boolean
|
||||
policy String
|
||||
key Bytes
|
||||
rotationPeriod Int?
|
||||
nextRotation DateTime?
|
||||
accountId String
|
||||
region String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
aliases KmsAlias[]
|
||||
}
|
||||
|
||||
model Secret {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Format } from "../abstract-action.handler";
|
|||
export interface RequestContext {
|
||||
action?: Action;
|
||||
format?: Format;
|
||||
requestId: string;
|
||||
readonly requestId: string;
|
||||
}
|
||||
|
||||
export interface IRequest extends Request {
|
||||
|
|
|
|||
|
|
@ -154,3 +154,12 @@ export class InvalidArnException extends AwsException {
|
|||
)
|
||||
}
|
||||
}
|
||||
export class UnsupportedOperationException extends AwsException {
|
||||
constructor(message: string) {
|
||||
super(
|
||||
message,
|
||||
UnsupportedOperationException.name,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ export class TagsService {
|
|||
}
|
||||
|
||||
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 => ({
|
||||
name: r.key,
|
||||
value: r.value,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
|||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { breakdownArn } from '../util/breakdown-arn';
|
||||
import { KmsService } from './kms.service';
|
||||
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) {
|
||||
|
||||
const searchable = KeyId.startsWith('arn') ? breakdownArn(KeyId) : {
|
||||
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);
|
||||
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
|
||||
|
||||
if (!keyRecord) {
|
||||
throw new NotFoundException();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`;
|
||||
// }
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
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 type KeyUsage = 'SIGN_VERIFY' | 'ENCRYPT_DECRYPT' | 'GENERATE_VERIFY_MAC';
|
||||
export const keySpecToUsageType: Record<KeySpec, KeyUsageType[]> = {
|
||||
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 {
|
||||
|
||||
id: string;
|
||||
usage: KeyUsage;
|
||||
enabled: boolean;
|
||||
usage: KeyUsageType;
|
||||
description: string;
|
||||
keySpec: KeySpec;
|
||||
key: string;
|
||||
keyState: KeyState;
|
||||
origin: OriginType;
|
||||
multiRegion: boolean;
|
||||
policy: string;
|
||||
key: Buffer;
|
||||
nextRotation: Date | null;
|
||||
rotationPeriod: number | null;
|
||||
accountId: string;
|
||||
region: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
constructor(p: PrismaKmsKey) {
|
||||
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.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.region = p.region;
|
||||
this.createdAt = p.createdAt;
|
||||
this.updatedAt = p.updatedAt;
|
||||
}
|
||||
|
||||
get arn() {
|
||||
|
|
@ -30,27 +60,58 @@ export class KmsKey implements PrismaKmsKey {
|
|||
}
|
||||
|
||||
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 {
|
||||
AWSAccountId: this.accountId,
|
||||
KeyId: this.id,
|
||||
Arn: this.arn,
|
||||
CreationDate: new Date(this.createdAt).toISOString(),
|
||||
Enabled: true,
|
||||
Description: this.description,
|
||||
KeyUsage: this.usage,
|
||||
KeyState: 'Enabled',
|
||||
KeyManager: "CUSTOMER",
|
||||
CreationDate: this.createdAt.getAwsTime(),
|
||||
CustomerMasterKeySpec: this.keySpec,
|
||||
Description: this.description,
|
||||
Enabled: true,
|
||||
KeyId: this.id,
|
||||
KeyManager: undefined,
|
||||
KeySpec: this.keySpec,
|
||||
DeletionDate: null,
|
||||
SigningAlgorithms: [
|
||||
"RSASSA_PSS_SHA_256",
|
||||
"RSASSA_PSS_SHA_384",
|
||||
"RSASSA_PSS_SHA_512",
|
||||
"RSASSA_PKCS1_V1_5_SHA_256",
|
||||
"RSASSA_PKCS1_V1_5_SHA_384",
|
||||
"RSASSA_PKCS1_V1_5_SHA_512"
|
||||
]
|
||||
KeyState: this.keyState,
|
||||
KeyUsage: this.usage,
|
||||
MultiRegion: this.multiRegion,
|
||||
Origin: this.origin,
|
||||
PendingDeletionWindowInDays: undefined,
|
||||
ValidTo: undefined,
|
||||
XksKeyConfiguration: undefined,
|
||||
...dynamicContent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,23 @@ import { KmsService } from './kms.service';
|
|||
import { KMSHandlers } from './kms.constants';
|
||||
import { DescribeKeyHandler } from './describe-key.handler';
|
||||
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 = [
|
||||
CreateAliasHandler,
|
||||
CreateKeyHandler,
|
||||
DescribeKeyHandler,
|
||||
EnableKeyRotationHandler,
|
||||
GetKeyPolicyHandler,
|
||||
GetKeyRotationStatusHandler,
|
||||
ListAliasesHandler,
|
||||
ListResourceTagsHandler,
|
||||
]
|
||||
|
||||
const actions = [
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
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 { KmsAlias } from './kms-alias.entity';
|
||||
import { AwsProperties } from '../abstract-action.handler';
|
||||
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||
|
||||
@Injectable()
|
||||
export class KmsService {
|
||||
|
|
@ -10,21 +14,103 @@ export class KmsService {
|
|||
private readonly prismaService: PrismaService,
|
||||
) {}
|
||||
|
||||
async findOneById(id: string): Promise<KmsKey | null> {
|
||||
const pRecord = await this.prismaService.kmsKey.findFirst({
|
||||
where: { id }
|
||||
});
|
||||
return pRecord ? new KmsKey(pRecord) : null;
|
||||
async findOneByRef(ref: string, awsProperties: AwsProperties): Promise<KmsKey> {
|
||||
if (ref.startsWith('arn')) {
|
||||
return await this.findOneByArn(ref);
|
||||
}
|
||||
return await this.findOneById(awsProperties.accountId, awsProperties.region, ref);
|
||||
}
|
||||
|
||||
async findKeyIdFromAlias(alias: string, arn: ArnParts): Promise<string | null> {
|
||||
const record = await this.prismaService.kmsAlias.findFirst({
|
||||
where: {
|
||||
name: alias,
|
||||
accountId: arn.accountId,
|
||||
region: arn.region,
|
||||
async findOneByArn(arn: string): Promise<KmsKey> {
|
||||
const parts = breakdownArn(arn);
|
||||
return await this.findOneById(parts.accountId, parts.region, parts.identifier.split('/')[1]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/main.ts
10
src/main.ts
|
|
@ -8,6 +8,16 @@ import { AwsExceptionFilter } from './_context/exception.filter';
|
|||
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
declare global {
|
||||
interface Date {
|
||||
getAwsTime(): number;
|
||||
}
|
||||
}
|
||||
|
||||
Date.prototype.getAwsTime = function (this: Date) {
|
||||
return Math.floor(this.getTime() / 1000);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
|
|
|||
Loading…
Reference in New Issue