Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fields that are selected through include are returned as cyphertext #92

Open
Serdans opened this issue Nov 14, 2023 · 6 comments
Open

Comments

@Serdans
Copy link

Serdans commented Nov 14, 2023

When doing a query which includes encrypted fields, rather than selecting them directly, the field will return encrypted.
Example of my prisma schema:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
    provider = "prisma-client-js"
}

generator fieldEncryptionMigrations {
    provider = "prisma-field-encryption"
    output   = "./migrations"

    // Optionally opt-in to concurrent model migration.
    // Since this can cause timeouts and performance issues,
    // it's off by default, and models are updated sequentially.
    concurrently = true
}

datasource db {
    provider  = "mongodb"
    url       = env("DATABASE_URL")
    directUrl = env("DIRECT_DATABASE_URL")
}

enum Role {
    USER
    ADMIN
}

model User {
    id                   String         @id @default(cuid()) @map("_id")
    accountId            String         @unique
    email                String /// @encrypted?mode=strict
    role                 Role           @default(USER)
    subscriptions        Subscription[] @relation("subscriptions")
    createdSubscriptions Subscription[] @relation("createdSubscriptions")
}

model Subscription {
    id         String   @id @default(cuid()) @map("_id")
    name       String
    paymentUrl String
    expiresOn  DateTime
    customerId String
    customer   User     @relation("subscriptions", fields: [customerId], references: [id])
    adminId    String
    admin      User     @relation("createdSubscriptions", fields: [adminId], references: [id])
}

@franky47
Copy link
Member

Could you paste the debug logs here (redacting any relevant values) please?

@Serdans
Copy link
Author

Serdans commented Nov 14, 2023

Actually I don't think the include matters. I'm also getting it for regular selects.

  prisma-field-encryption:setup Keys: {
  prisma-field-encryption:setup   encryptionKey: {
  prisma-field-encryption:setup     raw: Uint8Array(32) [
xX
  prisma-field-encryption:setup     ],
  prisma-field-encryption:setup     fingerprint: '83fa61c5'
  prisma-field-encryption:setup   },
  prisma-field-encryption:setup   keychain: { '83fa61c5': { key: [Object], createdAt: 1699968990545 } }
  prisma-field-encryption:setup } +0ms
  prisma-field-encryption:setup Models: {
  prisma-field-encryption:setup   User: {
  prisma-field-encryption:setup     cursor: 'id',
  prisma-field-encryption:setup     fields: { email: [Object] },
  prisma-field-encryption:setup     connections: { subscriptions: [Object], createdSubscriptions: [Object] }
  prisma-field-encryption:setup   },
  prisma-field-encryption:setup   Subscription: {
  prisma-field-encryption:setup     cursor: 'id',
  prisma-field-encryption:setup     fields: {},
  prisma-field-encryption:setup     connections: { customer: [Object], admin: [Object] }
  prisma-field-encryption:setup   }
  prisma-field-encryption:setup } +20ms
 ✓ Compiled in 333ms (332 modules)
prisma:query db.User.aggregate([ { $match: { $expr: { $and: [ { $eq: [ "$accountId", { $literal: "30712252-966d-4b17-a2ca-650f0011c106", }, ], }, { $ne: [ "$accountId", "$$REMOVE", ], }, ], }, }, }, { $limit: 1, }, { $project: { _id: 1, accountId: 1, email: 1, role: 1, }, }, ])
prisma:query db.User.aggregate([ { $match: { $expr: { $and: [ { $eq: [ "$accountId", { $literal: "30712252-966d-4b17-a2ca-650f0011c106", }, ], }, { $ne: [ "$accountId", "$$REMOVE", ], }, ], }, }, }, { $limit: 1, }, { $project: { _id: 1, accountId: 1, email: 1, role: 1, }, }, ])
AUTH USER {
  id: 'clojv3dbz0000o60gfmquqw6u',
  accountId: '30712252-966d-4b17-a2ca-650f0011c106',
  email: 'v1.aesgcm256.83fa61c5.b_AMLTv_7VbNSS9n.cFmJw2yujwmdYTEa3yePa5Hdby2wD0BVp97b6q_IBY9VOvI=',
  role: 'ADMIN'
}
 ○ Compiling /favicon.ico/route ...
 ✓ Compiled /api/user/[userId]/subscription/route in 1523ms (864 modules)
prisma:warn In production, we recommend using `prisma generate --no-engine` (See: `prisma generate --help`)
2023-11-14T13:36:33.669Z prisma-field-encryption:setup Keys: {
  encryptionKey: {
    raw: Uint8Array(32) [
      214, 112,  94, 194, 189,  90,  64, 162,
       98, 147, 152, 213,  33, 131,  83, 253,
      245,  19,  20, 191,  36,   3, 152,   3,
       38, 190, 186,  61, 241, 154,  57, 201
    ],
    fingerprint: '83fa61c5'
  },
  keychain: { '83fa61c5': { key: [Object], createdAt: 1699968993665 } }
}
2023-11-14T13:36:33.699Z prisma-field-encryption:setup Models: {
  User: {
    cursor: 'id',
    fields: { email: [Object] },
    connections: { subscriptions: [Object], createdSubscriptions: [Object] }
  },
  Subscription: {
    cursor: 'id',
    fields: {},
    connections: { customer: [Object], admin: [Object] }
  }
}
prisma:query db.Subscription.aggregate([ { $match: { $expr: { $and: [ { $eq: [ "$customerId", { $literal: "clojv3dbz0000o60gfmquqw6u", }, ], }, { $ne: [ "$customerId", "$$REMOVE", ], }, ], }, }, }, { $sort: { expiresOn: -1, }, }, { $project: { _id: 1, name: 1, paymentUrl: 1, expiresOn: 1, customerId: 1, adminId: 1, }, }, ])

@franky47
Copy link
Member

Ah I see, you have to do special things with Next.js to prevent hot-reloading to stack up the middlewares, see #61 (comment).

Let me know if that fixes it for you.

@Serdans
Copy link
Author

Serdans commented Nov 14, 2023

I instantiate my prisma client like so, which is roughly the same as in that comment I think and it doesn't work:

import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";
import { fieldEncryptionExtension } from 'prisma-field-encryption'

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma =
    globalForPrisma.prisma ||
    new PrismaClient({
        log:
            process.env.NODE_ENV === "development"
                ? ["query", "error", "warn"]
                : ["error"],
    })
        .$extends(withAccelerate())
        .$extends(fieldEncryptionExtension());

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

export default prisma;

@srosato
Copy link
Contributor

srosato commented Nov 22, 2023

I also have the same problem, with a similar setup (I tried accelerate too, but removed it for now).

When I am on prisma ~4.3 and prisma-field-encryption@~1.4.5 the cypher gets decoded correctly. I tried prisma ~5.1 and then ~5.5 on prisma-field-encryption@^1.5.0 and the cypher does not get decoded. I use the same encryption keys.

Here is my debug output:

Debug output
 ✓ Compiled /api/graphql in 5.9s (5591 modules)
2023-11-22T21:21:01.453Z prisma-field-encryption:setup Keys: {
  encryptionKey: {
    raw: Uint8Array(32) [/* redacted */],
    fingerprint: '/* redacted */'
  },
  keychain: { /* redacted */: { key: [Object], createdAt: 1700688061450 } }
}
2023-11-22T21:21:01.463Z prisma-field-encryption:setup Models: {
  Job: {
    cursor: 'id',
    fields: {},
    connections: {
      address: [Object],
      positions: [Object],
      requestedPositions: [Object],
      workShiftEvents: [Object],
      workShiftViolations: [Object]
    }
  },
  JobClockInCode: { cursor: 'id', fields: {}, connections: {} },
  Address: {
    cursor: 'id',
    fields: {},
    connections: { job: [Object], contact: [Object] }
  },
  JobPosition: {
    cursor: 'id',
    fields: {},
    connections: { job: [Object], user: [Object] }
  },
  JobPositionRequest: {
    cursor: 'id',
    fields: {},
    connections: { job: [Object], requestedBy: [Object] }
  },
  WorkShift: {
    cursor: 'id',
    fields: {},
    connections: { user: [Object], events: [Object], violations: [Object] }
  },
  WorkShiftEvent: {
    cursor: 'id',
    fields: {},
    connections: {
      workShift: [Object],
      job: [Object],
      successor: [Object],
      predecessor: [Object]
    }
  },
  WorkShiftViolation: {
    cursor: 'id',
    fields: {},
    connections: { workShift: [Object], job: [Object] }
  },
  User: {
    cursor: 'id',
    fields: { pin: [Object] },
    connections: {
      profile: [Object],
      jobPositions: [Object],
      jobPositionRequests: [Object],
      workShifts: [Object],
      notifications: [Object],
      devicePushTokens: [Object],
      employeeNotes: [Object],
      authoredNotes: [Object]
    }
  },
  Profile: {
    cursor: 'id',
    fields: {},
    connections: { users: [Object], contacts: [Object] }
  },
  EmployeeNote: {
    cursor: 'id',
    fields: {},
    connections: { employee: [Object], author: [Object] }
  },
  Contact: {
    cursor: 'id',
    fields: {},
    connections: { address: [Object], profile: [Object] }
  },
  Notification: { cursor: 'id', fields: {}, connections: { user: [Object] } },
  DevicePushToken: { cursor: 'id', fields: {}, connections: { user: [Object] } }
}

And my globally configured prisma on NextJs 14:

prisma.ts
import { Prisma, PrismaClient } from './client';
import { fieldEncryptionExtension } from 'prisma-field-encryption';
import { loadEnvironment } from '@ss/environment-loader';
import { isProd } from '@ss/environment';

loadEnvironment();

declare global {
  // eslint-disable-next-line no-var
  var prisma: PrismaClient | undefined;
}

export const prisma =
  global.prisma ||
  new PrismaClient({
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
    log: isProd() ? ['query', 'info', 'warn', 'error'] : [],
  });

if (!global.prisma) {
  prisma.$extends(fieldEncryptionExtension({ dmmf: Prisma.dmmf }));
}

if (!isProd()) {
  global.prisma = prisma;
}

On my case I am not doing this within an include although might be why I did a root resolver in this sample code

/*
 * This is a workaround, as I am not sure how type-graphql-prisma handles queries. Nested resolvers that
 * need the pin found themselves with an encrypted value instead of an unencrypted one. This works around that,
 * although not performant.
 */
@FieldResolver(() => String)
async pin(@Root() user: User, @Ctx() { prisma }: Context): Promise<string> {
  const foundUser = await prisma.user.findUnique({ where: { id: user.id } });

  return foundUser?.pin || user.pin;
}

I also double checked that the encrypted pin is not double encrypted, it is the same as found within the database

@srosato
Copy link
Contributor

srosato commented Nov 29, 2023

Another interesting thing here is that I tried using the deprecated $use and it works

// prisma.$extends(fieldEncryptionExtension({ dmmf: Prisma.dmmf }));
prisma.$use(fieldEncryptionMiddleware({ dmmf: Prisma.dmmf }))

I noticed that the difference between the extension and the middleware is that the extension uses Prisma.defineExtension. Since my setup uses a custom output directory, I tried removing that from the compiled extension.js file in favor of just returning an object like documented here, but that did not work either.

Maybe that is a separate issue though, not too sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants