Skip to content

Commit

Permalink
POST /import/encryption-key generating RSA keypair for imports
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Mar 25, 2024
1 parent 977928b commit 599249c
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 6 deletions.
23 changes: 19 additions & 4 deletions apps/vault/src/vault/core/service/import.service.ts
Original file line number Diff line number Diff line change
@@ -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<Wallet> {
async generateEncryptionKey(clientId: string): Promise<RsaPublicKey> {
const privateKey = await generateJwk<RsaPrivateKey>(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<Wallet> {
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
Expand Down
10 changes: 10 additions & 0 deletions apps/vault/src/vault/http/rest/controller/import.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
50 changes: 50 additions & 0 deletions apps/vault/src/vault/persistence/repository/import.repository.ts
Original file line number Diff line number Diff line change
@@ -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<typeof importKeySchema>

@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<ImportKey | null> {
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<ImportKey> {
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))
}
}
2 changes: 2 additions & 0 deletions apps/vault/src/vault/vault.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -46,6 +47,7 @@ import { VaultService } from './vault.service'
ProvisionService,
SigningService,
WalletRepository,
ImportRepository,
{
provide: APP_PIPE,
useFactory: () =>
Expand Down
1 change: 1 addition & 0 deletions packages/policy-engine-shared/src/lib/dto/index.ts
Original file line number Diff line number Diff line change
@@ -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'
46 changes: 46 additions & 0 deletions packages/policy-engine-shared/src/lib/dto/rsa-public-key.dto.ts
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,7 +38,7 @@ describe('sign', () => {
it('should sign build & sign es256 JWT correctly with a PEM', async () => {
const key = await importPKCS8(PRIVATE_KEY_PEM, Alg.ES256)
const jwk = await exportJWK(key)
const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256, crv: 'P-256', kty: 'EC', kid: 'somekid' })
const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256, crv: 'P-256', kty: 'EC', kid: 'somekid' } as PrivateKey)

const verified = await jwtVerify(jwt, key)
expect(verified.payload).toEqual(payload)
Expand Down

0 comments on commit 599249c

Please sign in to comment.