Fields that are selected through include are returned as cyphertext
Opened this issue · 6 comments
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])
}
Could you paste the debug logs here (redacting any relevant values) please?
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, }, }, ])
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.
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;
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
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.