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

feat: Add support for filtering encrypted field by list of values #120

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions src/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function encryptOnWrite<Models extends string, Actions extends string>(
models,
function encryptFieldValue({
fieldConfig,
value: clearText,
value: unHashedValue,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Keep the original clearText name, as the value is not only used for hashing, but also for encryption in most (writing) cases.

path,
model,
field
Expand All @@ -81,7 +81,17 @@ export function encryptOnWrite<Models extends string, Actions extends string>(
if (!fieldConfig.hash) {
console.warn(warnings.whereConnectClauseNoHash(operation, path))
} else {
const hash = hashString(clearText, fieldConfig.hash)
const fieldConfigHash = fieldConfig.hash

let hash: string | string[]
// If unHashedValue is a list, hash each value
if (Array.isArray(unHashedValue)) {
hash = unHashedValue.map(value =>
hashString(value, fieldConfigHash)
)
} else {
hash = hashString(unHashedValue, fieldConfigHash)
}
debug.encryption(
`Swapping encrypted search of ${model}.${field} with hash search under ${fieldConfig.hash.targetField} (hash: ${hash})`
)
Expand All @@ -90,12 +100,18 @@ export function encryptOnWrite<Models extends string, Actions extends string>(
return
}
}
if (isOrderBy(path, field, clearText)) {

// Encrypting list values is not yet supported
if (typeof unHashedValue !== 'string') {
return
}

if (isOrderBy(path, field, unHashedValue)) {
// Remove unsupported orderBy clause on encrypted text
// (makes no sense to sort ciphertext nor to encrypt 'asc' | 'desc')
console.error(errors.orderByUnsupported(model, field))
debug.encryption(
`Removing orderBy clause on ${model}.${field} at path \`${path}: ${clearText}\``
`Removing orderBy clause on ${model}.${field} at path \`${path}: ${unHashedValue}\``
)
objectPath.del(draft.args, path)
return
Expand All @@ -104,11 +120,14 @@ export function encryptOnWrite<Models extends string, Actions extends string>(
return
}
try {
const cipherText = encryptStringSync(clearText, keys.encryptionKey)
const cipherText = encryptStringSync(
unHashedValue,
keys.encryptionKey
)
objectPath.set(draft.args, path, cipherText)
debug.encryption(`Encrypted ${model}.${field} at path \`${path}\``)
if (fieldConfig.hash) {
const hash = hashString(clearText, fieldConfig.hash)
const hash = hashString(unHashedValue, fieldConfig.hash)
const hashPath = rewriteWritePath(
path,
field,
Expand Down Expand Up @@ -176,9 +195,15 @@ export function decryptOnRead<Models extends string, Actions extends string>(
field
}) {
try {
// Decrypting list values is not yet supported
if (typeof cipherText !== 'string') {
return
}

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

const decryptionKey = findKeyForMessage(cipherText, keys.keychain)
const clearText = decryptStringSync(cipherText, decryptionKey)
objectPath.set(result, path, clearText)
Expand Down Expand Up @@ -213,7 +238,11 @@ function rewriteHashedFieldPath(
) {
const items = path.split('.').reverse()
// Special case for `where field equals or not` clause
if (items.includes('where') && items[1] === field && ['equals', 'not'].includes(items[0])) {
if (
items.includes('where') &&
items[1] === field &&
['equals', 'not', 'in'].includes(items[0])
) {
items[1] = hashField
return items.reverse().join('.')
}
Expand Down
21 changes: 21 additions & 0 deletions src/tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,25 @@ describe.each(clients)('integration ($type)', ({ client }) => {
expect(received!.name).toEqual(' François') // clear text in returned value
expect(received!.email).toEqual(normalizeTestEmail)
})

test('query entries in list', async () => {
await client.user.create({
data: {
name: 'Test User 1',
email: '[email protected]'
}
})
await client.user.create({
data: {
name: 'Test User 2',
email: '[email protected]'
}
})

const foundUserCount = await client.user.count({
where: { name: { in: ['Test User 1', 'Test User 2'] } }
})

expect(foundUserCount).toBe(2)
})
})
10 changes: 7 additions & 3 deletions src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface VisitorState {

export interface TargetField {
path: string
value: string
value: string | string[]
model: string
field: string
fieldConfig: FieldConfiguration
Expand Down Expand Up @@ -46,7 +46,11 @@ const makeVisitor = (
if (
type === 'object' &&
key in model.fields &&
typeof (node as any)?.[specialSubField] === 'string'
(
// Used for where: { field: in: []} queries
typeof (node as any)?.[specialSubField] === 'string' ||
typeof (node as any)?.[specialSubField] === 'object'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Same test here, using Array.isArray.

)
) {
const value: string = (node as any)[specialSubField]
const targetField: TargetField = {
Expand Down Expand Up @@ -84,7 +88,7 @@ export function visitInputTargetFields<
) {
traverseTree(
params.args,
makeVisitor(models, visitor, ['equals', 'set', 'not'], debug.encryption),
makeVisitor(models, visitor, ['equals', 'set', 'not', 'in'], debug.encryption),
{
currentModel: params.model!
}
Expand Down