index.js 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. 'use strict';
  2. const crypto = require('crypto');
  3. const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
  4. const numericCharacters = '0123456789'.split('');
  5. const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split('');
  6. const generateForCustomCharacters = (length, characters) => {
  7. // Generating entropy is faster than complex math operations, so we use the simplest way
  8. const characterCount = characters.length;
  9. const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
  10. const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low
  11. let string = '';
  12. let stringLength = 0;
  13. while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
  14. const entropy = crypto.randomBytes(entropyLength);
  15. let entropyPosition = 0;
  16. while (entropyPosition < entropyLength && stringLength < length) {
  17. const entropyValue = entropy.readUInt16LE(entropyPosition);
  18. entropyPosition += 2;
  19. if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
  20. continue;
  21. }
  22. string += characters[entropyValue % characterCount];
  23. stringLength++;
  24. }
  25. }
  26. return string;
  27. };
  28. const allowedTypes = [
  29. undefined,
  30. 'hex',
  31. 'base64',
  32. 'url-safe',
  33. 'numeric',
  34. 'distinguishable'
  35. ];
  36. module.exports = ({length, type, characters}) => {
  37. if (!(length >= 0 && Number.isFinite(length))) {
  38. throw new TypeError('Expected a `length` to be a non-negative finite number');
  39. }
  40. if (type !== undefined && characters !== undefined) {
  41. throw new TypeError('Expected either `type` or `characters`');
  42. }
  43. if (characters !== undefined && typeof characters !== 'string') {
  44. throw new TypeError('Expected `characters` to be string');
  45. }
  46. if (!allowedTypes.includes(type)) {
  47. throw new TypeError(`Unknown type: ${type}`);
  48. }
  49. if (type === undefined && characters === undefined) {
  50. type = 'hex';
  51. }
  52. if (type === 'hex' || (type === undefined && characters === undefined)) {
  53. return crypto.randomBytes(Math.ceil(length * 0.5)).toString('hex').slice(0, length); // Need 0.5 byte entropy per character
  54. }
  55. if (type === 'base64') {
  56. return crypto.randomBytes(Math.ceil(length * 0.75)).toString('base64').slice(0, length); // Need 0.75 byte of entropy per character
  57. }
  58. if (type === 'url-safe') {
  59. return generateForCustomCharacters(length, urlSafeCharacters);
  60. }
  61. if (type === 'numeric') {
  62. return generateForCustomCharacters(length, numericCharacters);
  63. }
  64. if (type === 'distinguishable') {
  65. return generateForCustomCharacters(length, distinguishableCharacters);
  66. }
  67. if (characters.length === 0) {
  68. throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
  69. }
  70. if (characters.length > 0x10000) {
  71. throw new TypeError('Expected `characters` string length to be less or equal to 65536');
  72. }
  73. return generateForCustomCharacters(length, characters.split(''));
  74. };