├── README.md ├── .vscode ├── settings.json └── tasks.json ├── tslint.json ├── src ├── util.js ├── util.ts ├── index.ts └── index.js ├── LICENSE ├── package.json ├── .gitignore └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # e2ee 2 | End-to-end encryption 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "god.tsconfig": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "typescript", 8 | "tsconfig": "tsconfig.json", 9 | "problemMatcher": [ 10 | "$tsc" 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Splits a buffer into chunks of a given size 3 | exports.chunk = function (buffer, chunkSize) { 4 | if (!Buffer.isBuffer(buffer)) 5 | throw new Error('Buffer is required'); 6 | var result = [], i = 0, len = buffer.length; 7 | while (i < len) { 8 | // If it does not equally divide then set last to whatever remains 9 | result.push(buffer.slice(i, Math.min((i += chunkSize), len))); 10 | } 11 | return result; 12 | }; 13 | // Converts a hex string into a Uint8Array 14 | exports.hexToUint8 = function (hex) { 15 | return Uint8Array.from(Buffer.from(hex, 'hex')); 16 | }; 17 | // Converts a string into a Uint8Array 18 | exports.strToUint8 = function (hex) { 19 | return Uint8Array.from(Buffer.from(hex)); 20 | }; 21 | exports.Uint8ToHex = function (uint8) { 22 | return Buffer.from(uint8).toString('hex'); 23 | }; 24 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Splits a buffer into chunks of a given size 4 | exports.chunk = (buffer: Buffer, chunkSize: number) => { 5 | if (!Buffer.isBuffer(buffer)) throw new Error('Buffer is required') 6 | 7 | let result = [], 8 | i = 0, 9 | len = buffer.length 10 | 11 | while (i < len) { 12 | // If it does not equally divide then set last to whatever remains 13 | result.push(buffer.slice(i, Math.min((i += chunkSize), len))) 14 | } 15 | 16 | return result 17 | } 18 | 19 | // Converts a hex string into a Uint8Array 20 | exports.hexToUint8 = (hex: string): Uint8Array => { 21 | return Uint8Array.from(Buffer.from(hex, 'hex')) 22 | } 23 | 24 | // Converts a string into a Uint8Array 25 | exports.strToUint8 = (hex: string): Uint8Array => { 26 | return Uint8Array.from(Buffer.from(hex)) 27 | } 28 | 29 | // Converts a Uint8Array to a hex string 30 | exports.Uint8ToHex = (uint8: Uint8Array): string => { 31 | return Buffer.from(uint8).toString('hex') 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Habib Rehman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2ee", 3 | "version": "1.0.0", 4 | "description": "End-to-End Encryption Client", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/HR/e2ee.git" 12 | }, 13 | "keywords": [ 14 | "e2ee", 15 | "encryption", 16 | "end-to-end encryption", 17 | "chat", 18 | "signal", 19 | "signal protocol", 20 | "messaging", 21 | "encrypted messaging", 22 | "secure", 23 | "chat" 24 | ], 25 | "author": "Habib Rehman", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/HR/e2ee/issues" 29 | }, 30 | "homepage": "https://github.com/HR/e2ee#readme", 31 | "devDependencies": { 32 | "@types/jest": "^26.0.23", 33 | "@types/node": "^15.12.4", 34 | "@typescript-eslint/eslint-plugin": "^4.28.0", 35 | "@typescript-eslint/parser": "^4.28.0", 36 | "jest": "^27.0.4", 37 | "ts-jest": "^27.0.3", 38 | "ts-node": "^10.0.0", 39 | "typescript": "^4.3.4" 40 | }, 41 | "dependencies": { 42 | "futoin-hkdf": "^1.3.3", 43 | "tweetnacl": "^1.0.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | "types": ["node"] /* Type declaration files to be included in compilation. */, 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "skipLibCheck": true /* Skip type checking of declaration files. */, 65 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | const scrypto = require('crypto'), 4 | fs = require('fs'), 5 | hkdf = require('futoin-hkdf'), 6 | // TODO: Replace with scrypto.diffieHellman once nodejs#26626 lands on v12 LTS 7 | { box, sign } = require('tweetnacl'), 8 | { chunk, hexToUint8, strToUint8, Uint8ToHex } = require('./util'), 9 | STORE_KEY = 'publicKey', 10 | CIPHER = 'aes-256-cbc', 11 | RATCHET_KEYS_LEN = 64, 12 | RATCHET_KEYS_HASH = 'SHA-256', 13 | MESSAGE_KEY_LEN = 80, 14 | MESSAGE_CHUNK_LEN = 32, 15 | MESSAGE_KEY_SEED = 1, // 0x01 16 | CHAIN_KEY_SEED = 2, // 0x02 17 | RACHET_MESSAGE_COUNT = 10 // Rachet after this no of messages sent 18 | 19 | interface Store { 20 | get: (key: string, defaultValue?: any) => any 21 | set: (key: string, value: any) => void 22 | } 23 | 24 | interface GetSecretIdentity { 25 | (publicKey: string): string 26 | } 27 | 28 | interface SetSecretIdentity { 29 | (publicKey: string, secretKey: string): void 30 | } 31 | 32 | interface Packet { 33 | message: any 34 | publicKey: string 35 | previousCounter: number 36 | counter: number 37 | signature: string 38 | } 39 | 40 | module.exports = class E2EE { 41 | private _store: Store 42 | private _sessions: any = {} 43 | private _identity: any 44 | private _getSecretIdentity: GetSecretIdentity 45 | private _setSecretIdentity: SetSecretIdentity 46 | 47 | // Default implementations 48 | async _getSecretIdentityDef(publicKey: string) { 49 | return this._store.get(publicKey) 50 | } 51 | async _setSecretIdentityDef(publicKey: string, secretKey: string) { 52 | return this._store.set(publicKey, secretKey) 53 | } 54 | 55 | constructor(options: any) { 56 | this._store = options.store || new Map() 57 | this._getSecretIdentity = options.getSecretIdentity || this._getSecretIdentityDef 58 | this._setSecretIdentity = options.setSecretIdentity || this._setSecretIdentityDef 59 | // Bindings 60 | this.init = this.init.bind(this) 61 | } 62 | 63 | async init() { 64 | const publicKey = this._store.get(STORE_KEY, false) 65 | const secretKey = publicKey && (await this._getSecretIdentity(publicKey)) 66 | // Restore keys if they exist 67 | if (publicKey && secretKey) { 68 | this._identity = { publicKey, secretKey: hexToUint8(secretKey) } 69 | return this._identity 70 | } 71 | // Generate new ones otherwise 72 | await this._generateIdentityKeyPair() 73 | return this._identity 74 | } 75 | 76 | async _saveIdentity() { 77 | this._store.set(STORE_KEY, this._identity.publicKey) 78 | // Save the private key in the OS's keychain under public key 79 | await this._setSecretIdentity(this._identity.publicKey, Uint8ToHex(this._identity.secretKey)) 80 | } 81 | 82 | // Generates a new Curve25519 key pair 83 | async _generateIdentityKeyPair() { 84 | let keyPair = sign.keyPair() 85 | // Encode in hex for easier handling 86 | keyPair.publicKey = Uint8ToHex(keyPair.publicKey) 87 | 88 | this._identity = keyPair 89 | await this._saveIdentity() 90 | } 91 | 92 | sign(data: string): string { 93 | return Uint8ToHex(sign.detached(strToUint8(data), this._identity.secretKey)) 94 | } 95 | 96 | verify(publicKey: string, data: string, signature: string): boolean { 97 | return sign.detached.verify(strToUint8(data), hexToUint8(signature), hexToUint8(publicKey)) 98 | } 99 | 100 | // Generates a server connection authentication request 101 | generateAuthRequest() { 102 | const timestamp = new Date().toISOString() 103 | const signature = this.sign(timestamp) 104 | const { publicKey } = this._identity 105 | return { publicKey, timestamp, signature } 106 | } 107 | 108 | // Returns a hash digest of the given data 109 | hash(data: any, enc = 'hex', alg = 'sha256') { 110 | return scrypto.createHash(alg).update(data).digest(enc) 111 | } 112 | 113 | // Returns a hash digest of the given file 114 | hashFile(path: string, enc = 'hex', alg = 'sha256') { 115 | return new Promise((resolve, reject) => 116 | fs 117 | .createReadStream(path) 118 | .on('error', reject) 119 | .pipe(scrypto.createHash(alg).setEncoding(enc)) 120 | .once('finish', function (this: any) { 121 | resolve(this.read()) 122 | }) 123 | ) 124 | } 125 | 126 | // Hash Key Derivation Function (based on HMAC) 127 | _HKDF(input: Buffer | string, salt: Buffer | string, info: Buffer | string | null, length = RATCHET_KEYS_LEN): Buffer { 128 | // input = input instanceof Uint8Array ? Buffer.from(input) : input 129 | // salt = salt instanceof Uint8Array ? Buffer.from(salt) : salt 130 | return hkdf(input, length, { 131 | salt, 132 | info, 133 | hash: RATCHET_KEYS_HASH, 134 | }) 135 | } 136 | 137 | // Hash-based Message Authentication Code 138 | _HMAC(key: Buffer | string, data: Buffer | string, enc = 'utf8', algo = 'sha256'): Buffer | string { 139 | return scrypto.createHmac(algo, key).update(data).digest(enc) 140 | } 141 | 142 | // Generates a new Curve25519 key pair 143 | _generateRatchetKeyPair() { 144 | let keyPair = box.keyPair() 145 | // Encode in hex for easier handling 146 | keyPair.publicKey = Buffer.from(keyPair.publicKey).toString('hex') 147 | return keyPair 148 | } 149 | 150 | // Initialises an end-to-end encryption session 151 | async initSession(id: string) { 152 | // Generates a new ephemeral ratchet Curve25519 key pair for chat 153 | let { publicKey, secretKey } = this._generateRatchetKeyPair() 154 | // Initialise session object 155 | this._sessions[id] = { 156 | currentRatchet: { 157 | sendingKeys: { 158 | publicKey, 159 | secretKey, 160 | }, 161 | previousCounter: 0, 162 | }, 163 | sending: {}, 164 | receiving: {}, 165 | } 166 | // Sign public key 167 | const timestamp = new Date().toISOString() 168 | const signature = await this.sign(publicKey + timestamp) 169 | console.log('Initialised new session', this._sessions[id]) 170 | return { publicKey, timestamp, signature } 171 | } 172 | 173 | // Starts the session 174 | async startSession(id: string, keyMessage: { publicKey: string; timestamp: string; signature: string }) { 175 | const { publicKey, timestamp, signature } = keyMessage 176 | // Validate sender public key 177 | const sigValid = await this.verify(id, publicKey + timestamp, signature) 178 | // Ignore if new encryption session if signature not valid 179 | if (!sigValid) return console.log('PubKey sig invalid', publicKey) 180 | 181 | const ratchet = this._sessions[id].currentRatchet 182 | const { secretKey } = ratchet.sendingKeys 183 | ratchet.receivingKey = publicKey 184 | // Derive shared master secret and root key 185 | const [rootKey] = this._calcRatchetKeys('E2eeSecret', secretKey, publicKey) 186 | ratchet.rootKey = rootKey 187 | console.log('Initialised Session', rootKey.toString('hex'), this._sessions[id]) 188 | } 189 | 190 | // Calculates the ratchet keys (root and chain key) 191 | _calcRatchetKeys(oldRootKey: Buffer | string, sendingSecretKey: Uint8Array, receivingKey: Uint8Array | string) { 192 | // Convert receivingKey to a Uint8Array if it isn't already 193 | if (typeof receivingKey === 'string') receivingKey = hexToUint8(receivingKey) 194 | // Derive shared ephemeral secret 195 | const sharedSecret = box.before(receivingKey, sendingSecretKey) 196 | // Derive the new ratchet keys 197 | const ratchetKeys = this._HKDF(sharedSecret, oldRootKey, 'E2eeRatchet') 198 | console.log('Derived ratchet keys', ratchetKeys.toString('hex')) 199 | // Chunk ratchetKeys output into its parts: root key and chain key 200 | return chunk(ratchetKeys, RATCHET_KEYS_LEN / 2) 201 | } 202 | 203 | // Calculates the next receiving or sending ratchet 204 | _calcRatchet(session: any, sending: any, receivingKey?: string) { 205 | let ratchet = session.currentRatchet 206 | let ratchetChains, publicKey, previousChain 207 | 208 | if (sending) { 209 | ratchetChains = session.sending 210 | previousChain = ratchetChains[ratchet.sendingKeys.publicKey] 211 | // Replace ephemeral ratchet sending keys with new ones 212 | ratchet.sendingKeys = this._generateRatchetKeyPair() 213 | publicKey = ratchet.sendingKeys.publicKey 214 | console.log('New sending keys generated', publicKey) 215 | } else { 216 | // TODO: Check counters to pre-compute skipped keys 217 | ratchetChains = session.receiving 218 | previousChain = ratchetChains[ratchet.receivingKey] 219 | publicKey = ratchet.receivingKey = receivingKey 220 | } 221 | 222 | if (previousChain) { 223 | // Update the previousCounter with the previous chain counter 224 | ratchet.previousCounter = previousChain.chain.counter 225 | } 226 | // Derive new ratchet keys 227 | const [rootKey, chainKey] = this._calcRatchetKeys(ratchet.rootKey, ratchet.sendingKeys.secretKey, ratchet.receivingKey) 228 | // Update root key 229 | ratchet.rootKey = rootKey 230 | // Initialise new chain 231 | ratchetChains[publicKey] = { 232 | messageKeys: {}, 233 | chain: { 234 | counter: -1, 235 | key: chainKey, 236 | }, 237 | } 238 | return ratchetChains[publicKey] 239 | } 240 | 241 | // Calculates the next message key for the ratchet and updates it 242 | // TODO: Try to get messagekey with message counter otherwise calculate all 243 | // message keys up to it and return it (instead of pre-comp on ratchet) 244 | _calcMessageKey(ratchet: any) { 245 | let chain = ratchet.chain 246 | // Calculate next message key 247 | const messageKey = this._HMAC(chain.key, Buffer.alloc(1, MESSAGE_KEY_SEED)) 248 | // Calculate next ratchet chain key 249 | chain.key = this._HMAC(chain.key, Buffer.alloc(1, CHAIN_KEY_SEED)) 250 | // Increment the chain counter 251 | chain.counter++ 252 | // Save the message key 253 | ratchet.messageKeys[chain.counter] = messageKey 254 | console.log('Calculated next messageKey', ratchet) 255 | // Derive encryption key, mac key and iv 256 | return chunk(this._HKDF(messageKey, 'E2eeCrypt', null, MESSAGE_KEY_LEN), MESSAGE_CHUNK_LEN) 257 | } 258 | 259 | // Encrypts a message 260 | async encrypt(id: string, message = {}, returnCipher = false) { 261 | let session = this._sessions[id] 262 | let ratchet = session.currentRatchet 263 | let sendingChain = session.sending[ratchet.sendingKeys.publicKey] 264 | // Ratchet after every RACHET_MESSAGE_COUNT of messages 265 | let shouldRatchet = sendingChain && sendingChain.chain.counter >= RACHET_MESSAGE_COUNT 266 | if (!sendingChain || shouldRatchet) { 267 | sendingChain = this._calcRatchet(session, true) 268 | console.log('Calculated new sending ratchet', session) 269 | } 270 | const { previousCounter } = ratchet 271 | const { publicKey } = ratchet.sendingKeys 272 | const [encryptKey, hmac, iv] = this._calcMessageKey(sendingChain) 273 | console.log('Calculated encryption creds', encryptKey.toString('hex'), iv.toString('hex')) 274 | const { counter } = sendingChain.chain 275 | 276 | // Encrypt message 277 | if (message) { 278 | const msgJson = JSON.stringify(message) 279 | const msgCipher = scrypto.createCipheriv(CIPHER, encryptKey, iv) 280 | message = msgCipher.update(msgJson, 'utf8', 'hex') + msgCipher.final('hex') 281 | } 282 | 283 | // Construct packet 284 | let packet = { 285 | message, 286 | publicKey, 287 | previousCounter, 288 | counter, 289 | } 290 | 291 | // Sign message with PGP 292 | packet.signature = await this.sign(JSON.stringify(packet)) 293 | 294 | console.log('Encrypted', packet) 295 | 296 | // Return cipher 297 | let res = { packet, cipher: null } 298 | if (returnCipher) res.cipher = scrypto.createCipheriv(CIPHER, encryptKey, iv) 299 | return res 300 | } 301 | 302 | // Decrypts a message 303 | async decrypt(id: string, packet: Packet, returnDecipher = false) { 304 | const { signature, ...packetBody } = packet 305 | const sigValid = await this.verify(id, JSON.stringify(packetBody), signature) 306 | // Ignore message if signature invalid 307 | if (!sigValid) { 308 | throw new Error('Message signature invalid!') 309 | } 310 | const { publicKey, counter, previousCounter } = packetBody 311 | let { message } = packetBody 312 | let session = this._sessions[id] 313 | let receivingChain = session.receiving[publicKey] 314 | if (!receivingChain) { 315 | // Receiving ratchet for key does not exist so create one 316 | receivingChain = this._calcRatchet(session, false, publicKey) 317 | console.log('Calculated new receiving ratchet', receivingChain) 318 | } 319 | // Derive decryption credentials 320 | const [decryptKey, hmac, iv] = this._calcMessageKey(receivingChain) 321 | console.log('Calculated decryption creds', decryptKey.toString('hex'), iv.toString('hex')) 322 | // Decrypt the message contents 323 | if (message) { 324 | const msgDecipher = scrypto.createDecipheriv(CIPHER, decryptKey, iv) 325 | message = msgDecipher.update(message, 'hex', 'utf8') + msgDecipher.final('utf8') 326 | message = JSON.parse(message) 327 | console.log('--> Decrypted message', message) 328 | } 329 | 330 | // Return Decipher 331 | let res = { message, decipher: null } 332 | if (returnDecipher) res.decipher = scrypto.createDecipheriv(CIPHER, decryptKey, iv) 333 | return res 334 | } 335 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /// 3 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 4 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 5 | return new (P || (P = Promise))(function (resolve, reject) { 6 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 7 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 8 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 9 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 10 | }); 11 | }; 12 | var __generator = (this && this.__generator) || function (thisArg, body) { 13 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 14 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 15 | function verb(n) { return function (v) { return step([n, v]); }; } 16 | function step(op) { 17 | if (f) throw new TypeError("Generator is already executing."); 18 | while (_) try { 19 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 20 | if (y = 0, t) op = [op[0] & 2, t.value]; 21 | switch (op[0]) { 22 | case 0: case 1: t = op; break; 23 | case 4: _.label++; return { value: op[1], done: false }; 24 | case 5: _.label++; y = op[1]; op = [0]; continue; 25 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 26 | default: 27 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 28 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 29 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 30 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 31 | if (t[2]) _.ops.pop(); 32 | _.trys.pop(); continue; 33 | } 34 | op = body.call(thisArg, _); 35 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 36 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 37 | } 38 | }; 39 | var __rest = (this && this.__rest) || function (s, e) { 40 | var t = {}; 41 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 42 | t[p] = s[p]; 43 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 44 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 45 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 46 | t[p[i]] = s[p[i]]; 47 | } 48 | return t; 49 | }; 50 | var scrypto = require('crypto'), fs = require('fs'), hkdf = require('futoin-hkdf'), 51 | // TODO: Replace with scrypto.diffieHellman once nodejs#26626 lands on v12 LTS 52 | _a = require('tweetnacl'), box = _a.box, sign = _a.sign, _b = require('./util'), chunk = _b.chunk, hexToUint8 = _b.hexToUint8, strToUint8 = _b.strToUint8, Uint8ToHex = _b.Uint8ToHex, STORE_KEY = 'publicKey', CIPHER = 'aes-256-cbc', RATCHET_KEYS_LEN = 64, RATCHET_KEYS_HASH = 'SHA-256', MESSAGE_KEY_LEN = 80, MESSAGE_CHUNK_LEN = 32, MESSAGE_KEY_SEED = 1, // 0x01 53 | CHAIN_KEY_SEED = 2, // 0x02 54 | RACHET_MESSAGE_COUNT = 10; // Rachet after this no of messages sent 55 | module.exports = /** @class */ (function () { 56 | function E2EE(options) { 57 | this._sessions = {}; 58 | this._store = options.store || new Map(); 59 | this._getSecretIdentity = options.getSecretIdentity || this._getSecretIdentityDef; 60 | this._setSecretIdentity = options.setSecretIdentity || this._setSecretIdentityDef; 61 | // Bindings 62 | this.init = this.init.bind(this); 63 | } 64 | // Default implementations 65 | E2EE.prototype._getSecretIdentityDef = function (publicKey) { 66 | return __awaiter(this, void 0, void 0, function () { 67 | return __generator(this, function (_a) { 68 | return [2 /*return*/, this._store.get(publicKey)]; 69 | }); 70 | }); 71 | }; 72 | E2EE.prototype._setSecretIdentityDef = function (publicKey, secretKey) { 73 | return __awaiter(this, void 0, void 0, function () { 74 | return __generator(this, function (_a) { 75 | return [2 /*return*/, this._store.set(publicKey, secretKey)]; 76 | }); 77 | }); 78 | }; 79 | E2EE.prototype.init = function () { 80 | return __awaiter(this, void 0, void 0, function () { 81 | var publicKey, secretKey, _a; 82 | return __generator(this, function (_b) { 83 | switch (_b.label) { 84 | case 0: 85 | publicKey = this._store.get(STORE_KEY, false); 86 | _a = publicKey; 87 | if (!_a) return [3 /*break*/, 2]; 88 | return [4 /*yield*/, this._getSecretIdentity(publicKey)]; 89 | case 1: 90 | _a = (_b.sent()); 91 | _b.label = 2; 92 | case 2: 93 | secretKey = _a; 94 | // Restore keys if they exist 95 | if (publicKey && secretKey) { 96 | this._identity = { publicKey: publicKey, secretKey: hexToUint8(secretKey) }; 97 | return [2 /*return*/, this._identity]; 98 | } 99 | // Generate new ones otherwise 100 | return [4 /*yield*/, this._generateIdentityKeyPair()]; 101 | case 3: 102 | // Generate new ones otherwise 103 | _b.sent(); 104 | return [2 /*return*/, this._identity]; 105 | } 106 | }); 107 | }); 108 | }; 109 | E2EE.prototype._saveIdentity = function () { 110 | return __awaiter(this, void 0, void 0, function () { 111 | return __generator(this, function (_a) { 112 | switch (_a.label) { 113 | case 0: 114 | this._store.set(STORE_KEY, this._identity.publicKey); 115 | // Save the private key in the OS's keychain under public key 116 | return [4 /*yield*/, this._setSecretIdentity(this._identity.publicKey, Uint8ToHex(this._identity.secretKey))]; 117 | case 1: 118 | // Save the private key in the OS's keychain under public key 119 | _a.sent(); 120 | return [2 /*return*/]; 121 | } 122 | }); 123 | }); 124 | }; 125 | // Generates a new Curve25519 key pair 126 | E2EE.prototype._generateIdentityKeyPair = function () { 127 | return __awaiter(this, void 0, void 0, function () { 128 | var keyPair; 129 | return __generator(this, function (_a) { 130 | switch (_a.label) { 131 | case 0: 132 | keyPair = sign.keyPair(); 133 | // Encode in hex for easier handling 134 | keyPair.publicKey = Uint8ToHex(keyPair.publicKey); 135 | this._identity = keyPair; 136 | return [4 /*yield*/, this._saveIdentity()]; 137 | case 1: 138 | _a.sent(); 139 | return [2 /*return*/]; 140 | } 141 | }); 142 | }); 143 | }; 144 | E2EE.prototype.sign = function (data) { 145 | return Uint8ToHex(sign.detached(strToUint8(data), this._identity.secretKey)); 146 | }; 147 | E2EE.prototype.verify = function (publicKey, data, signature) { 148 | return sign.detached.verify(strToUint8(data), hexToUint8(signature), hexToUint8(publicKey)); 149 | }; 150 | // Generates a server connection authentication request 151 | E2EE.prototype.generateAuthRequest = function () { 152 | var timestamp = new Date().toISOString(); 153 | var signature = this.sign(timestamp); 154 | var publicKey = this._identity.publicKey; 155 | return { publicKey: publicKey, timestamp: timestamp, signature: signature }; 156 | }; 157 | // Returns a hash digest of the given data 158 | E2EE.prototype.hash = function (data, enc, alg) { 159 | if (enc === void 0) { enc = 'hex'; } 160 | if (alg === void 0) { alg = 'sha256'; } 161 | return scrypto.createHash(alg).update(data).digest(enc); 162 | }; 163 | // Returns a hash digest of the given file 164 | E2EE.prototype.hashFile = function (path, enc, alg) { 165 | if (enc === void 0) { enc = 'hex'; } 166 | if (alg === void 0) { alg = 'sha256'; } 167 | return new Promise(function (resolve, reject) { 168 | return fs 169 | .createReadStream(path) 170 | .on('error', reject) 171 | .pipe(scrypto.createHash(alg).setEncoding(enc)) 172 | .once('finish', function () { 173 | resolve(this.read()); 174 | }); 175 | }); 176 | }; 177 | // Hash Key Derivation Function (based on HMAC) 178 | E2EE.prototype._HKDF = function (input, salt, info, length) { 179 | if (length === void 0) { length = RATCHET_KEYS_LEN; } 180 | // input = input instanceof Uint8Array ? Buffer.from(input) : input 181 | // salt = salt instanceof Uint8Array ? Buffer.from(salt) : salt 182 | return hkdf(input, length, { 183 | salt: salt, 184 | info: info, 185 | hash: RATCHET_KEYS_HASH, 186 | }); 187 | }; 188 | // Hash-based Message Authentication Code 189 | E2EE.prototype._HMAC = function (key, data, enc, algo) { 190 | if (enc === void 0) { enc = 'utf8'; } 191 | if (algo === void 0) { algo = 'sha256'; } 192 | return scrypto.createHmac(algo, key).update(data).digest(enc); 193 | }; 194 | // Generates a new Curve25519 key pair 195 | E2EE.prototype._generateRatchetKeyPair = function () { 196 | var keyPair = box.keyPair(); 197 | // Encode in hex for easier handling 198 | keyPair.publicKey = Buffer.from(keyPair.publicKey).toString('hex'); 199 | return keyPair; 200 | }; 201 | // Initialises an end-to-end encryption session 202 | E2EE.prototype.initSession = function (id) { 203 | return __awaiter(this, void 0, void 0, function () { 204 | var _a, publicKey, secretKey, timestamp, signature; 205 | return __generator(this, function (_b) { 206 | switch (_b.label) { 207 | case 0: 208 | _a = this._generateRatchetKeyPair(), publicKey = _a.publicKey, secretKey = _a.secretKey; 209 | // Initialise session object 210 | this._sessions[id] = { 211 | currentRatchet: { 212 | sendingKeys: { 213 | publicKey: publicKey, 214 | secretKey: secretKey, 215 | }, 216 | previousCounter: 0, 217 | }, 218 | sending: {}, 219 | receiving: {}, 220 | }; 221 | timestamp = new Date().toISOString(); 222 | return [4 /*yield*/, this.sign(publicKey + timestamp)]; 223 | case 1: 224 | signature = _b.sent(); 225 | console.log('Initialised new session', this._sessions[id]); 226 | return [2 /*return*/, { publicKey: publicKey, timestamp: timestamp, signature: signature }]; 227 | } 228 | }); 229 | }); 230 | }; 231 | // Starts the session 232 | E2EE.prototype.startSession = function (id, keyMessage) { 233 | return __awaiter(this, void 0, void 0, function () { 234 | var publicKey, timestamp, signature, sigValid, ratchet, secretKey, rootKey; 235 | return __generator(this, function (_a) { 236 | switch (_a.label) { 237 | case 0: 238 | publicKey = keyMessage.publicKey, timestamp = keyMessage.timestamp, signature = keyMessage.signature; 239 | return [4 /*yield*/, this.verify(id, publicKey + timestamp, signature) 240 | // Ignore if new encryption session if signature not valid 241 | ]; 242 | case 1: 243 | sigValid = _a.sent(); 244 | // Ignore if new encryption session if signature not valid 245 | if (!sigValid) 246 | return [2 /*return*/, console.log('PubKey sig invalid', publicKey)]; 247 | ratchet = this._sessions[id].currentRatchet; 248 | secretKey = ratchet.sendingKeys.secretKey; 249 | ratchet.receivingKey = publicKey; 250 | rootKey = this._calcRatchetKeys('E2eeSecret', secretKey, publicKey)[0]; 251 | ratchet.rootKey = rootKey; 252 | console.log('Initialised Session', rootKey.toString('hex'), this._sessions[id]); 253 | return [2 /*return*/]; 254 | } 255 | }); 256 | }); 257 | }; 258 | // Calculates the ratchet keys (root and chain key) 259 | E2EE.prototype._calcRatchetKeys = function (oldRootKey, sendingSecretKey, receivingKey) { 260 | // Convert receivingKey to a Uint8Array if it isn't already 261 | if (typeof receivingKey === 'string') 262 | receivingKey = hexToUint8(receivingKey); 263 | // Derive shared ephemeral secret 264 | var sharedSecret = box.before(receivingKey, sendingSecretKey); 265 | // Derive the new ratchet keys 266 | var ratchetKeys = this._HKDF(sharedSecret, oldRootKey, 'E2eeRatchet'); 267 | console.log('Derived ratchet keys', ratchetKeys.toString('hex')); 268 | // Chunk ratchetKeys output into its parts: root key and chain key 269 | return chunk(ratchetKeys, RATCHET_KEYS_LEN / 2); 270 | }; 271 | // Calculates the next receiving or sending ratchet 272 | E2EE.prototype._calcRatchet = function (session, sending, receivingKey) { 273 | var ratchet = session.currentRatchet; 274 | var ratchetChains, publicKey, previousChain; 275 | if (sending) { 276 | ratchetChains = session.sending; 277 | previousChain = ratchetChains[ratchet.sendingKeys.publicKey]; 278 | // Replace ephemeral ratchet sending keys with new ones 279 | ratchet.sendingKeys = this._generateRatchetKeyPair(); 280 | publicKey = ratchet.sendingKeys.publicKey; 281 | console.log('New sending keys generated', publicKey); 282 | } 283 | else { 284 | // TODO: Check counters to pre-compute skipped keys 285 | ratchetChains = session.receiving; 286 | previousChain = ratchetChains[ratchet.receivingKey]; 287 | publicKey = ratchet.receivingKey = receivingKey; 288 | } 289 | if (previousChain) { 290 | // Update the previousCounter with the previous chain counter 291 | ratchet.previousCounter = previousChain.chain.counter; 292 | } 293 | // Derive new ratchet keys 294 | var _a = this._calcRatchetKeys(ratchet.rootKey, ratchet.sendingKeys.secretKey, ratchet.receivingKey), rootKey = _a[0], chainKey = _a[1]; 295 | // Update root key 296 | ratchet.rootKey = rootKey; 297 | // Initialise new chain 298 | ratchetChains[publicKey] = { 299 | messageKeys: {}, 300 | chain: { 301 | counter: -1, 302 | key: chainKey, 303 | }, 304 | }; 305 | return ratchetChains[publicKey]; 306 | }; 307 | // Calculates the next message key for the ratchet and updates it 308 | // TODO: Try to get messagekey with message counter otherwise calculate all 309 | // message keys up to it and return it (instead of pre-comp on ratchet) 310 | E2EE.prototype._calcMessageKey = function (ratchet) { 311 | var chain = ratchet.chain; 312 | // Calculate next message key 313 | var messageKey = this._HMAC(chain.key, Buffer.alloc(1, MESSAGE_KEY_SEED)); 314 | // Calculate next ratchet chain key 315 | chain.key = this._HMAC(chain.key, Buffer.alloc(1, CHAIN_KEY_SEED)); 316 | // Increment the chain counter 317 | chain.counter++; 318 | // Save the message key 319 | ratchet.messageKeys[chain.counter] = messageKey; 320 | console.log('Calculated next messageKey', ratchet); 321 | // Derive encryption key, mac key and iv 322 | return chunk(this._HKDF(messageKey, 'E2eeCrypt', null, MESSAGE_KEY_LEN), MESSAGE_CHUNK_LEN); 323 | }; 324 | // Encrypts a message 325 | E2EE.prototype.encrypt = function (id, message, returnCipher) { 326 | if (message === void 0) { message = {}; } 327 | if (returnCipher === void 0) { returnCipher = false; } 328 | return __awaiter(this, void 0, void 0, function () { 329 | var session, ratchet, sendingChain, shouldRatchet, previousCounter, publicKey, _a, encryptKey, hmac, iv, counter, msgJson, msgCipher, packet, _b, res; 330 | return __generator(this, function (_c) { 331 | switch (_c.label) { 332 | case 0: 333 | session = this._sessions[id]; 334 | ratchet = session.currentRatchet; 335 | sendingChain = session.sending[ratchet.sendingKeys.publicKey]; 336 | shouldRatchet = sendingChain && sendingChain.chain.counter >= RACHET_MESSAGE_COUNT; 337 | if (!sendingChain || shouldRatchet) { 338 | sendingChain = this._calcRatchet(session, true); 339 | console.log('Calculated new sending ratchet', session); 340 | } 341 | previousCounter = ratchet.previousCounter; 342 | publicKey = ratchet.sendingKeys.publicKey; 343 | _a = this._calcMessageKey(sendingChain), encryptKey = _a[0], hmac = _a[1], iv = _a[2]; 344 | console.log('Calculated encryption creds', encryptKey.toString('hex'), iv.toString('hex')); 345 | counter = sendingChain.chain.counter; 346 | // Encrypt message 347 | if (message) { 348 | msgJson = JSON.stringify(message); 349 | msgCipher = scrypto.createCipheriv(CIPHER, encryptKey, iv); 350 | message = msgCipher.update(msgJson, 'utf8', 'hex') + msgCipher.final('hex'); 351 | } 352 | packet = { 353 | message: message, 354 | publicKey: publicKey, 355 | previousCounter: previousCounter, 356 | counter: counter, 357 | }; 358 | // Sign message with PGP 359 | _b = packet; 360 | return [4 /*yield*/, this.sign(JSON.stringify(packet))]; 361 | case 1: 362 | // Sign message with PGP 363 | _b.signature = _c.sent(); 364 | console.log('Encrypted', packet); 365 | res = { packet: packet, cipher: null }; 366 | if (returnCipher) 367 | res.cipher = scrypto.createCipheriv(CIPHER, encryptKey, iv); 368 | return [2 /*return*/, res]; 369 | } 370 | }); 371 | }); 372 | }; 373 | // Decrypts a message 374 | E2EE.prototype.decrypt = function (id, packet, returnDecipher) { 375 | if (returnDecipher === void 0) { returnDecipher = false; } 376 | return __awaiter(this, void 0, void 0, function () { 377 | var signature, packetBody, sigValid, publicKey, counter, previousCounter, message, session, receivingChain, _a, decryptKey, hmac, iv, msgDecipher, res; 378 | return __generator(this, function (_b) { 379 | switch (_b.label) { 380 | case 0: 381 | signature = packet.signature, packetBody = __rest(packet, ["signature"]); 382 | return [4 /*yield*/, this.verify(id, JSON.stringify(packetBody), signature) 383 | // Ignore message if signature invalid 384 | ]; 385 | case 1: 386 | sigValid = _b.sent(); 387 | // Ignore message if signature invalid 388 | if (!sigValid) { 389 | throw new Error('Message signature invalid!'); 390 | } 391 | publicKey = packetBody.publicKey, counter = packetBody.counter, previousCounter = packetBody.previousCounter; 392 | message = packetBody.message; 393 | session = this._sessions[id]; 394 | receivingChain = session.receiving[publicKey]; 395 | if (!receivingChain) { 396 | // Receiving ratchet for key does not exist so create one 397 | receivingChain = this._calcRatchet(session, false, publicKey); 398 | console.log('Calculated new receiving ratchet', receivingChain); 399 | } 400 | _a = this._calcMessageKey(receivingChain), decryptKey = _a[0], hmac = _a[1], iv = _a[2]; 401 | console.log('Calculated decryption creds', decryptKey.toString('hex'), iv.toString('hex')); 402 | // Decrypt the message contents 403 | if (message) { 404 | msgDecipher = scrypto.createDecipheriv(CIPHER, decryptKey, iv); 405 | message = msgDecipher.update(message, 'hex', 'utf8') + msgDecipher.final('utf8'); 406 | message = JSON.parse(message); 407 | console.log('--> Decrypted message', message); 408 | } 409 | res = { message: message, decipher: null }; 410 | if (returnDecipher) 411 | res.decipher = scrypto.createDecipheriv(CIPHER, decryptKey, iv); 412 | return [2 /*return*/, res]; 413 | } 414 | }); 415 | }); 416 | }; 417 | return E2EE; 418 | }()); 419 | --------------------------------------------------------------------------------