Skip to content

Commit

Permalink
feat(normalize-hash): Rename sanitize to normalize
Browse files Browse the repository at this point in the history
  • Loading branch information
Martijn de Voogd committed Jun 25, 2024
1 parent 98b6ce2 commit 532b591
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 48 deletions.
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,29 +235,29 @@ rainbow table attacks. There are multiple ways to do so, listed by order of prec

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

### Sanitize hash
### Normalize hash

> _Support: introduced in version 1.6.0_
You can sanitize a hash before creation and querying. This might be useful in case you would like to find a User with the name of `François ` with a query input of `francois`.
You can normalize a hash before creation and querying. This might be useful in case you would like to find a User with the name of `François ` with a query input of `francois`.

There are several sanitize options:
There are several normalize options:

```
/// @encryption:hash(email)?sanitize=lowercase <- lowercase hash
/// @encryption:hash(email)?sanitize=uppercase <- uppercase hash
/// @encryption:hash(email)?sanitize=trim <- trim start and end of hash
/// @encryption:hash(email)?sanitize=spaces <- remove spaces in hash
/// @encryption:hash(email)?sanitize=diacritics <- remove diacritics like ç or é in hash
/// @encryption:hash(email)?normalize=lowercase <- lowercase hash
/// @encryption:hash(email)?normalize=uppercase <- uppercase hash
/// @encryption:hash(email)?normalize=trim <- trim start and end of hash
/// @encryption:hash(email)?normalize=spaces <- remove spaces in hash
/// @encryption:hash(email)?normalize=diacritics <- remove diacritics like ç or é in hash
```

You can also combine the sanitize options:
You can also combine the normalize options:

```
/// @encryption:hash(email)?sanitize=lowercase&sanitize=trim&sanitize=trim&sanitize=diacritics
/// @encryption:hash(email)?normalize=lowercase&normalize=trim&normalize=trim&normalize=diacritics
```

> Be aware: Using the sanitize hash feature in combination with `unique` could cause conflicts. Example: Users with the name `François` and `francois` result in the same hash which could result in a database conflict.
> Be aware: Using the normalize hash feature in combination with `unique` could cause conflicts. Example: Users with the name `François` and `francois` result in the same hash which could result in a database conflict.
## Migrations

Expand Down
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ model User {
id Int @id @default(autoincrement())
email String @unique
name String? @unique /// @encrypted
nameHash String? @unique /// @encryption:hash(name)?sanitize=lowercase&sanitize=diacritics&sanitize=trim
nameHash String? @unique /// @encryption:hash(name)?normalize=lowercase&normalize=diacritics&normalize=trim
posts Post[]
pinnedPost Post? @relation(fields: [pinnedPostId], references: [id], name: "pinnedPost")
pinnedPostId Int?
Expand Down
6 changes: 3 additions & 3 deletions src/dmmf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
parseEncryptedAnnotation,
parseHashAnnotation
} from './dmmf'
import { HashFieldSanitizeOptions } from './types'
import { HashFieldNormalizeOptions } from './types'

describe('dmmf', () => {
describe('parseEncryptedAnnotation', () => {
Expand Down Expand Up @@ -121,7 +121,7 @@ describe('dmmf', () => {
id Int @id @default(autoincrement())
email String @unique
name String? /// @encrypted
nameHash String? /// @encryption:hash(name)?sanitize=lowercase
nameHash String? /// @encryption:hash(name)?normalize=lowercase
posts Post[]
pinnedPost Post? @relation(fields: [pinnedPostId], references: [id], name: "pinnedPost")
pinnedPostId Int?
Expand Down Expand Up @@ -164,7 +164,7 @@ describe('dmmf', () => {
algorithm: 'sha256',
inputEncoding: 'utf8',
outputEncoding: 'hex',
sanitize: [HashFieldSanitizeOptions.lowercase]
normalize: [HashFieldNormalizeOptions.lowercase]
}
}
},
Expand Down
22 changes: 12 additions & 10 deletions src/dmmf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
DMMFDocument,
FieldConfiguration,
HashFieldConfiguration,
HashFieldSanitizeOptions,
HashFieldNormalizeOptions,
dmmfDocumentParser
} from './types'

Expand Down Expand Up @@ -209,16 +209,18 @@ export function parseHashAnnotation(
? process.env[saltEnv]
: process.env.PRISMA_FIELD_ENCRYPTION_HASH_SALT)

const sanitize =
(query.getAll('sanitize') as HashFieldSanitizeOptions[]) ?? []
console.log(sanitize)
const normalize =
(query.getAll('normalize') as HashFieldNormalizeOptions[]) ?? []

if (
!isValidSanitizeOptions(sanitize) &&
!isValidNormalizeOptions(normalize) &&
process.env.NODE_ENV === 'development' &&
model &&
field
) {
console.warn(warnings.unsupportedSanitize(model, field, sanitize, 'output'))
console.warn(
warnings.unsupportedNormalize(model, field, normalize, 'output')
)
}

return {
Expand All @@ -228,16 +230,16 @@ export function parseHashAnnotation(
salt,
inputEncoding,
outputEncoding,
sanitize
normalize
}
}

function isValidEncoding(encoding: string): encoding is Encoding {
return ['hex', 'base64', 'utf8'].includes(encoding)
}

function isValidSanitizeOptions(
function isValidNormalizeOptions(
options: string[]
): options is HashFieldSanitizeOptions[] {
return options.every(option => option in HashFieldSanitizeOptions)
): options is HashFieldNormalizeOptions[] {
return options.every(option => option in HashFieldNormalizeOptions)
}
10 changes: 5 additions & 5 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { namespace } from './debugger'
import {
HashFieldSanitizeOptions,
HashFieldNormalizeOptions,
type DMMFField,
type DMMFModel
} from './types'
Expand Down Expand Up @@ -110,12 +110,12 @@ export const warnings = {
) => `${warning}: unsupported ${io} encoding \`${encoding}\` for hash field ${model}.${field}
-> Valid values are utf8, base64, hex
`,
unsupportedSanitize: (
unsupportedNormalize: (
model: string,
field: string,
sanitize: string,
normalize: string,
io: string
) => `${warning}: unsupported ${io} sanitize \`${sanitize}\` for hash field ${model}.${field}
-> Valid values are ${Object.values(HashFieldSanitizeOptions)}
) => `${warning}: unsupported ${io} normalize \`${normalize}\` for hash field ${model}.${field}
-> Valid values are ${Object.values(HashFieldNormalizeOptions)}
`
}
20 changes: 10 additions & 10 deletions src/hash.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { decoders, encoders } from '@47ng/codec'
import crypto from 'node:crypto'
import { HashFieldConfiguration, HashFieldSanitizeOptions } from './types'
import { HashFieldConfiguration, HashFieldNormalizeOptions } from './types'

export function hashString(
input: string,
config: Omit<HashFieldConfiguration, 'sourceField'>
) {
const decode = decoders[config.inputEncoding]
const encode = encoders[config.outputEncoding]
const sanitized = sanitizeHashString(input, config.sanitize)
const normalized = normalizeHashString(input, config.normalize)

const data = decode(sanitized)
const data = decode(normalized)
const hash = crypto.createHash(config.algorithm)
hash.update(data)
if (config.salt) {
Expand All @@ -19,24 +19,24 @@ export function hashString(
return encode(hash.digest())
}

export function sanitizeHashString(
export function normalizeHashString(
input: string,
options: HashFieldSanitizeOptions[] = []
options: HashFieldNormalizeOptions[] = []
) {
let output = input
if (options.includes(HashFieldSanitizeOptions.lowercase)) {
if (options.includes(HashFieldNormalizeOptions.lowercase)) {
output = output.toLowerCase()
}
if (options.includes(HashFieldSanitizeOptions.uppercase)) {
if (options.includes(HashFieldNormalizeOptions.uppercase)) {
output = output.toUpperCase()
}
if (options.includes(HashFieldSanitizeOptions.trim)) {
if (options.includes(HashFieldNormalizeOptions.trim)) {
output = output.trim()
}
if (options.includes(HashFieldSanitizeOptions.spaces)) {
if (options.includes(HashFieldNormalizeOptions.spaces)) {
output = output.replace(/\s/g, '')
}
if (options.includes(HashFieldSanitizeOptions.diacritics)) {
if (options.includes(HashFieldNormalizeOptions.diacritics)) {
output = output.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
}
return output
Expand Down
12 changes: 6 additions & 6 deletions src/tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,24 +425,24 @@ describe.each(clients)('integration ($type)', ({ client }) => {
expect(existingUsers).toEqual(users)
})

const sanitizeTestEmail = 'sanitize@example.com'
const normalizeTestEmail = 'normalize@example.com'

test('create user with sanitizable name', async () => {
test('create user with normalizeable name', async () => {
const received = await client.user.create({
data: {
email: sanitizeTestEmail,
email: normalizeTestEmail,
name: ' François'
}
})
const dbValue = await sqlite.get({
table: 'User',
where: { email: sanitizeTestEmail }
where: { email: normalizeTestEmail }
})
expect(received.name).toEqual(' François') // clear text in returned value
expect(dbValue.name).toMatch(cloakedStringRegex) // encrypted in database
})

test('query user by encrypted and hashed name field with a sanitized input (with equals)', async () => {
test('query user by encrypted and hashed name field with a normalized input (with equals)', async () => {
const received = await client.user.findFirst({
where: {
name: {
Expand All @@ -451,6 +451,6 @@ describe.each(clients)('integration ($type)', ({ client }) => {
}
})
expect(received!.name).toEqual(' François') // clear text in returned value
expect(received!.email).toEqual(sanitizeTestEmail)
expect(received!.email).toEqual(normalizeTestEmail)
})
})
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ export type HashFieldConfiguration = {
salt?: string
inputEncoding: Encoding
outputEncoding: Encoding
sanitize?: HashFieldSanitizeOptions[]
normalize?: HashFieldNormalizeOptions[]
}

export enum HashFieldSanitizeOptions {
export enum HashFieldNormalizeOptions {
lowercase = 'lowercase',
uppercase = 'uppercase',
trim = 'trim',
Expand Down

0 comments on commit 532b591

Please sign in to comment.