47ng / prisma-field-encryption

Transparent field-level encryption at rest for Prisma
https://github.com/franky47/prisma-field-encryption-sandbox
MIT License
246 stars 29 forks source link

NestJS PrismaService and Extending it With Encryption #78

Open jymchng opened 1 year ago

jymchng commented 1 year ago

I am trying to encrypt a particular field in my Prisma model.

Here is the full codes of my PrismaService residing my in NestJS's PrismaModule:

import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'
import { OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { fieldEncryptionExtension } from 'prisma-field-encryption';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleDestroy, OnModuleInit {

    constructor() {
        super({
            log: ['query'],
            errorFormat: 'pretty',
        });
        // Extend PrismaClient with fieldEncryptionExtension
        this.$extends(fieldEncryptionExtension()); // extending `PrismaClient`
    }

    async onModuleInit() {
        await this.$connect();
    }

    async onModuleDestroy() {
        await this.$disconnect();
    }
}

On my prisma.schema:

model User {
  telegramId Int    @id @map("telegram_id")
  mnemonics  String @unique @db.VarChar(1024) /// @encrypted 

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")
}

Within my database, after using prismaService.user.create, the resulting entry in the PostgreSQL database is NOT encrypted.

import { Injectable } from '@nestjs/common';
import { User as UserModel } from '@prisma/client';
import { PrismaService } from 'src/prisma/services/prisma/prisma.service';
import { Mnemonic } from 'src/common/entities/mnemonic/mnemonic';

@Injectable()
export class AuthService {

    constructor(private readonly prismaSvc: PrismaService) {}

    async signUp(telegramId: number): Promise<Pick<UserModel, "telegramId">> {
        const mnemonic = Mnemonic.createNew()
        const user = await this.prismaSvc.user.create({data: {telegramId: telegramId, mnemonics: mnemonic.toString()}, select: {telegramId: true}});
        return user
    }
}

Any idea how does it work? Thank you.

franky47 commented 1 year ago

Your issue is with the extension mechanism.

NestJS uses a class that inherits from the PrismaClient, but applying extensions require to use the return value of $extends(), so applying extensions in the derived class constructor won't work.

There are issues on the Prisma repo about how to deal with Nest and extensions, you should be able to find a solution there.

jymchng commented 1 year ago

Your issue is with the extension mechanism.

NestJS uses a class that inherits from the PrismaClient, but applying extensions require to use the return value of $extends(), so applying extensions in the derived class constructor won't work.

There are issues on the Prisma repo about how to deal with Nest and extensions, you should be able to find a solution there.

Thanks for your reply @franky47, https://github.com/prisma/prisma/issues/20833

jymchng commented 1 year ago

@franky47 Btw, do you have a quick workaround? Or can you point me more directly to issues that have some quick solutions?

franky47 commented 1 year ago

https://nestjs-prisma.dev/docs/prisma-client-extensions/

jymchng commented 1 year ago

https://nestjs-prisma.dev/docs/prisma-client-extensions/

Thank you sir!

jymchng commented 1 year ago

@franky47 Sir, would it be better practice to have two PrismaService, one specifically for creating records for tables with encrypted column(s) and another for general database operations? I see the link that you shared requires me to purposefully inject the CustomPrismaService whenever I need it.

constructor(
    // ✅ use `extendedPrismaClient` type for correct type-safety of your extended PrismaClient
    @Inject('PrismaService')
    private prismaService: CustomPrismaService<extendedPrismaClient>
  ) {}
franky47 commented 1 year ago

That sounds like a recipe for disaster, as a lot can go wrong: overwriting encrypted records with clear-text values (leaking data), or sending ciphertext to the front-end.

The extension makes operations transparent, so inject it to configure your client and you can forget about it being here (albeit losing some features like partial search and sorting on encrypted fields).

jymchng commented 1 year ago

@franky47 Thank you for your reply, oh, I am just trying to encrypt one or two columns of some of my tables, I am not sure how I can actually 'inject it to configure your client', is the example in the documentation sufficient? I read it.