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