File

src/crypto/key/filesystem-key.service.ts

Description

The key service is responsible for managing the keys of the issuer.

Extends

KeyService

Index

Properties
Methods

Constructor

constructor(configService: ConfigService, cryptoService: CryptoImplementationService, certRepository: Repository<CertEntity>, logger: PinoLogger)
Parameters :
Name Type Optional
configService ConfigService No
cryptoService CryptoImplementationService No
certRepository Repository<CertEntity> No
logger PinoLogger No

Methods

Async create
create(tenantId: string)
Inherited from KeyService
Defined in KeyService:204

Creates a new keypair and wrtites the private key to the file system.

Parameters :
Name Type Optional
tenantId string No
Returns : Promise<string>

key id of the generated key.

Async getKeys
getKeys(tenantId: string)
Inherited from KeyService
Defined in KeyService:153

Returns the keys for the given tenant.

Parameters :
Name Type Optional
tenantId string No
Returns : Promise<KeyObj[]>
getKid
getKid(tenantId: string, type: CertificateType)
Inherited from KeyService
Defined in KeyService:275

Gets one key id for the tenant. If no key exists, it will throw an error.

Parameters :
Name Type Optional Default value
tenantId string No
type CertificateType No 'signing'
Returns : Promise<string>
Private Async getPrivateKey
getPrivateKey(tenantId: string, keyId?: string)

Get the keys from the file system or generate them if they do not exist

Parameters :
Name Type Optional
tenantId string No
keyId string Yes
Returns : unknown
Private getPubFromPrivateKey
getPubFromPrivateKey(privateKey: JWK)

Get the puvlic key from the private key.

Parameters :
Name Type Optional
privateKey JWK No
Returns : JWK
getPublicKey
getPublicKey(type, tenantId: string, keyId?: string)
Inherited from KeyService
Defined in KeyService:291

Get the public key

Parameters :
Name Type Optional
type No
tenantId string No
keyId string Yes
Returns : Promise<JWK>
getPublicKey
getPublicKey(type, tenantId: string, keyId?: string)
Parameters :
Name Type Optional
type No
tenantId string No
keyId string Yes
Returns : Promise<string>
Async getPublicKey
getPublicKey(type: "pem" | "jwk", tenantId: string, keyId?: string)
Parameters :
Name Type Optional
type "pem" | "jwk" No
tenantId string No
keyId string Yes
Returns : Promise<JWK | string>
import
import(tenantId: string, body: KeyImportDto)
Inherited from KeyService
Defined in KeyService:128

Import a key into the key service.

Parameters :
Name Type Optional
tenantId string No
body KeyImportDto No
Returns : Promise<string>
init
init(tenant: string)
Inherited from KeyService
Defined in KeyService:195

Initialize the key service for a specific tenant. This will create the keys if they do not exist.

Parameters :
Name Type Optional
tenant string No
Returns : Promise<string>
Async onApplicationBootstrap
onApplicationBootstrap()
Returns : any
Async signer
signer(tenantId: string, keyId?: string)
Inherited from KeyService
Defined in KeyService:236

Get the signer for the key service

Parameters :
Name Type Optional
tenantId string No
keyId string Yes
Returns : Promise<Signer>
Async signJWT
signJWT(payload: JWTPayload, header: JWTHeaderParameters, tenantId: string, keyId?: string)
Inherited from KeyService
Defined in KeyService:332
Parameters :
Name Type Optional
payload JWTPayload No
header JWTHeaderParameters No
tenantId string No
keyId string Yes
Returns : Promise<string>
Protected getCertificate
getCertificate(tenantId: string, keyId: string)
Inherited from KeyService
Defined in KeyService:105

Get the certificate for the given key id.

Parameters :
Name Type Optional
tenantId string No
keyId string No
Returns : Promise<string>

Properties

Private crypto
Type : CryptoImplementation
import {
    existsSync,
    mkdirSync,
    writeFileSync,
    readFileSync,
    readdirSync,
} from 'node:fs';
import {
    JWK,
    JWTHeaderParameters,
    JWTPayload,
    CryptoKey,
    SignJWT,
    importJWK,
    exportSPKI,
    exportJWK,
} from 'jose';
import { v4 } from 'uuid';
import { KeyObj, KeyService } from './key.service';
import {
    ConflictException,
    Injectable,
    OnApplicationBootstrap,
} from '@nestjs/common';
import { Signer } from '@sd-jwt/types';
import { ConfigService } from '@nestjs/config';
import { CryptoImplementation } from './crypto/crypto-implementation';
import { CryptoImplementationService } from './crypto/crypto.service';
import { join } from 'node:path';
import { KeyImportDto } from './dto/key-import.dto';
import { CertEntity, CertificateType } from './entities/cert.entity';
import { Repository } from 'typeorm/repository/Repository';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { PinoLogger } from 'nestjs-pino';

/**
 * The key service is responsible for managing the keys of the issuer.
 */
@Injectable()
export class FileSystemKeyService
    extends KeyService
    implements OnApplicationBootstrap
{
    private crypto: CryptoImplementation;

    constructor(
        configService: ConfigService,
        private cryptoService: CryptoImplementationService,
        certRepository: Repository<CertEntity>,
        private logger: PinoLogger,
    ) {
        super(configService, certRepository);
        this.crypto = cryptoService.getCrypto();
    }

    async onApplicationBootstrap() {
        if (this.configService.get<boolean>('CONFIG_IMPORT')) {
            const configPath = this.configService.getOrThrow('CONFIG_FOLDER');
            const subfolder = 'keys';
            const force = this.configService.get<boolean>(
                'CONFIG_IMPORT_FORCE',
            );
            if (this.configService.get<boolean>('CONFIG_IMPORT')) {
                const tenantFolders = readdirSync(configPath, {
                    withFileTypes: true,
                }).filter((tenant) => tenant.isDirectory());
                let counter = 0;
                for (const tenant of tenantFolders) {
                    //iterate over all elements in the folder and import them
                    const path = join(configPath, tenant.name, subfolder);
                    const files = readdirSync(path);
                    for (const file of files) {
                        const payload = JSON.parse(
                            readFileSync(join(path, file), 'utf8'),
                        );

                        payload.id = file.replace('.json', '');
                        const exists = await this.getPrivateKey(
                            tenant.name,
                            payload.id,
                        ).catch(() => false);
                        if (exists && !force) {
                            continue; // Skip if config already exists and force is not set
                        }

                        // Validate the payload against KeyImportDto
                        const config = plainToClass(KeyImportDto, payload);
                        const validationErrors = await validate(config);

                        if (validationErrors.length > 0) {
                            this.logger.error(
                                {
                                    event: 'ValidationError',
                                    file,
                                    tenant: tenant.name,
                                    errors: validationErrors.map((error) => ({
                                        property: error.property,
                                        constraints: error.constraints,
                                        value: error.value,
                                    })),
                                },
                                `Validation failed for key config ${file} in tenant ${tenant.name}`,
                            );
                            continue; // Skip this invalid config
                        }

                        await this.import(tenant.name, config);
                        counter++;
                    }
                    this.logger.info(
                        {
                            event: 'Import',
                        },
                        `${counter} keys imported for ${tenant.name}`,
                    );
                }
            }
        }
    }

    /**
     * Import a key into the key service.
     * @param tenantId
     * @param body
     * @returns
     */
    import(tenantId: string, body: KeyImportDto): Promise<string> {
        const folder = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            'keys',
            'keys',
        );
        if (!existsSync(folder)) {
            mkdirSync(folder, { recursive: true });
        }

        const privateKey = body.privateKey;
        writeFileSync(
            join(folder, `${privateKey.kid}.json`),
            JSON.stringify(privateKey, null, 2),
        );

        return Promise.resolve(privateKey.kid);
    }

    /**
     * Returns the keys for the given tenant.
     * @param tenantId
     * @returns
     */
    async getKeys(tenantId: string): Promise<KeyObj[]> {
        const folder = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            'keys',
            'keys',
        );
        if (!existsSync(folder)) {
            mkdirSync(folder, { recursive: true });
        }
        const files = readdirSync(folder);
        const keys: KeyObj[] = [];
        for (const file of files) {
            const keyData = readFileSync(join(folder, file), 'utf-8');
            const privateKey = JSON.parse(keyData) as JWK;

            const publicKey = this.getPubFromPrivateKey(privateKey);
            const crt = await this.getCertificate(
                tenantId,
                privateKey.kid as string,
            );
            keys.push({ id: privateKey.kid as string, publicKey, crt });
        }
        return Promise.resolve(keys);
    }

    /**
     * Get the puvlic key from the private key.
     * @param privateKey
     * @returns
     */
    private getPubFromPrivateKey(privateKey: JWK): JWK {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { d, key_ops, ext, ...publicKey } = privateKey;
        return publicKey;
    }

    /**
     * Initialize the key service for a specific tenant.
     * This will create the keys if they do not exist.
     * @param tenant
     */
    init(tenant: string): Promise<string> {
        return this.getKid(tenant).catch(async () => this.create(tenant));
    }

    /**
     * Creates a new keypair and wrtites the private key to the file system.
     * @param tenantId
     * @returns key id of the generated key.
     */
    async create(tenantId: string): Promise<string> {
        const folder = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            'keys',
            'keys',
        );
        if (!existsSync(folder)) {
            mkdirSync(folder, { recursive: true });
        }

        const keys = await this.crypto.generateKeyPair();
        const privateKey = keys.privateKey as JWK;
        //add a random key id for reference
        privateKey.kid = v4();
        privateKey.alg = this.crypto.alg;

        //remove exportable and key_ops from the private key
        delete privateKey.ext;
        delete privateKey.key_ops;

        writeFileSync(
            join(folder, `${privateKey.kid}.json`),
            JSON.stringify(privateKey, null, 2),
        );

        return privateKey.kid;
    }

    /**
     * Get the signer for the key service
     */
    async signer(tenantId: string, keyId?: string): Promise<Signer> {
        const privateKey = await this.getPrivateKey(tenantId, keyId);
        return this.crypto.getSigner(privateKey);
    }

    /**
     * Get the keys from the file system or generate them if they do not exist
     * @returns
     */
    private async getPrivateKey(tenantId: string, keyId?: string) {
        keyId = keyId || (await this.getKid(tenantId));
        // use the first key that is stored there.
        const folder = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            'keys',
            'keys',
        );
        if (!existsSync(folder)) {
            mkdirSync(folder, { recursive: true });
        }
        const file = join(folder, `${keyId}.json`);
        if (!existsSync(file)) {
            // If the file does not exist, generate a new keypair
            await this.create(tenantId);
        }
        if (!existsSync(file)) {
            throw new ConflictException(`Key ${file} does not exist`);
        }
        const keyData = readFileSync(file, 'utf-8');
        const privateKey = JSON.parse(keyData) as JWK;
        return privateKey;
    }

    /**
     * Gets one key id for the tenant.
     * If no key exists, it will throw an error.
     * @returns
     */
    getKid(
        tenantId: string,
        type: CertificateType = 'signing',
    ): Promise<string> {
        return this.certRepository
            .findOneByOrFail({
                tenantId,
                type,
            })
            .then((cert) => cert.keyId);
    }

    /**
     * Get the public key
     * @returns
     */
    getPublicKey(type: 'jwk', tenantId: string, keyId?: string): Promise<JWK>;
    getPublicKey(
        type: 'pem',
        tenantId: string,
        keyId?: string,
    ): Promise<string>;
    async getPublicKey(
        type: 'pem' | 'jwk',
        tenantId: string,
        keyId?: string,
    ): Promise<JWK | string> {
        const privateKey = await this.getPrivateKey(tenantId, keyId);

        // Convert the private key to a public key
        // First import the private key as a CryptoKey
        const privateKeyInstance = await importJWK(
            privateKey,
            this.cryptoService.getAlg(),
            { extractable: true },
        );

        // Export it as a JWK to get the public key components
        const privateKeyJWK = await exportJWK(privateKeyInstance);

        // Remove private key components to get only the public key

        const publicKey = this.getPubFromPrivateKey(privateKeyJWK);

        if (type === 'pem') {
            // Import the public key and export as PEM
            const publicKeyInstance = await importJWK(
                publicKey,
                this.cryptoService.getAlg(),
                { extractable: true },
            );
            return exportSPKI(publicKeyInstance as CryptoKey);
        } else {
            return publicKey;
        }
    }

    async signJWT(
        payload: JWTPayload,
        header: JWTHeaderParameters,
        tenantId: string,
        keyId?: string,
    ): Promise<string> {
        const privateKey = await this.getPrivateKey(tenantId, keyId);
        const privateKeyInstance = (await importJWK(privateKey)) as CryptoKey;
        return new SignJWT(payload)
            .setProtectedHeader(header)
            .sign(privateKeyInstance);
    }
}

results matching ""

    No results matching ""