1 | /**
|
2 | * Copyright 2017-2018 F5 Networks, Inc.
|
3 | *
|
4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | * you may not use this file except in compliance with the License.
|
6 | * You may obtain a copy of the License at
|
7 | *
|
8 | * http://www.apache.org/licenses/LICENSE-2.0
|
9 | *
|
10 | * Unless required by applicable law or agreed to in writing, software
|
11 | * distributed under the License is distributed on an "AS IS" BASIS,
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | * See the License for the specific language governing permissions and
|
14 | * limitations under the License.
|
15 | */
|
16 |
|
17 | ;
|
18 |
|
19 | const assert = require('assert');
|
20 | const childProcess = require('child_process');
|
21 | const q = require('q');
|
22 | const util = require('./util');
|
23 | const cryptoUtil = require('./cryptoUtil');
|
24 | const localKeyUtil = require('./localKeyUtil');
|
25 | const Logger = require('./logger');
|
26 |
|
27 | let logger = Logger.getLogger({
|
28 | logLevel: 'none',
|
29 | module
|
30 | });
|
31 |
|
32 | const KEYS = require('./sharedConstants').KEYS;
|
33 |
|
34 | /**
|
35 | * This routines are utilities for decrypting data from files on disk
|
36 | *
|
37 | * These routines are meant to be used locally on a BIG-IP and operate via tmsh
|
38 | * rather than iControl REST. This is so that we do not need to take in
|
39 | * unencrypted passwords as parameters either on the command line or via
|
40 | * the filesystem.
|
41 | *
|
42 | * Notes:
|
43 | * + Only runs locally on a BIG-IP. Cannot run on a remote BIG-IP.
|
44 | * + Uses tmsh rather than iControl REST so that we do not need to take in a password
|
45 | *
|
46 | * @module
|
47 | */
|
48 | module.exports = {
|
49 |
|
50 | /**
|
51 | * Decrypts data
|
52 | *
|
53 | * @param {String} data - Data to decrypt
|
54 | * @param {String} privateKeyFolder - BIG-IP folder in which private key is installed
|
55 | * @param {String} privateKeyName - Name of private key installed on BIG-IP
|
56 | * @param {Object} [options] - Optional parameters
|
57 | * @param {String} [options.encryptedKey] - The encrypted symmetric key. Required if symmetric encryption
|
58 | * was used.
|
59 | * @param {String | Buffer} [options.iv] - The initialization vector that was used for
|
60 | * encryption. Required if symmetric encryption was used.
|
61 | *
|
62 | * @returns {Promise} A promise which is resolved with the decrypted data or
|
63 | * rejected if an error occurs.
|
64 | */
|
65 | decryptData(data, privateKeyFolder, privateKeyName, options) {
|
66 | assert.ok(privateKeyFolder, 'privateKeyFolder is required');
|
67 | assert.ok(privateKeyName, 'privateKeyName is required');
|
68 | assert.ok(data, 'data is required');
|
69 |
|
70 | let privateKeyFile;
|
71 | let existingPrivateKeyName;
|
72 |
|
73 | return localKeyUtil.getExistingPrivateKeyName(privateKeyFolder, privateKeyName)
|
74 | .then((response) => {
|
75 | existingPrivateKeyName = response;
|
76 | return localKeyUtil.getPrivateKeyFilePath(privateKeyFolder, existingPrivateKeyName);
|
77 | })
|
78 | .then((privateKeyFilePath) => {
|
79 | if (!privateKeyFilePath) {
|
80 | return q.reject(new Error('No private key found'));
|
81 | }
|
82 |
|
83 | privateKeyFile = privateKeyFilePath;
|
84 | return localKeyUtil.getPrivateKeyMetadata(privateKeyFolder, existingPrivateKeyName);
|
85 | })
|
86 | .then((metadata) => {
|
87 | if (!metadata) {
|
88 | return q.reject(new Error('No private key metadata'));
|
89 | }
|
90 |
|
91 | const decryptOptions = {
|
92 | passphrase: metadata.passphrase,
|
93 | passphraseEncrypted: !!metadata.passphrase
|
94 | };
|
95 |
|
96 | if (options && options.encryptedKey && options.iv) {
|
97 | return cryptoUtil.symmetricDecrypt(
|
98 | privateKeyFile,
|
99 | options.encryptedKey,
|
100 | options.iv,
|
101 | data,
|
102 | decryptOptions
|
103 | );
|
104 | }
|
105 | return cryptoUtil.decrypt(privateKeyFile, data, decryptOptions);
|
106 | })
|
107 | .then((decryptedData) => {
|
108 | return decryptedData;
|
109 | })
|
110 | .catch((err) => {
|
111 | logger.info('Error decrypting data', err && err.message ? err.message : err);
|
112 | return q.reject(err);
|
113 | });
|
114 | },
|
115 |
|
116 | /**
|
117 | * Decrypts a secret, typically a password that was encrypted with our
|
118 | * local private keys.
|
119 | *
|
120 | * This is just a shortcut for {@link decryptData}
|
121 | *
|
122 | * @static
|
123 | *
|
124 | * @param {String} password - secret to decrypt
|
125 | *
|
126 | * @returns {Promise} A promise which is resolved with the decrypted
|
127 | * secret or rejected if an error occurs
|
128 | */
|
129 | decryptPassword(password) {
|
130 | assert.ok(password, 'password is required');
|
131 |
|
132 | return this.decryptData(password, KEYS.LOCAL_PRIVATE_KEY_FOLDER, KEYS.LOCAL_PRIVATE_KEY);
|
133 | },
|
134 |
|
135 | /**
|
136 | * Decrypts a secret that was encrypted symmetrically, typically a large password
|
137 | * or object (JSON) with multiple values that was encrypted with our local private keys.
|
138 | *
|
139 | * This is just a shortcut for {@link decryptData} with symmetric options
|
140 | *
|
141 | * @static
|
142 | *
|
143 | * @param {Object} data - Object (JSON) created by symmetric encrypt operation
|
144 | *
|
145 | * @returns {Promise} A promise which is resolved with the decrypted
|
146 | * secret or rejected if an error occurs
|
147 | */
|
148 | symmetricDecryptPassword(data) {
|
149 | assert.ok(data, 'data is required');
|
150 |
|
151 | let parsedData;
|
152 | try {
|
153 | parsedData = typeof data === 'object' ? data : JSON.parse(data);
|
154 | } catch (err) {
|
155 | return q.reject(err && err.message ? err.message : err);
|
156 | }
|
157 |
|
158 | return this.decryptData(
|
159 | parsedData.encryptedData,
|
160 | parsedData.privateKey.folder,
|
161 | parsedData.privateKey.name,
|
162 | {
|
163 | encryptedKey: parsedData.encryptedKey,
|
164 | iv: parsedData.iv
|
165 | }
|
166 | );
|
167 | },
|
168 |
|
169 | /**
|
170 | * Decrypts data from a file on disk
|
171 | *
|
172 | * @param {String} dataFile - File to decrypt
|
173 | * @param {Object} [options] - Optional parameters
|
174 | * @param {Boolean} [options.symmetric] - Data was symmetrically encrypted
|
175 | *
|
176 | * @returns {Promise} A promise which is resolved with the decrypted data or
|
177 | * rejected if an error occurs.
|
178 | */
|
179 | decryptDataFromFile(dataFile, options) {
|
180 | let dataToDecrypt;
|
181 |
|
182 | assert.ok(dataFile, 'dataFile is required');
|
183 |
|
184 | return util.readDataFromFile(dataFile)
|
185 | .then((data) => {
|
186 | dataToDecrypt = data.toString();
|
187 | if (options && options.symmetric) {
|
188 | return this.symmetricDecryptPassword(dataToDecrypt);
|
189 | }
|
190 | return this.decryptPassword(dataToDecrypt);
|
191 | })
|
192 | .catch((err) => {
|
193 | logger.info('Error decrypting data from file', err && err.message ? err.message : err);
|
194 | return q.reject(err);
|
195 | });
|
196 | },
|
197 |
|
198 | /**
|
199 | * Decrypts a BIG-IP configuration value.
|
200 | *
|
201 | * Must be run on a BIG-IP.
|
202 | *
|
203 | * @param {String} value - The configuragtion value to decrypt
|
204 | *
|
205 | * @returns {Promse} A promise which is resolved with the decrypted configuration
|
206 | * value or rejected if an error occurs.
|
207 | */
|
208 | decryptConfValue(value) {
|
209 | const deferred = q.defer();
|
210 |
|
211 | childProcess.execFile(
|
212 | `${__dirname}/../scripts/decryptConfValue`,
|
213 | [value],
|
214 | (error, stdout, stderr) => {
|
215 | if (error) {
|
216 | logger.info(
|
217 | 'Error decrypting conf value',
|
218 | error && error.message ? error.message : error
|
219 | );
|
220 | deferred.reject(new Error(stderr));
|
221 | } else {
|
222 | deferred.resolve(stdout);
|
223 | }
|
224 | }
|
225 | );
|
226 |
|
227 | return deferred.promise;
|
228 | },
|
229 |
|
230 | setLoggerOptions(loggerOptions) {
|
231 | const loggerOpts = Object.assign({}, loggerOptions);
|
232 | loggerOpts.module = module;
|
233 | logger = Logger.getLogger(loggerOpts);
|
234 | },
|
235 | };
|