Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@stdlib/math-base-special-ldexp": "^0.0.5",
"dlv": "^1.1.3",
"dset": "^3.1.1",
"node-forge": "^1.3.1",
"tiny-hashes": "^1.0.1"
},
"devDependencies": {
Expand Down
44 changes: 32 additions & 12 deletions src/__tests__/transformers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,14 @@ describe('drop_properties', () => {
simpleEvent.properties = {
product: [
{ id: 875134, category: 'Clothing' },
{ id: 875135, category: 'Sports' }
]
{ id: 875135, category: 'Sports' },
],
}
transformer.config = { drop: { 'properties.product': ['category'] } }

transform(simpleEvent, [transformer])
expect(simpleEvent.properties).toStrictEqual({
product: [
{ id: 875134 },
{ id: 875135 }
]
product: [{ id: 875134 }, { id: 875135 }],
})
})

Expand Down Expand Up @@ -182,17 +179,14 @@ describe('allow_properties', () => {
simpleEvent.properties = {
product: [
{ id: 875134, category: 'Clothing' },
{ id: 875135, category: 'Sports' }
]
{ id: 875135, category: 'Sports' },
],
}
transformer.config = { allow: { 'properties.product': ['id'] } }

transform(simpleEvent, [transformer])
expect(simpleEvent.properties).toStrictEqual({
product: [
{ id: 875134 },
{ id: 875135 }
]
product: [{ id: 875134 }, { id: 875135 }],
})
})

Expand Down Expand Up @@ -447,3 +441,29 @@ describe('map_properties', () => {
expect(simpleEvent.mapMe).toBe('false')
})
})

describe('encrypt_properties', () => {
beforeEach(() => {
transformer.type = 'encrypt_properties'
transformer.config = {
encrypt: {
key: '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAoxNilY9QL6OOIfh3laZzihp/0JfOj7sSN/MForSpGVPAAFaKkH8q\nGq+cwmiFRInjROvKJ/S2AwHKbuD1kHzb/c8CUqRdjwPhfajowSlS6QojbtC8BSJs\nFSG23v+5qYoF7GIgZ2klZDsLoLFdHPT/OsqhzzL1ORrIjIWPHbuAO0+oxDICMN68\nT3MMzfAHWs48wbHm7HaeyrOn7ZxbYpbAVpTklPMZOdHc8fJU+5gtZAoLiBTDlGz/\n2H+w62aYrFXE/XpJfg9vFckiz88BCSDUxtpuVZNf+IIk/aFOP+T5iH5a0NDeRa1L\nFm+WjAw98N9zku3lXHa+dS3cG8zlBxq+lwIDAQAB\n-----END RSA PUBLIC KEY-----',
properties: ['citizenship', 'sex', 'phoneNumber'],
label: 'mylabel',
seed: 'myseed',
},
}
})

it('should encrypt properties with provided public key and seed', () => {
const payload = {
properties: {
citizenship: 'Indian',
},
}

transform(payload, [transformer])
expect(payload.properties['citizenship']).not.toEqual('Indian')
expect(payload.properties['citizenship']).toBe('EhHhLw+WdZbbAV3q7ZwNIhCEPHN3LrlRqNwQEJCPxrlD5VZQtCBT9UfwJBHvHL+lfwIOn8G2egVb2lLM1uHlbpxW+atcwcV6JjGkxhBMkn5SQpVQ2BwRgsFGcS3DZdGFKFSY2XQhARe2Z0+xyGQ1s+OCaLbayegekKtVbmBK/kC1XqYfNW+Pvq3gdGMhoLn6ruQe/YihEtnKxBC5UyOxYp30EZ2VvpixqT0Z0DDP997W6Y7Rlt+eO2S2sbWB0tGhq/JkasKk4y9z7gYD77Asq1/faROy9+8NRcAtkowgU2qDfGuWiG44MZdhxbHsfJJS8g3VGvZ3IRyCCshPGzUvWw==')
})
})
113 changes: 108 additions & 5 deletions src/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Transformer } from './store'
import MD5 from 'tiny-hashes/md5'
import get from 'dlv'
import ldexp from '@stdlib/math-base-special-ldexp'
import { dset } from 'dset';
import { dset } from 'dset'
import { unset } from './unset'
import * as forge from 'node-forge'

export type KeyTarget = Record<string, string[]>

Expand All @@ -12,8 +13,15 @@ export interface TransformerConfig {
drop?: KeyTarget
sample?: TransformerConfigSample
map?: Record<string, TransformerConfigMap>
encrypt?: EncryptConfig
}

export interface EncryptConfig {
key: string
properties: string[]
label: string
seed: string
}
export interface TransformerConfigSample {
percent: number
path: string
Expand Down Expand Up @@ -50,6 +58,9 @@ export default function transform(payload: any, transformers: Transformer[]): an
case 'hash_properties':
// Not yet supported, but don't throw an error. Just ignore.
break
case 'encrypt_properties':
EncryptProperties(transformedPayload, transformer.config)
break
default:
throw new Error(`Transformer of type "${transformer.type}" is unsupported.`)
}
Expand All @@ -61,30 +72,34 @@ export default function transform(payload: any, transformers: Transformer[]): an
// dropProperties removes all specified props from the object.
function dropProperties(payload: any, config: TransformerConfig) {
filterProperties(payload, config.drop, (matchedObj, dropList) => {
dropList.forEach(k => delete matchedObj[k])
dropList.forEach((k) => delete matchedObj[k])
})
}

// allowProperties ONLY allows the specific targets within the keys. (e.g. "a.foo": ["bar", "baz"]
// on {a: {foo: {bar: 1, baz: 2}, other: 3}} will not have any drops, as it only looks inside a.foo
function allowProperties(payload: any, config: TransformerConfig) {
filterProperties(payload, config.allow, (matchedObj, preserveList) => {
Object.keys(matchedObj).forEach(key => {
Object.keys(matchedObj).forEach((key) => {
if (!preserveList.includes(key)) {
delete matchedObj[key]
}
})
})
}

function filterProperties(payload: any, ruleSet: KeyTarget, filterCb: (matchedObject: any, targets: string[]) => void) {
function filterProperties(
payload: any,
ruleSet: KeyTarget,
filterCb: (matchedObject: any, targets: string[]) => void,
) {
Object.entries(ruleSet).forEach(([key, targets]) => {
const filter = (obj: any) => {
// Can only act on objects.
if (typeof obj !== 'object' || obj === null) {
return
}

filterCb(obj, targets)
}

Expand Down Expand Up @@ -260,3 +275,91 @@ function consumeDigest(digest: number[], arr: number[]) {
}
}
}

function EncryptProperties(payload: any, config: TransformerConfig) {
if (!config.encrypt.key) {
throw new Error('public key not present')
}

encryptWithPublicKey(
config.encrypt.key,
config.encrypt.label,
config.encrypt.properties,
config.encrypt.seed,
payload,
)
// Parse the properties back into a JSON object
payload.properties = JSON.parse(payload.properties)
}

type StringSet = { [key: string]: boolean }

function encryptWithPublicKey(
key: string,
label: string,
fields: string[],
seed: string,
payload: any,
) {
const publicKey = forge.pki.publicKeyFromPem(key)

const hlsSet: StringSet = {}

for (const value of fields) {
hlsSet[value] = true
}
const properties: { [key: string]: string } = { ...payload.properties }
const sha256 = forge.md.sha256.create()
sha256.update(seed)

for (const key in hlsSet) {
if (!properties.hasOwnProperty(key)) {
continue
}
const toBeEncrypted = properties[key]
const labelBytes = forge.util.encodeUtf8(label)
let ciphertextBase64: string

if (seed) {
Copy link

Choose a reason for hiding this comment

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

Should this be !seed?

const ciphertextBuffer = publicKey.encrypt(forge.util.encodeUtf8(toBeEncrypted), 'RSA-OAEP', {
md: sha256,
mgf1: {
md: sha256,
},
label: labelBytes,
})
ciphertextBase64 = forge.util.encode64(ciphertextBuffer)
} else {
const prng = new Prng(seed, toBeEncrypted)
const ciphertextBuffer = publicKey.encrypt(prng.getBytes(), 'RSA-OAEP', {
md: forge.md.sha256.create(),
mgf1: {
md: forge.md.sha256.create(),
},
label: labelBytes,
})
ciphertextBase64 = forge.util.encode64(ciphertextBuffer)
}

properties[key] = ciphertextBase64
}
const jsonData = JSON.stringify(properties)
payload.properties = jsonData
}

class Prng {
private seed: string
private input: string

constructor(seed: string, input: string) {
this.seed = seed
this.input = input
}

getBytes(): Uint8Array {
const hmac = forge.md.sha256.create()
hmac.update(this.seed)
hmac.update(this.input)
return hmac.digest().getBytes()
}
}
Loading