├── .circleci └── config.yml ├── .gitignore ├── .node-version ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── crypto.ts ├── entity.ts ├── index.ts ├── options │ ├── EncryptionOptions.ts │ ├── ExtendedColumnOptions.ts │ └── index.ts ├── subscribers │ ├── AutoEncryptSubscriber.ts │ └── index.ts └── transformer.ts ├── test ├── active-record.test.ts ├── crypto.test.ts ├── data-mapper.test.ts ├── encryption-predicate.test.ts ├── entities │ ├── ColumnOptionsEntity1.ts │ ├── ColumnOptionsEntity2.ts │ ├── ColumnOptionsEntity3.ts │ ├── ColumnOptionsEntity4.ts │ ├── TransformerOptionsAES256GCMEntity1.ts │ ├── TransformerOptionsAES256GCMEntity2.ts │ ├── TransformerOptionsEntity1.ts │ ├── TransformerOptionsEntity2.ts │ ├── TransformerOptionsEntity3.ts │ ├── TransformerOptionsEntityEmptyString1.ts │ ├── TransformerOptionsEntityNullable1.ts │ └── TransformerOptionsEntityNullable2.ts ├── find-operator.test.ts ├── transformer.test.ts └── utils.ts ├── tsconfig.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:12.16.1 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | - run: npm install typeorm 37 | 38 | # run tests! 39 | - run: npm test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # build files 61 | lib 62 | 63 | # generated by unit test 64 | *.db 65 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.22.12 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .circleci 3 | test 4 | tsconfig.json 5 | tslint.json 6 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Abraham Elmahrek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typeorm-encrypted 2 | 3 | Encrypted field for [typeorm](http://typeorm.io). 4 | 5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | ## Installation 19 | 20 | ``` 21 | npm install --save typeorm-encrypted 22 | ``` 23 | 24 | ## Example 25 | 26 | This library can invoked in 2 ways: transformers or subscribers. In both of the examples below, the `Key` and `IV` vary based on the algorithm. See the [node docs](https://nodejs.org/api/crypto.html#crypto_crypto_createcipheriv_algorithm_key_iv_options) for more info. 27 | 28 | ### Transformers (Recommended) 29 | 30 | The following example has the field automatically encrypted/decrypted on save/fetch respectively. 31 | 32 | ```typescript 33 | import { Entity, Column } from "typeorm"; 34 | import { EncryptionTransformer } from "typeorm-encrypted"; 35 | 36 | @Entity() 37 | class User { 38 | ... 39 | 40 | @Column({ 41 | type: "varchar", 42 | nullable: false, 43 | transformer: new EncryptionTransformer({ 44 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 45 | algorithm: 'aes-256-gcm', 46 | ivLength: 16 47 | }) 48 | }) 49 | secret: string; 50 | 51 | ... 52 | } 53 | 54 | ``` 55 | 56 | For JSON fields you can use `JSONEncryptionTransformer`. 57 | 58 | 59 | ```typescript 60 | import { Entity, Column } from "typeorm"; 61 | import { EncryptionTransformer } from "typeorm-encrypted"; 62 | 63 | @Entity() 64 | class User { 65 | ... 66 | 67 | @Column({ 68 | type: "json", 69 | nullable: false, 70 | transformer: new JSONEncryptionTransformer({ 71 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 72 | algorithm: 'aes-256-gcm', 73 | ivLength: 16 74 | }) 75 | }) 76 | secret: object; 77 | 78 | ... 79 | } 80 | 81 | ``` 82 | 83 | More information about transformers is available in the [typeorm docs](https://typeorm.io/#/entities/column-options). 84 | 85 | ### Subscribers 86 | 87 | The following example has the field automatically encrypted/decrypted on save/fetch respectively. 88 | 89 | ```typescript 90 | import { BaseEntity, Entity, Column, createConnection } from "typeorm"; 91 | import { ExtendedColumnOptions, AutoEncryptSubscriber } from "typeorm-encrypted"; 92 | 93 | @Entity() 94 | class User extends BaseEntity { 95 | ... 96 | 97 | @Column({ 98 | type: "varchar", 99 | nullable: false, 100 | encrypt: { 101 | key: "d85117047fd06d3afa79b6e44ee3a52eb426fc24c3a2e3667732e8da0342b4da", 102 | algorithm: "aes-256-gcm", 103 | ivLength: 16 104 | } 105 | }) 106 | secret: string; 107 | 108 | ... 109 | } 110 | 111 | let connection = createConnection({ 112 | ... 113 | entities: [ User, ... ], 114 | subscribers: [ AutoEncryptSubscriber, ... ] 115 | ... 116 | }); 117 | 118 | ``` 119 | 120 | Entities and subscribers can be configured via `ormconfig.json` and environment variables as well. See the [typeorm docs](http://typeorm.io/#/using-ormconfig) for more details. 121 | 122 | ### How to use a configuration file 123 | 124 | The following example is how you can create a config stored in a separate and use it 125 | 126 | encryption-config.ts 127 | ```typescript 128 | // it is recommended to not store encryption keys directly in config files, 129 | // it's better to use an environment variable or to use dotenv in order to load the value 130 | export const MyEncryptionTransformerConfig = { 131 | key: process.env.ENCRYPTION_KEY, 132 | algorithm: 'aes-256-gcm', 133 | ivLength: 16 134 | }; 135 | ``` 136 | 137 | user.entity.ts 138 | ```typescript 139 | import { Entity, Column } from "typeorm"; 140 | import { EncryptionTransformer } from "typeorm-encrypted"; 141 | import { MyEncryptionTransformerConfig } from './encryption-config.ts'; // path to where you stored your config file 142 | 143 | @Entity() 144 | class User { 145 | // ... 146 | 147 | @Column({ 148 | type: "varchar", 149 | nullable: false, 150 | transformer: new EncryptionTransformer(MyEncryptionTransformerConfig) 151 | }) 152 | secret: string; 153 | 154 | // ... 155 | } 156 | ``` 157 | 158 | It's possible to customize the config if you need to use a different ivLength or customize other fields, a brief example below 159 | 160 | `user.entity.ts` 161 | ```typescript 162 | class User { 163 | // same as before, but for the transformer line 164 | @Column({ 165 | type: "varchar", 166 | nullable: false, 167 | transformer: new EncryptionTransformer({...MyEncryptionTransformerConfig, ivLength: 24}) 168 | }) 169 | secret: string; 170 | // ... 171 | } 172 | ``` 173 | 174 | ## FAQ 175 | 176 | ### Why won't complex queries work? 177 | 178 | Queries that transform the encrypted column wont work because transformers and subscribers operate outside of the DBMS. 179 | 180 | ### What alogorithm should I use? 181 | 182 | Unless you need to maintain compatibility with an older system (or you know exactly what you're doing), 183 | you should use "aes-256-gcm" for the mode. 184 | This means that the encryption keys are are 256 bits (32-bytes) long and that the mode of operation 185 | is GCM ([Galois Counter Mode](https://en.wikipedia.org/wiki/Galois/Counter_Mode)). 186 | 187 | GCM provides both secrecy and authenticity and can generally use CPU acceleration where available. 188 | 189 | ### Should I hardcode the IV? 190 | 191 | No. Don't ever do this. 192 | It will break the encryption and is vulnerable to a "repeated nonce" attack. 193 | 194 | If you don't provide an IV, the library will randomly generate a secure one for you. 195 | 196 | 197 | ### Error: Invalid IV length 198 | 199 | The most likely reasons you're receiving this error: 200 | 201 | 1. Column definition is wrong. Probably an issue with the key or IV. 202 | 2. There is existing data in your DBMS. In this case, please migrate the data. 203 | 3. Your query cache needs to be cleared. The typeorm query cache can be cleared globally using the [typeorm-cli](https://typeorm.io/#/using-cli): `typeorm cache:clear`. For other, more specific, solutions, see the [typeorm documentation](https://typeorm.io/#/caching). 204 | 205 | ### How can an encrypted column be added to a table with data? 206 | 207 | Follow these steps to add an encrypted column. 208 | 209 | 1. Add a new column (col B) to the table. Configure the column to be encrypted. Remove the transformer from the original column (col A). 210 | 2. Write a script that queries all of the entries in the table. Set the value of col B to col A. 211 | 3. Save all the records. 212 | 4. Rename col A to something else manually. 213 | 5. Rename col B to the original name of col A manually. 214 | 6. Remove the typeorm configuration for col A. 215 | 7. Rename the typeorm configuration for col B to col A's name. 216 | 8. Remove col A (unencrypted column) from the table manually. 217 | 218 | ### Can typeorm-encrypted encrypt the entire database? 219 | 220 | No. This library encrypts specific fields in a database. 221 | 222 | Popular databases like [MySQL](https://dev.mysql.com/doc/refman/8.0/en/innodb-data-encryption.html) and [PostgreSQL](https://www.postgresql.org/docs/8.1/encryption-options.html) are capable of data-at-rest and in-flight encryption. Refer to your database manual to figure out how to encrypt the entirety of the database. 223 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeorm-encrypted", 3 | "version": "0.8.0", 4 | "description": "encrypted typeorm fields", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "devDependencies": { 8 | "@types/chai": "^4.2.13", 9 | "@types/mocha": "^8.0.3", 10 | "@types/node": "^14.11.2", 11 | "chai": "^4.1.2", 12 | "mocha": "^10.2.0", 13 | "prettier": "^2.0.5", 14 | "sqlite3": "^5.0.10", 15 | "ts-node": "^10.9.1", 16 | "tslint": "^6.1.3", 17 | "typeorm": "^0.3.7", 18 | "typescript": "^4.7.4" 19 | }, 20 | "peerDependencies": { 21 | "typeorm": "^0.3.7" 22 | }, 23 | "scripts": { 24 | "prepare": "npm run build", 25 | "clean": "rm -rf lib", 26 | "build": "tsc", 27 | "test": "mocha --reporter spec --require ts-node/register 'test/**/*.test.ts'" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/generalpiston/typeorm-encrypted.git" 32 | }, 33 | "keywords": [ 34 | "typescript", 35 | "typeorm", 36 | "encrypted", 37 | "encryption", 38 | "encrypt" 39 | ], 40 | "author": { 41 | "name": "Abraham Elmahrek", 42 | "email": "abraham@elmahrek.com" 43 | }, 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/generalpiston/typeorm-encrypted/issues" 47 | }, 48 | "homepage": "https://github.com/generalpiston/typeorm-encrypted#readme" 49 | } 50 | -------------------------------------------------------------------------------- /src/crypto.ts: -------------------------------------------------------------------------------- 1 | import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; 2 | import { EncryptionOptions } from './options'; 3 | 4 | const DEFAULT_AUTH_TAG_LENGTH = 16; 5 | 6 | function hasAuthTag(algorithm: string):boolean { 7 | return algorithm.endsWith('-gcm') || algorithm.endsWith('-ccm') || algorithm.endsWith('-ocb') 8 | } 9 | 10 | /** 11 | * Encrypt data. 12 | */ 13 | export function encryptData(data: Buffer, options: EncryptionOptions): Buffer { 14 | const { algorithm, authTagLength, ivLength, key } = options; 15 | const iv = options.iv 16 | ? Buffer.from(options.iv, 'hex') 17 | : randomBytes(ivLength); 18 | const cipherOptions = { authTagLength: authTagLength ?? DEFAULT_AUTH_TAG_LENGTH }; 19 | const cipher = (createCipheriv as any)( 20 | algorithm, 21 | Buffer.from(key, 'hex'), 22 | iv, 23 | cipherOptions 24 | ); 25 | const start = cipher.update(data); 26 | const final = cipher.final(); 27 | 28 | if (hasAuthTag(options.algorithm)) { 29 | return Buffer.concat([iv, cipher.getAuthTag(), start, final]); 30 | } else { 31 | return Buffer.concat([iv, start, final]); 32 | } 33 | } 34 | 35 | /** 36 | * Decrypt data. 37 | */ 38 | export function decryptData(data: Buffer, options: EncryptionOptions): Buffer { 39 | const { algorithm, ivLength, key } = options; 40 | const authTagLength = options.authTagLength ?? DEFAULT_AUTH_TAG_LENGTH; 41 | const iv = data.slice(0, ivLength); 42 | const decipher = createDecipheriv( 43 | algorithm, 44 | Buffer.from(key, 'hex'), 45 | iv 46 | ); 47 | 48 | let dataToUse = data.slice(options.ivLength); 49 | 50 | if (hasAuthTag(options.algorithm)) { 51 | // Add ts-ignore due to build error TS2339: Property 'setAuthTag' does not exist on type 'Decipher'. 52 | // @ts-ignore 53 | decipher.setAuthTag(dataToUse.slice(0, authTagLength)); 54 | dataToUse = dataToUse.slice(authTagLength); 55 | } 56 | 57 | const start = decipher.update(dataToUse); 58 | const final = decipher.final(); 59 | 60 | return Buffer.concat([start, final]); 61 | } 62 | -------------------------------------------------------------------------------- /src/entity.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLiteral, getMetadataArgsStorage } from 'typeorm'; 2 | import { ExtendedColumnOptions } from './options'; 3 | import { decryptData, encryptData } from './crypto'; 4 | 5 | /** 6 | * Encrypt fields on entity. 7 | */ 8 | export function encrypt(entity: any): any { 9 | if (!entity) { 10 | return entity; 11 | } 12 | 13 | for (let columnMetadata of getMetadataArgsStorage().columns) { 14 | let { propertyName, mode, target } = columnMetadata; 15 | let options: ExtendedColumnOptions = columnMetadata.options; 16 | let encrypt = options.encrypt; 17 | if ( 18 | encrypt && !(encrypt?.encryptionPredicate && !encrypt?.encryptionPredicate(entity)) && 19 | mode === 'regular' && 20 | (encrypt.looseMatching || entity.constructor === target) 21 | ) { 22 | if (entity[propertyName]) { 23 | entity[propertyName] = encryptData( 24 | Buffer.from(entity[propertyName], 'utf8'), 25 | encrypt 26 | ).toString('base64'); 27 | } 28 | } 29 | } 30 | return entity; 31 | } 32 | 33 | /** 34 | * Decrypt fields on entity. 35 | */ 36 | export function decrypt(entity: any): any { 37 | if (!entity) { 38 | return entity; 39 | } 40 | 41 | for (let columnMetadata of getMetadataArgsStorage().columns) { 42 | let { propertyName, mode, target } = columnMetadata; 43 | let options: ExtendedColumnOptions = columnMetadata.options; 44 | let encrypt = options.encrypt; 45 | if ( 46 | encrypt && !(encrypt?.encryptionPredicate && !encrypt?.encryptionPredicate(entity)) && 47 | mode === "regular" && 48 | (encrypt.looseMatching || entity.constructor === target) 49 | ) { 50 | if (entity[propertyName]) { 51 | entity[propertyName] = decryptData( 52 | Buffer.from(entity[propertyName], 'base64'), 53 | encrypt 54 | ).toString('utf8'); 55 | } 56 | } 57 | } 58 | return entity; 59 | } 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './subscribers'; 2 | export * from './crypto'; 3 | export * from './entity'; 4 | export * from './options'; 5 | export * from './transformer'; -------------------------------------------------------------------------------- /src/options/EncryptionOptions.ts: -------------------------------------------------------------------------------- 1 | export interface EncryptionOptions { 2 | key: string; 3 | algorithm: string; 4 | ivLength: number; 5 | iv?: string; //// For testing mainly. 6 | authTagLength?: number; 7 | looseMatching?: boolean; 8 | encryptionPredicate?: (entity: any) => boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/options/ExtendedColumnOptions.ts: -------------------------------------------------------------------------------- 1 | import { ColumnOptions } from "typeorm"; 2 | import { EncryptionOptions } from "./EncryptionOptions"; 3 | 4 | export interface ExtendedColumnOptions extends ColumnOptions { 5 | encrypt?: EncryptionOptions 6 | } -------------------------------------------------------------------------------- /src/options/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ExtendedColumnOptions"; 2 | export * from "./EncryptionOptions"; 3 | -------------------------------------------------------------------------------- /src/subscribers/AutoEncryptSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventSubscriber, 3 | EntitySubscriberInterface, 4 | InsertEvent, 5 | UpdateEvent 6 | } from 'typeorm'; 7 | import { encrypt, decrypt } from '../entity'; 8 | 9 | @EventSubscriber() 10 | export class AutoEncryptSubscriber implements EntitySubscriberInterface { 11 | /** 12 | * Encrypt before insertion. 13 | */ 14 | beforeInsert(event: InsertEvent): void { 15 | encrypt(event.entity); 16 | } 17 | 18 | /** 19 | * Encrypt before update. 20 | */ 21 | beforeUpdate(event: UpdateEvent): void { 22 | encrypt(event.entity); 23 | } 24 | 25 | /** 26 | * Decrypt after find. 27 | */ 28 | afterLoad(entity: any): void { 29 | decrypt(entity); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/subscribers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AutoEncryptSubscriber"; 2 | -------------------------------------------------------------------------------- /src/transformer.ts: -------------------------------------------------------------------------------- 1 | import { ValueTransformer, FindOperator, In, Equal, Not } from 'typeorm'; 2 | import { EncryptionOptions } from './options'; 3 | import { decryptData, encryptData } from './crypto'; 4 | 5 | export class EncryptionTransformer implements ValueTransformer { 6 | constructor(private options: EncryptionOptions) {} 7 | 8 | public from(value?: string | null): string | undefined { 9 | if (!value) { 10 | return; 11 | } 12 | 13 | return decryptData( 14 | Buffer.from(value as string, 'base64'), 15 | this.options 16 | ).toString('utf8'); 17 | } 18 | 19 | public to(value?: string | FindOperator | null): string | FindOperator | undefined { 20 | if ((value ?? null) === null) { 21 | return; 22 | } 23 | if (typeof value === 'string') { 24 | return encryptData( 25 | Buffer.from(value as string, 'utf8'), 26 | this.options 27 | ).toString('base64'); 28 | } 29 | if (!value) { 30 | return; 31 | } 32 | // Support FindOperator. 33 | // Just support "Equal", "In", "Not", and "IsNull". 34 | // Other operators aren't work correctly, because values are encrypted on the db. 35 | if (value.type === `in`) { 36 | return In((value.value as string[]).map(s => 37 | encryptData( 38 | Buffer.from(s, 'utf-8'), 39 | this.options 40 | ).toString('base64') 41 | )); 42 | } else if (value.type === 'equal') { 43 | return Equal(encryptData( 44 | Buffer.from(value.value as string, 'utf-8'), 45 | this.options 46 | ).toString('base64')); 47 | } else if (value.type === 'not') { 48 | return Not( 49 | this.to(value.child ?? value.value) 50 | ); 51 | } else if (value.type === 'isNull') { 52 | return value 53 | } else { 54 | throw new Error('Only "Equal","In", "Not", and "IsNull" are supported for FindOperator'); 55 | } 56 | } 57 | } 58 | 59 | export class JSONEncryptionTransformer implements ValueTransformer { 60 | constructor(private options: EncryptionOptions) {} 61 | 62 | public from(value?: null | any): any | undefined { 63 | if (!value || !value.encrypted) { 64 | return; 65 | } 66 | 67 | const decrypted = decryptData( 68 | Buffer.from(value.encrypted as string, 'base64'), 69 | this.options 70 | ).toString('utf8'); 71 | 72 | return JSON.parse(decrypted); 73 | } 74 | 75 | public to(value?: any | FindOperator | null): Object | FindOperator | undefined { 76 | if ((value ?? null) === null) { 77 | return; 78 | } 79 | 80 | if (typeof value === 'object' && !value?.type) { 81 | const encrypted = encryptData( 82 | Buffer.from(JSON.stringify(value) as string, 'utf8'), 83 | this.options 84 | ).toString('base64'); 85 | 86 | return { encrypted } 87 | } 88 | 89 | if (!value) { 90 | return; 91 | } 92 | 93 | // FindOperators are not supported. 94 | throw new Error('Filter operators are not supported for JSON encrypted fields'); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/active-record.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { encrypt, decrypt } from '../src/entity'; 3 | import { getConnection } from './utils'; 4 | import ColumnOptionsEntity1 from './entities/ColumnOptionsEntity1'; 5 | 6 | describe('Column Options - Active Record', function() { 7 | this.timeout(10000); 8 | 9 | before(async function () { 10 | await getConnection(); 11 | }) 12 | 13 | it('should encrypt', function() { 14 | let result = new ColumnOptionsEntity1(); 15 | result.secret = 'test'; 16 | encrypt(result); 17 | expect(result.secret).to.equal( 18 | '/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A=' 19 | ); 20 | }); 21 | 22 | it('should decrypt', function() { 23 | let result = new ColumnOptionsEntity1(); 24 | result.secret = '/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A='; 25 | decrypt(result); 26 | expect(result.secret).to.equal('test'); 27 | }); 28 | 29 | it('should automatically encrypt and decrypt', async function() { 30 | let result = new ColumnOptionsEntity1(); 31 | result.secret = 'test'; 32 | await result.save(); 33 | expect(result.secret).to.equal( 34 | '/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A=' 35 | ); 36 | 37 | let results = await ColumnOptionsEntity1.find(); 38 | expect(results.length).to.equal(1); 39 | expect(results[0].secret).to.equal('test'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { encryptData, decryptData } from '../src/crypto'; 3 | 4 | describe('Crypto', function() { 5 | it('should encrypt', function() { 6 | let result = encryptData(Buffer.from('test', 'utf8'), { 7 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 8 | algorithm: 'aes-256-cbc', 9 | ivLength: 16, 10 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 11 | }); 12 | expect(result.toString('base64')).to.equal( 13 | '/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A=' 14 | ); 15 | }); 16 | 17 | it('should decrypt', function() { 18 | let result = decryptData( 19 | Buffer.from('/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A=', 'base64'), 20 | { 21 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 22 | algorithm: 'aes-256-cbc', 23 | ivLength: 16 24 | } 25 | ); 26 | expect(result.toString('utf8')).to.equal('test'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/data-mapper.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Connection } from "typeorm"; 3 | import { getConnection } from "./utils"; 4 | import ColumnOptionsEntity2 from "./entities/ColumnOptionsEntity2"; 5 | 6 | describe("Column Options - Data Mapper", function () { 7 | let connection: Connection; 8 | 9 | this.timeout(10000); 10 | 11 | before(async function () { 12 | connection = await getConnection(); 13 | }); 14 | 15 | it("should automatically encrypt and decrypt with loose matching", async function () { 16 | const repo = connection.getRepository(ColumnOptionsEntity2); 17 | 18 | try { 19 | let result = await repo.save({ looseSecret: "test" }); 20 | expect(result.looseSecret).to.equal("/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A="); 21 | 22 | let results = await repo.find(); 23 | expect(results.length).to.equal(1); 24 | expect(results[0].looseSecret).to.equal("test"); 25 | } finally { 26 | await repo.clear(); 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/encryption-predicate.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { encrypt, decrypt } from "../src/entity"; 3 | import { getConnection } from "./utils"; 4 | import ColumnOptionsEntity4 from "./entities/ColumnOptionsEntity4"; 5 | 6 | describe("Column Options - Encryption Predicate", function () { 7 | this.timeout(10000); 8 | 9 | before(async function () { 10 | await getConnection(); 11 | }); 12 | 13 | it("should encrypt", function () { 14 | let result = new ColumnOptionsEntity4(); 15 | result.enablePredicate = true; 16 | result.secret = "test"; 17 | encrypt(result); 18 | expect(result.secret).to.equal( 19 | "/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A=" 20 | ); 21 | }); 22 | 23 | it("should not encrypt", function () { 24 | let result = new ColumnOptionsEntity4(); 25 | result.enablePredicate = false; 26 | result.secret = "test"; 27 | encrypt(result); 28 | expect(result.secret).to.equal("test"); 29 | }); 30 | 31 | it("should decrypt", function () { 32 | let result = new ColumnOptionsEntity4(); 33 | result.enablePredicate = true; 34 | result.secret = "/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A="; 35 | decrypt(result); 36 | expect(result.secret).to.equal("test"); 37 | }); 38 | 39 | it("should not decrypt", function () { 40 | let result = new ColumnOptionsEntity4(); 41 | result.enablePredicate = false; 42 | result.secret = "test"; 43 | decrypt(result); 44 | expect(result.secret).to.equal("test"); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/entities/ColumnOptionsEntity1.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { ExtendedColumnOptions } from '../../src/options'; 3 | 4 | @Entity() 5 | export default class ColumnOptionsEntity1 extends BaseEntity { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: false, 12 | encrypt: { 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-cbc', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 17 | } 18 | }) 19 | secret: string; 20 | } 21 | -------------------------------------------------------------------------------- /test/entities/ColumnOptionsEntity2.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { ExtendedColumnOptions } from '../../src/options'; 3 | 4 | @Entity() 5 | export default class ColumnOptionsEntity2 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: false, 12 | encrypt: { 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-cbc', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56', 17 | looseMatching: true 18 | } 19 | }) 20 | looseSecret: string; 21 | } 22 | -------------------------------------------------------------------------------- /test/entities/ColumnOptionsEntity3.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 2 | import { ExtendedColumnOptions } from "../../src/options"; 3 | 4 | @Entity() 5 | export default class ColumnOptionsEntity3 extends BaseEntity { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: "varchar", 11 | nullable: false, 12 | encrypt: { 13 | key: "e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61", 14 | algorithm: "aes-256-cbc", 15 | ivLength: 16, 16 | iv: "ff5ac19190424b1d88f9419ef949ae56", 17 | }, 18 | }) 19 | secret: string; 20 | } 21 | -------------------------------------------------------------------------------- /test/entities/ColumnOptionsEntity4.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 2 | import { ExtendedColumnOptions } from "../../src/options"; 3 | 4 | @Entity() 5 | export default class ColumnOptionsEntity4 extends BaseEntity { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ type: "boolean" }) 10 | enablePredicate: boolean; 11 | 12 | @Column({ 13 | type: "varchar", 14 | nullable: false, 15 | encrypt: { 16 | key: "e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61", 17 | algorithm: "aes-256-cbc", 18 | ivLength: 16, 19 | iv: "ff5ac19190424b1d88f9419ef949ae56", 20 | encryptionPredicate: (entity: ColumnOptionsEntity4) => 21 | entity.enablePredicate, 22 | }, 23 | }) 24 | secret: string; 25 | } 26 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsAES256GCMEntity1.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { EncryptionTransformer } from '../../src/transformer'; 3 | 4 | @Entity({ name: "transformer_options_aes256gcm_entity1" }) 5 | export default class TransformerOptionsAES256GCMEntity1 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: false, 12 | transformer: new EncryptionTransformer({ 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-gcm', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 17 | }) 18 | }) 19 | secret: string; 20 | } 21 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsAES256GCMEntity2.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { EncryptionTransformer } from '../../src/transformer'; 3 | 4 | @Entity({ name: "transformer_options_aes256gcm_entity2" }) 5 | export default class TransformerOptionsAES256GCMEntity2 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: false, 12 | transformer: new EncryptionTransformer({ 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-gcm', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56', 17 | authTagLength: 8 18 | }) 19 | }) 20 | secret: string; 21 | } 22 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsEntity1.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { EncryptionTransformer } from '../../src/transformer'; 3 | 4 | @Entity() 5 | export default class TransformerOptionsEntity1 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: false, 12 | transformer: new EncryptionTransformer({ 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-cbc', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 17 | }) 18 | }) 19 | secret: string; 20 | } 21 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsEntity2.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 2 | import { EncryptionTransformer } from "../../src/transformer"; 3 | 4 | @Entity() 5 | export default class TransformerOptionsEntity2 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: "varchar", 11 | nullable: false, 12 | transformer: new EncryptionTransformer({ 13 | key: "e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61", 14 | algorithm: "aes-256-cbc", 15 | ivLength: 16, 16 | iv: "ff5ac19190424b1d88f9419ef949ae56", 17 | }), 18 | }) 19 | secret: string; 20 | } 21 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsEntity3.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { ExtendedColumnOptions } from '../../src/options'; 3 | import { EncryptionTransformer } from '../../src/transformer'; 4 | 5 | @Entity() 6 | export default class TransformerOptionsEntity3 extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column({ 11 | type: 'varchar', 12 | nullable: false, 13 | transformer: new EncryptionTransformer({ 14 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 15 | algorithm: 'aes-256-cbc', 16 | ivLength: 16, 17 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 18 | }) 19 | }) 20 | secret: string; 21 | } 22 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsEntityEmptyString1.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { EncryptionTransformer } from '../../src/transformer'; 3 | 4 | @Entity() 5 | export default class TransformerOptionsEntityEmptyString1 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: false, 12 | transformer: new EncryptionTransformer({ 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-cbc', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 17 | }) 18 | }) 19 | secret: string; 20 | } 21 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsEntityNullable1.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { EncryptionTransformer } from '../../src/transformer'; 3 | 4 | @Entity() 5 | export default class TransformerOptionsEntityNullable1 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: true, 12 | transformer: new EncryptionTransformer({ 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-cbc', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 17 | }) 18 | }) 19 | secret: string|null; 20 | } 21 | -------------------------------------------------------------------------------- /test/entities/TransformerOptionsEntityNullable2.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import { EncryptionTransformer } from '../../src/transformer'; 3 | 4 | @Entity() 5 | export default class TransformerOptionsEntityNullable2 { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ 10 | type: 'varchar', 11 | nullable: true, 12 | transformer: new EncryptionTransformer({ 13 | key: 'e41c966f21f9e1577802463f8924e6a3fe3e9751f201304213b2f845d8841d61', 14 | algorithm: 'aes-256-cbc', 15 | ivLength: 16, 16 | iv: 'ff5ac19190424b1d88f9419ef949ae56' 17 | }) 18 | }) 19 | secret?: string; 20 | } 21 | -------------------------------------------------------------------------------- /test/find-operator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Connection, In, Not, IsNull, Equal, Like, LessThan } from "typeorm"; 3 | import { getConnection } from "./utils"; 4 | import TransformerOptionsEntity3 from "./entities/TransformerOptionsEntity3"; 5 | 6 | describe("Find operator", function () { 7 | let connection: Connection; 8 | 9 | this.timeout(10000); 10 | 11 | before(async function () { 12 | connection = await getConnection(); 13 | }); 14 | it("should find by supported FindOperator", async function () { 15 | const repo = connection.getRepository(TransformerOptionsEntity3); 16 | 17 | try { 18 | const secret1 = "test1"; 19 | const secret2 = "test2"; 20 | await repo.save([{ secret: secret1 }, { secret: secret2 }]); 21 | // Where in 22 | const whereIn = await repo.find({ 23 | where: { 24 | secret: In([secret1, secret2]), 25 | }, 26 | }); 27 | expect(whereIn.length).to.equal(2); 28 | expect(whereIn[0].secret).to.equal(secret1); 29 | expect(whereIn[1].secret).to.equal(secret2); 30 | // Where not 31 | const whereNot = await repo.find({ 32 | where: { 33 | secret: Not(secret2), 34 | }, 35 | }); 36 | expect(whereNot.length).to.equal(1); 37 | expect(whereNot[0].secret).to.equal(secret1); 38 | // Where equal 39 | const whereEqual = await repo.find({ 40 | where: { 41 | secret: Equal(secret1), 42 | }, 43 | }); 44 | expect(whereEqual.length).to.equal(1); 45 | expect(whereEqual[0].secret).to.equal(secret1); 46 | // Where not in 47 | const whereNotIn = await repo.find({ 48 | where: { 49 | secret: Not(In([secret2])), 50 | }, 51 | }); 52 | expect(whereNotIn.length).to.equal(1); 53 | expect(whereNotIn[0].secret).to.equal(secret1); 54 | // Where IsNull 55 | const whereIsNull = await repo.find({ 56 | where: { 57 | secret: IsNull(), 58 | }, 59 | }); 60 | expect(whereIsNull.length).to.equal(0); 61 | } finally { 62 | await repo.clear(); 63 | } 64 | }); 65 | it("should throw error by not supported FindOperator", async function () { 66 | const repo = connection.getRepository(TransformerOptionsEntity3); 67 | 68 | try { 69 | const secret1 = "test3"; 70 | const secret2 = "test4"; 71 | await repo.save([{ secret: secret1 }, { secret: secret2 }]); 72 | // Can't use FindOperator except supported ones 73 | for (const notSupportedOperator of [LessThan(secret1), Like(secret1)]) { 74 | await repo 75 | .find({ 76 | where: { 77 | secret: notSupportedOperator, 78 | }, 79 | }) 80 | .then( 81 | () => { 82 | throw new Error("Never resolved"); 83 | }, 84 | (reason) => { 85 | expect(reason.message).to.equal( 86 | 'Only "Equal","In", "Not", and "IsNull" are supported for FindOperator' 87 | ); 88 | } 89 | ); 90 | } 91 | } finally { 92 | await repo.clear(); 93 | } 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Connection } from "typeorm"; 3 | import { getConnection } from "./utils"; 4 | import TransformerOptionsEntityEmptyString1 from "./entities/TransformerOptionsEntityEmptyString1"; 5 | import TransformerOptionsEntity1 from "./entities/TransformerOptionsEntity1"; 6 | import TransformerOptionsEntityNullable1 from "./entities/TransformerOptionsEntityNullable1"; 7 | import TransformerOptionsEntityNullable2 from "./entities/TransformerOptionsEntityNullable2"; 8 | import TransformerOptionsAES256GCMEntity1 from "./entities/TransformerOptionsAES256GCMEntity1"; 9 | import TransformerOptionsAES256GCMEntity2 from "./entities/TransformerOptionsAES256GCMEntity2"; 10 | 11 | describe("Transformer", function () { 12 | let connection: Connection; 13 | 14 | this.timeout(10000); 15 | 16 | before(async function () { 17 | connection = await getConnection(); 18 | }); 19 | 20 | it("should automatically encrypt and decrypt", async function () { 21 | const manager = connection.manager; 22 | const repo = connection.getRepository(TransformerOptionsEntity1); 23 | const instance = await repo.create({ secret: "test" }); 24 | 25 | try { 26 | await repo.save(instance); 27 | 28 | const result = await manager.query("SELECT secret FROM transformer_options_entity1"); 29 | 30 | expect(result[0].secret).to.equal("/1rBkZBCSx2I+UGe+UmuVhKzmHsDDv0EvRtMBFiaE3A="); 31 | 32 | const results = await repo.find(); 33 | 34 | expect(results.length).to.equal(1); 35 | expect(results[0].secret).to.equal("test"); 36 | } finally { 37 | await repo.clear(); 38 | } 39 | }); 40 | 41 | it("should automatically encrypt and decrypt aes-256-gcm", async function () { 42 | const manager = connection.manager; 43 | const repo = connection.getRepository(TransformerOptionsAES256GCMEntity1); 44 | const instance = await repo.create({ secret: "test" }); 45 | 46 | try { 47 | await repo.save(instance); 48 | 49 | const result = await manager.query("SELECT secret FROM transformer_options_aes256gcm_entity1"); 50 | 51 | expect(result[0].secret).to.equal("/1rBkZBCSx2I+UGe+UmuVpb6nX/euiab5zJklG0vyFvOSkuV"); 52 | 53 | const results = await repo.find(); 54 | 55 | expect(results.length).to.equal(1); 56 | expect(results[0].secret).to.equal("test"); 57 | } finally { 58 | await repo.clear(); 59 | } 60 | }); 61 | 62 | it("should automatically encrypt and decrypt aes-256-gcm with 8-byte long auth tag length", async function () { 63 | const manager = connection.manager; 64 | const repo = connection.getRepository(TransformerOptionsAES256GCMEntity2); 65 | const instance = await repo.create({ secret: "test" }); 66 | 67 | try { 68 | await repo.save(instance); 69 | 70 | // console.log(await manager.query("SELECT name FROM sqlite_master WHERE type='table'")); 71 | 72 | const result = await manager.query("SELECT secret FROM transformer_options_aes256gcm_entity2"); 73 | 74 | expect(result[0].secret).to.equal("/1rBkZBCSx2I+UGe+UmuVpb6nX/euiabzkpLlQ=="); 75 | 76 | const results = await repo.find(); 77 | 78 | expect(results.length).to.equal(1); 79 | expect(results[0].secret).to.equal("test"); 80 | } finally { 81 | await repo.clear(); 82 | } 83 | }); 84 | 85 | it("should not encrypt / decrypt null values", async function () { 86 | const manager = connection.manager; 87 | const repo = connection.getRepository(TransformerOptionsEntityNullable1); 88 | const instance = await repo.create({ secret: null }); 89 | 90 | try { 91 | await repo.save(instance); 92 | 93 | const result = await manager.query("SELECT secret FROM transformer_options_entity_nullable1"); 94 | 95 | expect(result[0].secret).to.equal(null); 96 | 97 | const results = await repo.find(); 98 | 99 | expect(results.length).to.equal(1); 100 | expect(results[0].secret).to.equal(undefined); 101 | } finally { 102 | await repo.clear(); 103 | } 104 | }); 105 | 106 | it("should not encrypt / decrypt undefined values", async function () { 107 | const manager = connection.manager; 108 | const repo = connection.getRepository(TransformerOptionsEntityNullable2); 109 | const instance = await repo.create({ secret: undefined }); 110 | await repo.save(instance); 111 | 112 | const result = await manager.query("SELECT secret FROM transformer_options_entity_nullable2"); 113 | 114 | expect(result[0].secret).to.equal(null); 115 | 116 | const results = await repo.find(); 117 | 118 | expect(results.length).to.equal(1); 119 | expect(results[0].secret).to.equal(undefined); 120 | }); 121 | 122 | it("should encrypt / decrypt empty strings", async function () { 123 | const manager = connection.manager; 124 | const repo = connection.getRepository(TransformerOptionsEntityEmptyString1); 125 | const instance = await repo.create({ secret: "" }); 126 | 127 | try { 128 | await repo.save(instance); 129 | 130 | const result = await manager.query("SELECT secret FROM transformer_options_entity_empty_string1"); 131 | 132 | expect(result[0].secret).to.equal("/1rBkZBCSx2I+UGe+UmuVvnjveB6onZ3uoKZCOZfzbk="); 133 | 134 | const results = await repo.find(); 135 | 136 | expect(results.length).to.equal(1); 137 | expect(results[0].secret).to.equal(""); 138 | } finally { 139 | await repo.clear(); 140 | } 141 | }); 142 | 143 | it("should decrypt in QueryBuilder", async function () { 144 | const manager = connection.manager; 145 | const repo = connection.getRepository(TransformerOptionsEntity1); 146 | const instance = await repo.create({ secret: "QueryBuilder" }); 147 | 148 | try { 149 | await repo.save(instance); 150 | 151 | const result = await manager.query("SELECT secret FROM transformer_options_entity1"); 152 | 153 | expect(result[0].secret).to.equal("/1rBkZBCSx2I+UGe+UmuVoscCrVZPUm9+v40quDkL+0="); 154 | 155 | const results = await repo.createQueryBuilder("query").getMany(); 156 | 157 | expect(results[0].secret).to.equal("QueryBuilder"); 158 | } finally { 159 | await repo.clear(); 160 | } 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { createConnection, Connection } from "typeorm"; 2 | 3 | import { AutoEncryptSubscriber } from "../src/subscribers"; 4 | 5 | let CONNECTION: Connection; 6 | let LOCK = false; 7 | 8 | export async function getConnection(): Promise { 9 | try { 10 | while (LOCK) { 11 | await new Promise((resolve) => setTimeout(resolve, 100)); 12 | } 13 | 14 | LOCK = true; 15 | 16 | if (!CONNECTION) { 17 | CONNECTION = await createConnection({ 18 | "type": "sqlite", 19 | "database": `/tmp/test.${process.pid}.sqlite`, 20 | "synchronize": true, 21 | "logging": false, 22 | "entities": [ 23 | "**/entities/**/*.ts" 24 | ], 25 | "subscribers": [ AutoEncryptSubscriber ] 26 | }); 27 | } 28 | 29 | return CONNECTION; 30 | } finally { 31 | LOCK = false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.1", 3 | "compilerOptions": { 4 | "lib": ["es5", "es6", "esnext.asynciterable"], 5 | "baseUrl": "src", 6 | "outDir": "lib", 7 | "target": "es6", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": true, 13 | "noImplicitAny": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitReturns": true, 16 | "stripInternal": true, 17 | "pretty": true, 18 | "strictNullChecks": true, 19 | "noUnusedLocals": true, 20 | "declaration": true, 21 | "downlevelIteration": true 22 | }, 23 | "include": ["src", "test"], 24 | "exclude": ["tmp", "temp", "lib", "node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-unused-variable": true, 13 | "no-duplicate-variable": true, 14 | "no-eval": true, 15 | "no-internal-module": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "double" 25 | ], 26 | "semicolon": true, 27 | "triple-equals": [ 28 | true, 29 | "allow-null-check" 30 | ], 31 | "typedef-whitespace": [ 32 | true, 33 | { 34 | "call-signature": "nospace", 35 | "index-signature": "nospace", 36 | "parameter": "nospace", 37 | "property-declaration": "nospace", 38 | "variable-declaration": "nospace" 39 | } 40 | ], 41 | "variable-name": [ 42 | true, 43 | "ban-keywords" 44 | ], 45 | "whitespace": [ 46 | true, 47 | "check-branch", 48 | "check-decl", 49 | "check-operator", 50 | "check-separator", 51 | "check-type" 52 | ] 53 | } 54 | } 55 | --------------------------------------------------------------------------------