├── .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 |
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 |
--------------------------------------------------------------------------------