1 | /**
|
2 | * Copyright 2016-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 childProcess = require('child_process');
|
20 | const crypto = require('crypto');
|
21 | const fs = require('fs');
|
22 | const q = require('q');
|
23 |
|
24 | const Logger = require('./logger');
|
25 | const util = require('./util');
|
26 |
|
27 | const SYMMETRIC_ALGORITHM = 'aes-256-ctr';
|
28 |
|
29 | let logger = Logger.getLogger({
|
30 | logLevel: 'none',
|
31 | module
|
32 | });
|
33 |
|
34 | /**
|
35 | * @module
|
36 | */
|
37 | module.exports = {
|
38 |
|
39 | /**
|
40 | * Encrypts data with a public key
|
41 | *
|
42 | * @param {String} publicKeyDataOrFile - Either the public key, or the full path to
|
43 | * a file containing the public key
|
44 | * @param {String} data - String version of the data to encrypt
|
45 | *
|
46 | * @returns {Promise} A promise which is resolved with a base64 encoded version
|
47 | * of the encrypted data, or rejected if an error occurs.
|
48 | */
|
49 | encrypt: function encrypt(publicKeyDataOrFile, data) {
|
50 | const deferred = q.defer();
|
51 | let publicKeyPromise;
|
52 |
|
53 | const getPublicKey = function getPublicKey(publicKeyFile) {
|
54 | const publicKeyDeferred = q.defer();
|
55 | fs.readFile(publicKeyFile, (err, publicKey) => {
|
56 | if (err) {
|
57 | logger.warn('Error reading public key:', err);
|
58 | publicKeyDeferred.reject(err);
|
59 | } else {
|
60 | publicKeyDeferred.resolve(publicKey);
|
61 | }
|
62 | });
|
63 |
|
64 | return publicKeyDeferred.promise;
|
65 | };
|
66 |
|
67 | if (typeof data !== 'string') {
|
68 | deferred.reject(new Error('data must be a string'));
|
69 | return deferred.promise;
|
70 | }
|
71 |
|
72 | if (publicKeyDataOrFile.startsWith('-----BEGIN PUBLIC KEY-----')) {
|
73 | publicKeyPromise = q(publicKeyDataOrFile);
|
74 | } else {
|
75 | publicKeyPromise = getPublicKey(publicKeyDataOrFile);
|
76 | }
|
77 |
|
78 | publicKeyPromise
|
79 | .then((publicKey) => {
|
80 | let encrypted;
|
81 |
|
82 | try {
|
83 | encrypted = crypto.publicEncrypt(publicKey, util.createBufferFrom(data));
|
84 | deferred.resolve(encrypted.toString('base64'));
|
85 | } catch (err) {
|
86 | logger.warn('Error encrypting data:', err);
|
87 | deferred.reject(err);
|
88 | }
|
89 | })
|
90 | .catch((err) => {
|
91 | logger.warn('Unable to get public key:', err);
|
92 | deferred.reject(err);
|
93 | });
|
94 |
|
95 | return deferred.promise;
|
96 | },
|
97 |
|
98 | /**
|
99 | * Decrypts data with a private key
|
100 | *
|
101 | * If there is an encrypted passphrase, this only works when running on the BIG-IP on
|
102 | * which the private key was installed.
|
103 | *
|
104 | * @param {String} privateKeyInFile - Full path to private key
|
105 | * @param {String} data - Base64 encoded version of the data to decrypt
|
106 | * @param {Object} [options] - Optional arguments
|
107 | * @param {String} [options.passphrase] - Passphrase for private key. Default no passphrase.
|
108 | * @param {Boolean} [options.passphraseEncrypted] - If there is a passphrase, whether or not it
|
109 | * is encrypted (by MCP). Default false.
|
110 | *
|
111 | * @returns {Promise} A promise which is resolve with a string version of the decrypted
|
112 | * data, or rejected if an error occurs.
|
113 | */
|
114 | decrypt(privateKeyInFile, data, options) {
|
115 | const deferred = q.defer();
|
116 |
|
117 | const passphrase = options ? options.passphrase : undefined;
|
118 | const passphraseEncrypted = options ? options.passphraseEncrypted : false;
|
119 |
|
120 | if (typeof data !== 'string') {
|
121 | deferred.reject(new Error('data must be a string'));
|
122 | return deferred.promise;
|
123 | }
|
124 |
|
125 | fs.readFile(privateKeyInFile, (readFileErr, privateKey) => {
|
126 | let decrypted;
|
127 | let passphraseDeferred;
|
128 |
|
129 | if (readFileErr) {
|
130 | logger.warn('Error reading private key:', readFileErr);
|
131 | deferred.reject(readFileErr);
|
132 | } else {
|
133 | if (passphrase) {
|
134 | passphraseDeferred = q.defer();
|
135 | if (passphraseEncrypted) {
|
136 | ready()
|
137 | .then(() => {
|
138 | childProcess.execFile(
|
139 | `${__dirname}/../scripts/decryptConfValue`,
|
140 | [passphrase],
|
141 | (error, stdout, stderr) => {
|
142 | if (error) {
|
143 | logger.warn('Error decrypting value:', stderr);
|
144 | deferred.reject(error);
|
145 | } else {
|
146 | passphraseDeferred.resolve(stdout);
|
147 | }
|
148 | }
|
149 | );
|
150 | })
|
151 | .catch((err) => {
|
152 | deferred.reject(err);
|
153 | });
|
154 | } else {
|
155 | passphraseDeferred.resolve(passphrase);
|
156 | }
|
157 | } else {
|
158 | passphraseDeferred = q.defer();
|
159 | passphraseDeferred.resolve();
|
160 | }
|
161 |
|
162 | passphraseDeferred.promise
|
163 | .then((decryptedPassphrase) => {
|
164 | try {
|
165 | decrypted = crypto.privateDecrypt(
|
166 | {
|
167 | key: privateKey,
|
168 | passphrase: decryptedPassphrase
|
169 | },
|
170 | util.createBufferFrom(data, 'base64')
|
171 | );
|
172 | deferred.resolve(decrypted.toString());
|
173 | } catch (err) {
|
174 | logger.warn('Error decrypting data:', err);
|
175 | deferred.reject(err);
|
176 | }
|
177 | });
|
178 | }
|
179 | });
|
180 |
|
181 | return deferred.promise;
|
182 | },
|
183 |
|
184 | /**
|
185 | * Generates a public/private key pair.
|
186 | *
|
187 | * @param {String} privateKeyOutFile - Full path where private key will be written
|
188 | * @param {Object} [options] - Optional arguments
|
189 | * @param {String} [options.keyLength] - Key length. Default is 2048.
|
190 | * @param {String} [options.publicKeyOutFile] - Full path where public key certificate will be written.
|
191 | * Default is to resolve with the public key.
|
192 | * @param {String} [options.passphrase] - Passphrase for private key. Default no passphrase.
|
193 | *
|
194 | * @returns {Promise} A promise which will be resolved when the data is written or rejected
|
195 | * if an error occurs. If options.publicKeyOutFile is not provided, promise
|
196 | * is resolved with the public key.
|
197 | */
|
198 | generateKeyPair(privateKeyOutFile, options) {
|
199 | const passphrase = options ? options.passphrase : undefined;
|
200 | const keyLength = options ? options.keyLength : undefined;
|
201 | const publicKeyOutFile = options ? options.publicKeyOutFile : undefined;
|
202 |
|
203 | const deferred = q.defer();
|
204 | let genrsaCmd = `/usr/bin/openssl genrsa -out ${privateKeyOutFile}`;
|
205 | let rsaCmd = `/usr/bin/openssl rsa -in ${privateKeyOutFile} -outform PEM -pubout`;
|
206 | let rsaChild;
|
207 | let publicKeyData;
|
208 |
|
209 | if (passphrase) {
|
210 | genrsaCmd += ' -aes256 -passout stdin';
|
211 | rsaCmd += ' -passin stdin';
|
212 | }
|
213 |
|
214 | genrsaCmd += ` ${keyLength || '2048'}`;
|
215 |
|
216 | if (publicKeyOutFile) {
|
217 | rsaCmd += ` -out ${publicKeyOutFile}`;
|
218 | }
|
219 |
|
220 | const genrsaChild = childProcess.exec(genrsaCmd, (genRsaError, genrsaStdout, genrsaStderr) => {
|
221 | if (genRsaError) {
|
222 | logger.warn('Error generating private key:', genrsaStderr);
|
223 | deferred.reject(genRsaError);
|
224 | } else {
|
225 | rsaChild = childProcess.exec(rsaCmd, (rsaError, rsaStdout, rsaStderr) => {
|
226 | if (rsaError) {
|
227 | logger.warn('Error extracting public key:', rsaStderr);
|
228 | deferred.reject(rsaError);
|
229 | } else {
|
230 | if (!publicKeyOutFile) {
|
231 | publicKeyData = rsaStdout;
|
232 | }
|
233 | deferred.resolve(publicKeyData);
|
234 | }
|
235 | });
|
236 |
|
237 | if (passphrase) {
|
238 | rsaChild.stdin.write(`${passphrase}\n`);
|
239 | rsaChild.stdin.end();
|
240 | }
|
241 | }
|
242 | });
|
243 |
|
244 | if (passphrase) {
|
245 | genrsaChild.stdin.write(`${passphrase}\n`);
|
246 | genrsaChild.stdin.end();
|
247 | }
|
248 |
|
249 | return deferred.promise;
|
250 | },
|
251 |
|
252 | /**
|
253 | * Generates random bytes of a certain length and encoding
|
254 | *
|
255 | * Note: If encoding is 'base64' and length is not a multiple of 6,
|
256 | * the returned bytes will always end in '=' or '==', which decreases
|
257 | * randomness.
|
258 | *
|
259 | * @param {Number} length - Number of random bytes to generate.
|
260 | * @param {String} encoding - Encoding to use ('ascii', 'base64', 'hex', etc)
|
261 | *
|
262 | * @returns {Promise} A promise which is resolved with the random bytes or
|
263 | * rejected if an error occurs
|
264 | */
|
265 | generateRandomBytes(length, encoding) {
|
266 | const deferred = q.defer();
|
267 |
|
268 | crypto.randomBytes(length, (err, buf) => {
|
269 | if (err) {
|
270 | logger.warn('Error generating random bytes:', err);
|
271 | deferred.reject(err);
|
272 | } else {
|
273 | deferred.resolve(buf.toString(encoding));
|
274 | }
|
275 | });
|
276 |
|
277 | return deferred.promise;
|
278 | },
|
279 |
|
280 | /**
|
281 | * Checks the generated password against common regexes.
|
282 | *
|
283 | * @param {Number} length - Number of characters in the password string.
|
284 | *
|
285 | * @param {String} password - Password string.
|
286 | *
|
287 | * @returns {Promise} A promise which is resolved with the password or
|
288 | * rejected if an error occurs
|
289 | */
|
290 | checkPasswordStrength(length, password) {
|
291 | const deferred = q.defer();
|
292 |
|
293 | const uppercaseMinCount = 1;
|
294 | const lowercaseMinCount = 1;
|
295 | const numberMinCount = 1;
|
296 | const specialMinCount = 1;
|
297 |
|
298 | const UPPERCASE_REGEX = /([A-Z])/g;
|
299 | const LOWERCASE_REGEX = /([a-z])/g;
|
300 | const NUMBER_REGEX = /([\d])/g;
|
301 | const SPECIAL_CHAR_REGEX = /([+/])/g;
|
302 | const NON_REPEATING_CHAR_REGEX = /([\w\d?-])\1{2,}/g;
|
303 |
|
304 | const uc = password.match(UPPERCASE_REGEX);
|
305 | const lc = password.match(LOWERCASE_REGEX);
|
306 | const n = password.match(NUMBER_REGEX);
|
307 | const sc = password.match(SPECIAL_CHAR_REGEX);
|
308 | const nr = password.match(NON_REPEATING_CHAR_REGEX);
|
309 |
|
310 | if (password.length >= length &&
|
311 | !nr &&
|
312 | uc && uc.length >= uppercaseMinCount &&
|
313 | lc && lc.length >= lowercaseMinCount &&
|
314 | n && n.length >= numberMinCount &&
|
315 | sc && sc.length >= specialMinCount) {
|
316 | deferred.resolve(password);
|
317 | } else {
|
318 | deferred.reject(new Error('Password failed regex test'));
|
319 | }
|
320 | return deferred.promise;
|
321 | },
|
322 |
|
323 | /**
|
324 | * Checks the generated password against the cracklib library.
|
325 | *
|
326 | * @param {String} password - Password string.
|
327 | *
|
328 | * @returns {Promise} A promise which is resolved with the password or
|
329 | * rejected if an error occurs
|
330 | */
|
331 | checkPasswordCrack(password) {
|
332 | const deferred = q.defer();
|
333 | childProcess.exec(`echo ${password} | /usr/sbin/cracklib-check`, (error, stdout, stderr) => {
|
334 | if (stdout.toString().includes('OK')) {
|
335 | deferred.resolve(password);
|
336 | } else {
|
337 | deferred.reject(new Error(stderr));
|
338 | }
|
339 | });
|
340 | return deferred.promise;
|
341 | },
|
342 |
|
343 | /**
|
344 | * Checks the generated password against common regexes.
|
345 | *
|
346 | * @param {Number} length - Number of characters in the password string.
|
347 | *
|
348 | * @param {String} password - Password string.
|
349 | *
|
350 | * @returns {Promise} A promise which is resolved when the password passes both checks.
|
351 | */
|
352 | checkPasswordAll(length, password) {
|
353 | const deferred = q.defer();
|
354 | q.all([this.checkPasswordStrength(length, password), this.checkPasswordCrack(password)])
|
355 | .then(() => {
|
356 | deferred.resolve(password);
|
357 | })
|
358 | .catch((err) => {
|
359 | deferred.reject(err);
|
360 | });
|
361 | return deferred.promise;
|
362 | },
|
363 |
|
364 | /**
|
365 | * Creates a random on BIG-IP/BIG-IQ.
|
366 | *
|
367 | * Only works if running on the device, not remotely.
|
368 | *
|
369 | * @returns {Promise} A promise which is resolved with user credentials
|
370 | * or rejected if an error occurs. Credentials are in
|
371 | * the form:
|
372 | *
|
373 | * {
|
374 | * user: user,
|
375 | * password: password
|
376 | * }
|
377 | */
|
378 | createRandomUser() {
|
379 | const USER_NAME_LENGTH = 10; // these are hex bytes - user name will be 20 chars
|
380 | const USER_PASSWORD_LENGTH = 24; // use a multiple of 6 to prevent '=' at the end
|
381 |
|
382 | let user;
|
383 | let password;
|
384 |
|
385 | // Get a random user name to use
|
386 | return this.generateRandomBytes(USER_NAME_LENGTH, 'hex')
|
387 | .then((data) => {
|
388 | user = data;
|
389 |
|
390 | // Get a random password for the user
|
391 | return this.generateRandomBytes(USER_PASSWORD_LENGTH, 'base64');
|
392 | })
|
393 | .then((data) => {
|
394 | password = data;
|
395 | return this.checkPasswordAll(USER_PASSWORD_LENGTH, password);
|
396 | })
|
397 | .then((data) => {
|
398 | password = data;
|
399 | return util.runTmshCommand(
|
400 | /* eslint-disable max-len */
|
401 | `create auth user ${user} password ${password} partition-access replace-all-with { all-partitions { role admin } }`
|
402 | /* eslint-enable max-len */
|
403 | );
|
404 | })
|
405 | .then(() => {
|
406 | return q({ user, password });
|
407 | })
|
408 | .catch((err) => {
|
409 | return q.reject(err);
|
410 | });
|
411 | },
|
412 |
|
413 | /**
|
414 | * Runs the create random user function on BIG-IP/BIG-IQ until valid credentials are returned.
|
415 | *
|
416 | * @returns {Promise} A promise which is resolved with user credentials
|
417 | * or re-runs createRandomUser if an error occurs. Credentials are in
|
418 | * the form:
|
419 | *
|
420 | * {
|
421 | * user: user,
|
422 | * password: password
|
423 | * }
|
424 | */
|
425 | nextRandomUser(iteration) {
|
426 | const numTries = iteration || 0;
|
427 | if (numTries > 100) {
|
428 | return q.reject(new Error('too many tries'));
|
429 | }
|
430 | return this.createRandomUser()
|
431 | .then((data) => {
|
432 | return q(data);
|
433 | })
|
434 | .catch((err) => {
|
435 | logger.info('Failed to create random user, retrying', err && err.message ? err.message : err);
|
436 | return this.nextRandomUser(numTries + 1);
|
437 | });
|
438 | },
|
439 |
|
440 | /**
|
441 | * Generate a random integer in a range
|
442 | *
|
443 | * This code courtesy of https://stackoverflow.com/a/33627342
|
444 | *
|
445 | * @param {Number} minimum - Lowest number to generate
|
446 | * @param {Number} maximum - Highest number to generate
|
447 | *
|
448 | * @returns {Number} - A random number in the specified range
|
449 | */
|
450 | generateRandomIntInRange(minimum, maximum) {
|
451 | const distance = maximum - minimum;
|
452 |
|
453 | if (minimum >= maximum) {
|
454 | logger.warn('Minimum number should be less than maximum');
|
455 | return false;
|
456 | } else if (distance > 281474976710655) {
|
457 | logger.warn('You can not get all possible random numbers if range is greater than 256^6-1');
|
458 | return false;
|
459 | } else if (maximum > Number.MAX_SAFE_INTEGER) {
|
460 | logger.warn('Maximum number should be safe integer limit');
|
461 | return false;
|
462 | }
|
463 |
|
464 | let maxBytes = 6;
|
465 | let maxDec = 281474976710656;
|
466 |
|
467 | if (distance < 256) {
|
468 | maxBytes = 1;
|
469 | maxDec = 256;
|
470 | } else if (distance < 65536) {
|
471 | maxBytes = 2;
|
472 | maxDec = 65536;
|
473 | } else if (distance < 16777216) {
|
474 | maxBytes = 3;
|
475 | maxDec = 16777216;
|
476 | } else if (distance < 4294967296) {
|
477 | maxBytes = 4;
|
478 | maxDec = 4294967296;
|
479 | } else if (distance < 1099511627776) {
|
480 | maxBytes = 4;
|
481 | maxDec = 1099511627776;
|
482 | }
|
483 |
|
484 | const randbytes = parseInt(crypto.randomBytes(maxBytes).toString('hex'), 16);
|
485 | /* eslint-disable no-mixed-operators */
|
486 | let result = Math.floor(randbytes / maxDec * (maximum - minimum + 1) + minimum);
|
487 | /* eslint-enable no-mixed-operators */
|
488 |
|
489 | if (result > maximum) {
|
490 | result = maximum;
|
491 | }
|
492 |
|
493 | return result;
|
494 | },
|
495 |
|
496 | setLogger(aLogger) {
|
497 | logger = aLogger;
|
498 | },
|
499 |
|
500 | setLoggerOptions(loggerOptions) {
|
501 | const loggerOpts = Object.assign({}, loggerOptions);
|
502 | loggerOpts.module = module;
|
503 | logger = Logger.getLogger(loggerOpts);
|
504 | },
|
505 |
|
506 | /**
|
507 | * Encrypts data using symmetric encryption
|
508 | *
|
509 | * A random symmetric key will be generated and encrypted using the public key.
|
510 | * The data will then be encrypted using the symmetric key. Encrypted symmetric
|
511 | * key will be returned with the data.
|
512 | *
|
513 | * @param {String} publicKeyDataOrFile - Either the public key, or the full path to
|
514 | * a file containing the public key. The symmetric
|
515 | * key will be encrypted with this key.
|
516 | * @param {String | Buffer} data - String version of the data to encrypt
|
517 | * @param {Object} [options] - Optional parameters
|
518 | * @param {String} [options.encoding] - Encoding for encrypted output. Default is base64.
|
519 | *
|
520 | * @returns {Promise} A promise which is resolved with a base64 encoded version
|
521 | * of the encrypted data, the encrypted symmetric key, and
|
522 | * then initialization vector, or rejected
|
523 | * if an error occurs. Resolved data is:
|
524 | *
|
525 | * {
|
526 | * encryptedKey: <encryptedKey>,
|
527 | * iv: <initializationVector>,
|
528 | * encryptedData: <base64_encoded_encryptedData>
|
529 | * }
|
530 | */
|
531 | symmetricEncrypt(publicKeyDataOrFile, data, options) {
|
532 | let encryptedData;
|
533 | let iv;
|
534 |
|
535 | const encoding = options && options.encoding ? options.encoding : 'base64';
|
536 |
|
537 | // get random initialization
|
538 | return this.generateRandomBytes(8, 'hex')
|
539 | .then((bytes) => {
|
540 | iv = bytes;
|
541 |
|
542 | // get random key
|
543 | return this.generateRandomBytes(16, 'hex');
|
544 | })
|
545 | .then((key) => {
|
546 | // encrypt data
|
547 | const cipher = crypto.createCipheriv(SYMMETRIC_ALGORITHM, key, iv);
|
548 | encryptedData = cipher.update(data, 'utf8', encoding);
|
549 | encryptedData += cipher.final(encoding);
|
550 |
|
551 | // encrypt key
|
552 | return this.encrypt(publicKeyDataOrFile, key);
|
553 | })
|
554 | .then((key) => {
|
555 | return {
|
556 | encryptedData,
|
557 | iv,
|
558 | encryptedKey: key,
|
559 | };
|
560 | })
|
561 | .catch((err) => {
|
562 | logger.warn('symmetricEncrypt failed', err && err.message ? err.message : err);
|
563 | return q.reject(err);
|
564 | });
|
565 | },
|
566 |
|
567 | /**
|
568 | * Decrypts data that was encrypted with symmetric encryption
|
569 | *
|
570 | * @param {String} privateKeyFile - The private key file matching the public key
|
571 | * that was used to encrypte the symmetric key.
|
572 | * @param {String} encryptedKey - The encrypted symmetric key.
|
573 | * @param {String | Buffer} iv - The initialization vector that was used for
|
574 | * encryption.
|
575 | * @param {String} data - Data to decrypt.
|
576 | * @param {Object} [options] - Optional arguments
|
577 | * @param {String} [options.inputEncoding] - Encoding of the encrypted output. Default is base64.
|
578 | * @param {String} [options.passphrase] - Passphrase for private key. Default no passphrase.
|
579 | * @param {Boolean} [options.passphraseEncrypted] - If there is a passphrase, whether or not it
|
580 | */
|
581 | symmetricDecrypt(privateKeyFile, encryptedKey, iv, data, options) {
|
582 | const inputEncoding = options && options.inputEncoding ? options.inputEncoding : 'base64';
|
583 | const passphrase = options ? options.passphrase : null;
|
584 | const passphraseEncrypted = options ? options.passphraseEncrypted : false;
|
585 |
|
586 | // decrypt the key
|
587 | return this.decrypt(
|
588 | privateKeyFile,
|
589 | encryptedKey,
|
590 | {
|
591 | passphrase,
|
592 | passphraseEncrypted
|
593 | }
|
594 | )
|
595 | .then((key) => {
|
596 | // decrypt the data
|
597 | const decipher = crypto.createDecipheriv(SYMMETRIC_ALGORITHM, key, iv);
|
598 | let decryptedData = decipher.update(data, inputEncoding, 'utf8');
|
599 | decryptedData += decipher.final('utf8');
|
600 | return decryptedData;
|
601 | })
|
602 | .catch((err) => {
|
603 | logger.warn('symmetricDecrypt failed', err && err.message ? err.message : err);
|
604 | return q.reject(err);
|
605 | });
|
606 | }
|
607 | };
|
608 |
|
609 | function ready() {
|
610 | const deferred = q.defer();
|
611 |
|
612 | childProcess.execFile(`${__dirname}/../scripts/waitForMcp.sh`, (error) => {
|
613 | if (error) {
|
614 | deferred.reject(error);
|
615 | } else {
|
616 | deferred.resolve();
|
617 | }
|
618 | });
|
619 |
|
620 | return deferred.promise;
|
621 | }
|