├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs └── README.md ├── index.d.ts ├── index.js ├── lib ├── backend │ ├── boringcrypto.js │ ├── fipsrypto.js │ ├── key │ │ └── symmetrickey.js │ └── moderncrypto.js ├── blindindex.js ├── ciphersweet.js ├── compoundindex.js ├── constants.js ├── contract │ ├── backend.js │ ├── keyprovider.js │ ├── multitenantawareprovider.js │ ├── rowtransformation.js │ ├── transform.js │ └── transformation.js ├── encryptedfield.js ├── encryptedfile.js ├── encryptedmultirows.js ├── encryptedrow.js ├── exception │ ├── arraykeyexception.js │ ├── blindindexnamecollisionexception.js │ ├── blindindexnotfoundexception.js │ ├── ciphersweetexception.js │ ├── cryptooperationexception.js │ ├── invalidciphertextexception.js │ └── plannerexception.js ├── keyprovider │ ├── multitenantprovider.js │ └── stringprovider.js ├── keyrotation │ ├── fieldrotator.js │ ├── multirowsrotator.js │ └── rowrotator.js ├── planner │ └── fieldindexplanner.js ├── transformation │ ├── alphacharactersonly.js │ ├── compound.js │ ├── firstcharacter.js │ ├── lastfourdigits.js │ └── lowercase.js └── util.js ├── package-lock.json ├── package.json └── test ├── boringcrypto-test.js ├── brng-encrypted.txt ├── encryptedfield-test.js ├── encryptedfile-test.js ├── encryptedmultirows-test.js ├── encryptedrow-test.js ├── fieldindexplanner-test.js ├── fips-encrypted.txt ├── fipscrypto-test.js ├── moderncrypto-test.js ├── multitenant-test.js ├── multitenant └── example-keyprovider.js ├── nacl-encrypted.txt ├── rotator-test.js └── util-test.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 13.x, 14.x, 15.x, 16.x, 17.x, 18.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm ci 25 | - run: npm run build --if-present 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | /package-lock.json 4 | /test/* 5 | !/test/*.js 6 | !/test/brng-encrypted.txt 7 | !/test/nacl-encrypted.txt 8 | !/test/fips-encrypted.txt 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /docs 3 | /node_modules 4 | /test/* 5 | !/test/*.js 6 | !/test/brng-encrypted.txt 7 | !/test/nacl-encrypted.txt 8 | !/test/fips-encrypted.txt 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ISC License 3 | * 4 | * Copyright (c) 2018 - 2019 5 | * Paragon Initiative Enterprises 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CipherSweet.js 2 | 3 | [![Build Status](https://github.com/paragonie/ciphersweet-js/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/ciphersweet-js/actions) 4 | [![npm version](https://img.shields.io/npm/v/ciphersweet-js.svg)](https://npm.im/ciphersweet-js) 5 | 6 | A JavaScript port of [CipherSweet](https://github.com/paragonie/ciphersweet), which is a PHP library that implements 7 | [searchable field-level encryption](https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql). 8 | 9 | ---- 10 | 11 | Before adding searchable encryption support to your project, make sure you understand 12 | the [appropriate threat model](https://adamcaudill.com/2016/07/20/threat-modeling-for-applications/) 13 | for your use case. At a minimum, you will want your application and database 14 | server to be running on separate cloud instances / virtual machines. 15 | (Even better: Separate bare-metal hardware.) 16 | 17 | CipherSweet is available under the very permissive [ISC License](https://github.com/paragonie/ciphersweet/blob/master/LICENSE) 18 | which allows you to use CipherSweet in any of your JavaScript projects, commercial 19 | or noncommercial, open source or proprietary, at no cost to you. 20 | 21 | ## CipherSweet Features at a Glance 22 | 23 | * Encryption that targets the 256-bit security level 24 | (using [AEAD](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken) modes 25 | with extended nonces to minimize users' rekeying burden). 26 | * **Compliance-Specific Protocol Support.** Multiple backends to satisfy a 27 | diverse range of compliance requirements. More can be added as needed: 28 | * `ModernCrypto` uses [libsodium](https://download.libsodium.org/doc/), the de 29 | facto standard encryption library for software developers. 30 | [Algorithm details](https://ciphersweet.paragonie.com/security#moderncrypto). 31 | * `FIPSCrypto` only uses the cryptographic algorithms covered by the 32 | FIPS 140-2 recommendations to avoid auditing complexity. 33 | [Algorithm details](https://ciphersweet.paragonie.com/security#fipscrypto). 34 | * **Key separation.** Each column is encrypted with a different key, all of which are derived from 35 | your master encryption key using secure key-splitting algorithms. 36 | * **Key management integration.** CipherSweet supports integration with Key 37 | Management solutions for storing and retrieving the master encryption key. 38 | * **Searchable Encryption.** CipherSweet uses 39 | [blind indexing](https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql#solution-literal-search) 40 | with the fuzzier and Bloom filter strategies to allow fast ciphertext search 41 | with [minimal data leakage](https://ciphersweet.paragonie.com/node.js/blind-index-planning). 42 | * Each blind index on each column uses a distinct key from your encryption key 43 | and each other blind index key. 44 | * This doesn't allow for `LIKE` operators or regular expression searching, but 45 | it does allow you to index transformations (e.g. substrings) of the plaintext, 46 | hashed under a distinct key. 47 | * **Adaptability.** CipherSweet has a database- and product-agnostic design, so 48 | it should be easy to write an adapter to use CipherSweet in any PHP-based 49 | software. 50 | * **File/stream encryption.** CipherSweet has an API for encrypting files (or 51 | other PHP streams) that provides authenticated encryption that defeats TOCTOU 52 | attacks with minimal overhead. [Learn more](https://ciphersweet.paragonie.com/internals/file-encryption). 53 | 54 | ## Install Instructions 55 | 56 | ``` 57 | npm install ciphersweet-js 58 | ``` 59 | 60 | **Optional:** 61 | 62 | CipherSweet uses [Sodium-Plus](https://github.com/paragonie/sodium-plus) internally. 63 | The default Sodium-Plus backend is cross-platform, but you can obtain greater 64 | performance by installing `sodium-native` too. 65 | 66 | ```terminal 67 | npm install --save sodium-native 68 | ``` 69 | 70 | This isn't strictly necessary, and sodium-native doesn't work in browsers, but 71 | if you're not targeting browsers, you can get a significant performance boost. 72 | 73 | ## Documentation 74 | 75 | The [**CipherSweet.js documentation**](https://ciphersweet.paragonie.com/node.js) is 76 | available online at `https://ciphersweet.paragonie.com`. 77 | 78 | ## Support Contracts 79 | 80 | If your company uses this library in their products or services, you may be 81 | interested in [purchasing a support contract from Paragon Initiative Enterprises](https://paragonie.com/enterprise). 82 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # CipherSweet (JavaScript) 2 | 3 | ## Setting up CipherSweet at Run-Time 4 | 5 | ### Select Your Backend 6 | 7 | First, you'll need to decide if you have any strict operational requirements for 8 | your encryption. This mostly boils down to whether or not you need all 9 | encryption to be FIPS 140-2 compliant or not, in which case, you'll need to use 10 | the `FIPSCrypto` backend. 11 | 12 | If you aren't sure, the answer is that you probably don't, and feel free to use 13 | `ModernCrypto` instead. 14 | 15 | ```javascript 16 | const FIPSCrypto = require('ciphersweet-js').FIPSCrypto; 17 | const ModernCrypto = require('ciphersweet-js').ModernCrypto; 18 | 19 | let fips = new FIPSCrypto(); // Use only FIPS 140-2 algorithms 20 | let nacl = new ModernCrypto(); // Uses libsodium 21 | ``` 22 | 23 | ### Define your Key Provider 24 | 25 | After you choose your backend, you'll need a KeyProvider. We provide a few 26 | out-of-the-box, but we also provide an interface that can be used to integrate 27 | with any key management service in your code. 28 | 29 | The simplest example of this is the `StringProvider`, which accepts a 30 | string containing your encryption key: 31 | 32 | ```javascript 33 | const StringProvider = require('ciphersweet-js').StringProvider; 34 | 35 | let provider = new StringProvider( 36 | // Example key, chosen randomly, hex-encoded: 37 | '4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc' 38 | ); 39 | ``` 40 | 41 | You can pass a raw binary string, hex-encoded string, or 42 | base64url-encoded string to the `StringProvider` constructor, 43 | provided the decoded key is 256 bits. 44 | 45 | Attempting to pass a key of an invalid size (i.e. not 256-bit) will 46 | result in a `CryptoOperationException` being thrown. The recommended 47 | way to generate a key is: 48 | 49 | ```javascript 50 | const sodium = require('sodium-native'); 51 | let keyMaterial = Buffer.alloc(32, 0); 52 | sodium.randombytes_buf(keyMaterial); 53 | 54 | console.log(keyMaterial.toString('hex')); 55 | ``` 56 | 57 | ### Start Your Engines 58 | 59 | Once you have these two, you can actually start the engine (`CipherSweet`). 60 | Building on the previous code example: 61 | 62 | ```javascript 63 | const {StringProvider, CipherSweet} = require('ciphersweet-js'); 64 | 65 | let provider = new StringProvider( 66 | // Example key, chosen randomly, hex-encoded: 67 | '4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc' 68 | ); 69 | let engine = new CipherSweet(provider); 70 | ``` 71 | 72 | If you want to use FIPSCrypto instead of ModernCrypto, you just need to pass 73 | it as the second argument of the `CipherSweet` constructor. The default is 74 | `ModernCrypto`. 75 | 76 | ```javascript 77 | const {FIPSCrypto, StringProvider, CipherSweet} = require('ciphersweet-js'); 78 | 79 | let provider = new StringProvider( 80 | // Example key, chosen randomly, hex-encoded: 81 | '4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc' 82 | ); 83 | let engine = new CipherSweet(provider, new FIPSCrypto()); 84 | ``` 85 | 86 | ## Basic CipherSweet Usage 87 | 88 | The JavaScript API [mirrors the PHP API](https://github.com/paragonie/ciphersweet/tree/master/docs#basic-ciphersweet-usage), 89 | except that many of our APIs are `async` functions and therefore return a `Promise` if you don't use `await`. 90 | 91 | For example: 92 | 93 | ```javascript 94 | const { 95 | BlindIndex, 96 | CipherSweet, 97 | CompoundIndex, 98 | EncryptedRow, 99 | LastFourDigits, 100 | StringProvider 101 | } = require('ciphersweet-js'); 102 | 103 | let provider = new StringProvider( 104 | // Example key, chosen randomly, hex-encoded: 105 | '4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc' 106 | ); 107 | let engine = new CipherSweet(provider); 108 | 109 | // Using the EncryptedRow abstraction: 110 | let contactEncrypter = new EncryptedRow(engine, 'contacts') 111 | .addTextField('first_name') 112 | .addTextField('last_name') 113 | .addBooleanField('hiv_status') 114 | .addTextField('insurance_id') 115 | .addTextField('ssn') 116 | .addCompoundIndex( 117 | new CompoundIndex( 118 | 'ssn_insurance_id_last4', 119 | ['insurance_id', 'ssn'], 120 | 16 121 | ) 122 | ) 123 | .addBlindIndex('insurance_id', new BlindIndex('insurance_id_idx', [], 8)) 124 | .addBlindIndex('ssn', new BlindIndex('ssn_last4_idx', [new LastFourDigits()], 8)); 125 | 126 | // An example row that we might want to store, encrypting some fields in the process... 127 | let exampleRow = { 128 | "id": 12345, 129 | "first_name": "Harvey", 130 | "last_name": "Dent", 131 | "hiv_status": false, 132 | "insurance_id": "A1234-567-89012", 133 | "ssn": "123-45-6789" 134 | }; 135 | 136 | // You can simply use the promisified API, like so: 137 | contactEncrypter.prepareRowForStorage(exampleRow).then( 138 | function (encryptedRow, indexes) { 139 | console.log(encryptedRow, indexes); 140 | } 141 | ); 142 | 143 | // Alternatively, if wrapped in an async function, use await instead: 144 | (async function() { 145 | [encryptedRow, indexes] = await contactEncrypter.prepareRowForStorage(exampleRow); 146 | })(); 147 | ``` 148 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ciphersweet-js" { 2 | export interface EncryptionBackend { 3 | multiTenantSafe(): boolean; 4 | getFileEncryptionSaltOffset(): number; 5 | decrypt(ciphertext: string, key: SymmetricKey, aad?: string): Promise; 6 | encrypt(plaintext: string|Buffer, key: SymmetricKey, aad?: string): Promise; 7 | } 8 | export class BoringCrypto implements EncryptionBackend { 9 | public multiTenantSafe(): true; 10 | public getFileEncryptionSaltOffset(): number; 11 | public decrypt(ciphertext: string, key: SymmetricKey, aad?: string): Promise; 12 | public encrypt(plaintext: string|Buffer, key: SymmetricKey, aad?: string): Promise; 13 | } 14 | export class FIPSCrypto implements EncryptionBackend { 15 | public multiTenantSafe(): true; 16 | public getFileEncryptionSaltOffset(): number; 17 | public decrypt(ciphertext: string, key: SymmetricKey, aad?: string): Promise; 18 | public encrypt(plaintext: string|Buffer, key: SymmetricKey, aad?: string): Promise; 19 | } 20 | export class ModernCrypto implements EncryptionBackend { 21 | public multiTenantSafe(): false; 22 | public getFileEncryptionSaltOffset(): number; 23 | public decrypt(ciphertext: string, key: SymmetricKey, aad?: string): Promise; 24 | public encrypt(plaintext: string|Buffer, key: SymmetricKey, aad?: string): Promise; 25 | } 26 | 27 | export interface CalculatedBlindIndex { 28 | type: string; 29 | value: string; 30 | } 31 | 32 | export class FieldIndexPlanner { 33 | public setEstimatedPopulation(population: number); 34 | public addExistingIndex( 35 | indexName: string, 36 | outputSizeInBits: number, 37 | bloomFilterSizeInBits: number 38 | ); 39 | public recommend( 40 | inputDomainInBits?: number 41 | ): { min: number; max: number }; 42 | } 43 | 44 | // Encrypted Field API 45 | export type FieldStorageTuple = [string, { [indexName: string]: string }]; 46 | 47 | export class EncryptedField { 48 | constructor( 49 | engine: CipherSweet, 50 | tableName: string, 51 | fieldName: string, 52 | usedTypedIndexes?: boolean 53 | ); 54 | public setTypedIndexes(enable: boolean); 55 | 56 | public addBlindIndex( 57 | blindIndex: BlindIndex, 58 | indexName?: string 59 | ): EncryptedField; 60 | 61 | public getBlindIndex( 62 | plaintext: string, 63 | indexName: string 64 | ): Promise; 65 | public getAllBlindIndexes( 66 | plaintext: string 67 | ): Promise<{ [indexName: string]: CalculatedBlindIndex }>; 68 | 69 | public encryptValue(plaintext: string, aad?: string): Promise; 70 | public decryptValue(ciphertext: string, aad?: string): Promise; 71 | 72 | // Returns [ciphertext, indexes] 73 | public prepareForStorage( 74 | plaintext: string, 75 | aad?: string 76 | ): Promise; 77 | } 78 | 79 | export interface ModernCryptoHashConfig { 80 | opslimit: number; 81 | memlimit: number; 82 | } 83 | 84 | export interface BoringCryptoHashConfig { 85 | opslimit: number; 86 | memlimit: number; 87 | } 88 | 89 | export interface FIPSCryptoHashConfig { 90 | iterations: number; 91 | } 92 | 93 | export class BlindIndex { 94 | constructor( 95 | name: string, 96 | transforms: Transformation[], 97 | bloomFilterSizeInBits?: number, 98 | fastHash?: boolean, 99 | config?: ModernCryptoHashConfig | FIPSCryptoHashConfig | BoringCryptoHashConfig 100 | ); 101 | } 102 | 103 | export class CompoundIndex { 104 | constructor( 105 | indexName: string, 106 | fieldNames: string[], 107 | bloomFilterSizeInBits?: number, 108 | fastHash?: boolean, 109 | config?: ModernCryptoHashConfig | FIPSCryptoHashConfig | BoringCryptoHashConfig 110 | ); 111 | public addTransform( 112 | fieldName: string, 113 | transform: Transformation 114 | ): CompoundIndex; 115 | public addRowTransform(transform: RowTransformation): CompoundIndex; 116 | } 117 | 118 | // Encrypted Row API 119 | export type RowStorageTuple = [any, { [fieldName: string]: any }]; 120 | 121 | export class EncryptedRow { 122 | constructor(engine: CipherSweet, tableName: string); 123 | public setTypedIndexes(boolean); 124 | public setFlatIndexes(boolean); 125 | public setAadSourceField(fieldName: string); 126 | 127 | public addTextField(fieldName: string, aad?: string): EncryptedRow; 128 | public addBooleanField(fieldName: string, aad?: string): EncryptedRow; 129 | public addFloatField(fieldName: string, aad?: string): EncryptedRow; 130 | 131 | public addBlindIndex(fieldName: string, index: BlindIndex); 132 | public addCompoundIndex(index: CompoundIndex); 133 | public createCompoundIndex( 134 | indexName: string, 135 | fieldNames: string[], 136 | bloomFilterSizeInBits?: number, 137 | fastHash?: boolean, 138 | config?: ModernCryptoHashConfig | FIPSCryptoHashConfig 139 | ): CompoundIndex; 140 | 141 | public decryptRow(row: Map): Promise>; 142 | public encryptRow(row: Map): Promise>; 143 | public prepareRowForStorage(row: any): Promise; 144 | } 145 | 146 | export class RowTransformation { 147 | /** 148 | * @param {Array} input 149 | * @return {string} 150 | */ 151 | public invoke(input: any): Promise; 152 | 153 | /** 154 | * @param {Array|Object} input 155 | * @return {string} 156 | */ 157 | public static processArray(input: any): Promise; 158 | } 159 | 160 | // Encrypted Multi Rows 161 | export type MultiRowStorageTuple = [ 162 | { [tableName: string]: { [fieldName: string]: any } }, 163 | { [tableName: string]: { [indexName: string]: any } } 164 | ]; 165 | 166 | export class EncryptedMultiRows { 167 | constructor(engine: CipherSweet); 168 | 169 | public setAadSourceField( 170 | tableName: string, 171 | indexName: string, 172 | aadFieldName: string 173 | ); 174 | 175 | public addTextField( 176 | tableName: string, 177 | fieldName: string, 178 | aad?: string 179 | ): EncryptedMultiRows; 180 | public addBooleanField( 181 | tableName: string, 182 | fieldName: string, 183 | aad?: string 184 | ): EncryptedMultiRows; 185 | public addFloatField( 186 | tableName: string, 187 | fieldName: string, 188 | aad?: string 189 | ): EncryptedMultiRows; 190 | 191 | public addCompoundIndex(tableName: string, index: CompoundIndex); 192 | public createCompoundIndex( 193 | tableName: string, 194 | indexName: string, 195 | fieldNames: string[], 196 | bloomFilterSizeInBits?: number, 197 | fastHash?: boolean, 198 | config?: ModernCryptoHashConfig | FIPSCryptoHashConfig 199 | ): CompoundIndex; 200 | 201 | public prepareForStorage(input: any): Promise; 202 | } 203 | 204 | // Encrypted Files 205 | export class EncryptedFile { 206 | constructor(engine: CipherSweet); 207 | 208 | public isFileEncrypted(inputPath: string): Promise; 209 | public encryptFile( 210 | inputPath: string, 211 | outputPath: string 212 | ): Promise; 213 | public decryptFile( 214 | inputPath: string, 215 | outputPath: string 216 | ): Promise; 217 | public encryptFileWithPassword( 218 | inputPath: string, 219 | outputPath: string, 220 | password: string 221 | ): Promise; 222 | public decryptFileWithPassword( 223 | inputPath: string, 224 | outputPath: string, 225 | password: string 226 | ): Promise; 227 | } 228 | 229 | // Field Rotation 230 | export class FieldRotator { 231 | constructor(oldField: EncryptedField, newField: EncryptedField); 232 | 233 | public needsReEncrypt(ciphertext: string): boolean; 234 | public prepareForUpdate( 235 | ciphertext: string, 236 | oldAuthenticationTag?: string, 237 | newAuthenticationTag?: string 238 | ): FieldStorageTuple; 239 | } 240 | 241 | export class RowRotator { 242 | constructor(oldField: EncryptedRow, newField: EncryptedRow); 243 | 244 | public needsReEncrypt(ciphertext: string): boolean; 245 | public prepareForUpdate(ciphertext: string): RowStorageTuple; 246 | } 247 | 248 | export class MultiRowsRotator { 249 | constructor(oldField: EncryptedMultiRows, newField: EncryptedMultiRows); 250 | 251 | public needsReEncrypt(ciphertext: string): boolean; 252 | public prepareForUpdate(ciphertext: string): MultiRowStorageTuple; 253 | } 254 | 255 | // Transforms 256 | export class Transform {} // leaving in for backwards compat 257 | export class Transformation extends Transform {} 258 | export class LastFourDigits extends Transformation {} 259 | export class AlphaCharactersOnly extends Transformation {} 260 | export class FirstCharacter extends Transformation {} 261 | export class Lowercase extends Transformation {} 262 | 263 | // Key Providers 264 | export class SymmetricKey { 265 | constructor(rawKeyMaterial: string | Buffer); 266 | static isSymmetricKey(key: any): boolean; 267 | getRawKey(): Buffer; 268 | } 269 | 270 | export class KeyProvider { 271 | getSymmetricKey(): SymmetricKey; 272 | } 273 | export class StringProvider extends KeyProvider { 274 | constructor(hexEncodedKey: string); 275 | } 276 | export class MultiTenantAwareProvider extends KeyProvider { 277 | public getActiveTenant(): KeyProvider; 278 | public getTenant(name: string): KeyProvider; 279 | public setActiveTenant(index: string): MultiTenantAwareProvider; 280 | public getTenantFromRow(row: Map, tableName: string): string; 281 | public injectTenantMetadata(row: Map, tableName: string): Map; 282 | } 283 | export class MultiTenantProvider extends MultiTenantAwareProvider { 284 | constructor(keyProviders: Map, active?: string); 285 | } 286 | 287 | // Main Engine 288 | export class CipherSweet { 289 | constructor( 290 | keyProvider: KeyProvider, 291 | encryptionBackend?: EncryptionBackend 292 | ); 293 | 294 | public getBackend(): EncryptionBackend; 295 | public getKeyProviderForActiveTenant(): KeyProvider; 296 | public getKeyProviderForTenant(name: string): KeyProvider; 297 | public getTenantFromRow(row: Map, tableName?: string): string; 298 | public setActiveTenant(tenant: string): void; 299 | public injectTenantMetadata(row: Map, tableName: string): Map; 300 | public isMultiTenantSupported(): boolean; 301 | 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | // ./lib/ 5 | BlindIndex: require('./lib/blindindex'), 6 | CipherSweet: require('./lib/ciphersweet'), 7 | CompoundIndex: require('./lib/compoundindex'), 8 | Constants: require('./lib/constants'), 9 | EncryptedField: require('./lib/encryptedfield'), 10 | EncryptedMultiRows: require('./lib/encryptedmultirows'), 11 | EncryptedRow: require('./lib/encryptedrow'), 12 | EncryptedFile: require('./lib/encryptedfile'), 13 | Util: require('./lib/util'), 14 | // ./lib/backend/ 15 | SymmetricKey: require('./lib/backend/key/symmetrickey'), 16 | BoringCrypto: require('./lib/backend/boringcrypto'), 17 | FIPSCrypto: require('./lib/backend/fipsrypto'), 18 | ModernCrypto: require('./lib/backend/moderncrypto'), 19 | // ./lib/contract/ 20 | Backend: require('./lib/contract/backend'), 21 | KeyProvider: require('./lib/contract/keyprovider'), 22 | MultiTenantAwareProvider: require('./lib/contract/multitenantawareprovider'), 23 | RowTransformation: require('./lib/contract/rowtransformation'), 24 | Transform: require('./lib/contract/transform'), 25 | Transformation: require('./lib/contract/transformation'), 26 | // ./lib/exception/ 27 | ArrayKeyException: require('./lib/exception/arraykeyexception'), 28 | BlindIndexNameCollisionException: require('./lib/exception/blindindexnamecollisionexception'), 29 | BlindIndexNotFoundException: require('./lib/exception/blindindexnotfoundexception'), 30 | CipherSweetException: require('./lib/exception/ciphersweetexception'), 31 | CryptoOperationException: require('./lib/exception/cryptooperationexception'), 32 | PlannerException: require('./lib/exception/plannerexception'), 33 | // ./lib/keyrotation 34 | FieldRotator: require('./lib/keyrotation/fieldrotator'), 35 | RowRotator: require('./lib/keyrotation/rowrotator'), 36 | MultiRowsRotator: require('./lib/keyrotation/multirowsrotator'), 37 | // ./lib/keyprovider 38 | StringProvider: require('./lib/keyprovider/stringprovider'), 39 | MultiTenantProvider: require('./lib/keyprovider/multitenantprovider'), 40 | // ./lib/planner 41 | FieldIndexPlanner: require('./lib/planner/fieldindexplanner'), 42 | // ./lib/transformation 43 | AlphaCharactersOnly: require('./lib/transformation/alphacharactersonly'), 44 | Compound: require('./lib/transformation/compound'), 45 | FirstCharacter: require('./lib/transformation/firstcharacter'), 46 | LastFourDigits: require('./lib/transformation/lastfourdigits'), 47 | Lowercase: require('./lib/transformation/lowercase') 48 | }; 49 | -------------------------------------------------------------------------------- /lib/backend/fipsrypto.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const base32 = require('rfc4648').base32; 4 | const base64url = require('rfc4648').base64url; 5 | const Backend = require('../contract/backend'); 6 | const Constants = require('../constants'); 7 | const crypto = require('crypto'); 8 | const fs = require('fs-extra'); 9 | const SodiumPlus = require('sodium-plus').SodiumPlus; 10 | const Util = require('../util'); 11 | const SymmetricKey = require('./key/symmetrickey'); 12 | const CryptoOperationException = require('../exception/cryptooperationexception'); 13 | 14 | let sodium; 15 | const MAGIC_HEADER = "fips:"; 16 | const MAC_SIZE = 48; 17 | const SALT_SIZE = 32; 18 | const NONCE_SIZE = 16; 19 | 20 | /** 21 | * Class FIPSCrypto 22 | * 23 | * This only uses algorithms supported by FIPS-140-2. 24 | * 25 | * Please consult your FIPS compliance auditor before you claim that your use 26 | * of this library is FIPS 140-2 compliant. 27 | * 28 | * @ref https://csrc.nist.gov/CSRC/media//Publications/fips/140/2/final/documents/fips1402annexa.pdf 29 | * @ref https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf 30 | * @ref https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf 31 | * 32 | * @package CipherSweet.backend 33 | * @author Paragon Initiative Enterprises 34 | */ 35 | module.exports = class FIPSCrypto extends Backend 36 | { 37 | /** 38 | * @returns {boolean} 39 | */ 40 | multiTenantSafe() { 41 | return true; 42 | } 43 | 44 | /** 45 | * AES-256-CTR encrypt 46 | * 47 | * @param {Buffer} plaintext 48 | * @param {Buffer} key 49 | * @param {Buffer} nonce 50 | * @returns {Buffer} 51 | */ 52 | async aes256ctr(plaintext, key, nonce) 53 | { 54 | let ciphertext; 55 | const cipher = crypto.createCipheriv('aes-256-ctr', key, nonce); 56 | ciphertext = cipher.update(plaintext); 57 | cipher.final(); 58 | return ciphertext; 59 | } 60 | 61 | /** 62 | * 63 | * @param {string|Buffer} plaintext 64 | * @param {SymmetricKey} key 65 | * @param {string|Buffer} aad 66 | * @returns {Promise} 67 | */ 68 | async encrypt(plaintext, key, aad = '') 69 | { 70 | if (!sodium) sodium = await SodiumPlus.auto(); 71 | if (!Buffer.isBuffer(plaintext)) { 72 | plaintext = await Util.toBuffer(plaintext); 73 | } 74 | const hkdfSalt = await Util.randomBytes(SALT_SIZE); 75 | const encKey = await Util.HKDF(key, hkdfSalt, 'AES-256-CTR'); 76 | const macKey = await Util.HKDF(key, hkdfSalt, 'HMAC-SHA-384'); 77 | const ctrNonce = await Util.randomBytes(NONCE_SIZE); 78 | 79 | const ciphertext = await this.aes256ctr(plaintext, encKey, ctrNonce); 80 | await sodium.sodium_memzero(encKey); 81 | 82 | let mac; 83 | if (aad.length > 0) { 84 | mac = await Util.hmac( 85 | 'sha384', 86 | Buffer.concat([ 87 | Util.pack([ 88 | Buffer.from(MAGIC_HEADER, 'binary'), 89 | hkdfSalt, 90 | ctrNonce, 91 | ciphertext 92 | ]), 93 | Buffer.from(aad) 94 | ]), 95 | macKey, 96 | true 97 | ); 98 | } else { 99 | mac = await Util.hmac( 100 | 'sha384', 101 | Util.pack([ 102 | Buffer.from(MAGIC_HEADER, 'binary'), 103 | hkdfSalt, 104 | ctrNonce, 105 | ciphertext 106 | ]), 107 | macKey, 108 | true 109 | ); 110 | } 111 | await sodium.sodium_memzero(macKey); 112 | 113 | return MAGIC_HEADER + base64url.stringify( 114 | Buffer.concat([ 115 | hkdfSalt, 116 | ctrNonce, 117 | mac, 118 | ciphertext 119 | ]) 120 | ); 121 | } 122 | 123 | /** 124 | * 125 | * @param {string|Buffer} ciphertext 126 | * @param {SymmetricKey} key 127 | * @param {string|Buffer} aad 128 | * @returns {Promise} 129 | */ 130 | async decrypt(ciphertext, key, aad = '') 131 | { 132 | if (!sodium) sodium = await SodiumPlus.auto(); 133 | const header = ciphertext.slice(0, 5); 134 | if (!await Util.hashEquals(MAGIC_HEADER, header)) { 135 | throw new CryptoOperationException('Invalid ciphertext header.'); 136 | } 137 | const decoded = await Util.toBuffer(base64url.parse(ciphertext.slice(5))); 138 | const hkdfSalt = decoded.slice(0, SALT_SIZE); 139 | const ctrNonce = decoded.slice( 140 | SALT_SIZE, 141 | SALT_SIZE + NONCE_SIZE 142 | ); 143 | const mac = decoded.slice( 144 | SALT_SIZE + NONCE_SIZE, 145 | SALT_SIZE + NONCE_SIZE + MAC_SIZE 146 | ); 147 | const cipher = decoded.slice(SALT_SIZE + NONCE_SIZE + MAC_SIZE); 148 | 149 | const macKey = await Util.HKDF(key, hkdfSalt, 'HMAC-SHA-384'); 150 | let recalc; 151 | if (aad.length > 0) { 152 | recalc = await Util.hmac( 153 | 'sha384', 154 | Buffer.concat([ 155 | Util.pack([ 156 | Buffer.from(MAGIC_HEADER, 'binary'), 157 | hkdfSalt, 158 | ctrNonce, 159 | cipher 160 | ]), 161 | Buffer.from(aad) 162 | ]), 163 | macKey, 164 | true 165 | ); 166 | } else { 167 | recalc = await Util.hmac( 168 | 'sha384', 169 | Util.pack([ 170 | Buffer.from(MAGIC_HEADER, 'binary'), 171 | hkdfSalt, 172 | ctrNonce, 173 | cipher 174 | ]), 175 | macKey, 176 | true 177 | ); 178 | } 179 | if (!await Util.hashEquals(recalc, mac)) { 180 | await sodium.sodium_memzero(macKey); 181 | throw new CryptoOperationException('Invalid MAC'); 182 | } 183 | const encKey = await Util.HKDF(key, hkdfSalt, 'AES-256-CTR'); 184 | 185 | const plaintext = await this.aes256ctr(cipher, encKey, ctrNonce); 186 | await sodium.sodium_memzero(encKey); 187 | return plaintext; 188 | } 189 | 190 | /** 191 | * Perform a fast blind index. Ideal for high-entropy inputs. 192 | * Algorithm: PBKDF2-SHA384 with only 1 iteration. 193 | * 194 | * @param {string|Buffer} plaintext 195 | * @param {SymmetricKey} key 196 | * @param {Number} length 197 | * @param {object} config 198 | * @returns {Buffer} 199 | */ 200 | async blindIndexFast(plaintext, key, length = 256, config = []) 201 | { 202 | let ikm; 203 | if (Buffer.isBuffer(key)) { 204 | ikm = key; 205 | } else if (SymmetricKey.isSymmetricKey(key)) { 206 | ikm = key.getRawKey(); 207 | } else { 208 | throw new TypeError('Argument 1 must be a SymmetricKey'); 209 | } 210 | return Util.andMask( 211 | crypto.pbkdf2Sync(plaintext, ikm, 1, length >>> 3, 'sha384'), 212 | length 213 | ); 214 | } 215 | 216 | /** 217 | * Perform a slower Blind Index calculation. 218 | * Algorithm: PBKDF2-SHA384 with at least 50,000 iterations. 219 | * 220 | * @param {string|Buffer} plaintext 221 | * @param {SymmetricKey} key 222 | * @param {Number} length 223 | * @param {object} config 224 | * @returns {Buffer} 225 | */ 226 | async blindIndexSlow(plaintext, key, length = 256, config = []) 227 | { 228 | let ikm; 229 | if (Buffer.isBuffer(key)) { 230 | ikm = key; 231 | } else if (SymmetricKey.isSymmetricKey(key)) { 232 | ikm = key.getRawKey(); 233 | } else { 234 | throw new TypeError('Argument 1 must be a SymmetricKey'); 235 | } 236 | let iterations = 50000; 237 | if (typeof config['iterations'] !== 'undefined') { 238 | if (config['iterations'] > 50000) { 239 | iterations = config['iterations']; 240 | } 241 | } 242 | plaintext = await Util.toBuffer(plaintext); 243 | return await Util.pbkdf2(plaintext, ikm, iterations, length >>> 3, 'sha384') 244 | .then((input) => { 245 | return Util.andMask(input, length); 246 | } 247 | ); 248 | } 249 | 250 | /** 251 | * 252 | * @param {string|Buffer} tableName 253 | * @param {string|Buffer} fieldName 254 | * @param {string|Buffer} indexName 255 | * @returns {string} 256 | */ 257 | async getIndexTypeColumn(tableName, fieldName, indexName) 258 | { 259 | const hash = await Util.hmac( 260 | 'sha384', 261 | Util.pack([ 262 | await Util.toBuffer(fieldName), 263 | await Util.toBuffer(indexName) 264 | ]), 265 | tableName, 266 | true 267 | ); 268 | return base32.stringify(hash.slice(0, 8)) 269 | .toLowerCase() 270 | .replace(/=+$/, ''); 271 | } 272 | 273 | /** 274 | * @returns {string} 275 | */ 276 | getPrefix() 277 | { 278 | return MAGIC_HEADER; 279 | } 280 | 281 | /** 282 | * @param {string|Buffer} password 283 | * @param {string|Buffer} salt 284 | */ 285 | async deriveKeyFromPassword(password, salt) 286 | { 287 | return new SymmetricKey( 288 | await Util.pbkdf2( 289 | password, 290 | salt, 291 | 100000, 292 | 32, 293 | 'sha384' 294 | ) 295 | ); 296 | } 297 | 298 | /** 299 | * 300 | * @param {number} inputFP 301 | * @param {number} outputFP 302 | * @param {SymmetricKey} key 303 | * @param {number} chunkSize 304 | * @returns {Promise} 305 | */ 306 | async doStreamDecrypt( 307 | inputFP, 308 | outputFP, 309 | key, 310 | chunkSize = 8192 311 | ) { 312 | if (!sodium) sodium = await SodiumPlus.auto(); 313 | const header = Buffer.alloc(5, 0); 314 | const storedMAC = Buffer.alloc(48, 0); 315 | const salt = Buffer.alloc(16, 0); // pbkdf2 316 | const hkdfSalt = Buffer.alloc(32, 0); // HKDF 317 | let ctrNonce = Buffer.alloc(16, 0); 318 | 319 | const inputFileSize = (await fs.fstat(inputFP)).size; 320 | if (inputFileSize < 5) { 321 | throw new CryptoOperationException('Input file is empty'); 322 | } 323 | await fs.read(inputFP, header, 0, 5); 324 | if (!await Util.hashEquals(MAGIC_HEADER, header)) { 325 | throw new CryptoOperationException('Invalid cipher backend for this file'); 326 | } 327 | await fs.read(inputFP, storedMAC, 0, 48, 5); 328 | await fs.read(inputFP, salt, 0, 16, 53); 329 | await fs.read(inputFP, hkdfSalt, 0, 32, 69); 330 | await fs.read(inputFP, ctrNonce, 0, 16, 101); 331 | 332 | const encKey = await Util.HKDF(key, hkdfSalt, 'AES-256-CTR'); 333 | const macKey = await Util.HKDF(key, hkdfSalt, 'HMAC-SHA-384'); 334 | const hmac = crypto.createHmac('sha384', macKey); 335 | hmac.update(MAGIC_HEADER); 336 | hmac.update(salt); 337 | hmac.update(hkdfSalt); 338 | hmac.update(ctrNonce); 339 | 340 | // Chunk HMAC 341 | let cHmac = crypto.createHmac('sha384', macKey); 342 | cHmac.update(MAGIC_HEADER); 343 | cHmac.update(salt); 344 | cHmac.update(hkdfSalt); 345 | cHmac.update(ctrNonce); 346 | 347 | const ctrIncrease = (chunkSize + 15) >>> 4; 348 | let outPos = 0; 349 | let inPos = 117; 350 | let toRead = chunkSize; 351 | let plaintext; 352 | const ciphertext = Buffer.alloc(chunkSize, 0); 353 | 354 | // First, validate the HMAC of the ciphertext. We're storing the MAC of each chunk 355 | // in memory, as well. 356 | let thisChunkMac; 357 | const chunkMacs = []; 358 | do { 359 | toRead = (inPos + chunkSize > inputFileSize) 360 | ? (inputFileSize - inPos) 361 | : chunkSize; 362 | 363 | await fs.read(inputFP, ciphertext, 0, toRead, inPos); 364 | hmac.update(ciphertext.slice(0, toRead)); 365 | 366 | // Append chunk MAC for TOCTOU protection 367 | cHmac.update(ciphertext.slice(0, toRead)); 368 | thisChunkMac = cHmac.digest(); 369 | chunkMacs.push(thisChunkMac); 370 | cHmac = crypto.createHmac('sha384', macKey); 371 | cHmac.update(thisChunkMac); 372 | 373 | outPos += toRead; 374 | inPos += toRead; 375 | } while (inPos < inputFileSize); 376 | 377 | const calcMAC = hmac.digest(); 378 | if (!await Util.hashEquals(calcMAC, storedMAC)) { 379 | throw new CryptoOperationException('Invalid authentication tag'); 380 | } 381 | thisChunkMac = cHmac.digest(); 382 | chunkMacs.push(thisChunkMac); 383 | 384 | cHmac = crypto.createHmac('sha384', macKey); 385 | cHmac.update(MAGIC_HEADER); 386 | cHmac.update(salt); 387 | cHmac.update(hkdfSalt); 388 | cHmac.update(ctrNonce); 389 | outPos = 0; 390 | inPos = 117; 391 | toRead = chunkSize; 392 | let shifted; 393 | do { 394 | toRead = (inPos + chunkSize > inputFileSize) 395 | ? (inputFileSize - inPos) 396 | : chunkSize; 397 | 398 | await fs.read(inputFP, ciphertext, 0, toRead, inPos); 399 | cHmac.update(ciphertext.slice(0, toRead)); 400 | thisChunkMac = cHmac.digest(); 401 | shifted = chunkMacs.shift(); 402 | if (typeof (shifted) === 'undefined') { 403 | throw new CryptoOperationException('TOCTOU + truncation attack'); 404 | } 405 | if (!await Util.hashEquals(thisChunkMac, shifted)) { 406 | throw new CryptoOperationException('TOCTOU + chosen ciphertext attack'); 407 | } 408 | 409 | // Reinitialize 410 | cHmac = crypto.createHmac('sha384', macKey); 411 | cHmac.update(thisChunkMac); 412 | 413 | plaintext = await this.aes256ctr( 414 | ciphertext.slice(0, toRead), 415 | encKey, 416 | ctrNonce 417 | ); 418 | await fs.write(outputFP, plaintext); 419 | ctrNonce = await Util.increaseCtrNonce(ctrNonce, ctrIncrease); 420 | outPos += toRead; 421 | inPos += toRead; 422 | } while (inPos < inputFileSize); 423 | 424 | await sodium.sodium_memzero(macKey); 425 | await sodium.sodium_memzero(encKey); 426 | return true; 427 | } 428 | 429 | /** 430 | * 431 | * @param {number} inputFP 432 | * @param {number} outputFP 433 | * @param {SymmetricKey} key 434 | * @param {number} chunkSize 435 | * @param {Buffer} salt 436 | * @returns {Promise} 437 | */ 438 | async doStreamEncrypt( 439 | inputFP, 440 | outputFP, 441 | key, 442 | chunkSize = 8192, 443 | salt = Constants.DUMMY_SALT 444 | ) { 445 | if (!sodium) sodium = await SodiumPlus.auto(); 446 | const hkdfSalt = await Util.randomBytes(SALT_SIZE); 447 | let ctrNonce = await Util.randomBytes(NONCE_SIZE); 448 | 449 | const encKey = await Util.HKDF(key, hkdfSalt, 'AES-256-CTR'); 450 | const macKey = await Util.HKDF(key, hkdfSalt, 'HMAC-SHA-384'); 451 | 452 | await fs.write(outputFP, await Util.toBuffer(MAGIC_HEADER), 0, 5); 453 | // Empty space for MAC 454 | await fs.write(outputFP, Buffer.alloc(48, 0), 0, 48, 5); 455 | await fs.write(outputFP, salt, 0, 16, 53); // pwhash salt 456 | await fs.write(outputFP, hkdfSalt, 0, 32, 69); // hkdf salt 457 | await fs.write(outputFP, ctrNonce, 0, 16, 101); 458 | 459 | // Init MAC state 460 | const hmac = crypto.createHmac('sha384', macKey); 461 | await sodium.sodium_memzero(macKey); 462 | hmac.update(MAGIC_HEADER); 463 | hmac.update(salt); 464 | hmac.update(hkdfSalt); 465 | hmac.update(ctrNonce); 466 | 467 | // We want to increase our CTR value by the number of blocks we used previously 468 | const ctrIncrease = (chunkSize + 15) >>> 4; 469 | const inputFileSize = (await fs.fstat(inputFP)).size; 470 | let outPos = 117; 471 | let inPos = 0; 472 | let toRead = chunkSize; 473 | const plaintext = Buffer.alloc(chunkSize, 0); 474 | let ciphertext; 475 | 476 | do { 477 | toRead = (inPos + chunkSize > inputFileSize) 478 | ? (inputFileSize - inPos) 479 | : chunkSize; 480 | 481 | await fs.read(inputFP, plaintext, 0, toRead, inPos); 482 | ciphertext = await this.aes256ctr( 483 | plaintext.slice(0, toRead), 484 | encKey, 485 | ctrNonce 486 | ); 487 | hmac.update(ciphertext); 488 | await fs.write(outputFP, ciphertext, 0, toRead, outPos); 489 | 490 | ctrNonce = await Util.increaseCtrNonce(ctrNonce, ctrIncrease); 491 | outPos += toRead; 492 | inPos += toRead; 493 | } while (inPos < inputFileSize); 494 | await sodium.sodium_memzero(encKey); 495 | 496 | const storedMAC = hmac.digest(); 497 | 498 | // Write the MAC at the beginning of the file. 499 | await fs.write(outputFP, storedMAC, 0, 48, 5); 500 | 501 | return true; 502 | } 503 | 504 | /** 505 | * @returns {number} 506 | */ 507 | getFileEncryptionSaltOffset() 508 | { 509 | return 53; 510 | } 511 | }; 512 | -------------------------------------------------------------------------------- /lib/backend/key/symmetrickey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @class CipherSweet 5 | * @package CipherSweet.backend.key 6 | * @author Paragon Initiative Enterprises 7 | */ 8 | module.exports = class SymmetricKey 9 | { 10 | /** 11 | * @param {string|Buffer} rawKeyMaterial 12 | */ 13 | constructor(rawKeyMaterial) 14 | { 15 | if (!Buffer.isBuffer(rawKeyMaterial)) { 16 | rawKeyMaterial = Buffer.from(rawKeyMaterial, 'binary'); 17 | } 18 | this.rawKeyMaterial = rawKeyMaterial; 19 | } 20 | 21 | /** 22 | * @param {object} key 23 | * @returns {boolean} 24 | */ 25 | static isSymmetricKey(key) 26 | { 27 | return Buffer.isBuffer(key.rawKeyMaterial); 28 | } 29 | 30 | /** 31 | * @returns {Buffer} 32 | */ 33 | getRawKey() 34 | { 35 | return this.rawKeyMaterial; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /lib/backend/moderncrypto.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const base32 = require('rfc4648').base32; 4 | const base64url = require('rfc4648').base64url; 5 | const ChaCha20 = require('xchacha20-js').ChaCha20; 6 | const Constants = require('../constants'); 7 | const fs = require('fs-extra'); 8 | const Backend = require('../contract/backend'); 9 | const HChaCha20 = require('xchacha20-js').HChaCha20; 10 | const Poly1305 = require('poly1305-js'); 11 | const {SodiumPlus, CryptographyKey} = require('sodium-plus'); 12 | const Util = require('../util'); 13 | const SymmetricKey = require('./key/symmetrickey'); 14 | const CryptoOperationException = require('../exception/cryptooperationexception'); 15 | 16 | let sodium; 17 | const MAGIC_HEADER = "nacl:"; 18 | const NONCE_SIZE = 24; 19 | const TAG_SIZE = 16; 20 | 21 | /** 22 | * Class ModernCrypto 23 | * 24 | * Use modern cryptography (e.g. Curve25519, Chapoly) 25 | * 26 | * @package CipherSweet.backend 27 | * @author Paragon Initiative Enterprises 28 | */ 29 | module.exports = class ModernCrypto extends Backend 30 | { 31 | /** 32 | * Encrypt a message using XChaCha20-Poly1305 33 | * 34 | * @param {string|Buffer} plaintext 35 | * @param {SymmetricKey} key 36 | * @param {string|Buffer} aad 37 | * @returns {Promise} 38 | */ 39 | async encrypt(plaintext, key, aad = '') { 40 | if (!sodium) sodium = await SodiumPlus.auto(); 41 | if (!Buffer.isBuffer(plaintext)) { 42 | plaintext = await Util.toBuffer(plaintext); 43 | } 44 | const encKey = Buffer.alloc(32, 0); 45 | if (Buffer.isBuffer(key)) { 46 | key.copy(encKey, 0); 47 | } else if (SymmetricKey.isSymmetricKey(key)) { 48 | key.getRawKey().copy(encKey, 0); 49 | } else { 50 | throw new TypeError('Argument 1 must be a SymmetricKey'); 51 | } 52 | 53 | const nonce = await Util.randomBytes(NONCE_SIZE); 54 | if (aad.length >= 0) { 55 | if (!Buffer.isBuffer(aad)) { 56 | aad = await Util.toBuffer(aad); 57 | } 58 | aad = Buffer.concat([nonce, aad]); 59 | } else { 60 | aad = nonce; 61 | } 62 | 63 | const ciphertext = await sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( 64 | plaintext, 65 | nonce, 66 | new CryptographyKey(encKey), 67 | aad 68 | ); 69 | await sodium.sodium_memzero(encKey); 70 | return MAGIC_HEADER + base64url.stringify( 71 | Buffer.concat([nonce, ciphertext]) 72 | ); 73 | } 74 | 75 | /** 76 | * Decrypt a message using XChaCha20-Poly1305 77 | * 78 | * @param {string|Buffer} ciphertext 79 | * @param {SymmetricKey} key 80 | * @param {string|Buffer} aad 81 | * @returns {Promise} 82 | */ 83 | async decrypt(ciphertext, key, aad = '') 84 | { 85 | if (!sodium) sodium = await SodiumPlus.auto(); 86 | const encKey = Buffer.alloc(32, 0); 87 | if (Buffer.isBuffer(key)) { 88 | key.copy(encKey, 0); 89 | } else if (SymmetricKey.isSymmetricKey(key)) { 90 | key.getRawKey().copy(encKey, 0); 91 | } else { 92 | throw new TypeError('Argument 1 must be a SymmetricKey'); 93 | } 94 | 95 | const header = ciphertext.slice(0, 5); 96 | if (!await Util.hashEquals(MAGIC_HEADER, header)) { 97 | throw new CryptoOperationException('Invalid ciphertext header.'); 98 | } 99 | const decoded = await Util.toBuffer(base64url.parse(ciphertext.slice(5))); 100 | const nonce = decoded.slice(0, NONCE_SIZE); 101 | const encrypted = decoded.slice(NONCE_SIZE); 102 | 103 | if (aad.length >= 0) { 104 | if (!Buffer.isBuffer(aad)) { 105 | aad = await Util.toBuffer(aad); 106 | } 107 | aad = Buffer.concat([nonce, aad]); 108 | } else { 109 | aad = nonce; 110 | } 111 | 112 | let decrypted; 113 | try { 114 | decrypted = await sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( 115 | encrypted, 116 | nonce, 117 | new CryptographyKey(encKey), 118 | aad 119 | ); 120 | } catch (e) { 121 | await sodium.sodium_memzero(encKey); 122 | throw new CryptoOperationException('Invalid MAC'); 123 | } 124 | await sodium.sodium_memzero(encKey); 125 | return decrypted; 126 | } 127 | 128 | /** 129 | * 130 | * @param {Buffer} plaintext 131 | * @param {SymmetricKey|Buffer} symmetricKey 132 | * @param {Number} bitLength 133 | * @returns {Buffer} 134 | */ 135 | async blindIndexFast(plaintext, symmetricKey, bitLength = 256) 136 | { 137 | if (!sodium) sodium = await SodiumPlus.auto(); 138 | const idxKey = Buffer.alloc(32, 0); 139 | if (Buffer.isBuffer(symmetricKey)) { 140 | symmetricKey.copy(idxKey, 0); 141 | } else if (SymmetricKey.isSymmetricKey(symmetricKey)) { 142 | symmetricKey.getRawKey().copy(idxKey, 0); 143 | } else { 144 | throw new TypeError('Argument 1 must be a SymmetricKey'); 145 | } 146 | plaintext = await Util.toBuffer(plaintext); 147 | let hashLength = 32; 148 | if (bitLength > 512) { 149 | throw new CryptoOperationException('Output length is too high'); 150 | } else if (bitLength > 64) { 151 | hashLength = bitLength >>> 3; 152 | } 153 | const hash = await sodium.crypto_generichash( 154 | plaintext, 155 | new CryptographyKey(idxKey), 156 | hashLength 157 | ); 158 | return Util.andMask(hash, bitLength); 159 | } 160 | 161 | /** 162 | * 163 | * @param {Buffer} plaintext 164 | * @param {SymmetricKey|Buffer} symmetricKey 165 | * @param {Number} bitLength 166 | * @param {object} config 167 | * @returns {Buffer} 168 | */ 169 | async blindIndexSlow(plaintext, symmetricKey, bitLength = 256, config = []) 170 | { 171 | if (!sodium) sodium = await SodiumPlus.auto(); 172 | const idxKey = Buffer.alloc(32, 0); 173 | if (Buffer.isBuffer(symmetricKey)) { 174 | symmetricKey.copy(idxKey, 0); 175 | } else if (SymmetricKey.isSymmetricKey(symmetricKey)) { 176 | symmetricKey.getRawKey().copy(idxKey, 0); 177 | } else { 178 | throw new TypeError('Argument 1 must be a SymmetricKey'); 179 | } 180 | let hashLength = bitLength >>> 3; 181 | if (bitLength > 4294967295) { 182 | throw new CryptoOperationException('Output length is too high'); 183 | } 184 | if (bitLength < 128) { 185 | hashLength = 16; 186 | } 187 | let opsLimit = 4; 188 | let memLimit = 33554432; 189 | if (typeof config['opslimit'] !== 'undefined') { 190 | if (config['opslimit'] > opsLimit) { 191 | opsLimit = config['opslimit']; 192 | } 193 | } 194 | if (typeof config['memlimit'] !== 'undefined') { 195 | if (config['memlimit'] > memLimit) { 196 | memLimit = config['memlimit']; 197 | } 198 | } 199 | 200 | const salt = await sodium.crypto_generichash(idxKey, null, 16); 201 | await sodium.sodium_memzero(idxKey); 202 | 203 | const hash = await sodium.crypto_pwhash( 204 | hashLength, 205 | await Util.toBuffer(plaintext), 206 | salt, 207 | opsLimit, 208 | memLimit 209 | ); 210 | return Util.andMask(hash.getBuffer(), bitLength); 211 | } 212 | 213 | /** 214 | * 215 | * @param {string|Buffer} tableName 216 | * @param {string|Buffer} fieldName 217 | * @param {string|Buffer} indexName 218 | * @returns {string} 219 | */ 220 | async getIndexTypeColumn(tableName, fieldName, indexName) 221 | { 222 | if (!sodium) sodium = await SodiumPlus.auto(); 223 | tableName = await Util.toBuffer(tableName); 224 | fieldName = await Util.toBuffer(fieldName); 225 | indexName = await Util.toBuffer(indexName); 226 | 227 | const hash = await sodium.crypto_generichash(tableName, null, 16); 228 | const shorthash = await sodium.crypto_shorthash( 229 | Util.pack([fieldName, indexName]), 230 | new CryptographyKey(hash) 231 | ); 232 | return base32.stringify(shorthash) 233 | .toLowerCase() 234 | .replace(/=+$/, ''); 235 | } 236 | 237 | /** 238 | * @param {string|Buffer} password 239 | * @param {string|Buffer} salt 240 | */ 241 | async deriveKeyFromPassword(password, salt) 242 | { 243 | if (!sodium) sodium = await SodiumPlus.auto(); 244 | const buf = await sodium.crypto_pwhash( 245 | 32, 246 | await Util.toBuffer(password), 247 | await Util.toBuffer(salt), 248 | 4, // SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE from PHP 249 | 33554432 // SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE from PHP 250 | ); 251 | return new SymmetricKey(buf.getBuffer()); 252 | } 253 | 254 | /** 255 | * @returns {string} 256 | */ 257 | getPrefix() 258 | { 259 | return MAGIC_HEADER; 260 | } 261 | 262 | /** 263 | * 264 | * @param {number} inputFP 265 | * @param {number} outputFP 266 | * @param {SymmetricKey} key 267 | * @param {number} chunkSize 268 | * @returns {Promise} 269 | */ 270 | async doStreamDecrypt( 271 | inputFP, 272 | outputFP, 273 | key, 274 | chunkSize = 8192 275 | ) { 276 | if (!sodium) sodium = await SodiumPlus.auto(); 277 | let adlen = 45; 278 | const encKey = Buffer.alloc(32, 0); 279 | if (Buffer.isBuffer(key)) { 280 | key.copy(encKey, 0); 281 | } else if (SymmetricKey.isSymmetricKey(key)) { 282 | key.getRawKey().copy(encKey, 0); 283 | } else { 284 | throw new TypeError('Argument 3 must be a SymmetricKey'); 285 | } 286 | 287 | const header = Buffer.alloc(5, 0); 288 | const storedMAC = Buffer.alloc(16, 0); 289 | const salt = Buffer.alloc(16, 0); // argon2id 290 | const nonce = Buffer.alloc(24, 0); 291 | 292 | const inputFileSize = (await fs.fstat(inputFP)).size; 293 | if (inputFileSize < 5) { 294 | throw new CryptoOperationException('Input file is empty'); 295 | } 296 | await fs.read(inputFP, header, 0, 5); 297 | if (!await Util.hashEquals(MAGIC_HEADER, header)) { 298 | throw new CryptoOperationException('Invalid cipher backend for this file'); 299 | } 300 | await fs.read(inputFP, storedMAC, 0, 16, 5); 301 | await fs.read(inputFP, salt, 0, 16, 21); 302 | await fs.read(inputFP, nonce, 0, 24, 37); 303 | 304 | const subkey = await (new HChaCha20()).hChaCha20Bytes(nonce.slice(0, 16), encKey); 305 | const nonceLast = Buffer.alloc(12, 0); 306 | nonce.copy(nonceLast, 4, 16, 24); 307 | 308 | const chacha = new ChaCha20(); 309 | const poly = new Poly1305(await chacha.ietfStream(32, nonceLast, subkey)); 310 | const chunkMacKey = new CryptographyKey( 311 | (await chacha.ietfStream(64, nonceLast, subkey)).slice(32) 312 | ); 313 | await poly.update(Buffer.from(MAGIC_HEADER)); 314 | await poly.update(salt); 315 | await poly.update(nonce); 316 | await poly.update(Buffer.alloc((0x10 - adlen) & 0xf, 0)); 317 | 318 | const ciphertext = Buffer.alloc(chunkSize, 0); 319 | let plaintext; 320 | let inPos = 61; 321 | let outPos = 0; 322 | let toRead = chunkSize; 323 | const chunkMacs = []; 324 | let len = 0; 325 | 326 | // Validate the Poly1305 tag, storing MACs of each chunk in memory 327 | let hash = await sodium.crypto_generichash_init(chunkMacKey, 16); 328 | let thisChunkMac = Buffer.alloc(16, 0); 329 | do { 330 | toRead = (inPos + chunkSize > inputFileSize) 331 | ? (inputFileSize - inPos) 332 | : chunkSize; 333 | len += toRead; 334 | 335 | await fs.read(inputFP, ciphertext, 0, toRead, inPos); 336 | await poly.update(ciphertext.slice(0, toRead)); 337 | 338 | // Chain chunk MACs based on the previous chunk's MAC 339 | await sodium.crypto_generichash_update(hash, ciphertext.slice(0, toRead)); 340 | thisChunkMac = await sodium.crypto_generichash_final(hash, 16); 341 | chunkMacs.push(Buffer.concat([thisChunkMac])); 342 | hash = await sodium.crypto_generichash_init(chunkMacKey, 16); 343 | await sodium.crypto_generichash_update(hash, thisChunkMac); 344 | 345 | inPos += chunkSize; 346 | outPos += chunkSize; 347 | } while(inPos <= inputFileSize); 348 | thisChunkMac = await sodium.crypto_generichash_final(hash, 16); 349 | chunkMacs.push(Buffer.concat([thisChunkMac])); 350 | 351 | await poly.update(Buffer.alloc((0x10 - len) & 0xf, 0)); 352 | await poly.update(Util.store64_le(adlen)); 353 | await poly.update(Util.store64_le(len)); 354 | 355 | const calcMAC = await poly.finish(); 356 | if (!(await Util.hashEquals(calcMAC, storedMAC))) { 357 | throw new CryptoOperationException('Invalid authentication tag'); 358 | } 359 | 360 | inPos = 61; 361 | outPos = 0; 362 | let block_counter = 1; 363 | const ctrIncrease = (chunkSize + 63) >>> 6; 364 | let storedChunkMac; 365 | hash = await sodium.crypto_generichash_init(chunkMacKey, 16); 366 | do { 367 | toRead = (inPos + chunkSize > inputFileSize) 368 | ? (inputFileSize - inPos) 369 | : chunkSize; 370 | 371 | await fs.read(inputFP, ciphertext, 0, toRead, inPos); 372 | 373 | // Chain chunk MACs based on the previous chunk's MAC 374 | await sodium.crypto_generichash_update(hash, ciphertext.slice(0, toRead)); 375 | thisChunkMac = await sodium.crypto_generichash_final(hash, 16); 376 | storedChunkMac = chunkMacs.shift(); 377 | if (typeof storedChunkMac === 'undefined') { 378 | throw new CryptoOperationException('Race condition'); 379 | } 380 | if (!(await Util.hashEquals(storedChunkMac, thisChunkMac))) { 381 | throw new CryptoOperationException('Race condition'); 382 | } 383 | 384 | hash = await sodium.crypto_generichash_init(chunkMacKey, 16); 385 | await sodium.crypto_generichash_update(hash, thisChunkMac); 386 | 387 | plaintext = await chacha.ietfStreamXorIc( 388 | ciphertext.slice(0, toRead), 389 | nonceLast, 390 | subkey, 391 | block_counter 392 | ); 393 | 394 | await fs.write(outputFP, plaintext, 0, toRead, outPos); 395 | 396 | inPos += chunkSize; 397 | outPos += chunkSize; 398 | block_counter += ctrIncrease; 399 | 400 | } while(inPos <= inputFileSize); 401 | 402 | thisChunkMac = await sodium.crypto_generichash_final(hash, 16); 403 | storedChunkMac = chunkMacs.shift(); 404 | if (typeof storedChunkMac === 'undefined') { 405 | throw new CryptoOperationException('Race condition'); 406 | } 407 | if (!(await Util.hashEquals(storedChunkMac, thisChunkMac))) { 408 | throw new CryptoOperationException('Race condition'); 409 | } 410 | if (chunkMacs.length > 0) { 411 | throw new CryptoOperationException('Race condition'); 412 | } 413 | return true; 414 | } 415 | 416 | /** 417 | * 418 | * @param {number} inputFP 419 | * @param {number} outputFP 420 | * @param {SymmetricKey} key 421 | * @param {number} chunkSize 422 | * @param {Buffer} salt 423 | * @returns {Promise} 424 | */ 425 | async doStreamEncrypt( 426 | inputFP, 427 | outputFP, 428 | key, 429 | chunkSize = 8192, 430 | salt = Constants.DUMMY_SALT 431 | ) { 432 | if (!sodium) sodium = await SodiumPlus.auto(); 433 | let adlen = 45; 434 | const encKey = Buffer.alloc(32, 0); 435 | if (Buffer.isBuffer(key)) { 436 | key.copy(encKey, 0); 437 | } else if (SymmetricKey.isSymmetricKey(key)) { 438 | key.getRawKey().copy(encKey, 0); 439 | } else { 440 | throw new TypeError('Argument 3 must be a SymmetricKey'); 441 | } 442 | const inputFileSize = (await fs.fstat(inputFP)).size; 443 | const nonce = await Util.randomBytes(NONCE_SIZE); 444 | const subkey = await ((new HChaCha20()).hChaCha20Bytes(nonce.slice(0, 16), encKey)); 445 | const nonceLast = Buffer.alloc(12, 0); 446 | nonce.copy(nonceLast, 4, 16, 24); 447 | 448 | await fs.write(outputFP, await Util.toBuffer(MAGIC_HEADER), 0, 5, 0); 449 | // Empty space for MAC 450 | await fs.write(outputFP, Buffer.alloc(16, 0), 0, 16, 5); 451 | await fs.write(outputFP, salt, 0, 16, 21); 452 | await fs.write(outputFP, nonce, 0, 24, 37); 453 | 454 | const chacha = new ChaCha20(); 455 | const poly = new Poly1305(await chacha.ietfStream(32, nonceLast, subkey)); 456 | await poly.update(Buffer.from(MAGIC_HEADER)); 457 | await poly.update(salt); 458 | await poly.update(nonce); 459 | await poly.update(Buffer.alloc((0x10 - adlen) & 0xf, 0)); 460 | 461 | const plaintext = Buffer.alloc(chunkSize, 0); 462 | let ciphertext; 463 | let block_counter = 1; 464 | const ctrIncrease = (chunkSize + 63) >>> 6; 465 | let inPos = 0; 466 | let outPos = 61; 467 | let toRead = chunkSize; 468 | let len = 0; 469 | do { 470 | toRead = (inPos + chunkSize > inputFileSize) 471 | ? (inputFileSize - inPos) 472 | : chunkSize; 473 | 474 | await fs.read(inputFP, plaintext, 0, toRead, inPos); 475 | ciphertext = await chacha.ietfStreamXorIc( 476 | plaintext.slice(0, toRead), 477 | nonceLast, 478 | subkey, 479 | block_counter 480 | ); 481 | await poly.update(ciphertext); 482 | await fs.write(outputFP, ciphertext, 0, toRead, outPos); 483 | 484 | len += toRead; 485 | inPos += chunkSize; 486 | outPos += chunkSize; 487 | block_counter += ctrIncrease; 488 | } while (inPos < inputFileSize); 489 | 490 | await poly.update(Buffer.alloc((0x10 - len) & 0xf, 0)); 491 | await poly.update(Util.store64_le(adlen)); 492 | await poly.update(Util.store64_le(len)); 493 | const authTag = await poly.finish(); 494 | await fs.write(outputFP, authTag, 0, 16, 5); 495 | 496 | return true; 497 | } 498 | 499 | /** 500 | * @returns {number} 501 | */ 502 | getFileEncryptionSaltOffset() 503 | { 504 | return 21; 505 | } 506 | }; 507 | -------------------------------------------------------------------------------- /lib/blindindex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Transformation = require('./contract/transformation'); 4 | const Util = require('./util'); 5 | 6 | /** 7 | * Class BlindIndex 8 | * 9 | * @package CipherSweet 10 | */ 11 | module.exports = class BlindIndex 12 | { 13 | /** 14 | * @param {string} name 15 | * @param {Transformation[]} transformations 16 | * @param {Number} filterBits 17 | * @param {boolean} fastHash 18 | * @param {object|Array} hashConfig 19 | */ 20 | constructor(name, transformations = [], filterBits = 256, fastHash = false, hashConfig = {}) 21 | { 22 | this.name = name; 23 | this.transformations = transformations; 24 | this.filterBits = filterBits; 25 | this.fastHash = fastHash; 26 | this.hashConfig = hashConfig; 27 | } 28 | 29 | /** 30 | * @param {BlindIndex} tf 31 | * @returns {BlindIndex} 32 | */ 33 | addTransformation(tf) 34 | { 35 | this.transformations.push(tf); 36 | return this; 37 | } 38 | 39 | /** 40 | * @returns {boolean} 41 | */ 42 | getFastHash() 43 | { 44 | return this.fastHash; 45 | } 46 | 47 | /** 48 | * @returns {Number} 49 | */ 50 | getFilterBitLength() 51 | { 52 | return this.filterBits; 53 | } 54 | 55 | /** 56 | * @returns {Object|Array|Object|Array} 57 | */ 58 | getHashConfig() 59 | { 60 | return this.hashConfig; 61 | } 62 | 63 | /** 64 | * @returns {string} 65 | */ 66 | getName() 67 | { 68 | return this.name; 69 | } 70 | 71 | /** 72 | * @param {string|Buffer} input 73 | * @returns {Buffer} 74 | */ 75 | async getTransformed(input) 76 | { 77 | if (this.transformations.length < 1) { 78 | return await Util.toBuffer(input); 79 | } 80 | let tf; 81 | let output = await Util.toBuffer(input); 82 | for (let i = 0; i < this.transformations.length; i++) { 83 | /** @var {transformation} tf */ 84 | tf = this.transformations[i]; 85 | output = await tf.invoke(output); 86 | } 87 | return output; 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /lib/ciphersweet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Backend = require('./contract/backend'); 4 | const Constants = require('./constants'); 5 | const KeyProvider = require('./contract/keyprovider'); 6 | const MultiTenantAwareProvider = require('./contract/multitenantawareprovider'); 7 | const BoringCrypto = require('./backend/boringcrypto'); 8 | const SymmetricKey = require('./backend/key/symmetrickey'); 9 | const Util = require('./util'); 10 | const CipherSweetException = require("./exception/ciphersweetexception"); 11 | 12 | /** 13 | * Class CipherSweet 14 | * 15 | * @package CipherSweet 16 | * @author Paragon Initiative Enterprises 17 | */ 18 | module.exports = class CipherSweet 19 | { 20 | /** 21 | * 22 | * @param {KeyProvider} keyProvider 23 | * @param {Backend|null} backend 24 | */ 25 | constructor(keyProvider, backend = null) 26 | { 27 | if (!(keyProvider instanceof KeyProvider)) { 28 | throw new TypeError("Argument 1 must be an instance of keyprovider"); 29 | } 30 | if (!backend) { 31 | backend = new BoringCrypto(); 32 | } 33 | if (!(backend instanceof Backend)) { 34 | throw new TypeError("Argument 2 must be an instance of backend"); 35 | } 36 | this.keyProvider = keyProvider; 37 | this.backend = backend; 38 | } 39 | 40 | /** 41 | * @returns {Backend} 42 | */ 43 | getBackend() 44 | { 45 | return this.backend; 46 | } 47 | 48 | /** 49 | * Get the key provider for the active tenant 50 | * 51 | * @returns {KeyProvider} 52 | */ 53 | getKeyProviderForActiveTenant() { 54 | if (!(this.keyProvider instanceof MultiTenantAwareProvider)) { 55 | throw new CipherSweetException('Your Key Provider is not multi-tenant aware'); 56 | } 57 | return this.keyProvider.getActiveTenant(); 58 | } 59 | 60 | /** 61 | * Get the key provider for a given tenant 62 | * 63 | * @param {string} name 64 | * @returns {KeyProvider} 65 | */ 66 | getKeyProviderForTenant(name) { 67 | if (!(this.keyProvider instanceof MultiTenantAwareProvider)) { 68 | throw new CipherSweetException('Your Key Provider is not multi-tenant aware'); 69 | } 70 | return this.keyProvider.getTenant(name); 71 | } 72 | 73 | /** 74 | * Get the tenant from a given row 75 | * 76 | * @param {object} row 77 | * @param {string} tableName 78 | * @returns {string} 79 | */ 80 | getTenantFromRow(row, tableName = '') { 81 | if (!(this.keyProvider instanceof MultiTenantAwareProvider)) { 82 | throw new CipherSweetException('Your Key Provider is not multi-tenant aware'); 83 | } 84 | return this.keyProvider.getTenantFromRow(row, tableName); 85 | } 86 | 87 | /** 88 | * @param {string} name 89 | * @returns {void} 90 | */ 91 | setActiveTenant(name) { 92 | if (!(this.keyProvider instanceof MultiTenantAwareProvider)) { 93 | throw new CipherSweetException('Your Key Provider is not multi-tenant aware'); 94 | } 95 | this.keyProvider.setActiveTenant(name); 96 | } 97 | 98 | /** 99 | * @param {object} row 100 | * @param {string} tableName 101 | * @returns {object} 102 | */ 103 | injectTenantMetadata(row, tableName = '') { 104 | if (!(this.keyProvider instanceof MultiTenantAwareProvider)) { 105 | throw new CipherSweetException('Your Key Provider is not multi-tenant aware'); 106 | } 107 | return this.keyProvider.injectTenantMetadata(row, tableName); 108 | } 109 | 110 | /** 111 | * Are we setup for multi-tenant data storage (each tenant gets a distinct key)? 112 | * 113 | * @returns {boolean} 114 | */ 115 | isMultiTenantSupported() { 116 | if (!this.backend.multiTenantSafe()) { 117 | // Backend doesn't provide the cryptographic properties we need. 118 | return false; 119 | } 120 | return this.keyProvider instanceof MultiTenantAwareProvider; 121 | } 122 | 123 | /** 124 | * 125 | * @param {string} tableName 126 | * @param {string} fieldName 127 | * @param {string} indexName 128 | * @returns string 129 | */ 130 | async getIndexTypeColumn(tableName, fieldName, indexName) 131 | { 132 | return this.backend.getIndexTypeColumn(tableName, fieldName, indexName); 133 | } 134 | 135 | /** 136 | * @param {string|Buffer} tableName 137 | * @param {string|Buffer} fieldName 138 | * @returns {SymmetricKey} 139 | */ 140 | async getBlindIndexRootKey(tableName, fieldName) 141 | { 142 | return new SymmetricKey( 143 | await Util.HKDF( 144 | this.keyProvider.getSymmetricKey(), 145 | tableName, 146 | Buffer.concat([ 147 | Constants.DS_BIDX, 148 | await Util.toBuffer(fieldName) 149 | ]) 150 | ) 151 | ) 152 | } 153 | 154 | /** 155 | * @param {string|Buffer} tableName 156 | * @param {string|Buffer} fieldName 157 | * @returns {SymmetricKey} 158 | */ 159 | async getFieldSymmetricKey(tableName, fieldName) 160 | { 161 | if (this.isMultiTenantSupported()) { 162 | return new SymmetricKey( 163 | await Util.HKDF( 164 | this.getKeyProviderForActiveTenant().getSymmetricKey(), 165 | tableName, 166 | Buffer.concat([ 167 | Constants.DS_FENC, 168 | await Util.toBuffer(fieldName) 169 | ]) 170 | ) 171 | ); 172 | 173 | } 174 | return new SymmetricKey( 175 | await Util.HKDF( 176 | this.keyProvider.getSymmetricKey(), 177 | tableName, 178 | Buffer.concat([ 179 | Constants.DS_FENC, 180 | await Util.toBuffer(fieldName) 181 | ]) 182 | ) 183 | ); 184 | } 185 | }; 186 | -------------------------------------------------------------------------------- /lib/compoundindex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Compound = require('./transformation/compound'); 4 | 5 | /** 6 | * Class CompoundIndex 7 | * 8 | * @package CipherSweet 9 | */ 10 | module.exports = class CompoundIndex 11 | { 12 | /** 13 | * 14 | * @param {string} name 15 | * @param {string[]} columns 16 | * @param {Number} filterBits 17 | * @param {boolean} fastHash 18 | * @param {Object} hashConfig 19 | */ 20 | constructor(name, columns = [], filterBits = 256, fastHash = false, hashConfig = {}) 21 | { 22 | this.name = name; 23 | this.columns = columns; 24 | this.filterBits = filterBits; 25 | this.fastHash = fastHash; 26 | this.hashConfig = hashConfig; 27 | this.columnTransforms = {}; 28 | this.rowTransforms = []; 29 | } 30 | 31 | /** 32 | * @param {string} column 33 | * @param {Transformation} tf 34 | * @returns {CompoundIndex} 35 | */ 36 | addTransform(column, tf) 37 | { 38 | if (!this.columnTransforms[column]) { 39 | this.columnTransforms[column] = []; 40 | } 41 | this.columnTransforms[column].push(tf); 42 | return this; 43 | } 44 | 45 | /** 46 | * @param {RowTransformation} tf 47 | */ 48 | addRowTransform(tf) 49 | { 50 | this.rowTransforms.push(tf); 51 | } 52 | 53 | /** 54 | * @returns {Array} 55 | */ 56 | getColumns() 57 | { 58 | return this.columns; 59 | } 60 | 61 | /** 62 | * @returns {string} 63 | */ 64 | getName() 65 | { 66 | return this.name; 67 | } 68 | 69 | /** 70 | * @returns {boolean} 71 | */ 72 | getFastHash() 73 | { 74 | return this.fastHash; 75 | } 76 | 77 | /** 78 | * @returns {Number} 79 | */ 80 | getFilterBitLength() 81 | { 82 | return this.filterBits; 83 | } 84 | 85 | /** 86 | * @returns {Object} 87 | */ 88 | getHashConfig() 89 | { 90 | return this.hashConfig; 91 | } 92 | 93 | /** 94 | * @returns {RowTransformation[]} 95 | */ 96 | getRowTransforms() 97 | { 98 | return this.rowTransforms; 99 | } 100 | 101 | /** 102 | * @param {Array} row 103 | * @returns {string} 104 | */ 105 | async getPacked(row) 106 | { 107 | let col; 108 | let piece; 109 | let pieces = {}; 110 | let tf; 111 | for (let i = 0; i < this.columns.length; i++) { 112 | col = this.columns[i]; 113 | if (typeof row[col] === 'undefined') { 114 | continue; 115 | } 116 | piece = row[col]; 117 | if (this.columnTransforms[col]) { 118 | /** @var {string} t */ 119 | for (let t = 0; t < this.columnTransforms[col].length; t++) { 120 | tf = this.columnTransforms[col][t]; 121 | piece = await tf.invoke(piece); 122 | } 123 | } 124 | pieces[col] = piece; 125 | } 126 | 127 | if (this.rowTransforms.length > 0) { 128 | for (let t = 0; t < this.rowTransforms[col].length; t++) { 129 | tf = this.rowTransforms[t]; 130 | pieces = await tf(pieces); 131 | } 132 | } 133 | 134 | if (typeof pieces === 'string') { 135 | return pieces; 136 | } 137 | return (new Compound()).invoke(pieces); 138 | } 139 | }; 140 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Constants = { 4 | DS_BIDX: Buffer.alloc(32, 126), 5 | DS_FENC: Buffer.alloc(32, 180), 6 | 7 | TYPE_BOOLEAN: 'bool', 8 | TYPE_TEXT: 'string', 9 | TYPE_INT: 'float', 10 | TYPE_FLOAT: 'float', 11 | 12 | COMPOUND_SPECIAL: 'special__compound__indexes', 13 | 14 | FILE_TABLE: "special__file__encryption", 15 | FILE_COLUMN: "special__file__ciphersweet", 16 | DUMMY_SALT: Buffer.alloc(16, 0) 17 | }; 18 | module.exports = Constants; 19 | -------------------------------------------------------------------------------- /lib/contract/backend.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * 5 | * @type {CipherSweet.Backend} 6 | */ 7 | module.exports = class Backend 8 | { 9 | /** 10 | * Is this an instance of backend? 11 | * 12 | * @param {object} obj 13 | * @returns {boolean} 14 | */ 15 | static isBackend(obj) 16 | { 17 | if (!obj) { 18 | return false; 19 | } 20 | if (typeof this.encrypt !== 'function') { 21 | return false; 22 | } 23 | if (typeof this.decrypt !== 'function') { 24 | return false; 25 | } 26 | if (typeof this.blindIndexSlow !== 'function') { 27 | return false; 28 | } 29 | return this.blindIndexFast === 'function'; 30 | } 31 | 32 | /** 33 | * @returns {boolean} 34 | */ 35 | multiTenantSafe() { 36 | return false; 37 | } 38 | 39 | getFileEncryptionSaltOffset() 40 | { 41 | return 0; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /lib/contract/keyprovider.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const SymmetricKey = require('../backend/key/symmetrickey'); 4 | 5 | module.exports = class KeyProvider 6 | { 7 | /** 8 | * @returns {SymmetricKey} 9 | */ 10 | getSymmetricKey() { 11 | throw new Error("Not implemented in the base class"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/contract/multitenantawareprovider.js: -------------------------------------------------------------------------------- 1 | const KeyProvider = require('./keyprovider'); 2 | 3 | module.exports = class MultiTenantAwareProvider extends KeyProvider 4 | { 5 | /** 6 | * @returns {KeyProvider} 7 | */ 8 | getActiveTenant() { 9 | throw new Error("Not implemented in the base class"); 10 | } 11 | 12 | /** 13 | * 14 | * @param {string} name 15 | * @returns {KeyProvider} 16 | */ 17 | getTenant(name) { 18 | throw new Error("Not implemented in the base class"); 19 | } 20 | 21 | /** 22 | * @param {string} index 23 | * @returns {this} 24 | */ 25 | setActiveTenant(index) { 26 | throw new Error("Not implemented in the base class"); 27 | } 28 | 29 | /** 30 | * OVERRIDE THIS in your own class! 31 | * 32 | * Given a row of data, determine which tenant should be selected. 33 | * 34 | * @param {object} row 35 | * @param {string} tableName 36 | * @returns {string} 37 | * 38 | * @throws CipherSweetException 39 | */ 40 | getTenantFromRow(row, tableName) { 41 | throw new Error("Not implemented in the base class"); 42 | } 43 | 44 | /** 45 | * @param {object} row 46 | * @param {string} tableName 47 | * @returns {object} 48 | */ 49 | injectTenantMetadata(row, tableName) { 50 | return row; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/contract/rowtransformation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Transformation = require('./transformation'); 4 | 5 | /** 6 | * Class RowTransformation 7 | * 8 | * @package CipherSweet.contract 9 | */ 10 | module.exports = class RowTransformation extends Transformation 11 | { 12 | /** 13 | * @param {Array} input 14 | * @returns {string} 15 | */ 16 | async invoke(input) 17 | { 18 | if (!Array.isArray(input)) { 19 | throw new TypeError('Compound transformation expects an array'); 20 | } 21 | return JSON.stringify( 22 | this.processArray(input, 0) 23 | ); 24 | } 25 | 26 | /** 27 | * @param {Array} input 28 | * @param {Number} layer 29 | * @returns {string|Array} 30 | */ 31 | static async processArray(input, layer = 0) 32 | { 33 | return input; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lib/contract/transform.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Util = require('../util'); 4 | 5 | /** 6 | * Class transformation 7 | * 8 | * @package CipherSweet.contract 9 | */ 10 | module.exports = class Transform 11 | { 12 | /** 13 | * @param {string|Buffer} input 14 | * @returns {Buffer} 15 | */ 16 | async invoke(input) 17 | { 18 | return await Util.toBuffer(input); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/contract/transformation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Transform = require('./transform'); 4 | 5 | /** 6 | * Class transformation 7 | * 8 | * @package CipherSweet.contract 9 | */ 10 | module.exports = class Transformation extends Transform {} 11 | -------------------------------------------------------------------------------- /lib/encryptedfield.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const SodiumPlus = require('sodium-plus').SodiumPlus; 4 | 5 | const BlindIndexNotFoundException = require('./exception/blindindexnotfoundexception'); 6 | const BlindIndexNameCollisionException = require('./exception/blindindexnamecollisionexception'); 7 | const SymmetricKey = require('./backend/key/symmetrickey'); 8 | const Util = require('./util'); 9 | const CipherSweetException = require("./exception/ciphersweetexception"); 10 | 11 | let sodium; 12 | 13 | /** 14 | * Class EncryptedField 15 | * 16 | * @package CipherSweet 17 | */ 18 | module.exports = class EncryptedField 19 | { 20 | /** 21 | * @param {CipherSweet} engine 22 | * @param {string} tableName 23 | * @param {string} fieldName 24 | * @param {boolean} usedTypedIndexes 25 | */ 26 | constructor(engine, tableName = '', fieldName = '', usedTypedIndexes = false) 27 | { 28 | this.blindIndexes = []; 29 | this.engine = engine; 30 | this.key = null; 31 | this.tableName = tableName; 32 | this.fieldName = fieldName; 33 | this.typedIndexes = usedTypedIndexes; 34 | } 35 | /** 36 | * 37 | * @param {CipherSweet} engine 38 | * @param {string} tableName 39 | * @param {string} fieldName 40 | * @param {boolean} usedTypedIndexes 41 | */ 42 | static async build(engine, tableName = '', fieldName = '', usedTypedIndexes = false) 43 | { 44 | return await new EncryptedField(engine, tableName, fieldName, usedTypedIndexes) 45 | .setFieldSymmetricKeyAndReturnSelf(); 46 | } 47 | 48 | async setFieldSymmetricKeyAndReturnSelf() 49 | { 50 | this.key = await this.engine.getFieldSymmetricKey( 51 | this.tableName, 52 | this.fieldName 53 | ); 54 | return this; 55 | } 56 | 57 | /** 58 | * 59 | * @param {string|Buffer} plaintext 60 | * @param {string|Buffer} aad 61 | * @returns {Promise} 62 | */ 63 | async prepareForStorage(plaintext, aad = '') 64 | { 65 | return [ 66 | await this.encryptValue(plaintext, aad), 67 | await this.getAllBlindIndexes(plaintext) 68 | ]; 69 | } 70 | 71 | /** 72 | * @param {string} tenantIndex 73 | * @returns {this} 74 | */ 75 | async setActiveTenant(tenantIndex) { 76 | if (!this.engine.isMultiTenantSupported()) { 77 | throw new CipherSweetException('This is only available for multi-tenant-aware engines/providers.'); 78 | } 79 | this.engine.setActiveTenant(tenantIndex); 80 | this.key = await this.engine.getFieldSymmetricKey(this.tableName, this.fieldName); 81 | return this; 82 | } 83 | 84 | /** 85 | * 86 | * @param {string|Buffer} plaintext 87 | * @param {string|Buffer} aad 88 | * @returns {Promise} 89 | */ 90 | async encryptValue(plaintext, aad = '') 91 | { 92 | if (!this.key) { 93 | await this.setFieldSymmetricKeyAndReturnSelf(); 94 | } 95 | return await this.getBackend().encrypt(plaintext, this.key, aad); 96 | } 97 | 98 | /** 99 | * 100 | * @param {string|Buffer} ciphertext 101 | * @param {string|Buffer} aad 102 | * @returns {Promise} 103 | */ 104 | async decryptValue(ciphertext, aad = '') 105 | { 106 | if (!this.key) { 107 | await this.setFieldSymmetricKeyAndReturnSelf(); 108 | } 109 | return this.getBackend().decrypt(ciphertext, this.key, aad); 110 | } 111 | 112 | /** 113 | * 114 | * @param {string|Buffer} plaintext 115 | * @returns {string[]|string[][]} 116 | */ 117 | async getAllBlindIndexes(plaintext) 118 | { 119 | let raw; 120 | const output = {}; 121 | const key = await this.engine.getBlindIndexRootKey(this.tableName, this.fieldName); 122 | 123 | if (this.typedIndexes) { 124 | for (let name in this.blindIndexes) { 125 | /** @var {Buffer} raw */ 126 | raw = await this.getBlindIndexRaw(plaintext, name, key); 127 | output[name] = { 128 | "type": await this.engine.getIndexTypeColumn(this.tableName, this.fieldName, name), 129 | "value": raw.toString('hex') 130 | }; 131 | } 132 | } else { 133 | for (let name in this.blindIndexes) { 134 | raw = await this.getBlindIndexRaw(plaintext, name, key); 135 | output[name] = raw.toString('hex'); 136 | } 137 | } 138 | return output; 139 | } 140 | 141 | /** 142 | * @param {string|Buffer} plaintext 143 | * @param {string} name 144 | * @returns {string|object} 145 | */ 146 | async getBlindIndex(plaintext, name) 147 | { 148 | const key = await this.engine.getBlindIndexRootKey(this.tableName, this.fieldName); 149 | if (this.typedIndexes) { 150 | let raw = await this.getBlindIndexRaw(plaintext, name, key); 151 | return { 152 | "type": await this.engine.getIndexTypeColumn(this.tableName, this.fieldName, name), 153 | "value": raw.toString('hex') 154 | }; 155 | } 156 | return (await this.getBlindIndexRaw(plaintext, name, key)).toString('hex'); 157 | } 158 | 159 | /** 160 | * 161 | * @param {string|Buffer} plaintext 162 | * @param {string} name 163 | * @param {SymmetricKey} key 164 | * @returns {Buffer} 165 | */ 166 | async getBlindIndexRaw(plaintext, name, key = null) 167 | { 168 | if (!sodium) sodium = await SodiumPlus.auto(); 169 | if (typeof(this.blindIndexes[name]) === 'undefined') { 170 | throw new BlindIndexNotFoundException(`Blind index ${name} not found`); 171 | } 172 | if (!key) { 173 | key = await this.engine.getBlindIndexRootKey(this.tableName, this.fieldName); 174 | } else { 175 | if (!SymmetricKey.isSymmetricKey(key)) { 176 | throw new TypeError("Argument 3 passed to getBlindIndexRaw() must be an instance of SymmetricKey"); 177 | } 178 | } 179 | /** @var {Buffer} subkey */ 180 | const subkey = await Util.hmac( 181 | 'sha256', 182 | Util.pack([ 183 | await Util.toBuffer(this.tableName), 184 | await Util.toBuffer(this.fieldName), 185 | await Util.toBuffer(name) 186 | ]), 187 | key.getRawKey(), 188 | true 189 | ); 190 | let result; 191 | const index = this.blindIndexes[name]; 192 | if (index.getFastHash()) { 193 | result = await this.getBackend().blindIndexFast( 194 | await index.getTransformed(plaintext), 195 | subkey, 196 | index.getFilterBitLength() 197 | ); 198 | } else { 199 | result = await this.getBackend().blindIndexSlow( 200 | await index.getTransformed(plaintext), 201 | subkey, 202 | index.getFilterBitLength(), 203 | index.getHashConfig() 204 | ); 205 | } 206 | await sodium.sodium_memzero(subkey); 207 | return result; 208 | } 209 | 210 | /** 211 | * 212 | * @returns {Array} 213 | */ 214 | getBlindIndexObjects() 215 | { 216 | return this.blindIndexes; 217 | } 218 | 219 | /** 220 | * 221 | * @param {string} name 222 | * @returns {string} 223 | */ 224 | async getBlindIndexType(name) 225 | { 226 | return this.engine.getIndexTypeColumn( 227 | this.tableName, 228 | this.fieldName, 229 | name 230 | ); 231 | } 232 | 233 | /** 234 | * @returns {Array} 235 | */ 236 | async getBlindIndexTypes() 237 | { 238 | const result = {}; 239 | for (let name in this.blindIndexes) { 240 | result[name] = await this.engine.getIndexTypeColumn( 241 | this.tableName, 242 | this.fieldName, 243 | name 244 | ); 245 | } 246 | return result; 247 | } 248 | 249 | /** 250 | * @param {BlindIndex} index 251 | * @param {string|null} name 252 | * @returns {EncryptedField} 253 | */ 254 | addBlindIndex(index, name = null) 255 | { 256 | if (!name) { 257 | name = index.getName(); 258 | } 259 | if (typeof this.blindIndexes[name] !== 'undefined') { 260 | throw new BlindIndexNameCollisionException(`Index ${name} is already defined`); 261 | } 262 | this.blindIndexes[name] = index; 263 | return this; 264 | } 265 | 266 | /** 267 | * @returns {boolean} 268 | */ 269 | getFlatIndexes() 270 | { 271 | return !this.typedIndexes; 272 | } 273 | 274 | /** 275 | * @returns {boolean} 276 | */ 277 | getTypedIndexes() 278 | { 279 | return this.typedIndexes; 280 | } 281 | 282 | /** 283 | * @returns {Backend} 284 | */ 285 | getBackend() 286 | { 287 | return this.engine.getBackend(); 288 | } 289 | 290 | /** 291 | * @returns {CipherSweet} 292 | */ 293 | getEngine() 294 | { 295 | return this.engine; 296 | } 297 | 298 | /** 299 | * 300 | * @param {boolean} bool 301 | * @returns {EncryptedField} 302 | */ 303 | setFlatIndexes(bool) 304 | { 305 | this.typedIndexes = !bool; 306 | return this; 307 | } 308 | 309 | /** 310 | * @param {boolean} bool 311 | * @returns {EncryptedField} 312 | */ 313 | setTypedIndexes(bool) 314 | { 315 | this.typedIndexes = bool && true; 316 | return this; 317 | } 318 | }; 319 | -------------------------------------------------------------------------------- /lib/encryptedfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Constants = require('./constants'); 4 | const CipherSweetException = require('./exception/ciphersweetexception'); 5 | const fs = require('fs-extra'); 6 | const Util = require('./util'); 7 | 8 | module.exports = class EncryptedFile 9 | { 10 | /** 11 | * 12 | * @param {CipherSweet} engine 13 | * @param {Number} chunkSize 14 | */ 15 | constructor(engine, chunkSize = 8192) 16 | { 17 | this.engine = engine; 18 | this.chunkSize = chunkSize; 19 | } 20 | 21 | /** 22 | * @returns {Backend} 23 | */ 24 | getBackend() 25 | { 26 | return this.engine.getBackend(); 27 | } 28 | 29 | /** 30 | * @returns {string} 31 | */ 32 | getBackendPrefix() 33 | { 34 | return this.engine.getBackend().getPrefix(); 35 | } 36 | 37 | /** 38 | * @returns {CipherSweet} 39 | */ 40 | getEngine() 41 | { 42 | return this.engine; 43 | } 44 | 45 | /** 46 | * @param {string} inputFile 47 | * @param {string} outputFile 48 | */ 49 | async decryptFile(inputFile, outputFile) 50 | { 51 | const inputStream = await fs.open(inputFile, 'r'); 52 | const outputStream = await fs.open(outputFile, 'w+'); 53 | try { 54 | return await this.decryptStream(inputStream, outputStream); 55 | } catch (e) { 56 | fs.close(inputStream); 57 | fs.close(outputStream); 58 | throw e; 59 | } 60 | } 61 | 62 | /** 63 | * @param {string} inputFile 64 | * @param {string} outputFile 65 | * @param {string} password 66 | */ 67 | async decryptFileWithPassword(inputFile, outputFile, password) 68 | { 69 | const inputStream = await fs.open(inputFile, 'r'); 70 | const outputStream = await fs.open(outputFile, 'w+'); 71 | try { 72 | return await this.decryptStreamWithPassword(inputStream, outputStream, password); 73 | } catch (e) { 74 | fs.close(inputStream); 75 | fs.close(outputStream); 76 | throw e; 77 | } 78 | } 79 | 80 | async decryptStream(inputFP, outputFP) 81 | { 82 | if (!await this.isStreamEncrypted(inputFP)) { 83 | throw new CipherSweetException('Input file is not encrypted'); 84 | } 85 | const key = await this.engine.getFieldSymmetricKey( 86 | Constants.FILE_TABLE, 87 | Constants.FILE_COLUMN 88 | ); 89 | return await this.getBackend().doStreamDecrypt( 90 | inputFP, 91 | outputFP, 92 | key, 93 | this.chunkSize 94 | ); 95 | } 96 | 97 | async decryptStreamWithPassword(inputFP, outputFP, password) 98 | { 99 | if (!this.isStreamEncrypted(inputFP)) { 100 | throw new CipherSweetException('Input file is not encrypted'); 101 | } 102 | const backend = this.getBackend(); 103 | const salt = this.getSaltFromStream(inputFP); 104 | const key = await backend.deriveKeyFromPassword(password, salt); 105 | return await backend.doStreamDecrypt( 106 | inputFP, 107 | outputFP, 108 | key, 109 | this.chunkSize, 110 | salt 111 | ); 112 | } 113 | 114 | /** 115 | * @param {string} inputFile 116 | * @param {string} outputFile 117 | */ 118 | async encryptFile(inputFile, outputFile) 119 | { 120 | const inputStream = await fs.open(inputFile, 'r'); 121 | const outputStream = await fs.open(outputFile, 'w+'); 122 | try { 123 | return await this.encryptStream(inputStream, outputStream); 124 | } catch (e) { 125 | fs.close(inputStream); 126 | fs.close(outputStream); 127 | throw e; 128 | } 129 | } 130 | 131 | /** 132 | * @param {string} inputFile 133 | * @param {string} outputFile 134 | * @param {string} password 135 | */ 136 | async encryptFileWithPassword(inputFile, outputFile, password) 137 | { 138 | const inputStream = await fs.open(inputFile, 'r'); 139 | const outputStream = await fs.open(outputFile, 'w+'); 140 | try { 141 | return await this.encryptStreamWithPassword(inputStream, outputStream, password); 142 | } finally { 143 | fs.close(inputStream); 144 | fs.close(outputStream); 145 | } 146 | } 147 | 148 | async encryptStream(inputFP, outputFP) 149 | { 150 | const key = await this.engine.getFieldSymmetricKey( 151 | Constants.FILE_TABLE, 152 | Constants.FILE_COLUMN 153 | ); 154 | return await this.getBackend().doStreamEncrypt( 155 | inputFP, 156 | outputFP, 157 | key, 158 | this.chunkSize 159 | ); 160 | } 161 | 162 | async encryptStreamWithPassword(inputFP, outputFP, password) 163 | { 164 | const salt = Buffer.alloc(16, 0); 165 | do { 166 | sodium.randombytes_buf(salt); 167 | } while (!Util.hashEquals(Constants.DUMMY_SALT, salt)); 168 | const backend = this.getBackend(); 169 | const key = await backend.deriveKeyFromPassword(password, salt); 170 | return await backend.doStreamEncrypt( 171 | inputFP, 172 | outputFP, 173 | key, 174 | this.chunkSize, 175 | salt 176 | ); 177 | } 178 | 179 | /** 180 | * 181 | * @param {Number} inputFP 182 | * @returns {Buffer} 183 | */ 184 | async getSaltFromStream(inputFP) 185 | { 186 | const backend = this.getBackend(); 187 | const salt = Buffer.alloc(16, 0); 188 | await fs.read( 189 | inputFP, 190 | salt, 191 | 0, 192 | 16, 193 | backend.getFileEncryptionSaltOffset() 194 | ); 195 | return salt; 196 | } 197 | 198 | /** 199 | * @param {string} inputFile 200 | * @returns {Promise} 201 | */ 202 | async isFileEncrypted(inputFile) 203 | { 204 | const inputFP = await fs.open(inputFile, 'r'); 205 | try { 206 | return await this.isStreamEncrypted(inputFP); 207 | } catch (e) { 208 | fs.close(inputFP); 209 | throw e; 210 | } 211 | } 212 | 213 | /** 214 | * @param {Number} inputFP 215 | * @returns {boolean} 216 | */ 217 | async isStreamEncrypted(inputFP) 218 | { 219 | const expect = this.getBackendPrefix(); 220 | const header = Buffer.alloc(5, 0); 221 | await fs.read(inputFP, header, 0, 5, 0); 222 | return Util.hashEquals(expect, header); 223 | } 224 | }; 225 | -------------------------------------------------------------------------------- /lib/encryptedmultirows.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CipherSweetException = require('./exception/ciphersweetexception'); 4 | const Constants = require('./constants'); 5 | const EncryptedRow = require('./encryptedrow'); 6 | 7 | module.exports = class EncryptedMultiRows 8 | { 9 | /** 10 | * 11 | * @param {CipherSweet} engine 12 | * @param {boolean} useTypedIndexes 13 | */ 14 | constructor(engine, useTypedIndexes = false) 15 | { 16 | this.engine = engine; 17 | this.typedIndexes = useTypedIndexes; 18 | this.tables = {}; 19 | } 20 | 21 | /** 22 | * @param {string} tableName 23 | * @returns {EncryptedMultiRows} 24 | */ 25 | addTable(tableName) 26 | { 27 | if (typeof this.tables[tableName] !== 'undefined') { 28 | throw new CipherSweetException('Table already exists'); 29 | } 30 | this.tables[tableName] = new EncryptedRow(this.engine, tableName); 31 | return this; 32 | } 33 | 34 | /** 35 | * 36 | * @param {string} tableName 37 | * @param {string} fieldName 38 | * @param {string} type 39 | * @param {string} aadSource 40 | * @returns {EncryptedMultiRows} 41 | */ 42 | addField(tableName, fieldName, type = Constants.TYPE_TEXT, aadSource = '') 43 | { 44 | this.getEncryptedRowObjectForTable(tableName).addField(fieldName, type, aadSource); 45 | return this; 46 | } 47 | 48 | /** 49 | * 50 | * @param {string} tableName 51 | * @param {string} fieldName 52 | * @param {string} aadSource 53 | * @returns {EncryptedMultiRows} 54 | */ 55 | addBooleanField(tableName, fieldName, aadSource = '') 56 | { 57 | return this.addField(tableName, fieldName, Constants.TYPE_BOOLEAN, aadSource); 58 | } 59 | 60 | /** 61 | * 62 | * @param {string} tableName 63 | * @param {string} fieldName 64 | * @param {string} aadSource 65 | * @returns {EncryptedMultiRows} 66 | */ 67 | addFloatField(tableName, fieldName, aadSource = '') 68 | { 69 | return this.addField(tableName, fieldName, Constants.TYPE_FLOAT, aadSource); 70 | } 71 | 72 | /** 73 | * 74 | * @param {string} tableName 75 | * @param {string} fieldName 76 | * @param {string} aadSource 77 | * @returns {EncryptedMultiRows} 78 | */ 79 | addIntegerField(tableName, fieldName, aadSource = '') 80 | { 81 | return this.addField(tableName, fieldName, Constants.TYPE_INT, aadSource); 82 | } 83 | 84 | /** 85 | * 86 | * @param {string} tableName 87 | * @param {string} fieldName 88 | * @param {string} aadSource 89 | * @returns {EncryptedMultiRows} 90 | */ 91 | addTextField(tableName, fieldName, aadSource = '') 92 | { 93 | return this.addField(tableName, fieldName, Constants.TYPE_TEXT, aadSource); 94 | } 95 | 96 | /** 97 | * 98 | * @param {string} tableName 99 | * @param {string} column 100 | * @param {BlindIndex} index 101 | * @returns {EncryptedMultiRows} 102 | */ 103 | addBlindIndex(tableName, column, index) 104 | { 105 | this.getEncryptedRowObjectForTable(tableName) 106 | .addBlindIndex(column, index); 107 | return this; 108 | } 109 | 110 | /** 111 | * 112 | * @param {string} tableName 113 | * @param {CompoundIndex} index 114 | * @returns {EncryptedMultiRows} 115 | */ 116 | addCompoundIndex(tableName, index) 117 | { 118 | this.getEncryptedRowObjectForTable(tableName) 119 | .addCompoundIndex(index); 120 | return this; 121 | } 122 | 123 | /** 124 | * 125 | * @param {string} tableName 126 | * @param {string} name 127 | * @param {string[]} columns 128 | * @param {Number} filterBits 129 | * @param {boolean} fastHash 130 | * @param {Object} hashConfig 131 | * @returns {EncryptedRow} 132 | */ 133 | createCompoundIndex( 134 | tableName, 135 | name, 136 | columns = [], 137 | filterBits = 256, 138 | fastHash = false, 139 | hashConfig = {} 140 | ) { 141 | return this.getEncryptedRowObjectForTable(tableName) 142 | .createCompoundIndex( 143 | name, 144 | columns, 145 | filterBits, 146 | fastHash, 147 | hashConfig 148 | ); 149 | } 150 | 151 | /** 152 | * 153 | * @param rows 154 | */ 155 | async decryptManyRows(rows) 156 | { 157 | // Make a copy 158 | let row; 159 | rows = Object.assign({}, rows); 160 | for (let table in this.tables) { 161 | if (typeof (rows[table]) === 'undefined') { 162 | continue; 163 | } 164 | row = await this.getEncryptedRowObjectForTable(table) 165 | .decryptRow(rows[table]); 166 | rows[table] = row; 167 | } 168 | return rows; 169 | } 170 | 171 | /** 172 | * 173 | * @param {object} rows 174 | * @returns {object} 175 | */ 176 | async encryptManyRows(rows) 177 | { 178 | // Make a copy 179 | rows = Object.assign({}, rows); 180 | for (let table in this.tables) { 181 | if (typeof (rows[table]) === 'undefined') { 182 | continue; 183 | } 184 | rows[table] = await this.getEncryptedRowObjectForTable(table) 185 | .encryptRow(rows[table]); 186 | } 187 | return rows; 188 | } 189 | 190 | /** 191 | * @param {string} tableName 192 | * @param {string} indexName 193 | * @param {object} row 194 | * @returns {string|Object} 195 | */ 196 | async getBlindIndex(tableName, indexName, row) 197 | { 198 | return await this.getEncryptedRowObjectForTable(tableName) 199 | .getBlindIndex(indexName, row); 200 | } 201 | 202 | /** 203 | * @param {string} tableName 204 | * @param {object} row 205 | * @returns {string|Object} 206 | */ 207 | async getBlindIndexesForTable(tableName, row) 208 | { 209 | return await this.getEncryptedRowObjectForTable(tableName) 210 | .getAllBlindIndexes(row); 211 | } 212 | 213 | /** 214 | * @param {object} rows 215 | * @returns {object} 216 | */ 217 | async getAllBlindIndexes(rows) 218 | { 219 | const tables = {}; 220 | for (let table in this.tables) { 221 | tables[table] = await this 222 | .getEncryptedRowObjectForTable(table) 223 | .getAllBlindIndexes(rows[table]); 224 | } 225 | return tables; 226 | } 227 | 228 | /** 229 | * @param {string} table 230 | * @param {string} column 231 | * @param {string} name 232 | * @returns {string} 233 | */ 234 | getBlindIndexType(table, column, name) 235 | { 236 | return this 237 | .getEncryptedRowObjectForTable(table) 238 | .getBlindIndexType(column, name); 239 | } 240 | 241 | /** 242 | * @param {string} table 243 | * @param {string} name 244 | * @returns {string} 245 | */ 246 | getCompoundIndexType(table, name) 247 | { 248 | return this 249 | .getEncryptedRowObjectForTable(table) 250 | .getCompoundIndexType(name); 251 | } 252 | 253 | /** 254 | * 255 | * @param {string} tableName 256 | * @returns {EncryptedRow} 257 | */ 258 | getEncryptedRowObjectForTable(tableName = '') 259 | { 260 | if (typeof(this.tables[tableName]) === 'undefined') { 261 | this.addTable(tableName); 262 | } 263 | 264 | const encryptedRow = this.tables[tableName]; 265 | encryptedRow.setTypedIndexes(this.typedIndexes); 266 | return encryptedRow; 267 | } 268 | 269 | /** 270 | * @returns {string[]} 271 | */ 272 | listTables() 273 | { 274 | return Object.keys(this.tables); 275 | } 276 | 277 | /** 278 | * @param {string} tableName 279 | * @param {string} fieldName 280 | * @param {string} aadSource 281 | * @returns {EncryptedMultiRows} 282 | */ 283 | setAadSourceField(tableName, fieldName, aadSource) 284 | { 285 | this.getEncryptedRowObjectForTable(tableName) 286 | .setAadSourceField(fieldName, aadSource); 287 | return this; 288 | } 289 | 290 | /** 291 | * @param {object} rows 292 | * @returns {Object[]} 293 | */ 294 | async prepareForStorage(rows) 295 | { 296 | const indexes = {}; 297 | const tables = {}; 298 | 299 | for (let table in this.tables) { 300 | tables[table] = await this 301 | .getEncryptedRowObjectForTable(table) 302 | .encryptRow(rows[table]); 303 | indexes[table] = await this 304 | .getEncryptedRowObjectForTable(table) 305 | .getAllBlindIndexes(rows[table]); 306 | } 307 | return [tables, indexes]; 308 | } 309 | 310 | /** 311 | * @returns {Backend} 312 | */ 313 | getBackend() 314 | { 315 | return this.engine.getBackend(); 316 | } 317 | 318 | /** 319 | * @returns {boolean} 320 | */ 321 | getFlatIndexes() 322 | { 323 | return !this.typedIndexes; 324 | } 325 | 326 | /** 327 | * @returns {boolean} 328 | */ 329 | getTypedIndexes() 330 | { 331 | return this.typedIndexes; 332 | } 333 | 334 | /** 335 | * @param bool 336 | * @returns {EncryptedMultiRows} 337 | */ 338 | setFlatIndexes(bool) 339 | { 340 | this.typedIndexes = !bool; 341 | return this; 342 | } 343 | 344 | /** 345 | * @param bool 346 | * @returns {EncryptedMultiRows} 347 | */ 348 | setTypedIndexes(bool) 349 | { 350 | this.typedIndexes = bool; 351 | return this; 352 | } 353 | }; 354 | -------------------------------------------------------------------------------- /lib/encryptedrow.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ArrayKeyException = require('./exception/arraykeyexception'); 4 | const BlindIndexNotFoundException = require('./exception/blindindexnotfoundexception'); 5 | const BlindIndex = require('./blindindex'); 6 | const CompoundIndex = require('./compoundindex'); 7 | const Constants = require('./constants'); 8 | const SymmetricKey = require('./backend/key/symmetrickey'); 9 | const Util = require('./util'); 10 | 11 | module.exports = class EncryptedRow 12 | { 13 | /** 14 | * 15 | * @param {CipherSweet} engine 16 | * @param {string} tableName 17 | * @param {boolean} usedTypedIndexes 18 | */ 19 | constructor(engine, tableName = '', usedTypedIndexes = false) 20 | { 21 | this.blindIndexes = []; 22 | this.compoundIndexes = []; 23 | this.fieldsToEncrypt = {}; 24 | this.aadSourceField = {}; 25 | this.engine = engine; 26 | this.tableName = tableName; 27 | this.typedIndexes = usedTypedIndexes; 28 | } 29 | 30 | /** 31 | * @param {string} fieldName 32 | * @param {string} type 33 | * @param {string|null} aadSource 34 | * @returns {EncryptedRow} 35 | */ 36 | addField(fieldName, type = Constants.TYPE_TEXT, aadSource = null) 37 | { 38 | this.fieldsToEncrypt[fieldName] = type; 39 | if (aadSource) { 40 | this.aadSourceField[fieldName] = aadSource; 41 | } 42 | return this; 43 | } 44 | 45 | /** 46 | * @param {string} fieldName 47 | * @param {string|null} aadSource 48 | * @returns {EncryptedRow} 49 | */ 50 | addBooleanField(fieldName, aadSource = null) 51 | { 52 | return this.addField(fieldName, Constants.TYPE_BOOLEAN, aadSource); 53 | } 54 | 55 | /** 56 | * @param {string} fieldName 57 | * @param {string|null} aadSource 58 | * @returns {EncryptedRow} 59 | */ 60 | addFloatField(fieldName, aadSource = null) 61 | { 62 | return this.addField(fieldName, Constants.TYPE_FLOAT, aadSource); 63 | } 64 | 65 | /** 66 | * @param {string} fieldName 67 | * @param {string|null} aadSource 68 | * @returns {EncryptedRow} 69 | */ 70 | addIntegerField(fieldName, aadSource = null) 71 | { 72 | return this.addField(fieldName, Constants.TYPE_INT, aadSource); 73 | } 74 | 75 | /** 76 | * @param {string} fieldName 77 | * @param {string|null} aadSource 78 | * @returns {EncryptedRow} 79 | */ 80 | addTextField(fieldName, aadSource = null) 81 | { 82 | return this.addField(fieldName, Constants.TYPE_TEXT, aadSource); 83 | } 84 | 85 | /** 86 | * 87 | * @param {string} column 88 | * @param {BlindIndex} index 89 | * @returns {EncryptedRow} 90 | */ 91 | addBlindIndex(column, index) 92 | { 93 | if (!(index instanceof BlindIndex)) { 94 | throw new TypeError("Argument 2 must be an instance of BlindIndex"); 95 | } 96 | if (typeof (this.blindIndexes[column]) === 'undefined') { 97 | this.blindIndexes[column] = {}; 98 | } 99 | this.blindIndexes[column][index.getName()] = index; 100 | return this; 101 | } 102 | 103 | /** 104 | * 105 | * @param {CompoundIndex} index 106 | * @returns {EncryptedRow} 107 | */ 108 | addCompoundIndex(index) 109 | { 110 | this.compoundIndexes[index.getName()] = index; 111 | return this; 112 | } 113 | 114 | /** 115 | * 116 | * @param {string} name 117 | * @param {string[]} columns 118 | * @param {Number} filterBits 119 | * @param {boolean} fastHash 120 | * @param {Object} hashConfig 121 | * @returns {EncryptedRow} 122 | */ 123 | createCompoundIndex(name, columns = [], filterBits = 256, fastHash = false, hashConfig = {}) 124 | { 125 | this.compoundIndexes.push( 126 | new CompoundIndex(name, columns, filterBits, fastHash, hashConfig) 127 | ); 128 | return this; 129 | } 130 | 131 | /** 132 | * 133 | * @param {string} indexName 134 | * @param {Object} row 135 | */ 136 | async getBlindIndex(indexName, row) 137 | { 138 | let blindIndexes; 139 | for (let column in this.blindIndexes) { 140 | blindIndexes = this.blindIndexes[column]; 141 | if (typeof blindIndexes[indexName] !== 'undefined') { 142 | return this.calcBlindIndex(row, column, blindIndexes[indexName]); 143 | } 144 | } 145 | 146 | let compoundIndex; 147 | for (let idx in this.compoundIndexes) { 148 | if (typeof(this.compoundIndexes[idx]) !== 'undefined') { 149 | compoundIndex = this.compoundIndexes[idx]; 150 | if (compoundIndex.getName() === indexName) { 151 | return this.calcCompoundIndex(row, compoundIndex); 152 | } 153 | } 154 | } 155 | throw new BlindIndexNotFoundException(); 156 | } 157 | 158 | /** 159 | * @param {Object} row 160 | * @returns {Object} 161 | */ 162 | async getAllBlindIndexes(row) 163 | { 164 | let blindIndexes, blindIndex; 165 | const ret = {}; 166 | for (let column in this.blindIndexes) { 167 | blindIndexes = this.blindIndexes[column]; 168 | for (let idx in blindIndexes) { 169 | blindIndex = blindIndexes[idx]; 170 | ret[blindIndex.getName()] = await this.calcBlindIndex(row, column, blindIndex); 171 | } 172 | } 173 | 174 | let compoundIndex; 175 | for (let idx in this.compoundIndexes) { 176 | compoundIndex = this.compoundIndexes[idx]; 177 | ret[compoundIndex.getName()] = await this.calcCompoundIndex(row, compoundIndex); 178 | } 179 | return ret; 180 | } 181 | 182 | /** 183 | * @param {string} column 184 | * @returns {Array} 185 | */ 186 | getBlindIndexObjectsForColumn(column) 187 | { 188 | if (typeof this.blindIndexes[column] === 'undefined') { 189 | this.blindIndexes[column] = {}; 190 | } 191 | return this.blindIndexes[column]; 192 | } 193 | 194 | /** 195 | * @param {string} column 196 | * @param {string} name 197 | * @returns {string} 198 | */ 199 | async getBlindIndexType(column, name) 200 | { 201 | return await this.engine.getIndexTypeColumn( 202 | this.tableName, 203 | column, 204 | name 205 | ); 206 | } 207 | 208 | /** 209 | * @param {string} column 210 | * @param {string} name 211 | * @returns {string} 212 | */ 213 | async getCompoundIndexType(name) 214 | { 215 | return await this.engine.getIndexTypeColumn( 216 | this.tableName, 217 | Constants.COMPOUND_SPECIAL, 218 | name 219 | ); 220 | } 221 | 222 | /** 223 | * @returns {Array} 224 | */ 225 | getCompoundIndexObjects() 226 | { 227 | return this.compoundIndexes; 228 | } 229 | 230 | /** 231 | * 232 | * @param {object} row 233 | * @returns {object} 234 | */ 235 | async decryptRow(row) 236 | { 237 | let plaintext; 238 | let type; 239 | let key; 240 | const ret = Object.assign({}, row); // copy 241 | const backend = this.engine.getBackend(); 242 | if (this.engine.isMultiTenantSupported()) { 243 | this.engine.setActiveTenant( 244 | this.engine.getTenantFromRow(row, this.tableName) 245 | ); 246 | } 247 | for (let field in this.fieldsToEncrypt) { 248 | /** @var {string} field */ 249 | type = this.fieldsToEncrypt[field]; 250 | key = await this.engine.getFieldSymmetricKey(this.tableName, field); 251 | if (typeof (this.aadSourceField[field]) !== 'undefined' && typeof (row[field]) !== 'undefined') { 252 | plaintext = await backend.decrypt( 253 | row[field], 254 | key, 255 | row[this.aadSourceField[field]] 256 | ); 257 | } else { 258 | plaintext = await backend.decrypt(row[field], key); 259 | } 260 | ret[field] = this.convertFromBuffer(plaintext, type); 261 | } 262 | return ret; 263 | } 264 | 265 | /** 266 | * @param {object} row 267 | * @returns {object} 268 | */ 269 | async encryptRow(row) 270 | { 271 | let plaintext; 272 | let type; 273 | let key; 274 | const ret = Object.assign({}, row); // copy 275 | const backend = this.engine.getBackend(); 276 | if (this.engine.isMultiTenantSupported()) { 277 | this.engine.setActiveTenant( 278 | this.engine.getTenantFromRow(row, this.tableName) 279 | ); 280 | } 281 | for (let field in this.fieldsToEncrypt) { 282 | /** @var {string} field */ 283 | type = this.fieldsToEncrypt[field]; 284 | plaintext = await this.convertToBuffer(row[field], type); 285 | key = await this.engine.getFieldSymmetricKey(this.tableName, field); 286 | if (typeof (this.aadSourceField[field]) !== 'undefined' && typeof (row[field]) !== 'undefined') { 287 | ret[field] = await backend.encrypt( 288 | plaintext, 289 | key, 290 | row[this.aadSourceField[field]] 291 | ); 292 | } else { 293 | ret[field] = await backend.encrypt(plaintext, key); 294 | } 295 | } 296 | if (this.engine.isMultiTenantSupported()) { 297 | return this.engine.injectTenantMetadata(ret, this.tableName); 298 | } 299 | return ret; 300 | } 301 | 302 | /** 303 | * 304 | * @param {Array} row 305 | * @returns {Array} 306 | */ 307 | async prepareRowForStorage(row) 308 | { 309 | return [ 310 | await this.encryptRow(row), 311 | await this.getAllBlindIndexes(row) 312 | ]; 313 | } 314 | 315 | /** 316 | * @returns {string[]} 317 | */ 318 | listEncryptedFields() 319 | { 320 | return Object.keys(this.fieldsToEncrypt); 321 | } 322 | 323 | /** 324 | * @param {string} fieldName 325 | * @param {string} aadSource 326 | * @returns {EncryptedRow} 327 | */ 328 | setAadSourceField(fieldName, aadSource) 329 | { 330 | this.aadSourceField[fieldName] = aadSource; 331 | return this; 332 | } 333 | 334 | /** 335 | * 336 | * @param {Array|Object} row 337 | * @param {string} column 338 | * @param {BlindIndex} index 339 | * @returns {string|Object} 340 | */ 341 | async calcBlindIndex(row, column, index) 342 | { 343 | const name = index.getName(); 344 | const key = await this.engine.getBlindIndexRootKey(this.tableName, column); 345 | if (this.typedIndexes) { 346 | return { 347 | "type": await this.engine.getIndexTypeColumn(this.tableName, column, name), 348 | "value": (await this.calcBlindIndexRaw(row, column, index, key)).toString('hex') 349 | } 350 | } 351 | return (await this.calcBlindIndexRaw(row, column, index, key)).toString('hex'); 352 | } 353 | 354 | /** 355 | * 356 | * @param {Array|Object} row 357 | * @param {CompoundIndex} index 358 | * @returns {string|Object} 359 | */ 360 | async calcCompoundIndex(row, index) 361 | { 362 | const name = index.getName(); 363 | const key = await this.engine.getBlindIndexRootKey(this.tableName, Constants.COMPOUND_SPECIAL); 364 | if (this.typedIndexes) { 365 | return { 366 | "type": await this.engine.getIndexTypeColumn(this.tableName, Constants.COMPOUND_SPECIAL, name), 367 | "value": (await this.calcCompoundIndexRaw(row, index, key)).toString('hex') 368 | } 369 | } 370 | return (await this.calcCompoundIndexRaw(row, index, key)).toString('hex'); 371 | } 372 | 373 | /** 374 | * 375 | * @param {Array|Object} row 376 | * @param {string} column 377 | * @param {BlindIndex} index 378 | * @param {SymmetricKey|null} key 379 | * @returns {Buffer} 380 | */ 381 | async calcBlindIndexRaw(row, column, index, key = null) 382 | { 383 | if (!key) { 384 | key = await this.engine.getBlindIndexRootKey( 385 | this.tableName, 386 | column 387 | ); 388 | } 389 | const backend = this.getBackend(); 390 | const name = index.getName(); 391 | const subKey = new SymmetricKey( 392 | await Util.hmac( 393 | 'sha256', 394 | Util.pack([ 395 | Buffer.from(this.tableName), 396 | Buffer.from(column), 397 | Buffer.from(name) 398 | ]), 399 | key.getRawKey(), 400 | true 401 | ) 402 | ); 403 | if (typeof(this.fieldsToEncrypt[column]) === 'undefined') { 404 | throw new ArrayKeyException( 405 | `The field ${column} is not defined in this encrypted row.` 406 | ); 407 | } 408 | const fieldType = this.fieldsToEncrypt[column]; 409 | 410 | const plaintext = await index.getTransformed( 411 | await this.convertToBuffer(row[column], fieldType) 412 | ); 413 | 414 | if (index.getFastHash()) { 415 | return backend.blindIndexFast( 416 | plaintext, 417 | subKey, 418 | index.getFilterBitLength() 419 | ); 420 | } 421 | return backend.blindIndexSlow( 422 | plaintext, 423 | subKey, 424 | index.getFilterBitLength(), 425 | index.getHashConfig() 426 | ); 427 | } 428 | 429 | /** 430 | * 431 | * @param {Array|Object} row 432 | * @param {CompoundIndex} index 433 | * @param {SymmetricKey|null} key 434 | * @returns {Buffer} 435 | */ 436 | async calcCompoundIndexRaw(row, index, key = null) 437 | { 438 | if (!key) { 439 | key = this.engine.getBlindIndexRootKey( 440 | this.tableName, 441 | Constants.COMPOUND_SPECIAL 442 | ); 443 | } 444 | const subKey = new SymmetricKey( 445 | await Util.hmac( 446 | 'sha256', 447 | Util.pack([ 448 | Buffer.from(this.tableName), 449 | Buffer.from(Constants.COMPOUND_SPECIAL), 450 | Buffer.from(index.getName()) 451 | ]), 452 | key.getRawKey(), 453 | true 454 | ) 455 | ); 456 | 457 | const backend = this.getBackend(); 458 | 459 | const plaintext = await index.getPacked(row); 460 | 461 | if (index.getFastHash()) { 462 | return backend.blindIndexFast( 463 | plaintext, 464 | subKey, 465 | index.getFilterBitLength() 466 | ); 467 | } 468 | return backend.blindIndexSlow( 469 | plaintext, 470 | subKey, 471 | index.getFilterBitLength(), 472 | index.getHashConfig() 473 | ); 474 | 475 | } 476 | 477 | /** 478 | * 479 | * @param {Buffer} data 480 | * @param {string} type 481 | * @returns {*} 482 | */ 483 | convertFromBuffer(data, type) 484 | { 485 | switch (type) { 486 | case Constants.TYPE_BOOLEAN: 487 | return Util.chrToBool(data.toString('binary')); 488 | case Constants.TYPE_FLOAT: 489 | return Util.bufferToFloat(data); 490 | case Constants.TYPE_INT: 491 | return Util.load64_le(data); 492 | case Constants.TYPE_TEXT: 493 | return Util.fromBuffer(data); 494 | default: 495 | return data; 496 | } 497 | } 498 | 499 | /** 500 | * 501 | * @param {*} data 502 | * @param {string} type 503 | * @returns {Buffer} 504 | */ 505 | async convertToBuffer(data, type) 506 | { 507 | switch (type) { 508 | case Constants.TYPE_BOOLEAN: 509 | return Buffer.from(Util.boolToChr(data), 'binary'); 510 | case Constants.TYPE_FLOAT: 511 | return Util.floatToBuffer(data); 512 | case Constants.TYPE_INT: 513 | return Util.store64_le(data); 514 | default: 515 | if (typeof data === 'undefined') { 516 | return Buffer.from(Util.boolToChr(null), 'binary'); 517 | } 518 | return Util.toBuffer(data); 519 | } 520 | } 521 | 522 | /** 523 | * @returns {Backend} 524 | */ 525 | getBackend() 526 | { 527 | return this.engine.getBackend(); 528 | } 529 | 530 | /** 531 | * 532 | * @returns {CipherSweet} 533 | */ 534 | getEngine() 535 | { 536 | return this.engine; 537 | } 538 | 539 | /** 540 | * @returns {boolean} 541 | */ 542 | getFlatIndexes() 543 | { 544 | return !this.typedIndexes; 545 | } 546 | 547 | /** 548 | * @returns {boolean} 549 | */ 550 | getTypedIndexes() 551 | { 552 | return this.typedIndexes; 553 | } 554 | 555 | /** 556 | * 557 | * @param {boolean} bool 558 | * @returns {EncryptedRow} 559 | */ 560 | setFlatIndexes(bool) 561 | { 562 | this.typedIndexes = !bool; 563 | return this; 564 | } 565 | 566 | /** 567 | * @param {boolean} bool 568 | * @returns {EncryptedRow} 569 | */ 570 | setTypedIndexes(bool) 571 | { 572 | this.typedIndexes = bool; 573 | return this; 574 | } 575 | }; 576 | -------------------------------------------------------------------------------- /lib/exception/arraykeyexception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CipherSweetException = require('./ciphersweetexception'); 4 | 5 | /** 6 | * @class ArrayKeyException 7 | * @package CipherSweet.exception 8 | * @author Paragon Initiative Enterprises 9 | */ 10 | module.exports = class ArrayKeyException extends CipherSweetException {}; 11 | -------------------------------------------------------------------------------- /lib/exception/blindindexnamecollisionexception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CipherSweetException = require('./ciphersweetexception'); 4 | 5 | /** 6 | * @class BlindIndexNameCollisionException 7 | * @package CipherSweet.exception 8 | * @author Paragon Initiative Enterprises 9 | */ 10 | module.exports = class BlindIndexNameCollisionException extends CipherSweetException {}; 11 | -------------------------------------------------------------------------------- /lib/exception/blindindexnotfoundexception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CipherSweetException = require('./ciphersweetexception'); 4 | 5 | /** 6 | * @class BlindIndexNotFoundException 7 | * @package CipherSweet.exception 8 | * @author Paragon Initiative Enterprises 9 | */ 10 | module.exports = class BlindIndexNotFoundException extends CipherSweetException {}; 11 | -------------------------------------------------------------------------------- /lib/exception/ciphersweetexception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @class CipherSweetException 5 | * @package CipherSweet.exception 6 | * @author Paragon Initiative Enterprises 7 | */ 8 | module.exports = class CipherSweetException extends Error {}; 9 | -------------------------------------------------------------------------------- /lib/exception/cryptooperationexception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CipherSweetException = require('./ciphersweetexception'); 4 | 5 | /** 6 | * @class CryptoOperationException 7 | * @package CipherSweet.exception 8 | * @author Paragon Initiative Enterprises 9 | */ 10 | module.exports = class CryptoOperationException extends CipherSweetException {}; 11 | -------------------------------------------------------------------------------- /lib/exception/invalidciphertextexception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CipherSweetException = require('./ciphersweetexception'); 4 | 5 | /** 6 | * @class InvalidCiphertextException 7 | * @package CipherSweet.exception 8 | * @author Paragon Initiative Enterprises 9 | */ 10 | module.exports = class InvalidCiphertextException extends CipherSweetException {}; 11 | -------------------------------------------------------------------------------- /lib/exception/plannerexception.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CipherSweetException = require('./ciphersweetexception'); 4 | 5 | /** 6 | * @class PlannerException 7 | * @package CipherSweet.exception 8 | * @author Paragon Initiative Enterprises 9 | */ 10 | module.exports = class PlannerException extends CipherSweetException {}; 11 | -------------------------------------------------------------------------------- /lib/keyprovider/multitenantprovider.js: -------------------------------------------------------------------------------- 1 | const MultiTenantAwareProvider = require('../contract/multitenantawareprovider'); 2 | const CipherSweetException = require('../exception/ciphersweetexception'); 3 | 4 | module.exports = class MultiTenantProvider extends MultiTenantAwareProvider { 5 | /** 6 | * @param {Map} keyProviders 7 | * @param {string|null} active 8 | */ 9 | constructor(keyProviders, active = null) { 10 | super(); 11 | this.tenants = {}; 12 | for (let name in keyProviders) { 13 | this.tenants[name] = keyProviders[name]; 14 | } 15 | this.active = active; 16 | } 17 | 18 | /** 19 | * @param {string} index 20 | * @param {KeyProvider} provider 21 | * @returns {MultiTenantProvider} 22 | */ 23 | addTenant(index, provider) { 24 | this.tenants[index] = provider; 25 | return this; 26 | } 27 | 28 | /** 29 | * @param {string|null} index 30 | * @returns {MultiTenantProvider} 31 | */ 32 | setActiveTenant(index) { 33 | this.active = index; 34 | return this; 35 | } 36 | 37 | /** 38 | * 39 | * @param {string} index 40 | * @returns {KeyProvider} 41 | */ 42 | getTenant(index) { 43 | if (!(index in this.tenants)) { 44 | throw new CipherSweetException('Tenant does not exist'); 45 | } 46 | return this.tenants[index]; 47 | } 48 | 49 | /** 50 | * @returns {KeyProvider} 51 | */ 52 | getActiveTenant() { 53 | if (this.active === null) { 54 | throw new CipherSweetException('Active tenant not set'); 55 | } 56 | if (!(this.active in this.tenants)) { 57 | throw new CipherSweetException('Tenant does not exist'); 58 | } 59 | return this.tenants[this.active]; 60 | } 61 | 62 | /** 63 | * @returns {SymmetricKey} 64 | */ 65 | getSymmetricKey() { 66 | if (this.active === null) { 67 | throw new CipherSweetException('Active tenant not set'); 68 | } 69 | return this.getActiveTenant().getSymmetricKey(); 70 | } 71 | /** 72 | * OVERRIDE THIS in your own class! 73 | * 74 | * Given a row of data, determine which tenant should be selected. 75 | * 76 | * @param {object} row 77 | * @param {string} tableName 78 | * @returns {string} 79 | */ 80 | getTenantFromRow(row, tableName) { 81 | if (this.active === null) { 82 | throw new CipherSweetException('This is not implemented. Please override in a child class.'); 83 | } 84 | return this.active; 85 | } 86 | 87 | /** 88 | * OVERRIDE THIS in your own class! 89 | * 90 | * @param {object} row 91 | * @param {string} tableName 92 | * @returns {object} 93 | */ 94 | injectTenantMetadata(row, tableName) { 95 | return row; 96 | } 97 | } -------------------------------------------------------------------------------- /lib/keyprovider/stringprovider.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CryptoOperationException = require('../exception/cryptooperationexception'); 4 | const KeyProvider = require('../contract/keyprovider'); 5 | const SymmetricKey = require('../backend/key/symmetrickey'); 6 | const Util = require('../util'); 7 | 8 | module.exports = class StringProvider extends KeyProvider 9 | { 10 | constructor(string) 11 | { 12 | let buf; 13 | super(); 14 | if (Buffer.isBuffer(string)) { 15 | if (string.length !== 32) { 16 | throw new CryptoOperationException('Invalid key size'); 17 | } 18 | buf = string; 19 | } else if (string.length === 64) { 20 | buf = Buffer.from(string, 'hex'); 21 | } else if (string.length === 32) { 22 | buf = Util.toBuffer(string); 23 | } 24 | this.symmetricKey = Buffer.alloc(32, 0); 25 | buf.copy(this.symmetricKey, 0); 26 | } 27 | 28 | /** 29 | * @returns {SymmetricKey} 30 | */ 31 | getSymmetricKey() 32 | { 33 | return new SymmetricKey(this.symmetricKey); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lib/keyrotation/fieldrotator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const InvalidCiphertextException = require('../exception/invalidciphertextexception'); 4 | const Util = require('../util'); 5 | 6 | module.exports = class FieldRotator 7 | { 8 | /** 9 | * 10 | * @param {EncryptedField} oldField 11 | * @param {EncryptedField} newField 12 | */ 13 | constructor(oldField, newField) 14 | { 15 | this.oldField = oldField; 16 | this.newField = newField; 17 | } 18 | 19 | /** 20 | * 21 | * @param {string} ciphertext 22 | * @param {string} aad 23 | * @returns {Promise} 24 | */ 25 | async needsReEncrypt(ciphertext, aad) 26 | { 27 | if (!(typeof ciphertext === 'string')) { 28 | throw new InvalidCiphertextException('FieldRotator expects a string, not an array'); 29 | } 30 | if (ciphertext.length < 5) { 31 | throw new InvalidCiphertextException('This message is not encrypted'); 32 | } 33 | const pre = ciphertext.slice(0, 5); 34 | if (!await Util.hashEquals(pre, this.newField.getBackend().getPrefix())) { 35 | return true; 36 | } 37 | 38 | try { 39 | await this.newField.decryptValue(ciphertext, aad); 40 | return false; 41 | } catch (e) { 42 | return true; 43 | } 44 | } 45 | 46 | /** 47 | * 48 | * @param {string} values 49 | * @param {string} oldAad 50 | * @param {string} newAad 51 | * @returns {object} 52 | */ 53 | async prepareForUpdate(values, oldAad = '', newAad = '') 54 | { 55 | const plaintext = await this.oldField.decryptValue(values, oldAad); 56 | return await this.newField.prepareForStorage(plaintext, newAad); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /lib/keyrotation/multirowsrotator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const InvalidCiphertextException = require('../exception/invalidciphertextexception'); 4 | 5 | module.exports = class MultiRowsRotator 6 | { 7 | /** 8 | * 9 | * @param {EncryptedMultiRows} oldMultiRows 10 | * @param {EncryptedMultiRows} newMultiRows 11 | */ 12 | constructor(oldMultiRows, newMultiRows) 13 | { 14 | this.oldMultiRows = oldMultiRows; 15 | this.newMultiRows = newMultiRows; 16 | } 17 | 18 | /** 19 | * 20 | * @param {object} ciphertext 21 | * @returns {Promise} 22 | */ 23 | async needsReEncrypt(ciphertext) 24 | { 25 | if (typeof ciphertext === 'string') { 26 | throw new InvalidCiphertextException('FieldRotator expects an array/object, not a string'); 27 | } 28 | try { 29 | await this.newMultiRows.decryptManyRows(ciphertext); 30 | return false; 31 | } catch (e) { 32 | return true; 33 | } 34 | } 35 | 36 | /** 37 | * 38 | * @param {object} values 39 | * @returns {Promise} 40 | */ 41 | async prepareForUpdate(values) 42 | { 43 | return await this.newMultiRows.prepareForStorage( 44 | await this.oldMultiRows.decryptManyRows(values) 45 | ); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /lib/keyrotation/rowrotator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const InvalidCiphertextException = require('../exception/invalidciphertextexception'); 4 | 5 | module.exports = class RowRotator 6 | { 7 | /** 8 | * 9 | * @param {EncryptedRow} oldRow 10 | * @param {EncryptedRow} newRow 11 | */ 12 | constructor(oldRow, newRow) 13 | { 14 | this.oldRow = oldRow; 15 | this.newRow = newRow; 16 | } 17 | 18 | /** 19 | * 20 | * @param {object} ciphertext 21 | * @returns {Promise} 22 | */ 23 | async needsReEncrypt(ciphertext) 24 | { 25 | if (typeof ciphertext === 'string') { 26 | throw new InvalidCiphertextException('FieldRotator expects an array/object, not a string'); 27 | } 28 | try { 29 | await this.newRow.decryptRow(ciphertext); 30 | return false; 31 | } catch (e) { 32 | return true; 33 | } 34 | } 35 | 36 | /** 37 | * 38 | * @param {object} values 39 | * @returns {Promise} 40 | */ 41 | async prepareForUpdate(values) 42 | { 43 | return await this.newRow.prepareRowForStorage( 44 | await this.oldRow.decryptRow(values) 45 | ); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /lib/planner/fieldindexplanner.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const PlannerException = require('../exception/plannerexception'); 4 | 5 | /** 6 | * @class FieldIndexPlanner 7 | * @type {FieldIndexPlanner} 8 | */ 9 | module.exports = class FieldIndexPlanner 10 | { 11 | constructor() 12 | { 13 | this.population = 0; 14 | this.indexes = {}; 15 | } 16 | 17 | /** 18 | * 19 | * @param {EncryptedField} field 20 | * @returns {FieldIndexPlanner} 21 | */ 22 | static fromEncryptedField(field) 23 | { 24 | const self = new FieldIndexPlanner(); 25 | let obj; 26 | const objects = field.getBlindIndexObjects(); 27 | for (let name in objects) { 28 | obj = objects[name]; 29 | self.addExistingIndex(name, obj.getFilterBitLength(), Number.MAX_SAFE_INTEGER); 30 | } 31 | return self; 32 | } 33 | 34 | /** 35 | * 36 | * @param {string} name 37 | * @param {number} L 38 | * @param {number} K 39 | * @returns {FieldIndexPlanner} 40 | */ 41 | addExistingIndex(name, L, K = Number.MAX_SAFE_INTEGER) 42 | { 43 | this.indexes[name] = {'L': L, 'K': K}; 44 | return this; 45 | } 46 | 47 | /** 48 | * @returns {number} 49 | */ 50 | getCoincidenceCount() 51 | { 52 | return FieldIndexPlanner.coincidenceCounter( 53 | Object.values(this.indexes), 54 | this.population 55 | ); 56 | } 57 | 58 | /** 59 | * @param extraFieldPopulationBits 60 | * @returns {{min: number, max: number}} 61 | */ 62 | recommend(extraFieldPopulationBits = Number.MAX_SAFE_INTEGER) 63 | { 64 | if (this.population < 1) { 65 | throw new PlannerException('An empty population is not useful for estimates'); 66 | } 67 | const existing = Object.values(this.indexes); 68 | /** @var {{min: number|null, max: number|null}} recommend */ 69 | const recommend = {'min': null, 'max': null}; 70 | const sqrtR = Math.sqrt(this.population); 71 | 72 | let tmp = Object.values(existing); 73 | tmp.push({'L': 257, 'K': extraFieldPopulationBits}); 74 | 75 | let coincidences = 0; 76 | let boundary = Math.max(2, FieldIndexPlanner.coincidenceCounter(tmp, this.population)); 77 | for (let l = 256; l >= 1; --l) { 78 | tmp = Object.values(existing); 79 | tmp.push({'L': l, 'K': extraFieldPopulationBits}); 80 | coincidences = FieldIndexPlanner.coincidenceCounter(tmp, this.population); 81 | if (!recommend['max'] && coincidences > boundary) { 82 | recommend['max'] = l + 1; 83 | } 84 | if (coincidences >= 2 && coincidences <= sqrtR) { 85 | recommend['min'] = l; 86 | } 87 | } 88 | 89 | if (!recommend['min']) { 90 | recommend['min'] = 1; 91 | } 92 | 93 | if (!recommend['max']) { 94 | throw new PlannerException('There is no safe upper bound'); 95 | } 96 | 97 | if (recommend['min'] > recommend['max']) { 98 | recommend['min'] = recommend['max']; 99 | } 100 | return recommend; 101 | } 102 | 103 | /** 104 | * 105 | * @param extraFieldPopulationBits 106 | * @returns {number} 107 | */ 108 | recommendLow(extraFieldPopulationBits = Number.MAX_SAFE_INTEGER) 109 | { 110 | return this.recommend(extraFieldPopulationBits).min; 111 | } 112 | 113 | /** 114 | * 115 | * @param extraFieldPopulationBits 116 | * @returns {number} 117 | */ 118 | recommendHigh(extraFieldPopulationBits = Number.MAX_SAFE_INTEGER) 119 | { 120 | return this.recommend(extraFieldPopulationBits).max; 121 | } 122 | 123 | /** 124 | * @param {number} num 125 | * @returns {FieldIndexPlanner} 126 | */ 127 | setEstimatedPopulation(num) 128 | { 129 | this.population = num; 130 | return this; 131 | } 132 | 133 | /** 134 | * @param {number} num 135 | * @returns {FieldIndexPlanner} 136 | */ 137 | withPopulation(num) 138 | { 139 | const self = new FieldIndexPlanner(); 140 | for (let i in this.indexes) { 141 | self.indexes[i] = Object.assign({}, this.indexes[i]); 142 | } 143 | self.population = num; 144 | return self; 145 | } 146 | 147 | /** 148 | * 149 | * @param {array} indexes 150 | * @param {number} R 151 | */ 152 | static coincidenceCounter(indexes, R) 153 | { 154 | let exponent = 0; 155 | const count = indexes.length; 156 | for (let i = 0; i < count; ++i) { 157 | let index = indexes[i]; 158 | exponent += Math.min(index.L, index.K); 159 | } 160 | return Math.max(1, R) / Math.pow(2, exponent); 161 | } 162 | }; 163 | -------------------------------------------------------------------------------- /lib/transformation/alphacharactersonly.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Transformation = require('../contract/transformation'); 4 | 5 | module.exports = class AlphaCharactersOnly extends Transformation 6 | { 7 | /** 8 | * @param {string|Buffer} input 9 | * @returns {Buffer} 10 | */ 11 | async invoke(input) 12 | { 13 | let str; 14 | if (Buffer.isBuffer(input)) { 15 | str = input.toString('binary'); 16 | } else if (typeof input === 'string') { 17 | str = input; 18 | } else { 19 | throw new TypeError(); 20 | } 21 | str = str.replace(/[^A-Za-z]/, ''); 22 | return Buffer.from(str); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/transformation/compound.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const base64url = require('rfc4648').base64url; 4 | const RowTransformation = require('../contract/rowtransformation'); 5 | const Util = require('../util'); 6 | 7 | module.exports = class Compound extends RowTransformation 8 | { 9 | /** 10 | * @param {Array} input 11 | * @returns {string} 12 | */ 13 | async invoke(input) 14 | { 15 | const result = JSON.stringify( 16 | await Compound.processArray(input, 0) 17 | ); 18 | if (result === '{}') { 19 | return '[]'; 20 | } 21 | return result; 22 | } 23 | 24 | /** 25 | * @param {Array|Object} input 26 | * @param {Number} layer 27 | * @returns {Array} 28 | */ 29 | static async processArray(input, layer = 0) 30 | { 31 | if (layer > 255) { 32 | throw new Error('Too much recursion'); 33 | } 34 | let value; 35 | const result = {}; 36 | /** @var {string} key */ 37 | for (let key in input) { 38 | value = input[key]; 39 | if (Array.isArray(value)) { 40 | result[key] = this.processArray(value, layer + 1); 41 | continue; 42 | } 43 | if (typeof value === 'object' && value !== null) { 44 | result[key] = this.processArray(value, layer + 1); 45 | continue; 46 | } 47 | if (typeof value === 'number') { 48 | result[key] = value.toString(); 49 | continue; 50 | } 51 | if (typeof value === 'string') { 52 | result[key] = Compound.packString(value); 53 | continue; 54 | } 55 | result[key] = value; 56 | } 57 | return result; 58 | } 59 | 60 | /** 61 | * @param {string} str 62 | * @returns {string} 63 | */ 64 | static packString(str) 65 | { 66 | return Util.store64_le(str.length).toString('hex') + 67 | base64url.stringify(Buffer.from(str)); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /lib/transformation/firstcharacter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Transformation = require('../contract/transformation'); 4 | 5 | module.exports = class FirstCharacter extends Transformation 6 | { 7 | /** 8 | * @param {string|Buffer} input 9 | * @returns {Buffer} 10 | */ 11 | async invoke(input) 12 | { 13 | let str; 14 | if (Buffer.isBuffer(input)) { 15 | str = input.toString('binary'); 16 | } else if (typeof input === 'string') { 17 | str = input; 18 | } else { 19 | throw new TypeError(); 20 | } 21 | return Buffer.from(str[0]); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/transformation/lastfourdigits.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Transformation = require('../contract/transformation'); 4 | 5 | module.exports = class LastFourDigits extends Transformation 6 | { 7 | /** 8 | * @param {string|Buffer} input 9 | * @returns {Buffer} 10 | */ 11 | async invoke(input) 12 | { 13 | let str; 14 | if (Buffer.isBuffer(input)) { 15 | str = input.toString('binary'); 16 | } else if (typeof input === 'string') { 17 | str = input; 18 | } else { 19 | throw new TypeError(); 20 | } 21 | const result = Buffer.alloc(4, 0); 22 | str = str.replace(/[^0-9]/g, ''); 23 | if (str.length < 4) { 24 | Buffer.from(str).copy(result, 4 - str.length); 25 | } else { 26 | Buffer.from(str.slice(str.length - 4)).copy(result, 0); 27 | } 28 | return result; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/transformation/lowercase.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Transformation = require('../contract/transformation'); 4 | 5 | module.exports = class Lowercase extends Transformation 6 | { 7 | /** 8 | * @param {string|Buffer} input 9 | * @returns {Buffer} 10 | */ 11 | async invoke(input) 12 | { 13 | let str; 14 | if (Buffer.isBuffer(input)) { 15 | str = input.toString('binary'); 16 | } else if (typeof input === 'string') { 17 | str = input; 18 | } else { 19 | throw new TypeError(); 20 | } 21 | const result = Buffer.alloc(str.length, 0); 22 | str = str.toLowerCase(); 23 | Buffer.from(str).copy(result, 0); 24 | return result; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const crypto = require('crypto'); 4 | const SodiumPlus = require('sodium-plus').SodiumPlus; 5 | const arrayToBuffer = require('typedarray-to-buffer'); 6 | const SymmetricKey = require('./backend/key/symmetrickey'); 7 | const CryptoOperationException = require('./exception/cryptooperationexception'); 8 | 9 | let sodium; 10 | 11 | /** 12 | * Class Util 13 | * 14 | * @package CipherSweet 15 | * @author Paragon Initiative Enterprises 16 | */ 17 | module.exports = class Util 18 | { 19 | /** 20 | * @param {boolean|null} bool 21 | * @returns {string} 22 | */ 23 | static boolToChr(bool) 24 | { 25 | if (bool === true) { 26 | return "\x02"; 27 | } else if (bool === false) { 28 | return "\x01"; 29 | } else if (bool === null) { 30 | return "\x00"; 31 | } else { 32 | throw new TypeError('Only TRUE, FALSE, or NULL allowed'); 33 | } 34 | } 35 | 36 | /** 37 | * @param {string} chr 38 | * @returns {boolean|null} 39 | */ 40 | static chrToBool(chr) 41 | { 42 | if (Util.hashEqualsSync(chr[0], "\x02")) { 43 | return true; 44 | } else if (Util.hashEqualsSync(chr[0], "\x01")) { 45 | return false; 46 | } else if (Util.hashEqualsSync(chr[0], "\x00")) { 47 | return null; 48 | } else { 49 | throw new TypeError('Internal integer is not 0, 1, or 2'); 50 | } 51 | } 52 | 53 | /** 54 | * 55 | * @param {Number} num 56 | * @returns {Buffer} 57 | */ 58 | static floatToBuffer(num) 59 | { 60 | return arrayToBuffer(new Float64Array([num]).buffer); 61 | } 62 | 63 | /** 64 | * @param {Buffer} buffer 65 | * @returns {Number} 66 | */ 67 | static bufferToFloat(buffer) 68 | { 69 | return buffer.readDoubleLE(0); 70 | } 71 | /** 72 | * 73 | * @param {string|Buffer} input 74 | * @param {Number} bits 75 | * @param {boolean} bitwiseLeft 76 | */ 77 | static async andMask(input, bits, bitwiseLeft = false) 78 | { 79 | input = await Util.toBuffer(input); 80 | const bytes = bits >>> 3; 81 | if (bytes >= input.length) { 82 | input = Buffer.concat([ 83 | input, 84 | Buffer.alloc(bytes - input.length + 1, 0) 85 | ]); 86 | } 87 | const output = input.slice(0, bytes); 88 | const leftOver = bits - (bytes << 3); 89 | const chr = Buffer.alloc(1, 0); 90 | if (leftOver > 0) { 91 | let mask = (1 << leftOver) - 1; 92 | if (!bitwiseLeft) { 93 | mask = (mask & 0xF0) >>> 4 | (mask & 0x0F) << 4; 94 | mask = (mask & 0xCC) >>> 2 | (mask & 0x33) << 2; 95 | mask = (mask & 0xAA) >>> 1 | (mask & 0x55) << 1; 96 | } 97 | chr[0] = input[bytes] & mask; 98 | return Buffer.concat([output, chr]); 99 | } 100 | return output; 101 | } 102 | 103 | /** 104 | * 105 | * @param {string|Buffer} input 106 | * @param {Number} bits 107 | * @param {boolean} bitwiseLeft 108 | */ 109 | static andMaskSync(input, bits, bitwiseLeft = false) 110 | { 111 | input = Util.toBufferSync(input); 112 | const bytes = bits >>> 3; 113 | if (bytes >= input.length) { 114 | input = Buffer.concat([ 115 | input, 116 | Buffer.alloc(bytes - input.length + 1, 0) 117 | ]); 118 | } 119 | let output = input.slice(0, bytes); 120 | const leftOver = bits - (bytes << 3); 121 | const chr = Buffer.alloc(1, 0); 122 | if (leftOver > 0) { 123 | let mask = (1 << leftOver) - 1; 124 | if (!bitwiseLeft) { 125 | mask = (mask & 0xF0) >>> 4 | (mask & 0x0F) << 4; 126 | mask = (mask & 0xCC) >>> 2 | (mask & 0x33) << 2; 127 | mask = (mask & 0xAA) >>> 1 | (mask & 0x55) << 1; 128 | } 129 | chr[0] = input[bytes] & mask; 130 | output = Buffer.concat([output, chr]); 131 | } 132 | return output; 133 | } 134 | 135 | /** 136 | * Gets the string representation of a Buffer. 137 | * 138 | * @param {Buffer} buffer 139 | * @returns {string} 140 | */ 141 | static fromBuffer(buffer) 142 | { 143 | if (!Buffer.isBuffer(buffer)) { 144 | throw new TypeError('Invalid type; string or buffer expected'); 145 | } 146 | return buffer.toString('binary'); 147 | } 148 | 149 | /** 150 | * Get the digest size based on a hash function name. 151 | * 152 | * @param {string} algo 153 | * @returns {Number} 154 | */ 155 | static hashDigestLength(algo) 156 | { 157 | if (algo === 'sha256') { 158 | return 32; 159 | } else if (algo === 'sha384') { 160 | return 48; 161 | } else if (algo === 'sha512') { 162 | return 64; 163 | } else if (algo === 'sha224') { 164 | return 24; 165 | } 166 | const hasher = crypto.createHash(algo); 167 | hasher.update(''); 168 | const digest = hasher.digest(); 169 | return digest.length; 170 | } 171 | 172 | /** 173 | * Compare two strings without timing leaks. 174 | * 175 | * @param {string|Buffer} a 176 | * @param {string|Buffer} b 177 | * @returns {boolean} 178 | */ 179 | static async hashEquals(a, b) 180 | { 181 | return crypto.timingSafeEqual( 182 | await Util.toBuffer(a), 183 | await Util.toBuffer(b) 184 | ); 185 | } 186 | 187 | /** 188 | * Compare two strings without timing leaks. 189 | * 190 | * @param {string|Buffer} a 191 | * @param {string|Buffer} b 192 | * @returns {boolean} 193 | */ 194 | static hashEqualsSync(a, b) 195 | { 196 | return crypto.timingSafeEqual( 197 | Util.toBufferSync(a), 198 | Util.toBufferSync(b) 199 | ); 200 | } 201 | 202 | /** 203 | * 204 | * @param {string} hash 205 | * @param {string|Buffer} message 206 | * @param {string|Buffer} key 207 | * @param {boolean} binary 208 | * @returns {string|Buffer} 209 | */ 210 | static async hmac(hash, message, key, binary = false) 211 | { 212 | const auth = crypto.createHmac(hash, key); 213 | auth.update(message); 214 | if (binary) { 215 | return auth.digest(); 216 | } 217 | return auth.digest('hex'); 218 | } 219 | 220 | /** 221 | * HKDF - RFC 5869 222 | * 223 | * @param {SymmetricKey|Buffer} key 224 | * @param {string|Buffer} salt 225 | * @param {string|Buffer} info 226 | * @param {Number} length 227 | * @param {string} hash 228 | * @returns {Buffer} 229 | * @constructor 230 | */ 231 | static async HKDF(key, salt, info = '', length = 32, hash = 'sha384') 232 | { 233 | let ikm; 234 | 235 | if (Buffer.isBuffer(key)) { 236 | ikm = key; 237 | } else if (SymmetricKey.isSymmetricKey(key)) { 238 | ikm = key.getRawKey(); 239 | } else { 240 | throw new TypeError('Argument 1 must be a SymmetricKey'); 241 | } 242 | if (!Buffer.isBuffer(info)) { 243 | info = await Util.toBuffer(info); 244 | } 245 | const digestLength = Util.hashDigestLength(hash); 246 | 247 | if (length < 0 || length > (255 * digestLength)) { 248 | throw new CryptoOperationException('Bad output length requested of HKDF.') 249 | } 250 | if (salt) { 251 | salt = await Util.toBuffer(salt); 252 | } else { 253 | salt = Buffer.alloc(digestLength, 0); 254 | } 255 | 256 | // HKDF-Extract: 257 | // PRK = HMAC-Hash(salt, IKM) 258 | // The salt is the HMAC key. 259 | const prk = await Util.hmac( 260 | hash, 261 | ikm, 262 | salt, 263 | true 264 | ); 265 | 266 | // HKDF-Expand: 267 | // T(0) = '' 268 | let last_block = Buffer.alloc(0); 269 | let t = Buffer.alloc(0); 270 | const c = Buffer.alloc(1); 271 | for (let i = 1; t.length < length; i++) { 272 | // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) 273 | c[0] = i; 274 | last_block = await Util.hmac( 275 | hash, 276 | Buffer.concat([ 277 | last_block, 278 | info, 279 | c 280 | ]), 281 | prk, 282 | true 283 | ); 284 | t = Buffer.concat([t, last_block]); 285 | } 286 | return t.slice(0, length); 287 | } 288 | 289 | /** 290 | * 291 | * @param {Buffer} nonce 292 | * @param {number} amount 293 | * @returns {Promise} 294 | */ 295 | static async increaseCtrNonce(nonce, amount = 1) 296 | { 297 | const outNonce = Buffer.alloc(16, 0); 298 | nonce.copy(outNonce, 0, 0, 16); 299 | let c = amount; 300 | let x; 301 | for (let i = 15; i >= 0; i--) { 302 | x = outNonce[i] + c; 303 | c = x >>> 8; 304 | outNonce[i] = x & 0xff; 305 | } 306 | return outNonce; 307 | } 308 | 309 | /** 310 | * Node.js only supports 32-bit numbers so we discard the top 4 bytes. 311 | * 312 | * @param {Buffer} buf 313 | * @returns {Number} 314 | */ 315 | static load64_le(buf) 316 | { 317 | return buf.readInt32LE(0); 318 | } 319 | 320 | /** 321 | * Pack chunks together for feeding into HMAC. 322 | * 323 | * @param {Buffer[]} pieces 324 | * @returns Buffer 325 | */ 326 | static pack(pieces) 327 | { 328 | let output = Util.store32_le(pieces.length); 329 | let piece; 330 | let pieceLen; 331 | for (let i = 0; i < pieces.length; i++) { 332 | piece = pieces[i]; 333 | pieceLen = Util.store64_le(piece.length); 334 | output = Buffer.concat([output, pieceLen, piece]); 335 | } 336 | return output; 337 | } 338 | 339 | /** 340 | * await-able PBKDF2 interface 341 | * 342 | * @param {Buffer} password 343 | * @param {Buffer} salt 344 | * @param {number} iterations 345 | * @param {number} keylen 346 | * @param {string} digest 347 | * @returns {Promise} 348 | */ 349 | static pbkdf2(password, salt, iterations, keylen, digest) 350 | { 351 | return new Promise( (res, rej) => { 352 | crypto.pbkdf2(password, salt, iterations, keylen, digest, (err, key) => { 353 | err ? rej(err) : res(key); 354 | }); 355 | }); 356 | } 357 | 358 | /** 359 | * @param {Number} len 360 | * @returns {Buffer} 361 | */ 362 | static async randomBytes(len) 363 | { 364 | if (!sodium) sodium = await SodiumPlus.auto(); 365 | return sodium.randombytes_buf(len); 366 | } 367 | 368 | /** 369 | * Store a 32-bit integer as a buffer of length 4 370 | * 371 | * @param {Number} num 372 | * @returns {Buffer} 373 | */ 374 | static store32_le(num) 375 | { 376 | const result = Buffer.alloc(4, 0); 377 | result[0] = num & 0xff; 378 | result[1] = (num >>> 8) & 0xff; 379 | result[2] = (num >>> 16) & 0xff; 380 | result[3] = (num >>> 24) & 0xff; 381 | return result; 382 | } 383 | 384 | /** 385 | * JavaScript only supports 32-bit integers, so we're going to 386 | * zero-fill the rightmost bytes. 387 | * 388 | * @param {Number} num 389 | * @returns {Buffer} 390 | */ 391 | static store64_le(num) 392 | { 393 | const result = Buffer.alloc(8, 0); 394 | result[0] = num & 0xff; 395 | result[1] = (num >>> 8) & 0xff; 396 | result[2] = (num >>> 16) & 0xff; 397 | result[3] = (num >>> 24) & 0xff; 398 | return result; 399 | } 400 | /** 401 | * Coerce input to a Buffer, throwing a TypeError if it cannot be coerced. 402 | * 403 | * @param {string|Buffer|Uint8Array} stringOrBuffer 404 | * @returns {Buffer} 405 | */ 406 | static async toBuffer(stringOrBuffer) 407 | { 408 | if (stringOrBuffer === null) { 409 | return Buffer.alloc(0); 410 | } 411 | if (Buffer.isBuffer(stringOrBuffer)) { 412 | return stringOrBuffer; 413 | } else if (typeof(stringOrBuffer) === 'number') { 414 | return Buffer.from("" + stringOrBuffer, 'utf-8'); 415 | } else if (typeof(stringOrBuffer) === 'string') { 416 | return Buffer.from(stringOrBuffer, 'binary'); 417 | } else if (stringOrBuffer instanceof Uint8Array) { 418 | return arrayToBuffer(stringOrBuffer); 419 | } else if (stringOrBuffer instanceof Promise) { 420 | return await stringOrBuffer; 421 | } else { 422 | throw new TypeError('Invalid type; string or buffer expected'); 423 | } 424 | } 425 | 426 | /** 427 | * Coerce input to a Buffer, throwing a TypeError if it cannot be coerced. 428 | * 429 | * @param {string|Buffer|Uint8Array} stringOrBuffer 430 | * @returns {Buffer} 431 | */ 432 | static toBufferSync(stringOrBuffer) 433 | { 434 | if (Buffer.isBuffer(stringOrBuffer)) { 435 | return stringOrBuffer; 436 | } else if (typeof(stringOrBuffer) === 'string') { 437 | return Buffer.from(stringOrBuffer, 'binary'); 438 | } else if (stringOrBuffer instanceof Uint8Array) { 439 | return arrayToBuffer(stringOrBuffer); 440 | } else if (stringOrBuffer instanceof Promise) { 441 | throw new TypeError('Promise passed instead of buffer. Please await your promises.'); 442 | } else { 443 | throw new TypeError('Invalid type; string or buffer expected'); 444 | } 445 | } 446 | }; 447 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ciphersweet-js", 3 | "version": "2.0.6", 4 | "description": "Searchable encryption for Node.js projects", 5 | "repository": "https://github.com/paragonie/ciphersweet-js", 6 | "main": "index.js", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "test": "mocha" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^18.7.23", 13 | "assert": "^2.0.0", 14 | "chai": "^4.3.6", 15 | "mocha": "^10.0.0" 16 | }, 17 | "keywords": [ 18 | "encrypt", 19 | "encryption", 20 | "searchable encryption", 21 | "ciphersweet", 22 | "paragonie", 23 | "sodium", 24 | "libsodium", 25 | "openssl" 26 | ], 27 | "author": "Paragon Initiative Enterprises, LLC", 28 | "license": "ISC", 29 | "dependencies": { 30 | "clone": "^2.1.2", 31 | "fs-extra": "^10.1.0", 32 | "load-json-file": "^7.0.1", 33 | "poly1305-js": "^0.4.4", 34 | "rfc4648": "^1.5.2", 35 | "sodium-plus": "^0.9.0", 36 | "typedarray-to-buffer": "^4.0.0", 37 | "xchacha20-js": "^0.3.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/boringcrypto-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const BoringCrypto = require('../lib/backend/boringcrypto'); 4 | const SymmetricKey = require('../lib/backend/key/symmetrickey'); 5 | const {SodiumPlus} = require('sodium-plus'); 6 | let sodium; 7 | 8 | describe('BoringCrypto Tests', function () { 9 | it('Encrypts and decrypts successfully', async function () { 10 | if (!sodium) sodium = await SodiumPlus.auto(); 11 | this.timeout(5000); 12 | let random_buf = await sodium.randombytes_buf(32); 13 | let brng = new BoringCrypto(); 14 | let key = new SymmetricKey(random_buf); 15 | let plaintext, exampleKey, exampleCipher; 16 | 17 | // plaintext = 'This is a secret message'; 18 | plaintext = 'This is just a test message'; 19 | brng.encrypt(plaintext, key).then( 20 | (encrypted) => { 21 | brng.decrypt(encrypted, key).then( 22 | (decrypted) => { 23 | expect(decrypted).to.be.equal(plaintext); 24 | } 25 | ); 26 | } 27 | ); 28 | brng.encrypt(plaintext, key, 'test aad') 29 | .then(encrypted => { 30 | let caught = false; 31 | brng.decrypt(encrypted, key) 32 | .catch((e) => { 33 | caught = true; 34 | expect(e.message).to.be.equal('Invalid MAC'); 35 | }) 36 | .then(() => { 37 | if (!caught) { 38 | assert(null, 'AAD not being used in calculation'); 39 | } 40 | }); 41 | 42 | brng.decrypt(encrypted, key, 'test aad').then( 43 | (decrypted) => { 44 | expect(decrypted).to.be.equal(plaintext); 45 | } 46 | ); 47 | }); 48 | 49 | let exampleDecrypt; 50 | exampleKey = Buffer.from('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex'); 51 | exampleCipher = await brng.encrypt('This is just a test message', exampleKey); 52 | exampleDecrypt = await brng.decrypt(exampleCipher, exampleKey); 53 | expect(exampleDecrypt.toString('utf-8')).to.be.equal('This is just a test message'); 54 | 55 | exampleCipher = 'brng:m3y71cMwhTB2e8YjPLzZ2mwBoMRP1BgqVs_He47bRT5DJbWVBwG_cNsn6xvsl4rT2Cu1QSOEFt_lRECl3w524LlzGwgZ30UDm1KfgaTi9scjmu4='; 56 | exampleDecrypt = await brng.decrypt(exampleCipher, exampleKey); 57 | expect(exampleDecrypt.toString('utf-8')).to.be.equal('This is just a test message'); 58 | 59 | exampleKey = Buffer.from('0b036de5605144ea7aeed8bd3a191c08fe1b0ed69d9c8ba0dcbe82372451bb31', 'hex'); 60 | 61 | exampleCipher = await brng.encrypt('This is just a test message', exampleKey); 62 | exampleDecrypt = await brng.decrypt(exampleCipher, exampleKey); 63 | expect(exampleDecrypt.toString('utf-8')).to.be.equal('This is just a test message'); 64 | 65 | exampleCipher = 'brng:s0oCG2qoJMTWNreJ3AYQhTYSL423gsDYFKmSMDBzOUubIbiNPWSFZmD8uXMO5dmAhuCf5dvTCtfVvl8MADVL0dmub-znB7nEDYH2eMJBCmX-Qyc='; 66 | exampleDecrypt = await brng.decrypt(exampleCipher, exampleKey); 67 | expect(exampleDecrypt.toString('utf-8')).to.be.equal('This is just a test message'); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/brng-encrypted.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paragonie/ciphersweet-js/6002e018c0a607b03b802641d929e48862981fac/test/brng-encrypted.txt -------------------------------------------------------------------------------- /test/encryptedfield-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const {SodiumPlus} = require('sodium-plus'); 4 | let sodium; 5 | 6 | const BlindIndex = require('../lib/blindindex'); 7 | const CipherSweet = require('../lib/ciphersweet'); 8 | const EncryptedField = require('../lib/encryptedfield'); 9 | const BoringCrypto = require('../lib/backend/boringcrypto'); 10 | const FIPSCrypto = require('../lib/backend/fipsrypto'); 11 | const ModernCrypto = require('../lib/backend/moderncrypto'); 12 | const LastFourDigits = require('../lib/transformation/lastfourdigits'); 13 | const StringProvider = require('../lib/keyprovider/stringprovider'); 14 | const Util = require('../lib/util'); 15 | 16 | let buf, fipsEngine, naclEngine, fipsRandom, naclRandom, brngEngine, brngRandom; 17 | let initialized = false; 18 | 19 | /** 20 | * @return {Promise} 21 | */ 22 | async function initialize() { 23 | if (initialized) return true; 24 | if (!sodium) sodium = await SodiumPlus.auto(); 25 | if (!buf) buf = await sodium.randombytes_buf(32); 26 | if (!fipsEngine) fipsEngine = new CipherSweet( 27 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 28 | new FIPSCrypto() 29 | ); 30 | if (!naclEngine) naclEngine = new CipherSweet( 31 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 32 | new ModernCrypto() 33 | ); 34 | if (!brngEngine) brngEngine = new CipherSweet( 35 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 36 | new BoringCrypto() 37 | ); 38 | if (!fipsRandom) fipsRandom = new CipherSweet( 39 | new StringProvider(buf.toString('hex')), 40 | new FIPSCrypto() 41 | ); 42 | if (!naclRandom) naclRandom = new CipherSweet( 43 | new StringProvider(buf.toString('hex')), 44 | new ModernCrypto() 45 | ) 46 | if (!brngRandom) brngRandom = new CipherSweet( 47 | new StringProvider(buf.toString('hex')), 48 | new BoringCrypto() 49 | ); 50 | initialized = true; 51 | return false; 52 | } 53 | 54 | // Testing utility function 55 | function getExampleField(backend, longer = false, fast = false) 56 | { 57 | return (new EncryptedField(backend, 'contacts', 'ssn')) 58 | .addBlindIndex( 59 | new BlindIndex( 60 | 'contact_ssn_last_four', 61 | [new LastFourDigits()], 62 | longer ? 64 : 16, 63 | fast 64 | ) 65 | ) 66 | .addBlindIndex( 67 | new BlindIndex( 68 | 'contact_ssn_last_4', 69 | [new LastFourDigits()], 70 | longer ? 64 : 16, 71 | fast 72 | ) 73 | ) 74 | .addBlindIndex( 75 | new BlindIndex( 76 | 'contact_ssn', 77 | [], 78 | longer ? 128 : 32, 79 | fast 80 | ) 81 | ); 82 | } 83 | 84 | describe('EncryptedField', function () { 85 | it('Encrypts / decrypts fields successfully', async function () { 86 | if (!initialized) await initialize(); 87 | let eF = new EncryptedField(fipsEngine); 88 | let eM = new EncryptedField(naclEngine); 89 | let eB = new EncryptedField(brngEngine); 90 | 91 | let message = 'This is a test message: ' + (await Util.randomBytes(16)).toString('hex'); 92 | let aad = 'Test AAD:' + (await Util.randomBytes(32)).toString('hex'); 93 | 94 | let fCipher = await eF.encryptValue(message); 95 | let fDecrypt = await eF.decryptValue(fCipher); 96 | expect(fDecrypt.toString()).to.be.equal(message); 97 | let thrown = false; 98 | 99 | try { 100 | await eF.decryptValue(fCipher, aad); 101 | } catch (e) { 102 | thrown = true; 103 | } 104 | assert(thrown, 'exception thrown when AAD supplied erroneously'); 105 | 106 | let mCipher = await eM.encryptValue(message); 107 | let mDecrypt = await eM.decryptValue(mCipher); 108 | expect(mDecrypt.toString()).to.be.equal(message); 109 | thrown = false; 110 | try { 111 | await eM.decryptValue(mCipher, aad); 112 | } catch (e) { 113 | thrown = true; 114 | } 115 | assert(thrown, 'exception thrown when AAD supplied erroneously'); 116 | 117 | let bCipher = await eB.encryptValue(message); 118 | let bDecrypt = await eB.decryptValue(bCipher); 119 | expect(bDecrypt.toString()).to.be.equal(message); 120 | thrown = false; 121 | try { 122 | await eB.decryptValue(bCipher, aad); 123 | } catch (e) { 124 | thrown = true; 125 | } 126 | assert(thrown, 'exception thrown when AAD supplied erroneously'); 127 | }); 128 | 129 | it('Blind Indexing (FIPSCrypto)', async function () { 130 | if (!initialized) await initialize(); 131 | let ssn = getExampleField(fipsEngine).setTypedIndexes(true); 132 | ssn.getBlindIndex('111-11-1111', 'contact_ssn_last_four') 133 | .then((example) => { 134 | expect(example.type).to.be.equal("idlzpypmia6qu"); 135 | expect(example.value).to.be.equal("334b"); 136 | }); 137 | 138 | ssn.getBlindIndex('111-11-2222', 'contact_ssn_last_four') 139 | .then((example) => { 140 | expect(example.type).to.be.equal("idlzpypmia6qu"); 141 | expect(example.value).to.be.equal("7947"); 142 | }); 143 | 144 | ssn.getBlindIndex('123-45-6788', 'contact_ssn_last_four') 145 | .then((example) => { 146 | expect(example.type).to.be.equal("idlzpypmia6qu"); 147 | expect(example.value).to.be.equal("d5ac"); 148 | }); 149 | ssn.getBlindIndex('123-45-6789', 'contact_ssn_last_four') 150 | .then((example) => { 151 | expect(example.type).to.be.equal("idlzpypmia6qu"); 152 | expect(example.value).to.be.equal("a88e"); 153 | }); 154 | 155 | ssn.getBlindIndex('invalid guess 123', 'contact_ssn') 156 | .then((example) => { 157 | expect(example.type).to.be.equal("stfodrsbpd4ls"); 158 | expect(example.value).to.be.equal("ee10e07b"); 159 | }); 160 | 161 | ssn.getBlindIndex('123-45-6789', 'contact_ssn') 162 | .then((example) => { 163 | expect(example.type).to.be.equal("stfodrsbpd4ls"); 164 | expect(example.value).to.be.equal("9a15fe14"); 165 | }); 166 | 167 | let random = getExampleField(fipsRandom, true); 168 | random.getBlindIndex('123-45-6789', 'contact_ssn') 169 | .then((example) => { 170 | expect(example).to.not.equal("ee10e07b213a922075a6ada22514528c"); 171 | }); 172 | }); 173 | 174 | it('Blind Indexing (ModernCrypto)', async function () { 175 | if (!initialized) await initialize(); 176 | let ssn = getExampleField(naclEngine).setTypedIndexes(true); 177 | ssn.getBlindIndex('111-11-1111', 'contact_ssn_last_four') 178 | .then((example) => { 179 | expect(example.type).to.be.equal("3dywyifwujcu2"); 180 | expect(example.value).to.be.equal("32ae"); 181 | }); 182 | ssn.getBlindIndex('111-11-2222', 'contact_ssn_last_four') 183 | .then((example) => { 184 | expect(example.type).to.be.equal("3dywyifwujcu2"); 185 | expect(example.value).to.be.equal("e538"); 186 | }); 187 | 188 | ssn.getBlindIndex('123-45-6788', 'contact_ssn_last_four') 189 | .then((example) => { 190 | expect(example.type).to.be.equal("3dywyifwujcu2"); 191 | expect(example.value).to.be.equal("8d1a"); 192 | }); 193 | ssn.getBlindIndex('123-45-6789', 'contact_ssn_last_four') 194 | .then((example) => { 195 | expect(example.type).to.be.equal("3dywyifwujcu2"); 196 | expect(example.value).to.be.equal("2acb"); 197 | }); 198 | ssn.getBlindIndex('invalid guess 123', 'contact_ssn') 199 | .then((example) => { 200 | expect(example.type).to.be.equal("2iztg3wbd7j5a"); 201 | expect(example.value).to.be.equal("499db508"); 202 | }); 203 | ssn.getBlindIndex('123-45-6789', 'contact_ssn') 204 | .then((example) => { 205 | expect(example.type).to.be.equal("2iztg3wbd7j5a"); 206 | expect(example.value).to.be.equal("311314c1"); 207 | }); 208 | 209 | let random = getExampleField(naclRandom, true); 210 | random.getBlindIndex('123-45-6789', 'contact_ssn') 211 | .then((example) => { 212 | expect(example).to.not.equal("499db5085e715c2f167c1e2c02f1c80f"); 213 | }); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /test/encryptedfile-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const expect = require('chai').expect; 4 | const fs = require('fs-extra'); 5 | 6 | const CipherSweet = require('../lib/ciphersweet'); 7 | const EncryptedFile = require('../lib/encryptedfile'); 8 | const BoringCrypto = require('../lib/backend/boringcrypto'); 9 | const FIPSCrypto = require('../lib/backend/fipsrypto'); 10 | const ModernCrypto = require('../lib/backend/moderncrypto'); 11 | const StringProvider = require('../lib/keyprovider/stringprovider'); 12 | 13 | let fipsEngine = new CipherSweet( 14 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 15 | new FIPSCrypto() 16 | ); 17 | let naclEngine = new CipherSweet( 18 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 19 | new ModernCrypto() 20 | ); 21 | let brngEngine = new CipherSweet( 22 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 23 | new BoringCrypto() 24 | ); 25 | 26 | describe('EncryptedFile', function () { 27 | it('FIPS Backend', async function () { 28 | await fs.writeFile(__dirname+'/file-test-0001.txt', 'This is just a test file.\n\nNothing special.'); 29 | let eF = new EncryptedFile(fipsEngine); 30 | await eF.encryptFile(__dirname+'/file-test-0001.txt', __dirname+'/file-test-0001.out'); 31 | await eF.decryptFile(__dirname+'/file-test-0001.out', __dirname+'/file-test-0001.dec'); 32 | 33 | let read0 = await fs.readFile(__dirname+'/file-test-0001.txt'); 34 | let read1 = await fs.readFile(__dirname+'/file-test-0001.dec'); 35 | expect(read0.toString('hex')).to.be.equals(read1.toString('hex')); 36 | }); 37 | 38 | it('Modern Backend', async function () { 39 | this.timeout(10000); 40 | await fs.writeFile(__dirname+'/file-test-0001.txt', 'This is just a test file.\n\nNothing special.'); 41 | let eF = new EncryptedFile(naclEngine); 42 | await eF.encryptFile(__dirname+'/file-test-0001.txt', __dirname+'/file-test-0001.sodium'); 43 | await eF.decryptFile(__dirname+'/file-test-0001.sodium', __dirname+'/file-test-0001.sodium-dec'); 44 | 45 | let read0 = await fs.readFile(__dirname+'/file-test-0001.txt'); 46 | let read1 = await fs.readFile(__dirname+'/file-test-0001.sodium-dec'); 47 | expect(read0.toString('hex')).to.be.equals(read1.toString('hex')); 48 | }); 49 | 50 | it('Boring Backend', async function () { 51 | this.timeout(10000); 52 | await fs.writeFile(__dirname+'/file-test-0001.txt', 'This is just a test file.\n\nNothing special.'); 53 | let eF = new EncryptedFile(brngEngine); 54 | await eF.encryptFile(__dirname+'/file-test-0001.txt', __dirname+'/file-test-0001.boring'); 55 | await eF.decryptFile(__dirname+'/file-test-0001.boring', __dirname+'/file-test-0001.boring-dec'); 56 | 57 | let read0 = await fs.readFile(__dirname+'/file-test-0001.txt'); 58 | let read1 = await fs.readFile(__dirname+'/file-test-0001.boring-dec'); 59 | expect(read0.toString('hex')).to.be.equals(read1.toString('hex')); 60 | }); 61 | 62 | it('PHP interop', async function () { 63 | let read; 64 | let eF = new EncryptedFile(fipsEngine); 65 | await eF.decryptFile( 66 | __dirname + '/fips-encrypted.txt', 67 | __dirname + '/fips-decrypted.txt' 68 | ); 69 | 70 | read = await fs.readFile(__dirname+'/fips-decrypted.txt'); 71 | expect(read.slice(0, 30).toString()).to.be.equal('Paragon Initiative Enterprises'); 72 | 73 | let eN = new EncryptedFile(naclEngine); 74 | await eN.decryptFile( 75 | __dirname + '/nacl-encrypted.txt', 76 | __dirname + '/nacl-decrypted.txt' 77 | ); 78 | read = await fs.readFile(__dirname+'/nacl-decrypted.txt'); 79 | expect(read.slice(0, 30).toString()).to.be.equal('Paragon Initiative Enterprises'); 80 | 81 | let eB = new EncryptedFile(brngEngine); 82 | await eB.decryptFile( 83 | __dirname + '/brng-encrypted.txt', 84 | __dirname + '/brng-decrypted.txt' 85 | ); 86 | read = await fs.readFile(__dirname+'/brng-decrypted.txt'); 87 | expect(read.slice(0, 30).toString()).to.be.equal('Paragon Initiative Enterprises'); 88 | }) 89 | }); 90 | -------------------------------------------------------------------------------- /test/encryptedmultirows-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require('assert'); 4 | const expect = require('chai').expect; 5 | const {SodiumPlus} = require('sodium-plus'); 6 | let sodium; 7 | 8 | const BlindIndex = require('../lib/blindindex'); 9 | const CipherSweet = require('../lib/ciphersweet'); 10 | const EncryptedMultiRows = require('../lib/encryptedmultirows'); 11 | const EncryptedRow = require('../lib/encryptedrow'); 12 | const FIPSCrypto = require('../lib/backend/fipsrypto'); 13 | const ModernCrypto = require('../lib/backend/moderncrypto'); 14 | const Lowercase = require('../lib/transformation/lowercase'); 15 | const StringProvider = require('../lib/keyprovider/stringprovider'); 16 | const Util = require('../lib/util'); 17 | 18 | let buf, fipsEngine, naclEngine, fipsRandom, naclRandom; 19 | let initialized = false; 20 | 21 | /** 22 | * @return {Promise} 23 | */ 24 | async function initialize() { 25 | if (initialized) return true; 26 | if (!sodium) sodium = await SodiumPlus.auto(); 27 | if (!buf) buf = await sodium.randombytes_buf(32); 28 | if (!fipsEngine) fipsEngine = new CipherSweet( 29 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 30 | new FIPSCrypto() 31 | ); 32 | if (!naclEngine) naclEngine = new CipherSweet( 33 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 34 | new ModernCrypto() 35 | ); 36 | if (!fipsRandom) fipsRandom = new CipherSweet( 37 | new StringProvider(buf.toString('hex')), 38 | new FIPSCrypto() 39 | ); 40 | if (!naclRandom) naclRandom = new CipherSweet( 41 | new StringProvider(buf.toString('hex')), 42 | new ModernCrypto() 43 | ); 44 | initialized = true; 45 | return false; 46 | } 47 | 48 | /** 49 | * @param {module.CipherSweet} engine 50 | */ 51 | function getExampleMultiRows(engine) 52 | { 53 | return new EncryptedMultiRows(engine) 54 | .addTable('foo') 55 | .addTable('bar') 56 | .addIntegerField('foo', 'column1') 57 | .addTextField('foo', 'column2') 58 | .addBooleanField('foo', 'column3') 59 | .addIntegerField('bar', 'column1') 60 | .addIntegerField('baz', 'column1') 61 | .addBlindIndex( 62 | 'foo', 63 | 'column2', 64 | new BlindIndex('foo_column2_idx', [new Lowercase()], 32, true) 65 | ); 66 | } 67 | 68 | describe('EncryptedMultiRows', function () { 69 | it('Sets up correctly when used correctly', async function () { 70 | if (!initialized) await initialize(); 71 | let mr = new EncryptedMultiRows(naclRandom) 72 | .addTable('foo') 73 | .addTable('bar'); 74 | expect('["foo","bar"]').to.be.equal(JSON.stringify(mr.listTables())); 75 | 76 | mr.addTextField('foo', 'column1') 77 | .addBooleanField('foo', 'column2'); 78 | expect('["foo","bar"]').to.be.equal(JSON.stringify(mr.listTables())); 79 | 80 | mr.addTextField('baz', 'column1'); 81 | expect('["foo","bar","baz"]').to.be.equal(JSON.stringify(mr.listTables())); 82 | 83 | 84 | expect('["column1","column2"]').to.be.equal(JSON.stringify( 85 | mr.getEncryptedRowObjectForTable('foo').listEncryptedFields() 86 | )); 87 | expect('[]').to.be.equal(JSON.stringify( 88 | mr.getEncryptedRowObjectForTable('bar').listEncryptedFields() 89 | )); 90 | expect('["column1"]').to.be.equal(JSON.stringify( 91 | mr.getEncryptedRowObjectForTable('baz').listEncryptedFields() 92 | )); 93 | }); 94 | 95 | it('Encrypts / decrypts rows successfully', async function () { 96 | if (!initialized) await initialize(); 97 | let mr = getExampleMultiRows(fipsEngine); 98 | 99 | let rows = { 100 | "foo": { 101 | "id": 123456, 102 | "column1": 654321, 103 | "column2": "paragonie", 104 | "column3": true, 105 | "extra": "text" 106 | }, 107 | "bar": { 108 | "id": 554353, 109 | "foo_id": 123456, 110 | "column1": 654321 111 | }, 112 | "baz": { 113 | "id": 3174521, 114 | "foo_id": 123456, 115 | "column1": 654322 116 | } 117 | }; 118 | let outRow = await mr.encryptManyRows(rows); 119 | expect(JSON.stringify(outRow)).to.not.equal(JSON.stringify(rows)); 120 | let decrypted = await mr.decryptManyRows(outRow); 121 | expect(JSON.stringify(decrypted)).to.be.equal(JSON.stringify(rows)); 122 | }); 123 | 124 | it('Handles blind indexes and compound indexes well', async function () { 125 | if (!initialized) await initialize(); 126 | let mr = getExampleMultiRows(fipsEngine); 127 | let rows = { 128 | "foo": { 129 | "id": 123456, 130 | "column1": 654321, 131 | "column2": "paragonie", 132 | "column3": true, 133 | "extra": "text" 134 | }, 135 | "bar": { 136 | "id": 554353, 137 | "foo_id": 123456, 138 | "column1": 654321 139 | }, 140 | "baz": { 141 | "id": 3174521, 142 | "foo_id": 123456, 143 | "column1": 654322 144 | } 145 | }; 146 | let indexes = await mr.getAllBlindIndexes(rows); 147 | expect('{"foo":{"foo_column2_idx":"65b71d96"},"bar":{},"baz":{}}').to.be.equal(JSON.stringify(indexes)); 148 | }); 149 | }); -------------------------------------------------------------------------------- /test/encryptedrow-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const {SodiumPlus} = require('sodium-plus'); 3 | let sodium; 4 | 5 | const BlindIndex = require('../lib/blindindex'); 6 | const CipherSweet = require('../lib/ciphersweet'); 7 | const EncryptedRow = require('../lib/encryptedrow'); 8 | const FIPSCrypto = require('../lib/backend/fipsrypto'); 9 | const ModernCrypto = require('../lib/backend/moderncrypto'); 10 | const LastFourDigits = require('../lib/transformation/lastfourdigits'); 11 | const StringProvider = require('../lib/keyprovider/stringprovider'); 12 | const Util = require('../lib/util'); 13 | 14 | let buf, fipsEngine, naclEngine, fipsRandom, naclRandom; 15 | let initialized = false; 16 | 17 | /** 18 | * @return {Promise} 19 | */ 20 | async function initialize() { 21 | if (initialized) return true; 22 | if (!sodium) sodium = await SodiumPlus.auto(); 23 | if (!buf) buf = await sodium.randombytes_buf(32); 24 | if (!fipsEngine) fipsEngine = new CipherSweet( 25 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 26 | new FIPSCrypto() 27 | ); 28 | if (!naclEngine) naclEngine = new CipherSweet( 29 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 30 | new ModernCrypto() 31 | ); 32 | if (!fipsRandom) fipsRandom = new CipherSweet( 33 | new StringProvider(buf.toString('hex')), 34 | new FIPSCrypto() 35 | ); 36 | if (!naclRandom) naclRandom = new CipherSweet( 37 | new StringProvider(buf.toString('hex')), 38 | new ModernCrypto() 39 | ); 40 | initialized = true; 41 | return false; 42 | } 43 | 44 | /** 45 | * 46 | * @param {module.CipherSweet} engine 47 | * @param {boolean} fast 48 | * @param {boolean} longer 49 | * @return {module.EncryptedRow} 50 | */ 51 | function getExampleRow(engine, longer = false, fast = false) 52 | { 53 | let row = new EncryptedRow(engine, 'contacts') 54 | .addTextField('ssn') 55 | .addBooleanField('hivstatus'); 56 | 57 | row.addBlindIndex('ssn', new BlindIndex( 58 | 'contact_ssn_last_four', 59 | [new LastFourDigits()], 60 | longer ? 64 : 16, 61 | fast 62 | )); 63 | 64 | row.createCompoundIndex( 65 | 'contact_ssnlast4_hivstatus', 66 | ['ssn', 'hivstatus'], 67 | longer ? 64 : 16, 68 | fast 69 | ); 70 | return row; 71 | } 72 | 73 | describe('EncryptedRow', function () { 74 | it('Encrypts / decrypts rows successfully', async function () { 75 | if (!initialized) await initialize(); 76 | let eF = new EncryptedRow(fipsEngine, 'contacts'); 77 | let eM = new EncryptedRow(naclEngine, 'contacts'); 78 | eF.addTextField('message'); 79 | eM.addTextField('message'); 80 | 81 | let message = 'This is a test message: ' + (await Util.randomBytes(16)).toString('hex'); 82 | 83 | let fCipher = await eF.encryptRow({"message": message}); 84 | let mCipher = await eM.encryptRow({"message": message}); 85 | 86 | let fDecrypt = await eF.decryptRow(fCipher); 87 | expect(fDecrypt['message']).to.be.equal(message); 88 | let mDecrypt = await eM.decryptRow(mCipher); 89 | expect(mDecrypt['message']).to.be.equal(message); 90 | 91 | let store; 92 | let eRF = getExampleRow(fipsRandom, true); 93 | let eRM = getExampleRow(naclRandom, true); 94 | let rows = [ 95 | {"ssn": "111-11-1111", "hivstatus": false}, 96 | {"ssn": "123-45-6789", "hivstatus": false}, 97 | {"ssn": "999-99-6789", "hivstatus": false}, 98 | {"ssn": "123-45-1111", "hivstatus": true}, 99 | {"ssn": "999-99-1111", "hivstatus": true}, 100 | {"ssn": "123-45-6789", "hivstatus": true} 101 | ]; 102 | for (let i = 0; i < rows.length; i++) { 103 | store = await eRF.encryptRow(rows[i]); 104 | expect(typeof (store)).to.be.equal('object'); 105 | expect(typeof (store.ssn)).to.be.equal('string'); 106 | expect(typeof (store.hivstatus)).to.be.equal('string'); 107 | expect(store.ssn).to.not.equal(rows[i].ssn); 108 | expect(store.hivstatus).to.not.equal(rows[i].hivstatus); 109 | expect(typeof (rows[i].ssn)).to.be.equal('string'); 110 | expect(typeof (rows[i].hivstatus)).to.be.equal('boolean'); 111 | 112 | store = await eRM.encryptRow(rows[i]); 113 | expect(typeof (store)).to.be.equal('object'); 114 | expect(typeof (store.ssn)).to.be.equal('string'); 115 | expect(typeof (store.hivstatus)).to.be.equal('string'); 116 | expect(store.ssn).to.not.equal(rows[i].ssn); 117 | expect(store.hivstatus).to.not.equal(rows[i].hivstatus); 118 | expect(typeof (rows[i].ssn)).to.be.equal('string'); 119 | expect(typeof (rows[i].hivstatus)).to.be.equal('boolean'); 120 | } 121 | }); 122 | 123 | it('Handles blind indexes and compound indexes well', async function () { 124 | if (!initialized) await initialize(); 125 | this.timeout(5000); 126 | let indexes; 127 | let eRF = getExampleRow(fipsEngine, true); 128 | let eRM = getExampleRow(naclEngine, true); 129 | let plain = { 130 | "extraneous": "this is unencrypted", 131 | "ssn": "123-45-6789", 132 | "hivstatus": true 133 | }; 134 | 135 | indexes = await eRF.getAllBlindIndexes(plain); 136 | expect('a88e74ada916ab9b').to.be.equal(indexes['contact_ssn_last_four']); 137 | expect('9c3d53214ab71d7f').to.be.equal(indexes['contact_ssnlast4_hivstatus']); 138 | expect('a88e74ada916ab9b').to.be.equal(await eRF.getBlindIndex('contact_ssn_last_four', plain)); 139 | expect('9c3d53214ab71d7f').to.be.equal(await eRF.getBlindIndex('contact_ssnlast4_hivstatus', plain)); 140 | 141 | 142 | indexes = await eRM.getAllBlindIndexes(plain); 143 | expect('2acbcd1c7c55c1db').to.be.equal(indexes['contact_ssn_last_four']); 144 | expect('1b8c1e1f8e122bd3').to.be.equal(indexes['contact_ssnlast4_hivstatus']); 145 | expect('2acbcd1c7c55c1db').to.be.equal(await eRM.getBlindIndex('contact_ssn_last_four', plain)); 146 | expect('1b8c1e1f8e122bd3').to.be.equal(await eRM.getBlindIndex('contact_ssnlast4_hivstatus', plain)); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /test/fieldindexplanner-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const FieldIndexPlanner = require('../lib/planner/fieldindexplanner'); 4 | 5 | let planner; 6 | 7 | describe('FieldIndexPlanner', function () { 8 | it('Coincidence counter', function () { 9 | planner = (new FieldIndexPlanner()) 10 | .setEstimatedPopulation(1 << 16) 11 | .addExistingIndex('name', 8, Math.MAX_SAFE_INTEGER) 12 | .addExistingIndex('first_initial_last_name', 4, Math.MAX_SAFE_INTEGER); 13 | 14 | assert(planner.getCoincidenceCount() > 0); 15 | assert(planner.withPopulation(1 << 20).getCoincidenceCount() > 20); 16 | assert(planner.getCoincidenceCount() < 20); 17 | }); 18 | 19 | it('Recommendations', function() { 20 | planner = (new FieldIndexPlanner()) 21 | .setEstimatedPopulation(1 << 16) 22 | .addExistingIndex('name', 4, Math.MAX_SAFE_INTEGER) 23 | .addExistingIndex('first_initial_last_name', 4, Math.MAX_SAFE_INTEGER); 24 | 25 | assert(planner.recommend().min === 1); 26 | assert(planner.recommend().max === 7); 27 | 28 | let plan2 = planner.withPopulation(2147483647); 29 | assert(plan2.recommend().min === 8); 30 | assert(plan2.recommend().max === 22); 31 | 32 | let plan3 = (new FieldIndexPlanner()).setEstimatedPopulation(1 << 16); 33 | assert(plan3.recommendLow() === 8); 34 | assert(plan3.recommendHigh() === 15); 35 | assert(plan3.recommendLow(14) === 8); 36 | assert(plan3.recommendHigh(14) === 14); 37 | assert(plan3.withPopulation(1 << 8).recommendLow() === 4); 38 | assert(plan3.withPopulation(1 << 8).recommendHigh() === 7); 39 | assert(plan3.withPopulation(1 << 8).recommendLow(7) === 4); 40 | assert(plan3.withPopulation(1 << 8).recommendHigh(7) === 7); 41 | assert(plan3.withPopulation(2147483647).recommendLow() === 16); 42 | assert(plan3.withPopulation(2147483647).recommendHigh() === 30); 43 | assert(plan3.withPopulation(2147483647).recommendLow(29) === 16); 44 | assert(plan3.withPopulation(2147483647).recommendHigh(29) === 29); 45 | assert(plan3.withPopulation(2147483647).recommendLow(24) === 16); 46 | assert(plan3.withPopulation(2147483647).recommendHigh(24) === 24); 47 | 48 | assert(plan3.withPopulation(2147483647).recommendLow(8) === 1); 49 | assert(plan3.withPopulation(2147483647).recommendHigh(8) === 8); 50 | }); 51 | }); -------------------------------------------------------------------------------- /test/fips-encrypted.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paragonie/ciphersweet-js/6002e018c0a607b03b802641d929e48862981fac/test/fips-encrypted.txt -------------------------------------------------------------------------------- /test/fipscrypto-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const FIPSCrypto = require('../lib/backend/fipsrypto'); 4 | const SymmetricKey = require('../lib/backend/key/symmetrickey'); 5 | 6 | const {SodiumPlus} = require('sodium-plus'); 7 | let sodium; 8 | 9 | describe('FIPSCrypto Tests', function () { 10 | it('Encrypts and decrypts successfully', async function () { 11 | if (!sodium) sodium = await SodiumPlus.auto(); 12 | this.timeout(5000); 13 | let random_buf = await sodium.randombytes_buf(32); 14 | let fips = new FIPSCrypto(); 15 | let key = new SymmetricKey(random_buf); 16 | let plaintext, exampleKey, exampleCipher; 17 | 18 | plaintext = 'This is a secret message'; 19 | fips.encrypt(plaintext, key).then( 20 | (encrypted) => { 21 | fips.decrypt(encrypted, key).then( 22 | (decrypted) => { 23 | expect(decrypted).to.be.equal(plaintext); 24 | } 25 | ); 26 | } 27 | ); 28 | 29 | 30 | fips.encrypt(plaintext, key, 'test aad') 31 | .then(encrypted => { 32 | let caught = false; 33 | fips.decrypt(encrypted, key) 34 | .catch((e) => { 35 | caught = true; 36 | expect(e.message).to.be.equal('Invalid MAC'); 37 | }) 38 | .then(() => { 39 | if (!caught) { 40 | assert(null, 'AAD not being used in calculation'); 41 | } 42 | }); 43 | 44 | fips.decrypt(encrypted, key, 'test aad').then( 45 | (decrypted) => { 46 | expect(decrypted).to.be.equal(plaintext); 47 | } 48 | ); 49 | }); 50 | 51 | let exampleDecrypt; 52 | exampleKey = Buffer.from('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex'); 53 | exampleCipher = 'fips:JkzlZgUUdwo6XDRYSKNTnuWMDVcIa7M4R0Xtg1c3aD14ZUiu5YGTiGu9PC2SAjRAZTTurWYa1KfrMJKSncc0llwcNeyEsWMytOir8oqskQtIF0XEkjTJEJSjxmkerxRfHNyBnOimLZ6fg31IjLWrzOW1UX3ARRwSjabK'; 54 | exampleDecrypt = await fips.decrypt(exampleCipher, exampleKey); 55 | expect(exampleDecrypt.toString('utf-8')).to.be.equal('This is just a test message'); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/moderncrypto-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const ModernCrypto = require('../lib/backend/moderncrypto'); 4 | const SymmetricKey = require('../lib/backend/key/symmetrickey'); 5 | const {SodiumPlus} = require('sodium-plus'); 6 | let sodium; 7 | 8 | describe('ModernCrypto Tests', function () { 9 | it('Encrypts and decrypts successfully', async function () { 10 | if (!sodium) sodium = await SodiumPlus.auto(); 11 | this.timeout(5000); 12 | let random_buf = await sodium.randombytes_buf(32); 13 | let nacl = new ModernCrypto(); 14 | let key = new SymmetricKey(random_buf); 15 | let plaintext, exampleKey, exampleCipher; 16 | 17 | // plaintext = 'This is a secret message'; 18 | plaintext = 'This is just a test message'; 19 | nacl.encrypt(plaintext, key).then( 20 | (encrypted) => { 21 | nacl.decrypt(encrypted, key).then( 22 | (decrypted) => { 23 | expect(decrypted).to.be.equal(plaintext); 24 | } 25 | ); 26 | } 27 | ); 28 | nacl.encrypt(plaintext, key, 'test aad') 29 | .then(encrypted => { 30 | let caught = false; 31 | nacl.decrypt(encrypted, key) 32 | .catch((e) => { 33 | caught = true; 34 | expect(e.message).to.be.equal('Invalid MAC'); 35 | }) 36 | .then(() => { 37 | if (!caught) { 38 | assert(null, 'AAD not being used in calculation'); 39 | } 40 | }); 41 | 42 | nacl.decrypt(encrypted, key, 'test aad').then( 43 | (decrypted) => { 44 | expect(decrypted).to.be.equal(plaintext); 45 | } 46 | ); 47 | }); 48 | 49 | let exampleDecrypt; 50 | exampleKey = Buffer.from('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex'); 51 | exampleCipher = 'nacl:J-Dvk60qe6hv0hsMmRSRiqInQHxaumU8K8uP2hnchA59W6HBxBHJp_Ki3oD3jqmUBdJ8Vtyp7p4o81rpc_Ca4VKkNg=='; 52 | exampleDecrypt = await nacl.decrypt(exampleCipher, exampleKey); 53 | expect(exampleDecrypt.toString('utf-8')).to.be.equal('This is just a test message'); 54 | 55 | exampleKey = Buffer.from('0b036de5605144ea7aeed8bd3a191c08fe1b0ed69d9c8ba0dcbe82372451bb31', 'hex'); 56 | exampleCipher = 'nacl:cASARO-I3Twm5QqPB2kWSkNLnlrPiZ2hXy2btWUx_QGt5-t6KmvJFOLUswIU6TICquCRpU39sauVb_6j684CEyLidA=='; 57 | 58 | exampleDecrypt = await nacl.decrypt(exampleCipher, exampleKey); 59 | expect(exampleDecrypt.toString('utf-8')).to.be.equal('This is just a test message'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/multitenant-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const crypto = require('crypto'); 4 | const {SodiumPlus} = require('sodium-plus'); 5 | let sodium; 6 | 7 | const CipherSweet = require('../lib/ciphersweet'); 8 | const BoringCrypto = require('../lib/backend/boringcrypto'); 9 | const FIPSCrypto = require('../lib/backend/fipsrypto'); 10 | const CompoundIndex = require('../lib/compoundindex'); 11 | const EncryptedField = require('../lib/encryptedfield'); 12 | const EncryptedRow = require('../lib/encryptedrow'); 13 | const EncryptedMultiRows = require('../lib/encryptedmultirows'); 14 | const TestMultiTenantKeyProvider = require('./multitenant/example-keyprovider'); 15 | const LastFourDigits = require('../lib/transformation/lastfourdigits'); 16 | const StringProvider = require('../lib/keyprovider/stringprovider'); 17 | 18 | let initialized = false; 19 | let provider, csBoring, csFips; 20 | 21 | /** 22 | * @return {Promise} 23 | */ 24 | async function initialize() { 25 | if (initialized) return true; 26 | if (!sodium) sodium = await SodiumPlus.auto(); 27 | 28 | provider = new TestMultiTenantKeyProvider({ 29 | 'foo': new StringProvider(crypto.randomBytes(32)), 30 | 'bar': new StringProvider(crypto.randomBytes(32)), 31 | 'baz': new StringProvider(crypto.randomBytes(32)) 32 | }) 33 | provider.setActiveTenant('foo'); 34 | 35 | csBoring = new CipherSweet(provider, new BoringCrypto()); 36 | csFips = new CipherSweet(provider, new FIPSCrypto()); 37 | initialized = true; 38 | return false; 39 | } 40 | 41 | /** 42 | * @param {CipherSweet} cs 43 | * @return {EncryptedRow} 44 | */ 45 | function getERClass(cs) { 46 | const ER = new EncryptedRow(cs, 'customer'); 47 | ER.addTextField('email', 'customerid'); 48 | ER.addTextField('ssn', 'customerid'); 49 | ER.addBooleanField('active', 'customerid'); 50 | const cidx = (new CompoundIndex( 51 | 'customer_ssnlast4_active', 52 | ['ssn', 'active'], 53 | 15, 54 | true 55 | )).addTransform('ssn', new LastFourDigits()); 56 | ER.addCompoundIndex(cidx); 57 | return ER; 58 | } 59 | 60 | /** 61 | * @param {CipherSweet} cs 62 | * @return {EncryptedMultiRows} 63 | */ 64 | function getMultiRows(cs) { 65 | const EMR = new EncryptedMultiRows(cs); 66 | EMR.addTable('meta'); 67 | EMR.addTextField('meta', 'data'); 68 | 69 | EMR.addTable('customer'); 70 | EMR.addTextField('customer', 'email', 'customerid'); 71 | EMR.addTextField('customer', 'ssn', 'customerid'); 72 | EMR.addBooleanField('customer', 'active', 'customerid'); 73 | const cidx = (new CompoundIndex( 74 | 'customer_ssnlast4_active', 75 | ['ssn', 'active'], 76 | 15, 77 | true 78 | )).addTransform('ssn', new LastFourDigits()); 79 | EMR.addCompoundIndex('customer', cidx); 80 | 81 | EMR.addTable('customer_secret'); 82 | EMR.addTextField('customer_secret', '2fa'); 83 | EMR.addTextField('customer_secret', 'pwhash'); 84 | 85 | return EMR; 86 | } 87 | 88 | 89 | describe('Multi-Tenant Test', function () { 90 | it('EncryptedField', async function () { 91 | await initialize(); 92 | /** @var {CipherSweet} cs */ 93 | for (let cs of [csBoring, csFips]) { 94 | const EF = new EncryptedField(cs, 'table', 'column'); 95 | await EF.setActiveTenant('foo'); 96 | let cipher = await EF.encryptValue('test plaintext', 'aad'); 97 | let plain = (await EF.decryptValue(cipher, 'aad')).toString(); 98 | expect(plain).to.be.equals('test plaintext'); 99 | 100 | let decryptFailed = false; 101 | await EF.setActiveTenant('bar'); 102 | try { 103 | await EF.decryptValue(cipher, 'aad'); 104 | } catch (e) { 105 | decryptFailed = true; 106 | } 107 | expect(true).to.be.equals(decryptFailed); 108 | } 109 | }); 110 | 111 | it('EncryptedRow', async function () { 112 | await initialize(); 113 | /** @var {CipherSweet} cs */ 114 | for (let cs of [csBoring, csFips]) { 115 | let ER = getERClass(cs); 116 | cs.setActiveTenant('foo'); 117 | 118 | let row1 = await ER.encryptRow({ 119 | 'customerid': 1, 120 | 'email': 'ciphersweet@paragonie.com', 121 | 'ssn': '123-45-6789', 122 | 'active': true 123 | }); 124 | expect(row1.tenant).to.be.equals('foo'); 125 | 126 | let plain1 = await ER.decryptRow(row1); 127 | expect(plain1.email).to.be.equals('ciphersweet@paragonie.com'); 128 | expect('tenant-extra' in plain1).to.be.equals(true); 129 | 130 | cs.setActiveTenant('bar'); 131 | let row2 = await ER.encryptRow({ 132 | 'customerid': 2, 133 | 'email': 'security@paragonie.com', 134 | 'ssn': '987-65-4321', 135 | 'active': true 136 | }); 137 | expect(row2.tenant).to.be.equals('bar'); 138 | let plain2 = await ER.decryptRow(row2); 139 | expect(plain2.email).to.be.equals('security@paragonie.com'); 140 | expect('tenant-extra' in plain2).to.be.equals(true); 141 | 142 | let decryptFailed = false; 143 | let row3 = row2; 144 | row3['tenant'] = 'foo'; 145 | try { 146 | await ER.decryptRow(row3); 147 | } catch (e) { 148 | decryptFailed = true; 149 | } 150 | expect(decryptFailed).to.be.equals(true); 151 | } 152 | }); 153 | 154 | it('EncryptedMultiRows', async function () { 155 | await initialize(); 156 | /** @var {CipherSweet} cs */ 157 | for (let cs of [csBoring, csFips]) { 158 | const EMR = getMultiRows(cs); 159 | 160 | cs.setActiveTenant('foo'); 161 | let many1 = await EMR.encryptManyRows({ 162 | 'meta': {'data': 'foo'}, 163 | 'customer': { 164 | 'customerid': 1, 165 | 'email': 'ciphersweet@paragonie.com', 166 | 'ssn': '123-45-6789', 167 | 'active': true 168 | }, 169 | 'customer_secret': { 170 | '2fa': 'jm2mes2ucvhck2kcw7er5l7ulwoyzfxa', 171 | 'pwhash': '$2y$10$s6gTREuS3dIOpiudUm6K/u0Wu3PoM1gZyr9sA9hAuu/hGiwO8agDa' 172 | } 173 | }); 174 | expect('wrapped-key' in many1['meta']).to.be.equals(true); 175 | expect('tenant-extra' in many1['meta']).to.be.equals(false); 176 | expect('tenant-extra' in many1['customer']).to.be.equals(true); 177 | expect('tenant-extra' in many1['customer_secret']).to.be.equals(true); 178 | let decrypt1 = await EMR.decryptManyRows(many1); 179 | expect(decrypt1.customer.email).to.be.equals('ciphersweet@paragonie.com'); 180 | 181 | cs.setActiveTenant('bar'); 182 | let many2 = await EMR.encryptManyRows({ 183 | 'meta': {'data': 'foo'}, 184 | 'customer': { 185 | 'customerid': 2, 186 | 'email': 'security@paragonie.com', 187 | 'ssn': '987-65-4321', 188 | 'active': true 189 | }, 190 | 'customer_secret': { 191 | '2fa': 'dyg27kjbe72hbiszv55lrxzmqs7zfn6o', 192 | 'pwhash': '$2y$10$Tvk8Uo338tK2AoqIwCnwiOV5tIKwGM/r93MzXbX.h/0iFYhpuRn3W' 193 | } 194 | }); 195 | expect('wrapped-key' in many2['meta']).to.be.equals(true); 196 | expect('tenant-extra' in many2['meta']).to.be.equals(false); 197 | expect('tenant-extra' in many2['customer']).to.be.equals(true); 198 | expect('tenant-extra' in many2['customer_secret']).to.be.equals(true); 199 | let decrypt2 = await EMR.decryptManyRows(many2); 200 | expect(decrypt2.customer.email).to.be.equals('security@paragonie.com'); 201 | 202 | let decryptFailed = false; 203 | let many3 = many2; 204 | for (let k in many3) { 205 | many3[k]['tenant'] = 'foo'; 206 | } 207 | try { 208 | await EMR.decryptManyRows(many3); 209 | } catch (e) { 210 | decryptFailed = true; 211 | } 212 | expect(decryptFailed).to.be.equals(true); 213 | } 214 | }); 215 | }); -------------------------------------------------------------------------------- /test/multitenant/example-keyprovider.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const StringProvider = require('../../lib/keyprovider/stringprovider'); 3 | const MultiTenantProvider = require('../../lib/keyprovider/multitenantprovider'); 4 | 5 | module.exports = class TestMultiTenantKeyProvider extends MultiTenantProvider { 6 | /** 7 | * @param {object} row 8 | * @param {string} tableName 9 | * @return {string} 10 | */ 11 | getTenantFromRow(row, tableName) { 12 | switch (row.tenant) { 13 | case 'foo': 14 | case 'bar': 15 | case 'baz': 16 | return row.tenant; 17 | default: 18 | return super.getTenantFromRow(row, tableName); 19 | } 20 | } 21 | 22 | injectTenantMetadata(row, tableName) { 23 | if (tableName !== 'meta') { 24 | row['tenant-extra'] = tableName; 25 | } else { 26 | row['wrapped-key'] = this.wrapKey(tableName); 27 | } 28 | row['tenant'] = this.active; 29 | return row; 30 | } 31 | 32 | getWrappingKey() { 33 | const hash = crypto.createHash('sha256'); 34 | return hash.update('unit tests').digest(); 35 | } 36 | 37 | /** 38 | * This is just a dummy key-wrapping example. 39 | * You'd really want to use KMS from AWS or GCP. 40 | * 41 | * @param {string} tableName 42 | * @return {string} 43 | */ 44 | wrapKey(tableName) { 45 | const wrappingKey = this.getWrappingKey(); 46 | const nonce = crypto.randomBytes(12); 47 | 48 | const cipher = crypto.createCipheriv('aes-256-gcm', wrappingKey, nonce); 49 | cipher.setAAD(Buffer.from(tableName)); 50 | const wrapped = cipher.update( 51 | this.getActiveTenant().getSymmetricKey().getRawKey() 52 | ); 53 | cipher.final(); 54 | 55 | return Buffer.concat([nonce, wrapped]).toString('base64'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/nacl-encrypted.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paragonie/ciphersweet-js/6002e018c0a607b03b802641d929e48862981fac/test/nacl-encrypted.txt -------------------------------------------------------------------------------- /test/rotator-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const {SodiumPlus} = require('sodium-plus'); 4 | 5 | const BlindIndex = require('../lib/blindindex'); 6 | const CipherSweet = require('../lib/ciphersweet'); 7 | const EncryptedField = require('../lib/encryptedfield'); 8 | const EncryptedMultiRows = require('../lib/encryptedmultirows'); 9 | const EncryptedRow = require('../lib/encryptedrow'); 10 | const FieldRotator = require('../lib/keyrotation/fieldrotator'); 11 | const MultiRowsRotator = require('../lib/keyrotation/multirowsrotator'); 12 | const RowRotator = require('../lib/keyrotation/rowrotator'); 13 | const FIPSCrypto = require('../lib/backend/fipsrypto'); 14 | const LastFourDigits = require('../lib/transformation/lastfourdigits'); 15 | const Lowercase = require('../lib/transformation/lowercase'); 16 | const ModernCrypto = require('../lib/backend/moderncrypto'); 17 | const StringProvider = require('../lib/keyprovider/stringprovider'); 18 | const Util = require('../lib/util'); 19 | 20 | function getExampleField(backend, longer = false, fast = false) 21 | { 22 | return (new EncryptedField(backend, 'contacts', 'ssn')) 23 | .addBlindIndex( 24 | new BlindIndex( 25 | 'contact_ssn_last_four', 26 | [new LastFourDigits()], 27 | longer ? 64 : 16, 28 | fast 29 | ) 30 | ) 31 | .addBlindIndex( 32 | new BlindIndex( 33 | 'contact_ssn_last_4', 34 | [new LastFourDigits()], 35 | longer ? 64 : 16, 36 | fast 37 | ) 38 | ) 39 | .addBlindIndex( 40 | new BlindIndex( 41 | 'contact_ssn', 42 | [], 43 | longer ? 128 : 32, 44 | fast 45 | ) 46 | ); 47 | } 48 | 49 | function getExampleRow(engine, longer = false, fast = false) 50 | { 51 | let row = new EncryptedRow(engine, 'contacts') 52 | .addTextField('ssn') 53 | .addBooleanField('hivstatus'); 54 | 55 | row.addBlindIndex('ssn', new BlindIndex( 56 | 'contact_ssn_last_four', 57 | [new LastFourDigits()], 58 | longer ? 64 : 16, 59 | fast 60 | )); 61 | 62 | row.createCompoundIndex( 63 | 'contact_ssnlast4_hivstatus', 64 | ['ssn', 'hivstatus'], 65 | longer ? 64 : 16, 66 | fast 67 | ); 68 | return row; 69 | } 70 | 71 | /** 72 | * @param {module.CipherSweet} engine 73 | */ 74 | function getExampleMultiRows(engine) 75 | { 76 | return new EncryptedMultiRows(engine) 77 | .addTable('foo') 78 | .addTable('bar') 79 | .addIntegerField('foo', 'column1') 80 | .addTextField('foo', 'column2') 81 | .addBooleanField('foo', 'column3') 82 | .addIntegerField('bar', 'column1') 83 | .addIntegerField('baz', 'column1') 84 | .addBlindIndex( 85 | 'foo', 86 | 'column2', 87 | new BlindIndex('foo_column2_idx', [new Lowercase()], 32, true) 88 | ); 89 | } 90 | 91 | let sodium; 92 | let buf, fipsEngine, naclEngine, fipsRandom, naclRandom; 93 | let initialized = false; 94 | 95 | /** 96 | * @return {Promise} 97 | */ 98 | async function initialize() { 99 | if (initialized) return true; 100 | if (!sodium) sodium = await SodiumPlus.auto(); 101 | if (!buf) buf = await sodium.randombytes_buf(32); 102 | if (!fipsEngine) fipsEngine = new CipherSweet( 103 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 104 | new FIPSCrypto() 105 | ); 106 | if (!naclEngine) naclEngine = new CipherSweet( 107 | new StringProvider('4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc'), 108 | new ModernCrypto() 109 | ); 110 | if (!fipsRandom) fipsRandom = new CipherSweet( 111 | new StringProvider(buf.toString('hex')), 112 | new FIPSCrypto() 113 | ); 114 | if (!naclRandom) naclRandom = new CipherSweet( 115 | new StringProvider(buf.toString('hex')), 116 | new ModernCrypto() 117 | ); 118 | initialized = true; 119 | return false; 120 | } 121 | 122 | let message = 'This is a test message'; 123 | 124 | describe('Key/Backend Rotation', function () { 125 | 126 | it('FieldRotator', async function () { 127 | this.timeout(0); 128 | if (!initialized) await initialize(); 129 | let eF = getExampleField(fipsRandom); 130 | let eM = getExampleField(naclRandom); 131 | let fieldRotator = new FieldRotator(eF, eM); 132 | 133 | let fCipher = await eF.encryptValue(message); 134 | let mCipher = await eM.encryptValue(message); 135 | 136 | assert(true === await fieldRotator.needsReEncrypt(fCipher)); 137 | assert(false === await fieldRotator.needsReEncrypt(mCipher)); 138 | 139 | let cipher, indices; 140 | [cipher, indices] = await fieldRotator.prepareForUpdate(fCipher); 141 | expect(cipher.slice(0, 5)).to.be.equal(naclRandom.getBackend().getPrefix()); 142 | eM.decryptValue(cipher).then(plaintext => { 143 | expect(plaintext).to.be.equal(message); 144 | }); 145 | }); 146 | 147 | it('RowRotator', async function () { 148 | this.timeout(0); 149 | if (!initialized) await initialize(); 150 | let eFR = getExampleRow(fipsRandom); 151 | let eMR = getExampleRow(naclRandom); 152 | let rowRotator = new RowRotator(eFR, eMR); 153 | let plainRow = { 154 | "first_name": "test", 155 | "last_name": "test", 156 | "ssn": "123-45-6789", 157 | "hivstatus": false 158 | }; 159 | let fipsRow = await eFR.encryptRow(plainRow); 160 | let naclRow = await eMR.encryptRow(plainRow); 161 | 162 | assert(true === await rowRotator.needsReEncrypt(fipsRow)); 163 | assert(false === await rowRotator.needsReEncrypt(naclRow)); 164 | 165 | let cipherRow, indices; 166 | [cipherRow, indices] = await rowRotator.prepareForUpdate(fipsRow); 167 | eMR.decryptRow(cipherRow).then(plaintext => { 168 | expect(plaintext.ssn).to.be.equal(plainRow.ssn); 169 | }); 170 | }); 171 | 172 | it('MultiRowsRotator', async function () { 173 | this.timeout(0); 174 | if (!initialized) await initialize(); 175 | 176 | let eFMR = getExampleMultiRows(fipsRandom); 177 | let eMMR = getExampleMultiRows(naclRandom); 178 | let mutliRowsRotator = new MultiRowsRotator(eFMR, eMMR); 179 | let plainRows = { 180 | "foo": { 181 | "column1": 12345, 182 | "column2": message, 183 | "column3": false, 184 | "column4": "testing" 185 | }, 186 | "bar": { 187 | "column1": 45, 188 | "extraneous": "test" 189 | }, 190 | "baz": { 191 | "column1": 67, 192 | "extraneous": true 193 | } 194 | }; 195 | let fipsRows = await eFMR.encryptManyRows(plainRows); 196 | let naclRows = await eMMR.encryptManyRows(plainRows); 197 | 198 | assert(true === await mutliRowsRotator.needsReEncrypt(fipsRows)); 199 | assert(false === await mutliRowsRotator.needsReEncrypt(naclRows)); 200 | 201 | let cipherRows, indices; 202 | [cipherRows, indices] = await mutliRowsRotator.prepareForUpdate(fipsRows); 203 | eMMR.decryptManyRows(cipherRows).then(plaintext => { 204 | expect(plaintext.foo.column2).to.be.equal(message); 205 | }); 206 | }); 207 | }); -------------------------------------------------------------------------------- /test/util-test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const Util = require('../lib/util'); 3 | const {SodiumPlus} = require('sodium-plus'); 4 | let sodium; 5 | 6 | describe('Util', function () { 7 | it('Util.increaseCtrNonce()', async function () { 8 | let testCases = [ 9 | [ 10 | '00000000000000000000000000000001', 11 | '00000000000000000000000000000000' 12 | ], 13 | [ 14 | '00000000000000000000000000000100', 15 | '000000000000000000000000000000ff' 16 | ], 17 | [ 18 | '0000000000000000000000000000ff00', 19 | '0000000000000000000000000000feff' 20 | ], 21 | [ 22 | '00000000000000000000000000000000', 23 | 'ffffffffffffffffffffffffffffffff' 24 | ] 25 | ]; 26 | let input, output, inBuf; 27 | for (let i = 0; i < testCases.length; i++) { 28 | [output, input] = testCases[i]; 29 | inBuf = Buffer.from(input, 'hex'); 30 | expect(output).to.be.equal( 31 | (await Util.increaseCtrNonce(inBuf)).toString('hex') 32 | ); 33 | } 34 | 35 | output = '0000000000000000000000000000feff'; 36 | inBuf = Buffer.from('0000000000000000000000000000fe00', 'hex'); 37 | expect(output).to.be.equal( 38 | (await Util.increaseCtrNonce(inBuf, 255)).toString('hex') 39 | ); 40 | }); 41 | 42 | it('Util.andMask()', async function () { 43 | let input, size, output, outputRight, masked; 44 | let testCases = [ 45 | ['ff', 4, 'f0', '0f'], 46 | ['ff', 9, 'ff00', 'ff00'], 47 | ['ffffffff', 16, 'ffff', 'ffff'], 48 | ['ffffffff', 17, 'ffff80', 'ffff01'], 49 | ['ffffffff', 18, 'ffffc0', 'ffff03'], 50 | ['ffffffff', 19, 'ffffe0', 'ffff07'], 51 | ['ffffffff', 20, 'fffff0', 'ffff0f'], 52 | ['ffffffff', 21, 'fffff8', 'ffff1f' ], 53 | ['ffffffff', 22, 'fffffc', 'ffff3f'], 54 | ['ffffffff', 23, 'fffffe', 'ffff7f'], 55 | ['ffffffff', 24, 'ffffff', 'ffffff'], 56 | ['ffffffff', 32, 'ffffffff', 'ffffffff'], 57 | ['ffffffff', 64, 'ffffffff00000000', 'ffffffff00000000'], 58 | ['55f6778c', 11, '55e0', '5506'], 59 | ['55f6778c', 12, '55f0', '5506'], 60 | ['55f6778c', 13, '55f0', '5516'], 61 | ['55f6778c', 14, '55f4', '5536'], 62 | ['55f6778c', 15, '55f6', '5576'], 63 | ['55f6778c', 16, '55f6', '55f6'], 64 | ['55f6778c', 17, '55f600', '55f601'], 65 | ['55f6778c', 32, '55f6778c', '55f6778c'] 66 | ]; 67 | for (let i = 0; i < testCases.length; i++) { 68 | [input, size, output, outputRight] = testCases[i]; 69 | masked = await Util.andMask(Buffer.from(input, 'hex'), size); 70 | expect(output).to.be.equal(masked.toString('hex')); 71 | masked = await Util.andMask(Buffer.from(input, 'hex'), size, true); 72 | expect(outputRight).to.be.equal(masked.toString('hex')); 73 | } 74 | }); 75 | 76 | it('Util.hmac()', function () { 77 | Util.hmac("sha256", "Paragon Initiative Enterprises", "Happy Pie Day!").then(out=>{ 78 | expect( 79 | '6f3e128164ab6edb5e1d61fd4657f778665d3f0b4d3f3d6e8a29d27eb68e14c8' 80 | ).to.be.equal( 81 | out.toString('hex') 82 | ) 83 | }); 84 | Util.hmac( 85 | 'sha256', 86 | Buffer.from('f0f1f2f3f4f5f6f7f8f901', 'hex'), 87 | Buffer.from('077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5', 'hex') 88 | ).then(out => { 89 | expect('3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf').to.be.equal(out); 90 | }); 91 | }); 92 | 93 | it('Util.HKDF() -- (RFC 5869) test vectors', function () { 94 | let ikm = Buffer.from('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', 'hex'); 95 | let salt = Buffer.from('000102030405060708090a0b0c', 'hex'); 96 | let info = Buffer.from('f0f1f2f3f4f5f6f7f8f9', 'hex'); 97 | 98 | expect('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b').to.be.equal(ikm.toString('hex')); 99 | expect('000102030405060708090a0b0c').to.be.equal(salt.toString('hex')); 100 | expect('f0f1f2f3f4f5f6f7f8f9').to.be.equal(info.toString('hex')); 101 | 102 | Util.HKDF(ikm, salt, info, 42, 'sha256').then(out=>{ 103 | expect( 104 | '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865' 105 | ).to.be.equal( 106 | out.toString('hex') 107 | ); 108 | }); 109 | }); 110 | 111 | it('Util -- type conversions', async function () { 112 | if (!sodium) sodium = await SodiumPlus.auto(); 113 | 114 | // BOOL 115 | expect(Util.chrToBool("\x02")).to.be.equal(true); 116 | expect(Util.chrToBool("\x01")).to.be.equal(false); 117 | expect(Util.chrToBool("\x00")).to.be.equal(null); 118 | expect(Util.boolToChr(true)).to.be.equal("\x02"); 119 | expect(Util.boolToChr(false)).to.be.equal("\x01"); 120 | expect(Util.boolToChr(null)).to.be.equal("\x00"); 121 | 122 | // FLOAT 123 | let float = Math.PI; 124 | expect( 125 | Util.bufferToFloat(Util.floatToBuffer(float)).toFixed(9) 126 | ).to.be.equal( 127 | float.toFixed(9) 128 | ); 129 | 130 | let a = await sodium.randombytes_uniform(0x7ffffff) + 1; 131 | let b = await sodium.randombytes_uniform(0x7fffffff) + 2; 132 | float = a/b; 133 | expect( 134 | Util.bufferToFloat(Util.floatToBuffer(float)).toFixed(9) 135 | ).to.be.equal( 136 | float.toFixed(9) 137 | ); 138 | 139 | // INTEGER 140 | for (let i = 0; i < 100; i++) { 141 | b = await sodium.randombytes_uniform(0x7fffffff); 142 | expect(b).to.be.equal(Util.load64_le(Util.store64_le(b))); 143 | } 144 | }); 145 | }); 146 | --------------------------------------------------------------------------------