From f5cbde418893f6c12d7d6f2978f74463f27109a4 Mon Sep 17 00:00:00 2001 From: Matt Schoch Date: Mon, 25 Mar 2024 12:09:16 -0400 Subject: [PATCH] POST /import/encryption-key generating RSA keypair for imports --- .../src/vault/core/service/import.service.ts | 23 +++++++-- .../http/rest/controller/import.controller.ts | 10 ++++ .../generate-encryption-key-response.dto.ts | 12 +++++ .../repository/import.repository.ts | 50 +++++++++++++++++++ apps/vault/src/vault/vault.module.ts | 2 + packages/nestjs-shared/src/lib/dto/index.ts | 1 + .../src/lib/dto/rsa-public-key.dto.ts | 46 +++++++++++++++++ .../src/lib/__test__/unit/sign.spec.ts | 2 +- 8 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 apps/vault/src/vault/http/rest/dto/generate-encryption-key-response.dto.ts create mode 100644 apps/vault/src/vault/persistence/repository/import.repository.ts create mode 100644 packages/nestjs-shared/src/lib/dto/rsa-public-key.dto.ts diff --git a/apps/vault/src/vault/core/service/import.service.ts b/apps/vault/src/vault/core/service/import.service.ts index 19e818e55..653b93250 100644 --- a/apps/vault/src/vault/core/service/import.service.ts +++ b/apps/vault/src/vault/core/service/import.service.ts @@ -1,23 +1,38 @@ import { Hex } from '@narval/policy-engine-shared' +import { Alg, RsaPrivateKey, RsaPublicKey, generateJwk, rsaPrivateKeyToPublicKey } from '@narval/signature' import { Injectable, Logger } from '@nestjs/common' import { privateKeyToAddress } from 'viem/accounts' import { Wallet } from '../../../shared/type/domain.type' +import { ImportRepository } from '../../persistence/repository/import.repository' import { WalletRepository } from '../../persistence/repository/wallet.repository' @Injectable() export class ImportService { private logger = new Logger(ImportService.name) - constructor(private walletRepository: WalletRepository) {} + constructor( + private walletRepository: WalletRepository, + private importRepository: ImportRepository + ) {} - async importPrivateKey(tenantId: string, privateKey: Hex, walletId?: string): Promise { + async generateEncryptionKey(clientId: string): Promise { + const privateKey = await generateJwk(Alg.RS256, { use: 'enc' }) + const publicKey = rsaPrivateKeyToPublicKey(privateKey) + + // Save the privateKey + await this.importRepository.save(clientId, privateKey) + + return publicKey + } + + async importPrivateKey(clientId: string, privateKey: Hex, walletId?: string): Promise { this.logger.log('Importing private key', { - tenantId + clientId }) const address = privateKeyToAddress(privateKey) const id = walletId || this.generateWalletId(address) - const wallet = await this.walletRepository.save(tenantId, { + const wallet = await this.walletRepository.save(clientId, { id, privateKey, address diff --git a/apps/vault/src/vault/http/rest/controller/import.controller.ts b/apps/vault/src/vault/http/rest/controller/import.controller.ts index 47daf4a9a..9ec7bd7af 100644 --- a/apps/vault/src/vault/http/rest/controller/import.controller.ts +++ b/apps/vault/src/vault/http/rest/controller/import.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Post, UseGuards } from '@nestjs/common' import { ClientId } from '../../../../shared/decorator/client-id.decorator' import { ClientSecretGuard } from '../../../../shared/guard/client-secret.guard' import { ImportService } from '../../../core/service/import.service' +import { GenerateEncryptionKeyResponseDto } from '../dto/generate-encryption-key-response.dto' import { ImportPrivateKeyDto } from '../dto/import-private-key-dto' import { ImportPrivateKeyResponseDto } from '../dto/import-private-key-response-dto' @@ -10,6 +11,15 @@ import { ImportPrivateKeyResponseDto } from '../dto/import-private-key-response- export class ImportController { constructor(private importService: ImportService) {} + @Post('/encryption-key') + async generateEncryptionKey(@ClientId() clientId: string) { + const publicKey = await this.importService.generateEncryptionKey(clientId) + + const response = new GenerateEncryptionKeyResponseDto(publicKey) + + return response + } + @Post('/private-key') async create(@ClientId() clientId: string, @Body() body: ImportPrivateKeyDto) { const importedKey = await this.importService.importPrivateKey(clientId, body.privateKey, body.walletId) diff --git a/apps/vault/src/vault/http/rest/dto/generate-encryption-key-response.dto.ts b/apps/vault/src/vault/http/rest/dto/generate-encryption-key-response.dto.ts new file mode 100644 index 000000000..6d7814bed --- /dev/null +++ b/apps/vault/src/vault/http/rest/dto/generate-encryption-key-response.dto.ts @@ -0,0 +1,12 @@ +import { RsaPublicKeyDto } from '@narval/policy-engine-shared' +import { RsaPublicKey } from '@narval/signature' +import { ApiProperty } from '@nestjs/swagger' + +export class GenerateEncryptionKeyResponseDto { + constructor(publicKey: RsaPublicKey) { + this.publicKey = publicKey + } + + @ApiProperty() + publicKey: RsaPublicKeyDto +} diff --git a/apps/vault/src/vault/persistence/repository/import.repository.ts b/apps/vault/src/vault/persistence/repository/import.repository.ts new file mode 100644 index 000000000..28a27bb92 --- /dev/null +++ b/apps/vault/src/vault/persistence/repository/import.repository.ts @@ -0,0 +1,50 @@ +import { RsaPrivateKey, rsaPrivateKeySchema } from '@narval/signature' +import { Injectable } from '@nestjs/common' +import { z } from 'zod' +import { EncryptKeyValueService } from '../../../shared/module/key-value/core/service/encrypt-key-value.service' + +const importKeySchema = z.object({ + jwk: rsaPrivateKeySchema, + createdAt: z.number() // epoch in seconds +}) +export type ImportKey = z.infer + +@Injectable() +export class ImportRepository { + private KEY_PREFIX = 'import:' + + constructor(private keyValueService: EncryptKeyValueService) {} + + getKey(clientId: string, id: string): string { + return `${this.KEY_PREFIX}:${clientId}:${id}` + } + + async findById(clientId: string, id: string): Promise { + const value = await this.keyValueService.get(this.getKey(clientId, id)) + + if (value) { + return this.decode(value) + } + + return null + } + + async save(clientId: string, privateKey: RsaPrivateKey): Promise { + const createdAt = Date.now() / 1000 + const importKey: ImportKey = { + jwk: privateKey, + createdAt + } + await this.keyValueService.set(this.getKey(clientId, privateKey.kid), this.encode(importKey)) + + return importKey + } + + private encode(importKey: ImportKey): string { + return JSON.stringify(importKey) + } + + private decode(value: string): ImportKey { + return importKeySchema.parse(JSON.parse(value)) + } +} diff --git a/apps/vault/src/vault/vault.module.ts b/apps/vault/src/vault/vault.module.ts index 36b653950..8e7ca729a 100644 --- a/apps/vault/src/vault/vault.module.ts +++ b/apps/vault/src/vault/vault.module.ts @@ -17,6 +17,7 @@ import { SigningService } from './core/service/signing.service' import { ImportController } from './http/rest/controller/import.controller' import { SignController } from './http/rest/controller/sign.controller' import { AppRepository } from './persistence/repository/app.repository' +import { ImportRepository } from './persistence/repository/import.repository' import { WalletRepository } from './persistence/repository/wallet.repository' import { VaultController } from './vault.controller' import { VaultService } from './vault.service' @@ -46,6 +47,7 @@ import { VaultService } from './vault.service' ProvisionService, SigningService, WalletRepository, + ImportRepository, { provide: APP_PIPE, useFactory: () => diff --git a/packages/nestjs-shared/src/lib/dto/index.ts b/packages/nestjs-shared/src/lib/dto/index.ts index 6e69c84b0..e290a648a 100644 --- a/packages/nestjs-shared/src/lib/dto/index.ts +++ b/packages/nestjs-shared/src/lib/dto/index.ts @@ -1,5 +1,6 @@ export * from './base-action-request.dto' export * from './base-action.dto' +export * from './rsa-public-key.dto' export * from './sign-message-request-data-dto' export * from './sign-transaction-request-data.dto' export * from './signature.dto' diff --git a/packages/nestjs-shared/src/lib/dto/rsa-public-key.dto.ts b/packages/nestjs-shared/src/lib/dto/rsa-public-key.dto.ts new file mode 100644 index 000000000..d06342ed6 --- /dev/null +++ b/packages/nestjs-shared/src/lib/dto/rsa-public-key.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsDefined, IsIn, IsOptional, IsString } from 'class-validator' + +export class RsaPublicKeyDto { + @IsString() + @IsDefined() + @ApiProperty() + kid: string + + @IsString() + @IsDefined() + @ApiProperty({ + enum: ['RSA'], + default: 'RSA' + }) + kty: 'RSA' + + @IsString() + @IsDefined() + @ApiProperty({ + enum: ['RS256'], + default: 'RS256' + }) + alg: 'RS256' + + @IsString() + @IsDefined() + @ApiProperty({ + description: 'A base64Url-encoded value' + }) + n: string + + @IsString() + @IsDefined() + @ApiProperty({ + description: 'A base64Url-encoded value' + }) + e: string + + @IsIn(['enc', 'sig']) + @IsOptional() + @ApiProperty({ + enum: ['enc', 'sig'] + }) + use?: 'enc' | 'sig' | undefined +} diff --git a/packages/signature/src/lib/__test__/unit/sign.spec.ts b/packages/signature/src/lib/__test__/unit/sign.spec.ts index 8c9c762a3..d90277c44 100644 --- a/packages/signature/src/lib/__test__/unit/sign.spec.ts +++ b/packages/signature/src/lib/__test__/unit/sign.spec.ts @@ -5,7 +5,7 @@ import { createPublicKey } from 'node:crypto' import { toHex, verifyMessage } from 'viem' import { privateKeyToAccount, signMessage } from 'viem/accounts' import { buildSignerEip191, buildSignerEs256k, signJwt } from '../../sign' -import { Alg, Payload, SigningAlg } from '../../types' +import { Alg, Payload, PrivateKey, SigningAlg } from '../../types' import { base64UrlToBytes, base64UrlToHex,