Skip to content

Commit

Permalink
encryption only
Browse files Browse the repository at this point in the history
  • Loading branch information
ipbyrne committed Jun 7, 2023
1 parent 199d2d7 commit ce39590
Show file tree
Hide file tree
Showing 15 changed files with 260 additions and 238 deletions.
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
MONGO_DB_CONNECTION_STRING=mongodb://localhost:27017/mongo-encryption-query
SALT='1234'
HASH_KEYS='true'
ENCRYPT_KEYS='true'
3 changes: 1 addition & 2 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
MONGO_DB_CONNECTION_STRING=mongodb://localhost:27017/mongo-encryption-query
SALT='1234'
HASH_KEYS='true'
ENCRYPT_KEYS='true'
69 changes: 33 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,52 +43,49 @@ Out of the box, this will support all MongoDB equality query operators.
Once data is added to the DB this will be it's shape:
```
{
"_id": "647a0c77d10ce31ab5a43104",
"0e84bb54f767306135740202fcdb538372e04fd2fd28b9321243073d3eb9b6e4": "eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZHQ00iLCJlcGsiOnsieCI6Im96WlhzMTZ2SU1JeC1LS2haWkpMaUVtRjBQdkFZOFM0eTFaWVdTWWxkazQiLCJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AifX0.X2YlOs8K10pYG9gaIwcNEO7ET93styvhy_OGl-AG5LBR6tk_cqeMIw.jJOldIv-WfX51DYA.gZK2hJgaXV5ksZecQlI-Vfqxbv44_eWt7Cwk48M0ZY76_42IGDmWRXEKFyHRZcN2COlEWcRYIQ9AI9LjIzTMMmBO1h21IkjRLzjH00j9Gc-MDl2Wax_AxoQG3YpouBpP6cMGWDFlBtEfdUCDO2yfe1cX_cH8WyuJArZCbPDcitTtiMgAShvu6VNBziB9_V_txesWck993IN45gXCJGOMVuyhtWjW8YsIvFb60v-98DmhhFX8lMlP1Qx2Y7vvnfmtIGZGC-d2zMdjAnuVQzY1G4I0iG70oGbpnuPg-QMdAPRiULTWGcL2zwUh-PMVXHEfeoEF1OTEceXLdXWzMpUL_fu-sk8MAeP_r-4_A9gh_ZGwEmbbb80DBy2jRLzLpsx7VK1kY_pynlo1SlD1TTOZvTVcgc4tHHRZnMctHnr2wZkm1N8A0RL4CfgxPUYLGBE3R3NXh-2f4a_JIjhZw220VRzImtVGCpb6bDerb9Q69eqBaStRnJopRHUgCXeRiq4IyhuRhuiOCZWB7Uw5fGkud6vOxUE-XJh2E7IazUCmeHjqNocUA-kz.nyUT3sa1qfPoMe1X_ANL-Q",
"52eea0dc4f17ca24fc34677d803d88ee52f506398a2b295d615636b78f9da2a7": {
"7a85fcba9eadcf8ccb29af78a06e5b9beb17bc1f4c860d4a4ecf2dfeca447ffa": [
"461b8e14bbae54cfe2f0216bbff41ec4fed64b7410c71751995f54dbff8d9d42",
"bf7b3eebbbad2b0a64c34aa16868847788ca31ee0c47ea1d67888bbb64c73a2d"
],
"f9008bb76a0e1523b236430a9561087e8995b36d3825b0a17627f7cc9170706a": [
"b0f3716dc7545863015358c59fc4cdcf32a544fcb8c7bd924d7aab9ec3d024ab"
],
"5f7af41d38929438da3be3d0bca2c5ee99acfd0b93fa5d02f625a37609aaefe4": "4e49b574f544e9b58535099290e866967040ba187de2585b69bca4e4c34268f9",
"6ef91db0f6dada1c87473fe9c5a34c2b07cd16de564d17615c53f9f8b06fb066": "e34d3c4394a16288510ee7f3d59ae1ee9f6bb95c6c3acc85fe910899a7cfa467",
"f02e12dde03990ea2d1335b1b68644332ecb1ba6cffa24ba3df823027d770fea": "2f5c3b84ab871ae0cf0876fd4357bafa9b6f548d252f8f553138bbf6dc0ee7d9",
"262ccd808d4554b91d2ca20fea330879fc594d281619e5b267296fb2b1e5267f": {
"4a0bbb40bfe0e6615b3d8f52c105f80daec2aeb5c3794bfdefe06b256ce4594b": "39ab32f4a399115a241f7f0fb26e0f6162ddfffca35e26477c9f93ebf2ec7295",
"558a7ed37ac12cfd0b2631b15625be552fadec9f50bc9ada97ebe14affee2499": {
"7955ec0d369752740d4e99f77f38df92060c8daf2331db8231e9b70e772ad874": "499ffa6077e6581f34ffc34f5382e80103d918b6dcab0e742ee11b7b6dd5f009"
},
"daa81628a89684f880f02d9b6b30dbe84d011dac43cce1b4b3ab668040c1c8fa": [
{
"beb80a21c8f3dfd87fa956c1f3d65c0ab2ed588d201433c9439ef67177b70a55": "f06cdeffc6a93113c15688976e9cbff790d9fe939aa33e835603390f3435ad73"
},
{
"beb80a21c8f3dfd87fa956c1f3d65c0ab2ed588d201433c9439ef67177b70a55": "4cdf70965c90e16936cc7baaa3264077ed75587331631966b7c1ffead24472b7"
}
]
},
"197b9fa9a4000d7b22ae435f3086966935c68d1f8ded0a9de12b6cab02657e92": {
"4a0bbb40bfe0e6615b3d8f52c105f80daec2aeb5c3794bfdefe06b256ce4594b": "4e27b072dd14b3d808399af7b2bce18cacd76d760d1a953a023ec7ee486727f4"
"7818": "640e9625f2d4c216e3786e36ad726f8e4445086202b8d0cda065b56c51704cfd9da8c7d75500cb88cbfd8ae328",
"511f9771f3c4d306": [
"79088c6ff49b845dae6a2b2ab92222d8040f15330ab8c5d6f226b625552608f2d1a1839e1b02",
"79088c6ff49b845dae2e3560e07e7ed0591c486059ec9c9bf838ba3549670aaa"
],
"6505887a": [
"47198a76e1c8ca10b5781f76ab7569d902015b6d"
],
"7f1d957a": "45198b6ba792",
"75198b7cf5c8db06b07232": "54049972f7cdce529a6f3960ab7f78de17041a32",
"780f8b6ae6cfc8179d7c2861": "234cc927aa91995feb290834fb2b3e8f4c580e5b",
"720e9d7be2cfdf1bb8710f71ac7b69d402": {
"7818": "75159c25e2d9ca1fa971393efc2238",
"70189c6de2d2d8": {
"6b15885ce8c5ce": "264bcb28be"
},
"4a0bbb40bfe0e6615b3d8f52c105f80daec2aeb5c3794bfdefe06b256ce4594b": "497c3876dfedb54c3e94aaf803480948380f155d09b1d0eb2121d3b80d7cb53b"
"7f198b6be2c5ea00ab7c2540af656d": [
{
"78129c7aff": "24"
},
{
"78129c7aff": "27"
},
{
"78129c7aff": "26"
}
]
},
"780f8b6ae2d3": {
"7818": "75159c25f0c4c948bc653d69be7d6999150757"
}
}
```

The `cipher` key contains the probablistic encryption of the actual data. The `searchable` data contains the determinate hashing of the data's key value pairs.

In practice, each user or organization should use their own salt value so no people will have matching hashes even if they are using the same values. This could be the thumbprint of the key the user or organization is going to use to encrypt/decrypt the data.
In practice, each user or organization should use their own private key for encryption so no users will have matching encrypted values even if they are storing the same values.

Additionally, each user/organization should use their own key pair for encryption. These keys should never be comitted to source or saved in the database directly and ideally would be kept in a service like Google Secret Manager or another providers equivelant service.
These keys should never be comitted to source or saved in the database directly and ideally would be kept in a service like Google Secret Manager or another providers equivelant service.

You would then retrieve the keys when encrypting and decrypting data.

The ones in the 'key.js' file are there so you can easily test and play with the repository to understand how everything works together.

## Optional Configurations

## Hashing Keys
If you are not worried about the keys being readable in the database you can opt out of hashing them. One benefit of doing this would be it will allow you to build indexes on fields to speed up queries.
## Encrypting Keys
If you are not worried about the keys being readable in the database you can opt out of encrypting them. One benefit of doing this would be it will allow you to build indexes on fields to speed up queries.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"author": "Isaac Byrne (me@ipbyrne.com)",
"module": "dist/index.esm.js",
"license": "Apache-2.0",
"version": "0.0.4",
"version": "0.0.5",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"keywords": [
Expand Down
14 changes: 6 additions & 8 deletions src/cipher/cipher.sanity.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { PrivateKeyJwk } from "src/types";
import { generate, encrypt, decrypt } from "./cipher";
import { PublicKeyJwk, PrivateKeyJwk } from "../types";

describe("X25519", () => {
it("encrypt and decrypt", async () => {
const { publicKeyJwk, privateKeyJwk } = await generate();
const encoder = new TextEncoder();
const { privateKeyJwk } = await generate();
const message = {
message: "It’s a dangerous business, Frodo, going out your door.",
};
const jwe = await encrypt(
Buffer.from(encoder.encode(JSON.stringify(message))),
publicKeyJwk as PublicKeyJwk
);
const decryptedData = await decrypt(jwe, privateKeyJwk as PrivateKeyJwk);
const jwe = encrypt(message, privateKeyJwk as PrivateKeyJwk);
const jwe2 = encrypt(message, privateKeyJwk as PrivateKeyJwk);
expect(jwe).toStrictEqual(jwe2);
const decryptedData = decrypt(jwe, privateKeyJwk as PrivateKeyJwk);
expect(decryptedData).toStrictEqual(message);
});
});
54 changes: 22 additions & 32 deletions src/cipher/cipher.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { CompactEncrypt } from "jose/jwe/compact/encrypt";
import { compactDecrypt } from "jose/jwe/compact/decrypt";
import { importJWK } from "jose/key/import";
import { generateKeyPair } from "jose/util/generate_key_pair";
import { exportJWK } from "jose/key/export";
import { PublicKeyJwk, PrivateKeyJwk } from "../types";
import { PrivateKeyJwk } from "../types";
import crypto from "crypto";

const enc = "aes-256-gcm";

const crvToAlg: { [x: string]: string } = {
X25519: "ECDH-ES+A256KW",
};

const enc = "A256GCM";

export const generate = async (crv = "X25519") => {
const alg = crvToAlg[crv];
const { publicKey, privateKey } = await generateKeyPair(alg, {
Expand All @@ -21,32 +19,24 @@ export const generate = async (crv = "X25519") => {
return { publicKeyJwk, privateKeyJwk };
};

export const encrypt = async (
plaintext: Buffer,
publicKeyJwk: PublicKeyJwk
) => {
const alg = crvToAlg[publicKeyJwk.crv];
const key = {
...publicKeyJwk,
alg,
};
const jwe = await new CompactEncrypt(Uint8Array.from(plaintext))
.setProtectedHeader({ alg, enc })
.encrypt(await importJWK(key));
return jwe;
export const encrypt = (data: any, privateKeyJwk: PrivateKeyJwk) => {
const encoder = new TextEncoder();
const encodedPrivateKey = encoder.encode(privateKeyJwk.d).slice(0, 32);
const initVector = encoder.encode(privateKeyJwk.d).slice(0, 16);
const cipher = crypto.createCipheriv(enc, encodedPrivateKey, initVector);
const dataType = typeof data;
return cipher.update(
dataType === "string" ? data : JSON.stringify(data),
"utf-8",
"hex"
);
};

export const decrypt = async (
ciphertext: string,
privateKeyJwk: PrivateKeyJwk
) => {
const decoder = new TextDecoder();
const alg = crvToAlg[privateKeyJwk.crv];
const key = {
...privateKeyJwk,
alg,
};
const { plaintext } = await compactDecrypt(ciphertext, await importJWK(key));
const buffer = Buffer.from(plaintext);
return JSON.parse(decoder.decode(buffer));
export const decrypt = (ciphertext: string, privateKeyJwk: PrivateKeyJwk) => {
const encoder = new TextEncoder();
const encodedPrivateKey = encoder.encode(privateKeyJwk.d).slice(0, 32);
const initVector = encoder.encode(privateKeyJwk.d).slice(0, 16);
const decipher = crypto.createDecipheriv(enc, encodedPrivateKey, initVector);
const decryptedData = decipher.update(ciphertext, "hex", "utf-8");
return decryptedData;
};
95 changes: 0 additions & 95 deletions src/hasher/index.ts

This file was deleted.

52 changes: 20 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,33 @@
import {
createHashedQuery,
createHashedObject,
blindIndexHash,
} from "./hasher";
import { encrypt, decrypt, generate } from "./cipher/cipher";
import { PrivateKeyJwk, PublicKeyJwk } from "./types";
createEntrypedQuery,
createEncryptedObject,
createDecryptedObject,
} from "./utils";
import { generate } from "./cipher/cipher";
import { PrivateKeyJwk, Data } from "./types";
import * as Types from "./types";

const hashKeys = (process.env.HASH_KEYS as string) === "true";

export const encryptQuery = (query: any, salt: string) => {
const hashedQuery = createHashedQuery(query, salt);
export const encryptQuery = (query: any, privateKeyJwk: PrivateKeyJwk) => {
const hashedQuery = createEntrypedQuery(query, privateKeyJwk);
return hashedQuery;
};

export const encryptData = async (
data: any,
publicKeyJwk: PublicKeyJwk,
salt: string
) => {
const hashedData = createHashedObject(data, salt);
const encoder = new TextEncoder();
const cipher = await encrypt(
Buffer.from(encoder.encode(JSON.stringify(data))),
publicKeyJwk as PublicKeyJwk
);
return {
[hashKeys ? `${blindIndexHash("cipher", salt)}` : "cipher"]: cipher,
[hashKeys ? `${blindIndexHash("search", salt)}` : "search"]: hashedData,
};
export const encryptData = (data: Data, privateKeyJwk: PrivateKeyJwk) => {
const encryptedData = createEncryptedObject(data, privateKeyJwk);
return encryptedData;
};

export const decryptData = async (
data: any,
privateKeyJwk: PrivateKeyJwk,
salt: string
) => {
const decryptedData = await decrypt(
data[`${blindIndexHash("cipher", salt)}`],
export const decryptData = (data: Data, privateKeyJwk: PrivateKeyJwk) => {
let id;
if (data._id) {
id = data._id;
delete data._id;
}
const decryptedData = createDecryptedObject(
data,
privateKeyJwk as PrivateKeyJwk
);
decryptedData._id = id;
return decryptedData;
};

Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export interface PublicKeyJwk extends KTY, CRV, X {}
export interface PrivateKeyJwk extends KTY, CRV, X {
d: string;
}

export interface Data {
[key: string]: any;
}

0 comments on commit ce39590

Please sign in to comment.