Giter Site home page Giter Site logo

47ng / prisma-field-encryption Goto Github PK

View Code? Open in Web Editor NEW
220.0 6.0 27.0 497 KB

Transparent field-level encryption at rest for Prisma

Home Page: https://github.com/franky47/prisma-field-encryption-sandbox

License: MIT License

Shell 0.16% TypeScript 97.10% JavaScript 2.75%
prisma middleware encryption

prisma-field-encryption's Introduction

prisma-field-encryption

NPM MIT License Continuous Integration Coverage Status

Transparent field-level encryption at rest for Prisma.

Installation

Using your package manager of choice:

pnpm add prisma-field-encryption
yarn add prisma-field-encryption
npm install prisma-field-encryption

Prisma version compatibility

This extension requires Prisma 4.7.0 or higher.

For Prisma versions 4.7.0 to 4.15.0, you will need to activate the clientExtensions preview feature, or use the middleware interface.

For Prisma versions 4.16.0 and higher, client extensions are generally available and don't require a preview feature flag.

Note: The previous middleware interface is still available for Prisma versions 3.8.0 to 4.6.x, but will be removed in a future update. It is recommended to update your Prisma client and use the extension mechanism, as support for middlewares will be removed from Prisma in the future.

Usage

1. Extend your Prisma client

import { PrismaClient } from '@prisma/client'
import { fieldEncryptionExtension } from 'prisma-field-encryption'

const globalClient = new PrismaClient()

export const client = globalClient.$extends(
  // This is a function, don't forget to call it:
  fieldEncryptionExtension()
)

Read more about how to use Prisma client extensions.

2. Setup your encryption key

Generate an encryption key:

$ cloak generate

Note: the cloak CLI comes pre-installed with prisma-field-encryption as part of the @47ng/cloak dependency.

The preferred method to provide your key is via the PRISMA_FIELD_ENCRYPTION_KEY environment variable:

# .env
PRISMA_FIELD_ENCRYPTION_KEY=k1.aesgcm256.DbQoar8ZLuUsOHZNyrnjlskInHDYlzF3q6y1KGM7DUM=

You can also pass it directly in the configuration:

fieldEncryptionExtension({
  // Don't version hardcoded keys though, this is an example:
  encryptionKey: 'k1.aesgcm256.DbQoar8ZLuUsOHZNyrnjlskInHDYlzF3q6y1KGM7DUM='
})

Tip: a key provided in code will take precedence over a key from the environment.

3. Annotate your schema

In your Prisma schema, add /// @encrypted to the fields you want to encrypt:

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String? /// @encrypted <- annotate fields to encrypt
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  authorId  Int?
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String? /// @encrypted <- can be optional
  posts Post[]
}

Tip: make sure you use a triple-slash. Double slash comments won't work.

Note on @db.VarChar & field max lengths

Encryption adds quite a bit of overhead, so you'll need to raise your database field maximum lengths (usually declared with @db.VarChar(someNumber) or similar).

You can calculate the corresponding ciphertext length for a given clear-text length here: https://cloak.47ng.com/ciphertext-length-calculator

4. Regenerate your client

Make sure you have a generator for the Prisma client:

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

Then generate it using the prisma CLI:

$ prisma generate

You're done!

Filtering using where

Support: introduced in version 1.4.0

You cannot filter directly on encrypted fields:

model User {
  id    String @id
  email String /// @encrypted
}
// This will return empty results:
prisma.user.findUnique({
  where: {
    email: '[email protected]'
  }
})

This is because the encryption is not deterministic: encrypting the same input multiple times will yield different outputs, due to the use of random initialisation vectors to keep ciphertext safe. Therefore Prisma cannot match the query to the data.

For the same reason, indexes should not be placed on encrypted fields.

To circumvent this issue, the extension provides support for a separate field containing a hash of the clear-text input, which is stable and can be used for exact matching (partial matching like startsWith, contains is not possible).

To use it, add a field next to your encrypted field with the following annotation:

model User {
  id        String  @id
  email     String  @unique /// @encrypted
  emailHash String? @unique /// @encryption:hash(email) <- the name of the source field

  // Note that the @unique directive on `email` is here to enable
  // the Prisma user.findUnique({ where: { email }}) API,
  // and the @unique directive on `emailHash` is where you actually
  // ensure that there will be no duplicates.
  // The emailHash field is marked as nullable so you don't need to specify it
  // when creating records (it will be computed for you).
}

The annotation will automatically keep the emailHash field up to date when creating or updating email values, and will allow the following:

// Now this works
prisma.user.findUnique({
  where: {
    email: '[email protected]'
  }
})

Internally, the where clause will be rewritten to match the emailHash field with the computed hash of the clear-text input (kind of like a password check).

Hashing options

The default hash is a SHA-256 of the input interpreted as UTF-8, with a hexadecimal output encoding (lowercase).

You can change those settings in the annotation, as follows:

/// @encryption:hash(email)?algorithm=sha512 <- anything supported by Node crypto.createHash
/// @encryption:hash(email)?inputEncoding=hex
/// @encryption:hash(email)?outputEncoding=base64

// Combine settings:
/// @encryption:hash(email)?algorithm=sha512&inputEncoding=base64&outputEncoding=base64

You can provide a salt to be appended after the input data, to protect from rainbow table attacks. There are multiple ways to do so, listed by order of precedence:

  1. Specify a salt directly in the Prisma schema:
/// @encryption:hash(email)?salt=0be97e77063ea3f7a0f128b06ef9b1ec
  1. Specify the name of an environment variable where to read the salt:
/// @encryption:hash(email)?saltEnv=EMAIL_HASH_SALT
  1. Use a global salt in the PRISMA_FIELD_ENCRYPTION_HASH_SALT environment variable that will apply to all hash fields.

The salt should be of the same encoding as the associated data to hash.

Migrations

Adding encryption to an existing field is a transparent operation: Prisma will encrypt data on new writes, and decrypt on read when data is encrypted, but your existing data will remain in clear text.

Encrypting existing data should be done in a migration. The package comes with a built-in automatic migration generator, in the form of a Prisma generator:

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

generator fieldEncryptionMigrations {
  provider     = "prisma-field-encryption"
  output       = "./where/you/want/your/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
}

Tip: the migrations generator makes use of the interactiveTransactions preview feature. Make sure it's enabled on your Prisma Client generator only if Prisma Client version is from 3.8.0 to 4.6.1. Otherwise ignore this.

Your migrations directory will contain:

  • One migration per model
  • An index.ts file that runs them all concurrently

All migrations files follow the same API:

export async function migrate(
  client: PrismaClient,
  reportProgress?: ProgressReportCallback
)

The progress report callback is optional, and will log progress to the console if ommitted.

Note: when using an extended client, you'll need to do an explicit cast to call the migrate function, like so:

// Import from your generated client location, not @prisma/client
import { PrismaClient } from '.prisma/client' // or custom path
import { migrate } from './where/you/want/your/migrations'
import { fieldEncryptionExtension } from 'prisma-field-encryption'

const client = new PrismaClient().$extends(fieldEncryptionExtension())

// Explicit cast needed here โ†ด
await migrate(client as PrismaClient)

See issue prisma/prisma#20326.

Following migrations progress

A progress report is an object with the following fields:

  • model: The model name
  • processed: How many records have been processed
  • totalCount: How many records were present at the start of the migration
  • performance: How long it took to update the last record (in ms)

Note: because the totalCount is only computed once, additions or deletions while a migration is running may cause the final processedCount to not equal totalCount.

Custom cursors

Records will be iterated upon by increasing order of a cursor field.

A cursor field has to respect the following constraints:

  • Be @unique
  • Not be encrypted itself

By default, records will try to use the @id field.

Note: Compound @@id primary keys are not supported.

If the @id field does not satisfy cursor constraints, the generator will fallback to the first field that satisfies those constraints.

If you wish to iterate over another field, you can do so by annotating the desired field with @encryption:cursor:

model User {
  id     Int    @id       // Generator would use this by default
  email  String @unique  /// @encryption:cursor <- iterate over this field instead
}

Migrations will look for cursor fields in your models in this order:

  1. Fields explictly annotated with @encryption:cursor
  2. The @id field
  3. The first @unique field

If no cursor is found for a model with encrypted fields, the generator will throw an error when running prisma generate.

Key management

This library is based on @47ng/cloak, which comes with key management built-in. Here are the basic principles:

  • You have one current encryption key
  • You can have many decryption keys for existing data

This allows seamless rotation of the encryption key:

  1. Generate a new encryption key
  2. Add the old one to the decryption keys

The PRISMA_FIELD_DECRYPTION_KEYS can contain a comma-separated list of keys to use for decryption:

PRISMA_FIELD_DECRYPTION_KEYS=key1,key2,key3

Or specify keys programmatically:

fieldEncryptionExtension({
  decryptionKeys: [
    'k1.aesgcm256.DbQoar8ZLuUsOHZNyrnjlskInHDYlzF3q6y1KGM7DUM='
    // Add other keys here. Order does not matter.
  ]
})

Tip: the current encryption key is already part of the decryption keys, no need to add it there.

Key rotation on existing fields (decrypt with old key and re-encrypt with the new one) is done by data migrations.

Custom Prisma client location

Support: introduced in version 1.4.0

If you are generating your Prisma client to a custom location, you'll need to tell the extension where to look for the DMMF (the internal AST generated by Prisma that we use to read those triple-slash comments):

import { Prisma } from '../my/prisma/client'

prismaClient.$extends(
  fieldEncryptionExtension({
    dmmf: Prisma.dmmf
  })
)

Encryption / decryption modes

Support: introduced in version 1.4.0

For each field with an /// @encrypted annotation, you can specify two extra modes of operation:

model User {
  // Default mode behaves as follows:
  // -> data coming into the database is encrypted
  // <- data coming from the database is only decrypted if necessary
  //    (allow existing clear-text data to pass through)
  name String /// @encrypted

  // Strict mode:
  // -> data coming into the database is encrypted
  // <- data coming from the database is decrypted, and throws an error
  //    if decryption fails.
  // This mode can be useful once you've run your data migrations
  // and know that all data should be encrypted, or when you add
  // a new encrypted field to a model.
  ssn String /// @encrypted?mode=strict

  // Readonly mode:
  // -> data coming into the database is NOT encrypted
  // <- data coming from the database is only decrypted if necessary
  // This mode can be use to phase out encryption on a field that no longer
  // requires encryption. Before removing the @encrypted annotation,
  // run a data migration with this mode to decrypt all values for this
  // field in the database.
  noLongerSecret String /// @encrypted?mode=readonly
}

Debugging

Support: introduced in version 1.4.0

The extension uses debug to print internal operations.

Note: it will log keys and clear-text data, so be mindful of your logs destination.

The following namespaces are available:

  • prisma-field-encryption:setup: Setup (encryption/decryption keys & schema analysis)
  • prisma-field-encryption:runtime: Various generic runtime (per-query) info
  • prisma-field-encryption:encryption: Encryption-specific operations (clear-text input, per-field information and encrypted input)
  • prisma-field-encryption:decryption: Decryption-specific operations (raw data from the database, per-field information and decrypted result)
  • prisma-field-encryption:*: Logs everything

Set the DEBUG environment variable to the namespaces you want to log:

# macOS/Unix:
$ DEBUG="prisma-field-encryption:*" npm run my-server-start-script

# Windows:
> set DEBUG=prisma-field-encryption:* & npm run my-server-start-script

Tip: you might want to set the DEBUG_DEPTH variable to control object printout depth.

Caveats & limitations

Field type

You can only encrypt String fields.

PRs are welcome to support more field types, see the following issues for reference:

  • #11 for JSON fields
  • #26 for Bytes fields

orderBy

You cannot order by encrypted fields, even if they use a hash. While using a hash would keep identical records together, the order of said records would not match the expected order.

For this reason, ordering can only be done post-decryption, at runtime, in your application code.

Miscellaneous

Raw database access operations are not supported.

Adding encryption adds overhead, both in storage space and in time to run queries, though its impact hasn't been measured yet.

Middleware interface

Note: Middlewares have been deprecated in Prisma 4.16.0 in favour of the client extensions mechanism described above. For retro-compatibility, we're providing a middleware interface until this this feature is removed altogether from the Prisma client.

import { PrismaClient } from '@prisma/client'
import { fieldEncryptionMiddleware } from 'prisma-field-encryption'

export const client = new PrismaClient()

client.$use(
  // This is a function, don't forget to call it:
  fieldEncryptionMiddleware()
)

Tip: place the middleware as low as you need cleartext data.

Any middleware registered after field encryption will receive encrypted data for the selected fields.

How does this work ?

The extension reads the Prisma AST (DMMF) to find annotations (only triple-slash comments make it there) and build a list of encrypted Model.field pairs.

When a query is received, if there's input data to encrypt (write operations), the relevant fields are encrypted. Then the encrypted data is sent to the database.

Data returned from the database is scanned for encrypted fields, and those are attempted to be decrypted. Errors will be logged and any unencrypted data will be passed through, allowing seamless setup.

The generated data migrations files iterate over models that contain encrypted fields, record by record, using the interactiveTransaction preview feature to ensure that a record is not overwritten by other concurrent updates.

Because of the transparent encryption provided by the extension, iterating over records looks like a no-op (reading then updating with the same data), but this will take care of:

  • Encrypting fields newly /// @encrypted
  • Rotating the encryption key when it changed
  • Decrypting fields where encryption is being disabled with /// @encrypted?mode=readonly. Once that migration has run, you can remove the annotation on those fields.

Do I need this ?

Some data is sensitive, and it's easy to give read access to the database to a contractor or have backups end up somewhere they shouldn't be.

For those cases, encrypting the data per-field can make sense.

An example use-case is Two Factor authentication TOTP secrets: your app needs them to authenticate your users, but nobody else should have access to them.

Cryptography

Cipher used: AES-GCM with 256 bit keys.

Disclaimers

The author cannot be made liable for any misuse of this software, as the MIT license states (the uppercase paragraph at the end).

That being said, a little SecOps common sense goes a long way:

Passwords

๐Ÿšจ DO NOT USE THIS TO ENCRYPT PASSWORDS WITHOUT ADDITIONAL SECURITY MEASURES ๐Ÿšจ

Passwords should be hashed & salted using a slow, constant-time one-way function. However, this library could be used to encrypt the salted and hashed password as a pepper to provide an additional layer of security. It is recommended that the encryption key be stored in a Hardware Security Module on the server.

For hashing passwords, don't reinvent the wheel: use Argon2id if you can, otherwise scrypt.

PCI-DSS

This software is not compliant with PCI-DSS standards. DO NOT use it to encrypt credit card numbers or any other payment method information.

Roadmap

  • Provide multiple decryption keys
  • Add facilities for migrations & key rotation
  • v2 cryptographic design with AEAD - RFC #54

License

MIT - Made with โค๏ธ by Franรงois Best

Using this package at work ? Sponsor me to help with support and maintenance.

prisma-field-encryption's People

Contributors

chenpercy avatar dependabot[bot] avatar franky47 avatar immortalin avatar jrangulod avatar qrtn avatar srosato avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

prisma-field-encryption's Issues

Erroneous `rotation migrations are disabled` warning

Comment

There is an id/unique on the model, it's not just a single field. I can't see how to disable the warning or how to resolve it.

Error Text

[prisma-field-encryption] Warning: could not find a field to use to iterate over rows in model RetailerDistributor.
  Automatic encryption/decryption/key rotation migrations are disabled for this model.
  Read more: https://github.com/47ng/prisma-field-encryption#migrations

Relevant Schema

model RetailerDistributor {
  retailerId     String
  distributorId  String
  isActive       Boolean     @default(true)
  authJsonString String? /// @encrypted
  retailer       Retailer    @relation(fields: [retailerId], references: [id])
  distributor    Distributor @relation(fields: [distributorId], references: [id])
  createdAt      DateTime    @default(now())
  updatedAt      DateTime    @updatedAt

  @@id([retailerId, distributorId])
}

Work on the edge with crypto providers

Prisma has just started letting some users test out a version of Prisma compiled to WASM so that it can run on the edge. I've tested it myself and it looks really promising, yet one problem I have encoutered is that it does not work with prisma-field-encryption yet because of prisma-field-encryption's reliance on Node.js's crypto module which is not available in edge contexts.

My suggestion is to have 'crypto providers' for different contexts, similar to what Stripe does with their Node.js module (see: https://github.com/stripe/stripe-node/tree/master/src/crypto) to support edge contexts. My thinking is that when instantiating prisma-field-encryption, you pass in a crypto provider that works for where you are running Prisma (likely just SublteCrypto and node:crypto are all that is required)

Let me know what you think!

Remove error for `interactiveTransactions`

BG: Prisma gives this warning when we use interactiveTransactions in previewFeatures.

warn Preview feature "interactiveTransactions" is deprecated. The functionality can be used without specifying it as a preview feature.

This feature is available in general now, can we remove the error in this module when not present?

Deep clone input params

To avoid mutating the params object or its children directly when encrypting.

This is because user code may reuse query objects, and we don't want to mutate those.

The result can safely be mutated in place before bubbling up the middleware chain.

For performance reasons, only the subtrees containing mutated data should be cloned (ie: no cloning when no encryption happens).

Candidate libraries:

Benchmarks

  • Small object with no encryption (idle overhead)
  • Small object with some encrypted fields
  • Small object with deeply nested encrypted fields
  • Large collection (eg: createMany) without encrypted fields
  • Large collection with encrypted fields
  • Large deeply nested objects with encrypted fields

Discussion on prisma/prisma

Filtering secured field with mode-insensitive

Hi, thanks for your effort to provide useful library, i like it.
I can see that filtering is not supporting for contains, startWith, endWith.
But is there anyway to make it working for mode-insensitive?

I am using equals with insensitive mode, but it is also not working, so i'd love to hear your suggestion for it.

Regard

Support for Json fields?

I see that it only supports String fields. Any reason why it doesn't support Json fields for example? I'm a total newbie when it comes to encryption ๐Ÿ˜…

feat request: Allow custom client path

I'm currently using two different database clients that live in a different directory from @prisma/lcient as a workaround to support multiple databases as mentioned here:

prisma/prisma#2443 (comment)

I'd like to be able to do something like this

    const prismacli1 = new PrismaClient1();
    const prismacli2 = new PrismaClient2();

    prismacli1.$use(fieldEncryptionMiddleware());
    prismacli2.$use(fieldEncryptionMiddleware());

But is looks like the middleware explicitly injects into @prisma/client

Error: Cannot find module '@prisma/client'
Require stack:
- /Users/jangu/Development/bitrebel/bitrebel.io/api/node_modules/prisma-field-encryption/dist/dmmf.js
- /Users/jangu/Development/bitrebel/bitrebel.io/api/node_modules/prisma-field-encryption/dist/index.js
- /Users/jangu/Development/bitrebel/bitrebel.io/api/src/plugins/brapp.ts
- /Users/jangu/Development/bitrebel/bitrebel.io/api/src/index.ts
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/jangu/Development/bitrebel/bitrebel.io/api/node_modules/prisma-field-encryption/dist/dmmf.js:4:18)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Module._compile (/Users/jangu/Development/bitrebel/bitrebel.io/api/node_modules/source-map-support/source-map-support.js:568:25)
    at Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
    at Object.nodeDevHook [as .js] (/Users/jangu/Development/bitrebel/bitrebel.io/api/node_modules/ts-node-dev/lib/hook.js:63:13)
    at Module.load (node:internal/modules/cjs/loader:981:32)

It would be nice to be able to pass in an option to specify the location of my prisma clients ๐Ÿ‘

Use Case: PII Personal Identifiable Information - Right to be Forgotten - Prisma Encryption

Hi, thank you for creating this library! I would like to provide GDPR / โ€žright to be forgottenโ€œ assurance while keeping schema flexibility. One way for that is pseudomization where each user has an assigned description key; when that is deleted, all data of that user is useless but doesnโ€™t require propagating deletes.

I was wondering how you would handle that architecture and if prisma-field-encryption would be a valid solution approach. Naturally Iโ€˜d try to push this further down to DB level (i.e. the with the PostgreSOL Anonymizer) but that seems to take flexibility out schema management (requiring raw queries?).

Iโ€˜m in the concept phase, everything should be azure and end-to-end typescript. Regarding stack, prisma & co still investigating.

Thank you very much!

PS: A data science team needs to also be able to access encrypted information without going through the GraphQL API. So being able to decrypt data outside of prisma-field-encryption is useful and seems to be already on the map when reading this #12 (comment)

Can't connect to an encrypted field "needs exactly one argument"

I have a Person table and a Password table. The email field in Person is encrypted. When inserting record on another table and trying to connect it to an encrypted field, I get an error:
Argument data.createdBy.connect of type PersonWhereUniqueInput needs exactly one argument, but you provided email and emailHash. Please choose one.

Prisma schema:

model Person {
[...]
    email       String  @unique /// @encrypted
    emailHash   String? @unique /// @encryption:hash(email)?algorithm=sha384&outputEncoding=base64
}

model Password {
[...]
createdBy   Person @relation("Password", fields: [createdById], references: [rowId])
createdById String

Query (first transaction):

const create = await tx.password.create({
					data: {
						password: input.content,
						createdBy: {
							connect: {
								email: "[email protected]"
							}
						},

					},

				})

Am I doing this right or is there a workaround without having to provide the row Id?

Thanks

`findUnique` isn't working

Here's my model

model ApiKey {
  id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid

  name      String  @map("name")
  token     String  @unique @map("token") /// @encrypted
  tokenHash String? @unique @map("token_hash") @db.Text /// @encryption:hash(token)

  userID String @map("user_id") @db.Uuid
  user   User   @relation("UserApiKeys", fields: [userID], references: [id], onDelete: Cascade, onUpdate: NoAction)

  workspaceID String    @map("workspace_id") @db.Uuid
  workspace   Workspace @relation("WorkspaceApiKeys", fields: [workspaceID], references: [id], onDelete: Cascade, onUpdate: NoAction)

  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
  updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(6)

  @@map("api_key")
}

It seems to be correctly encrypting the keys e.g. if I look at the database directly I see

  • token = "v1.aesgcm256.b52f8efd.uujweP0t9Bi6kEur.lk9R-BR2_o..."
  • token_hash = "1307434863b6c28b94c625628664d5bade226b00325d684d810507c999bfeaea"

And then when I do prisma.apiKey.findMany() the values are correct

I turned on the debugger using DEBUG="prisma-field-encryption:*" yarn dev and when I search for the API key by token e.g.

2023-06-10T04:00:26.194Z prisma-field-encryption:encryption Swapping encrypted search of ApiKey.token with hash search under tokenHash (hash: 6dee1f26153dcfe0d645244f8bdd4ae0d3f43b23c0d877bee5053ecd4bd7a862)
2023-06-10T04:00:26.195Z prisma-field-encryption:encryption Encrypted input: {
  args: {
    where: {
      tokenHash: '6dee1f26153dcfe0d645244f8bdd4ae0d3f43b23c0d877bee5053ecd4bd7a862'
    }
  },
  dataPath: [],
  runInTransaction: false,
  action: 'findMany',
  model: 'ApiKey'
}
2023-06-10T04:00:26.331Z prisma-field-encryption:decryption Raw result from database: []
2023-06-10T04:00:26.331Z prisma-field-encryption:decryption Decrypted result: []

and you'll notice that the tokenHash is different which is why the return value is empty but I'm not sure why the hash is different

Within my .env I've got just PRISMA_FIELD_ENCRYPTION_KEY set and then I just do this within clients/prisma.ts

import { PrismaClient } from "@prisma/client";
import { fieldEncryptionMiddleware } from "prisma-field-encryption";

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

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

const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: ["error"],
  });

prisma.$use(fieldEncryptionMiddleware());

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

export default prisma;

Any idea where I might be going wrong? I'm pretty close and imagine it's just one setting or variable I didn't set correctly

Filter with "contains"

Hello, thank you for an amazing library. How can I enable "contains" filtering, if it's possible?

Encrypted strings are becoming more and more extensive with Next.js hot reload

Problem:

I started using this library a few days ago with the goal to encrypt chat messages. The max length of a message is 1000 characters. I used this tool to calculate the max cypher length of 1395, wich I used to define the size of the VarChar column.

Fist it started to work as expected and the cipher texts are looking ok. F.e. "test" has a cypher text length of 67.

After using it for a few hour in development Im starting to see that the cypher texts are growing exponentially.
To give an example:
same word: "test" becomes a cypher of 1227 characters.
Longer words are failing because of the size limit of the varchar column.

Is it possible that the middleware of this library runs encryption multiple times after multiple hot-reloads?

Quick fix

Restarting the project solves this issue BUT the encrypted messages with these huge cyphers are not getting decrypted anymore.

Setup:

Prisma Model:

model Message {
    id          String   @id @default(cuid())
    createdAt   DateTime @default(now())
    updatedAt   DateTime @updatedAt
    text        String   @db.VarChar(1395) /// @encrypted
    createdBy   User     @relation(fields: [createdById], references: [id])
    createdById String
    isRead      Boolean  @default(false)

    @@index([createdById])
}

Spec:

  • Nextjs 13.2.4
  • Prisma 4.11.0
  • prisma-field-encryption 1.4.1
  • Turbo Repo:
    • apps/web (Nextjs)
    • packages/db (Prisma)

Issue related to decrypt

The library is used to encrypt data, but the basic structure is in the form of "v1.aesgcm256.~~~~~.~~~~~~~". However, if the content is decoded using online decrypt in the "AES-256-GCM" mode, the content cannot be confirmed normally.

We are concerned about problems that may arise in the future. For example, although the library has been developed and later converted, the encryption method has been changed, but the encryption of existing encrypted data cannot be broken.

First of all, if the current encryption is normal, the data will come out in a structure called "v1.aesgcm256.~~~~~.~~~~~~~", is that correct?

Support custom path of PrismaClient

Is there a possibility to add support for custom path of prisma client?

I am using prisma client like that: import { PrismaClient } from './prisma/dir/generated/generalClient';

because of multiple connections.
Is there any possibility to add new configuration property like:

prisma.$use(fieldEncryptionMiddleware(
prismaClient: mainPrismaClient
))

because @prisma/client is not that much flexible

Detect invalid custom cursor fields

When specifying a custom cursor field for the migrations to iterate upon, make sure (and throw error if invalid) that the field is:

  • @unique
  • of type Int or String
  • not @encrypted itself

Configuration option to use web-crypto instead of node crypto for Cloudflare Workers

Hi

First of all, thanks for this amazing library. I'm using it in conjunction with SvelteKit, Prisma (hooked up to a neon db) and lucia-auth along side an upstash redis instance. The whole thing should be deployed to Cloudflare Workers at the end of the day.

However, using the SvelteKit Cloudlfare Adapter Plugin uses the Cloudflare version of Node, which is much closer to browsers than node, the build fails because they don't have the crypto package. Instead we'd have to use the web-crypto API.

I could spend some time on this and code it up, but my relationship with security is the same as it is with electricity.

I know enough about it that I don't know enough about it.

UnhandledSchemeError: Reading from "node:crypto" is not handled by plugins (Unhandled scheme).

Hi @franky47 - I tried prisma-field-encryption and it worked great for

  • prisma.table.create
  • prisma.table.findUnique

But when I tried prisma.table.update, I got a build error:

error - node:crypto
Module build failed: UnhandledSchemeError: Reading from "node:crypto" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
Import trace for requested module:
node:crypto
./node_modules/prisma-field-encryption/dist/hash.js
./node_modules/prisma-field-encryption/dist/encryption.js
./node_modules/prisma-field-encryption/dist/index.js
./lib/prisma.ts

I'm using Next 13.3.0, Prisma 4.12.0, prisma-field-encryption 1.4.1

Hash annotations not doing anything when creating records

I've taken the schema straight out of the Readme:

model User {
  id String    @id(map: "user_pk") @unique(map: "user_uid_uindex") @db.Uuid
  email     String @unique /// @encrypted
  emailHash String @unique /// @encryption:hash(email) <- the name of the source field
}

Firstly, when creating a User, if I don't supply a value for the emailHash field it gives a TS error and doesn't compile as emailHash is a required property.

// Error:  Property 'emailHash' is missing
prisma.user.create({
    data: {
      id: 'b0429ab9-1299-474c-b7ba-b7f3176f0ff9',
      email: '1234567',
    //   emailHash: '13',
    },
  });

Secondly, if I do give a value to emailHash, that value gets written to the DB as-is. I.e. if I uncomment the emailHash line the row is created with value 13 in the emailHash column. I've tried making the emailHash column nullable but this didn't change anything.

What's the proper way to use this functionality?

Doesn't work with the Prisma fluent API

Describe the Bug
I have trouble in using prisma-field-encryption middleware when using prisma fluent API. Not decrypted data are returned when I request data through prisma fluent API. To avoid N+1 problem, I want to use fluent API which can use prisma data loader.

To Reproduce
Add prisma-field-encryption middleware. Make schema which has parent-children relation model and @Encrypted field in children model. (see my code below)

Expected Behavior
I expect data which are requested through prisma fluent API to be decrypted.

Environment:

OS: ubuntu 22.04.1
Node 20.2.0
Prisma version 5.2.0
prisma-field-encryption 1.5.0
TypeScript version 5.2.2
Express 4.18.2

Additional Context

This is my github repository

Prisma schema

// schema.prisma
// 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"
}

datasource db {
  provider = "postgresql"
  url      = "postgresql://postgres:postgres@db:5432/adgame?schema=public"
}

model User {
  id    Int     @id @default(autoincrement())
  email String
  name  String? /// @encrypted
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String? /// @encrypted
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

Express Server

// server.ts

import express from "express";
import { PrismaClient } from "@prisma/client";
import { fieldEncryptionExtension } from "prisma-field-encryption";

const globalClient = new PrismaClient();

const client = globalClient.$extends(
  fieldEncryptionExtension({
    encryptionKey: "k1.aesgcm256.DbQoar8ZLuUsOHZNyrnjlskInHDYlzF3q6y1KGM7DUM=",
  })
);

const app = express();

const server = app.listen(3000, function () {
  console.log("start server");
});

// data are decrypted when using normal query
app.get("/userposts1", async (req, res, next) => {
  const user = await client.user.findFirst();
  const authorId = user?.id || 1;
  const posts = await client.post.findMany({
    where: {
      authorId: authorId,
    },
  });
  console.log(posts);
  res.send(posts);
});

// data are not decrypted when using fluent API
app.get("/userposts2", async (req, res, next) => {
  const posts = await client.user.findFirst().posts();
  console.log(posts);
  res.send(posts);
});

[Question] DES Support

First of all sorry if this isn't the way to contact you, but couldn't find any other way.

I'm looking for guidance on how i can add DES support instead of AES, the main reason is because I want to use this library for my university project which wants us to only use DES, I would appreciate any help.

Thank you for the awesome library definitely looking forward to using it in the future :)

prisma.$use() deprecated

Since 4.16.0, released today, prisma.$use() is deprecated so we should change the way we add the middleware.

Plaintext values with mode=strict do not throw errors

Given this schema:

model EncryptionTest {
  id                 String                 @id
  data               String? /// @encrypted?mode=strict
}

and this test:

    await prisma.$queryRawUnsafe(
      `INSERT INTO encryption_test (id, data) VALUES ('id', 'plaintextdata')`,
    );

    const encryptionTest = await prisma.encryptionTest.findFirst({
      where: { id: 'id' },
    });

I would expect this test case to throw an exception. Is my understanding incorrect?

It's a simple fix if indeed it's unexpected behaviour. https://github.com/47ng/prisma-field-encryption/blob/next/src/encryption.ts#L179 is currently

        if (!cloakedStringRegex.test(cipherText)) {
          return
        }

so if a field is in the database as plaintext (which doesn't match the cloakedStringRegex) then strict mode is not taken into account. Proposed fix:

        if (!cloakedStringRegex.test(cipherText)) {
          if (fieldConfig.strictDecryption) {
            throw new Error('Value is not encrypted and mode=strict')
          }
          return
        }

Prisma where clause 'startWith, endWith and contains' are not working.

Environment

Node: Fastify
DB: MySql
ORM: Prisma

Prisma schema:

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

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique /// @encrypted
  emailHash     String?    @unique /// @encryption:hash(email)
  name      String?
  password  String
  salt      String
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  expiredAt DateTime  @updatedAt
  products  Product[]
}

Prisma Client

import { Prisma, PrismaClient } from "@prisma/client";
import { fieldEncryptionMiddleware } from "prisma-field-encryption";

const prisma = new PrismaClient();
prisma.$use(
  fieldEncryptionMiddleware({
    dmmf: Prisma.dmmf
  })
)
export default prisma;

Service for making Prisma request

import prisma from "../../utils/prisma";
export async function findUserContains(str: string) {
  return prisma.user.findMany({
    where:{
      email: {
        contains: str,
      }
    },
    select: {
      email: true,
      name: true,
      id: true,
      createdAt: true,
      expiredAt: true
    },
  });
}

this return [] even if the string that pass is contained

github link: https://github.com/codewithmecoder/fastify-test

Do we need both `CLOAK_KEYCHAIN` and `CLOAK_MASTER_KEY` or one of them

I'm integrating this extension and it's working great so far! It's simple to set up and it works transparently, I love it! ๐Ÿ™Œ

I'm curious if we need to provide both CLOACK_KEYCHAIN and CLOAK_MASTER_KEY to our environment when deploying the API.

When using the extension, I see only CLOAK_MASTER_KEY is documented, what to do with CLOAK_KEYCHAIN?

export const prisma = new PrismaClient().$extends(
  fieldEncryptionExtension({
    encryptionKey: process.env['CLOAK_MASTER_KEY'],
  }),
)

Some clarity here would be highly appreciated.

Disable warning: could not find a field to use to iterate over rows in model ___

Hello!

I'm using this plugin in on a model where I unique with @@unique([created_at, cf_ray, request_num]). This table is in the database is actually partitioned on created_at /daily using timescaledb so I cannot have another column as PK, unless I lie in the schema and change in the production model

So, I would appreciate if I have a way to disable the warning, or a way to implement a workarround

  console.warn
    [prisma-field-encryption] Warning: could not find a field to use to iterate over rows in model api_request_log.
      Automatic encryption/decryption/key rotation migrations are disabled for this model.
      Read more: https://github.com/47ng/prisma-field-encryption#migrations

I've tried using the @encryption:cursor, but it as well only has the same limitation as with the @id field, so it do not accept compound fields

schema, if need, to reproduce the log message:

model api_request_log {
    created_at  DateTime @default(now()) @db.Timestamp(6)
    cf_ray      String
    request_num Int

    ip                  String  @db.Inet
    response_time       Int
    response_size       Int
    req_method          String
    req_path            String
    req_host            String
    req_headers         String /// @encrypted?mode=strict
    req_query           String /// @encrypted?mode=strict
    req_body            String /// @encrypted?mode=strict
    req_body_size       Int?
    res_code            Int     @db.SmallInt
    created_customer_id Int?

    @@unique([created_at, cf_ray, request_num])
}

Getting ciphertext back from the database

This issue somehow got turned into a thread about ciphertext not being decrypted and returned as-is from the database.

Such issues usually indicate:

  1. That the setup is correct (encryption works, which means fields are correctly interpreted, and the encryption key is supplied correctly)
  2. That there is something wrong with the decryption process.

Decryption will throw an error when in strict mode (using /// @encrypted?mode=strict), but will log a warning to the console in other modes. This could be the sign that you are missing the right key to decrypt data.

If there are no errors, it usually means that the ciphertext has been corrupted somehow, and failed to be detected by the middleware (so is passed through as any other data). Make sure your field maximum length is high enough to contain the largest expected ciphertext, which is larger than the clear-text. Calculator available here.


Original issue content:

Adding a global strict decryption mode that throws errors when decryption fails (missing key) should help avoid building layers of encryption in migrations.

Use-case:

  1. The decryption key is missing, encrypted data is returned in ciphertext (A)
  2. The data migration re-encrypts ciphertext A with encryption key, producing ciphertext B
  3. Decryption of re-encrypted records yield ciphertext A

Fields that are selected through include are returned as cyphertext

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])
}

'cursor' conversion is wrong

The situation is the following:

The prisma schema:

model Visitor {
  id             String            @id @default(uuid())
  key            String            @unique /// @encrypted
  keyHash        String?           @unique /// @encryption:hash(key)
  created_at     DateTime          @default(now())

  @@map("visitors")
}

I would like to do a findMany with a cursor. So I will do the following:

 this.prisma.visitor.findMany({
           take: 5,
           skip: 1,
           cursor: {
             key: 'xxxxx-key-xxxxx',
           },
         })

But this gets converted to:

 this.prisma.visitor.findMany({
           take: 5,
           skip: 1,
           cursor: {
             key: 'v1.aesgcm256.xxxxxxx.8xxxxxxxxxxxxxxxxxxWhfw==',
             keyHash: 'xxxxxxxxx-hash-xxxxxxxxxxxx'
           },
         })

Which results in a Prisma error:
Argument cursor of type VisitorWhereUniqueInput needs exactly one argument, but you provided key and keyHash. Please choose one.

I would expect it to convert to:

 this.prisma.visitor.findMany({
           take: 5,
           skip: 1,
           cursor: {
             keyHash: 'xxxxxxxxx-hash-xxxxxxxxxxxx',
           },
         })

Please let me know if you need more information!

Decrypt to view the database in prisma studio

Hello,

Is there a possibility to decrypt the data to view it decrypted in prisma studio ? (prisma studio is the browser app accessible with npx prisma studio)

Thank you for your answer

Feature request: HMAC Support for Hashing

Upon reviewing the hashing portion of the current code, I noticed that it does not include support for HMAC.

const hash = crypto.createHash(config.algorithm)

To fortify the security capabilities of the library, I propose the addition of an option to use HMAC.
(Perhaps use: crypto.createHmac)

Benefits:

  1. Implementing HMAC support enhances security. HMAC is notably known for its strengths in terms of security for data integrity and authentication.
  2. It increases versatility for users in terms of their choice of hashing methods.

If there's a demand for this feature, I would be more than happy to create a pull request to implement it.

Please share your thoughts and feedback regarding this proposal.

Breaks with 5.9.1 release of Prisma

Here is the error that gets thrown

Type 'BaseDMMF' is not assignable to type '{ datamodel: { models: { name: string; fields: { name: string; isList: boolean; isUnique: boolean; isId: boolean; type?: any; documentation?: string | undefined; }[]; }[]; }; }'.
  The types of 'datamodel.models' are incompatible between these types.
    The type 'readonly ReadonlyDeep_2<ReadonlyDeep_2<ReadonlyDeep_2<{ name: string; dbName: string | null; fields: ReadonlyDeep_2<{ kind: FieldKind; name: string; isRequired: boolean; isList: boolean; isUnique: boolean; isId: boolean; isReadOnly: boolean; ... 10 more ...; documentation?: string | undefined; }>[]; ... 4 more ...; ...' is 'readonly' and cannot be assigned to the mutable type '{ name: string; fields: { name: string; isList: boolean; isUnique: boolean; isId: boolean; type?: any; documentation?: string | undefined; }[]; }[]'.

It is related to this step in the setup: https://www.npmjs.com/package/prisma-field-encryption#custom-prisma-client-location

Warn on `where` clause used on encrypted field

The middleware should log a warning when trying to run a query with a where clause on an encrypted field.

Developers might have an idea that something is up when the query returns nothing, but having good and helpful warnings helps with DX.

[RFC] v2 cryptographic overview

Overview

v1.x used a naive AES-GCM cipher with a single key and random nonces, which is not scalable. Another issue was a vulnerability to confused deputy attacks (CDA).

v2 aims to improve the cryptographic layer using the following properties:

  • AEAD construction where AAD is set to the path of the field (model name + field name)
  • Key derivation to increase the cipher input entropy (pseudorandom key + random IV)

Caveats

Note that the following operations will still not be supported on encrypted fields in v2, and are not planned to be:

  • Partial matching (startsWith, endsWith, contains etc..)
  • Ordering

AEAD

In order to strongly bind a ciphertext to its storage location - to defend against CDA and field value swapping - the path where a record is stored should be part of the additional authenticated data (AAD).

This path is made of three dimensions:

  • The table
  • The column
  • The row

While it is fairly easy to pin the column (by setting the table and column name in AAD), pinning the row is more challenging. Usually, pinning the row is done by setting the row ID as AAD. However, this does not work in cases where the row ID is not available.

When encrypting a new record, the row ID may be omitted to be automatically generated by the database engine (eg: autoincremental integer and UUIDs primary keys).

When decrypting a record, the ID may be absent from either the query or the returned data.

Options here are:

  1. Not including any row pinning, and only pin table & column. Pros: simple to implement. Cons: no practical defense against CDA.
  2. Have 1 as default for auto-incrementing (database-generated) IDs, but allow runtime-generated IDs (eg: UUIDs, CUIDs) by parsing the Prisma schema. Pros: allows defense against CDA. Cons: cannot use autoincremental IDs, may be difficult to implement, especially regarding connections & relations. Won't work when using queries that don't supply the ID (where clause on other @unique fields).
  3. Perform operations in multiple steps. Eg: write an empty record to the database to obtain a row and its ID, then encrypt fields with full AAD pinning, and write the ciphertexts, all in a transaction. Pros: can use autogenerated IDs. Cons: performance cost, increased risks of conflicts leaving data in an inconsistent state, possible data race conditions.

Rejected ideas:

  • Having a separate column managed internally by the middleware to serve as an AAD row reference. Rejected as it would be trivial for an attacker to swap these references along with ciphertext and still cause a valid decryption across rows.

Note: model and column renaming may also cause AAD mismatches, such cases should be covered by data migrations.

Composite IDs (using @@id) could be supported, with extra care about canonicalisation attacks. For example, with a naive string concatenation, those two rows would have the same AAD data:

model User {
  firstName String
  lastName  String
  @@id([firstName, lastName])
}
firstName lastName Resulting AAD
John Doe UserJohnDoe
Joh nDoe UserJohnDoe

Algorithm selection

The use of AES-GCM with 256 bit keys will be maintained, not for retrocompatibility (there won't be any due to the additional use of AAD), but because it's a common cipher available in most implementations. A non-NIST alternative native to Node.js would be ChaCha20-Poly1305, which conveniently has the same nonce and auth tag sizes as AES GCM.

Key derivation

Rather than specifying a single encryption key in the middleware configuration and use random nonces, a root secret will be used to derive individual keys and nonces, using HKDF-SHA-256.

This still assumes a cryptographically strong root secret.

Key derivation takes care of reducing the reuse of keys across a database. While a per-field derivation would be possible, it may be costly and redundant with the use of AAD, so a per-row derivation may be preferable. Since column count is usually in low numbers, the use of random nonces would be acceptable.

Rotation

Rotation of the root derivation secret will be planned just as multiple keys were supported in V1. Ciphertext rotations will require the same data migration technique.

Ciphertext format

todo: Document v2 ciphertext format

v1 to v2 migration

While both versions may require different configurations (root secrets/keys differences), it would be recommended to provide a data migration strategy, to ease with adoption.

That being said, the migration workflow may require several deployment phases (expand & contract pattern, akin to database migrations) if a zero-downtime upgrade is desired.

Therefore, v2 will ship with a read-only compatibility layer for v1, which will be removed altogether in a later update.

Resources

https://soatok.blog/2023/03/01/database-cryptography-fur-the-rest-of-us/
https://scottarc.blog/2022/10/17/lucid-multi-key-deputies-require-commitment/
https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html

'orderBy' conversion is wrong in case I also include a non encrypted value

The situation is the following:

The prisma schema:

model Visitor {
  id             String            @id @default(uuid())
  key            String            @unique /// @encrypted
  keyHash        String?           @unique /// @encryption:hash(key)
  created_at     DateTime          @default(now())

  @@map("visitors")
}

I would like to do a findMany with a orderBy on the key and on the created_at.

I understand it is quite useless to sort on a encrypted value and I kinda understand the desision made in issue #43. If I only sort on a encrypted field I get the silent error: Error: Running orderBy on encrypted field Visitor.key is not supported (results won't be sorted).

But I will do the following:

this.prisma.visitor.findMany({
           take: 5,
           skip: 0,
           cursor: undefined,
           orderBy: [
             {
               key: 'asc',
             },
             {
               created_at: 'asc'
             }
           ],
         })

which does not result in a silent error but in the following:

this.prisma.visitor.findMany({
           take: 5,
           skip: 0,
           cursor: undefined,
           orderBy: [
             {
               key: 'v1.aesgcm256.xxxxxxx.8xxxxxxxxxxxxxxxxxxWhfw==',
             keyHash: 'xxxxxxxxx-hash-xxxxxxxxxxxx'
             },
             {
               created_at: 'asc'
             }
           ] 
         })

Which results in a Primsa error Argument orderBy: Got invalid value since you should only provide 1 property per object.

I would expect it to convert to:

this.prisma.visitor.findMany({
           take: 5,
           skip: 0,
           cursor: undefined,
           orderBy: [
             {
             keyHash: 'asc'
             },
             {
               created_at: 'asc'
             }
           ] 
         })

or in case you want to prevent sorting on encrypted fields

this.prisma.visitor.findMany({
           take: 5,
           skip: 0,
           cursor: undefined,
           orderBy: [
             {
               created_at: 'asc'
             }
           ] 
         })

Please let me know if you need more information!

[question]: would it technically be possible to do per user encryption using `prisma-field-encryption`?

Perhaps a stupid question, but rather a stupid question than a stupid implementation without popping the question first ๐Ÿ˜…

How I understand it by looking at docs and a very quick peek in the implementation, it looks like running the fieldEncryptionExtension function sets the encryption key as some sort of middleware for the query to be called with. This is for application-wide encryption if I understand correctly. So if I as application owner have the key, I can decrypt all data for every user, correct?

Would it then technically be possible to call the fieldEncryptionExtension everytime a user inserts / reads his data to make sure that that data is encrypted with the key that is e.g. hashed and stored in his JWT token, hence only the user who performs the query with that key has access to it's data and not the application owner? On first sight it looks like it just configures some prisma middleware and not some db-level setting, right? Even if it's technically possible in a web environment (concurrent api requests for example), is it even advisable to do it like this or things to keep in mind if it is?

My inspiration comes from https://marcopeg.com/per-user-encryption-with-postgres/ where he uses an application key on top of a user's key, to assure that even the application owner can't read sensitive data. I'm just not sure if it's possible to do with this package or not and how I would go about it otherwise.

Thanks!

`connectOrCreate` query seems to be encrypted twice?

I'm adding this to an already working app.

If I have a booking model with a relation to a user model and I do something like

const newBookingAndUser = await prisma.booking.create({
  data: {
    ...bookingData,
    users: {
      connectOrCreate: [{
        where: {
          email: '[email protected]'
        },
        create: {
          name: 'Some name',
          email: '[email protected]'
        }
      }]
    }
  },
  include: {
    users: true
  }
});

const booking = await prisma.booking.findUnique({
  where: { id: newBookingAndUser.id },
  include: { users: true}
});

// OR

const user = await prisma.user.findUnique({ where: { id: newBookingAndUser.users[0].id } })

In this example only the name field on the user should be encrypted.
The encrypted fields in the user object appear to return the encrypted string, but when I compare the output to what is stored in the database it's not the same value, suggesting the value in the db is being decrypted, but into a previously encrypted string.

[mongo]: Hashes are not computed

I created a brand new little nodejs project and installed prisma + this package.

After creating a document in my MongoDB Atlas (normal mongodb+srv connection), Hashes are not computed...

Bildschirmfoto 2023-08-28 um 01 11 23

is this a bug?

Because as I know: Mongo is not saving nullable fields into the db. But I don't know why prisma is not computing the hashes...

'asc' and 'desc' are encrypted when using orderBy on String values

prisma.user.findMany({
        where: {
            archived: false
        },
        take: 25,
        skip: 0,
        orderBy: {
           lastName: 'asc'
        }
    })

lastName is not encrypted in the Prisma model.

'asc' becomes encrypted when passed into Prisma

Argument lastName: Provided value '*encryptedvalue*' of type String on prisma.findManyUser is not a SortOrder.

Using SortOrders on other types works as expected.

"not" filter not works in hash fields

This is my prisma schema, and for each patient, they have 0 - N cases.

model Patient {
  uid             String           @id
  cases           Case[]
  ...

  @@map("patient")
}

model Case {
  cid                   BigInt                 @id @default(autoincrement())
  caseNumber            String                 @default("") @map("case_number") /// @encrypted
  caseNumberHash        String?                @map("case_number_hash") /// @encryption:hash(caseNumber)
  patient               Patient?               @relation(fields: [patientId], references: [uid])
  patientId             String?                @map("patient_id")
  ...

  @@map("case")
}

If I query entries with empty caseNumber, it works correctly. However, when I query entries with non-empty caseNumber, it does not work.

# exclude empty caseNumber in table case
db.case.findMany({
  where: { caseNumber: { not: '' } },
});

I still get empty chartNumber in the response

[
  {
    "cid": 47,
    "caseNumber": "",
    "patientId": "ce0d1b78-8858-4122-8c9c-d262927b5480",
    ...
  },
  ...
]

I try to trace code, and finally find the issue would be here. When query parameter is object type, function makeVisitor uses specialSubFields ['equals', 'set'] to handle it, which does not contain not operator.

const makeVisitor = (
  models: DMMFModels,
  visitor: TargetFieldVisitorFn,
  specialSubFields: string[],
  debug: Debugger
) =>
  function visitNode(state: VisitorState, { key, type, node, path }: Item) {
    const model = models[state.currentModel]
    if (!model || !key) {
      return state
    }
    if (type === 'string' && key in model.fields) {
     ...
    }
    // Special cases: {field}.set for updates, {field}.equals for queries
    for (const specialSubField of specialSubFields) {
      if (
        type === 'object' &&
        key in model.fields &&
        typeof (node as any)?.[specialSubField] === 'string'
      ) {
        const value: string = (node as any)[specialSubField]
        const targetField: TargetField = {
          field: key,
          model: state.currentModel,
          fieldConfig: model.fields[key],
          path: [...path, specialSubField].join('.'),
          value
        }
        debug('Visiting %O', targetField)
        visitor(targetField)
        return state
      }
    }
    ...
    return state
  }

export function visitInputTargetFields<
  Models extends string,
  Actions extends string
>(
  params: MiddlewareParams<Models, Actions>,
  models: DMMFModels,
  visitor: TargetFieldVisitorFn
) {
  traverseTree(
    params.args,
    makeVisitor(models, visitor, ['equals', 'set'], debug.encryption), // <---- here
    {
      currentModel: params.model!
    }
  )
}

Solve

Should add other filters like not into specialSubField?

NestJS PrismaService and Extending it With Encryption

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.

'where' conversion is wrong for OR/AND/NOT operators

The situation is the following:

The prisma schema:

model Visitor {
  id             String            @id @default(uuid())
  name           String? /// @encrypted
  nameHash       String? /// @encryption:hash(name)
  identifier     String /// @encrypted
  identifierHash String? /// @encryption:hash(identifier)

  @@map("visitors")
}

I would like to search for multiple visitors with a specific name or identifier. So I will do the following:

  this.prisma.visitor.findMany({
        where: {
          OR: [
            {
              identifier: '[email protected]'
            },
            {
              name: 'test'
            }
          ]
        }
      })

This results in the following SQL query;

SELECT "public"."visitors"."id", "public"."visitors"."name", "public"."visitors"."nameHash", "public"."visitors"."identifier", "public"."visitors"."identifierHash" FROM "public"."visitors" WHERE (("public"."visitors"."identifier" = "v1.aesgcm256.1234567.xxxxxxxxxxxxxxxxxx" AND "public"."visitors"."identifierHash" = "xxxxxxxxxxxx-hash-xxxxxx") OR ("public"."visitors"."name" = "v1.aesgcm256.1234567.xxxxxxxxxxxxxxxxxx" AND "public"."visitors"."nameHash" = "xxxxxxxxxxxx-hash-xxxxxx")) OFFSET 0

Which won't work since it is searching for the encrypted value together with the hash value.
I would expect the query to be like:

SELECT "public"."visitors"."id", "public"."visitors"."name", "public"."visitors"."nameHash", "public"."visitors"."identifier", "public"."visitors"."identifierHash" FROM "public"."visitors" WHERE ("public"."visitors"."identifierHash" = "xxxxxxxxxxxx-hash-xxxxxx" OR "public"."visitors"."nameHash" = "xxxxxxxxxxxx-hash-xxxxxx") OFFSET 0

So only searching on the hash values.

The same seems to occur when I use AND and NOT operators in my where query.

Please let me know if you need more information!

Pepper password disclaimer

The disclaimer should be amended to state that this library can be used to pepper a hashed+salted password field, when used in conjunction with a HSM.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.