UNPKG

23.2 kBJavaScriptView Raw
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'use strict';
18
19const childProcess = require('child_process');
20const crypto = require('crypto');
21const fs = require('fs');
22const q = require('q');
23
24const Logger = require('./logger');
25const util = require('./util');
26
27const SYMMETRIC_ALGORITHM = 'aes-256-ctr';
28
29let logger = Logger.getLogger({
30 logLevel: 'none',
31 module
32});
33
34/**
35 * @module
36 */
37module.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
609function 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}