Безопасный случайный токен в Node.js

274

В этом вопросе Эрику нужно создать безопасный случайный токен в Node.js. Есть метод, crypto.randomBytesкоторый генерирует случайный буфер. Однако кодировка base64 в узле не является url-безопасной, она включает в себя /и +вместо -и _. Поэтому самый простой способ сгенерировать такой токен, который я нашел, это

require('crypto').randomBytes(48, function(ex, buf) {
    token = buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-');
});

Есть ли более элегантный способ?

Юбер О.Г.
источник
Каков остальной код?
Lion789
3
Там больше ничего не нужно. Какой отдых вы хотели бы увидеть?
Хьюберт О.Г.
Неважно, я заставил это работать, был просто не уверен, как ты это бросил, но получил лучшее понимание концепции
Lion789
1
Бесстыдное самостоятельное подключение, я создал еще один пакет npm: tokgen . Вы можете указать разрешенные символы, используя синтаксис диапазона, подобный классам символов в регулярных выражениях ( 'a-zA-Z0-9_-').
Макс Трукса
1
Это может быть удобно для любого, кто хотел бы определенной длины строки. 3/4-е - для обработки базового преобразования. / * возвращает строку длины в кодировке base64 * / function randomString (length) {return crypto.randomBytes (length * 3/4) .toString ('base64'); } Хорошо работает для тех баз данных с этими ограничениями символов.
TheUnknownGeek

Ответы:

353

Попробуйте crypto.randomBytes () :

require('crypto').randomBytes(48, function(err, buffer) {
  var token = buffer.toString('hex');
});

Шестнадцатеричное кодирование работает в узле v0.6.x или новее.

thejh
источник
3
Это кажется лучше, спасибо! Хотя кодировка base64-url была бы хороша.
Юбер О.Г.
2
Спасибо за совет, но я думаю, что OP просто хотел уже стандартный раздел 4 RFC 3548 "Кодировка Base 64 с URL-адресом и безопасным алфавитом имени файла". ИМО, замена персонажей "достаточно элегантна".
natevw
8
Если вы ищете вышеперечисленное как однострочник, вы можете это сделатьnode -e "require('crypto').randomBytes(48, function(ex, buf) { console.log(buf.toString('hex')) });"
Дмитрий Минковский
24
И вы всегда можете сделать, buf.toString('base64')чтобы получить кодированный Base64 номер.
Дмитрий Минковский
1
См. Этот ответ ниже для кодировки base 64 с URL и именем файла Безопасный алфавит
Ив М.
233

Синхронный вариант в случае, если вы не являетесь экспертом JS, как я. Пришлось потратить некоторое время на то, как получить доступ к встроенной переменной функции

var token = crypto.randomBytes(64).toString('hex');
phoenix2010
источник
7
Также, если вы не хотите, чтобы все было вложено. Спасибо!
Михаил Озерянский
2
Хотя это определенно работает, обратите внимание, что в большинстве случаев вам понадобится асинхронная опция, продемонстрированная в ответе jj.
Triforcey
1
const generateToken = (): Promise<string> => new Promise(resolve => randomBytes(48, (err, buffer) => resolve(buffer.toString('hex'))));
Янтраб
1
@Triforcey, можете ли вы объяснить, почему вы обычно хотели бы использовать асинхронную опцию?
томас
2
@thomas Случайные данные могут занять некоторое время для расчета в зависимости от оборудования. В некоторых случаях, если на компьютере заканчиваются случайные данные, он просто возвращает что-то на свое место. Однако в других случаях возможно, что компьютер задержит возврат случайных данных (что на самом деле то, что вам нужно), что приведет к медленному вызову.
Triforcey
80

0. Использование сторонней библиотеки наноидов [NEW!]

Крошечный, безопасный, удобный для URL, уникальный генератор идентификаторов строк для JavaScript

https://github.com/ai/nanoid

import { nanoid } from "nanoid";
const id = nanoid(48);


1. Кодировка Base 64 с URL и именем файла Безопасный алфавит

Страница 7 из RCF 4648 описывает, как кодировать в базе 64 с безопасностью URL. Вы можете использовать существующую библиотеку, такую ​​как base64url, чтобы сделать эту работу.

Функция будет:

var crypto = require('crypto');
var base64url = require('base64url');

/** Sync */
function randomStringAsBase64Url(size) {
  return base64url(crypto.randomBytes(size));
}

Пример использования:

randomStringAsBase64Url(20);
// Returns 'AXSGpLVjne_f7w5Xg-fWdoBwbfs' which is 27 characters length.

Обратите внимание, что возвращаемая длина строки не будет соответствовать аргументу размера (размер! = Конечная длина).


2. Криптослучайные значения из ограниченного набора символов

Помните, что с этим решением сгенерированная случайная строка не распределена равномерно.

Вы также можете создать сильную случайную строку из ограниченного набора символов:

var crypto = require('crypto');

/** Sync */
function randomString(length, chars) {
  if (!chars) {
    throw new Error('Argument \'chars\' is undefined');
  }

  var charsLength = chars.length;
  if (charsLength > 256) {
    throw new Error('Argument \'chars\' should not have more than 256 characters'
      + ', otherwise unpredictability will be broken');
  }

  var randomBytes = crypto.randomBytes(length);
  var result = new Array(length);

  var cursor = 0;
  for (var i = 0; i < length; i++) {
    cursor += randomBytes[i];
    result[i] = chars[cursor % charsLength];
  }

  return result.join('');
}

/** Sync */
function randomAsciiString(length) {
  return randomString(length,
    'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
}

Пример использования:

randomAsciiString(20);
// Returns 'rmRptK5niTSey7NlDk5y' which is 20 characters length.

randomString(20, 'ABCDEFG');
// Returns 'CCBAAGDGBBEGBDBECDCE' which is 20 characters length.
Ив М.
источник
2
@Lexynux Solution 1 (кодировка Base 64 с URL-адресом и безопасным алфавитом имени файла), потому что это самое надежное решение с точки зрения безопасности. Это решение только кодирует ключ и не вмешивается в процесс производства ключа.
Ив М.
Спасибо за поддержку. Есть ли у вас рабочий пример, которым можно поделиться с сообществом? Будет приветствоваться?
alexventuraio
6
Помните, что сгенерированная случайная строка не распределена равномерно. Простой пример, демонстрирующий это, заключается в том, что для набора символов длиной 255 и длиной строки 1 вероятность появления первого символа в два раза выше.
Флориан Вендельборн
@ Dodekeract Да, вы говорите о решении 2 .. Вот почему решение 1 намного сильнее
Ив М.
Я добавил стороннюю библиотеку наноидов в своем ответе github.com/ai/nanoid
Ив М.
13

Современный правильный способ сделать это асинхронно с использованием стандартов ES 2016 async и await (начиная с узла 7) будет следующим:

const crypto = require('crypto');

function generateToken({ stringBase = 'base64', byteLength = 48 } = {}) {
  return new Promise((resolve, reject) => {
    crypto.randomBytes(byteLength, (err, buffer) => {
      if (err) {
        reject(err);
      } else {
        resolve(buffer.toString(stringBase));
      }
    });
  });
}

async function handler(req, res) {
   // default token length
   const newToken = await generateToken();
   console.log('newToken', newToken);

   // pass in parameters - adjust byte length
   const shortToken = await generateToken({byteLength: 20});
   console.log('newToken', shortToken);
}

Это работает из коробки в Node 7 без каких-либо преобразований Вавилона

real_ate
источник
Я обновил этот пример, чтобы включить более новый метод передачи именованных параметров, как описано здесь: 2ality.com/2011/11/keyword-parameters.html
real_ate
7

Случайный URL и имя файла безопасны (1 строка)

Crypto.randomBytes(48).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
Кедем
источник
Прекрасный ответ в своей простоте! Просто имейте в виду, что он может остановить цикл событий неопределенным образом (актуально только в том случае, если он используется часто, в несколько загруженной, чувствительной ко времени системе). В противном случае, сделайте то же самое, но с использованием асинхронной версии randomBytes. См nodejs.org/api/...
Алек Thilenius
6

Проверять, выписываться:

var crypto = require('crypto');
crypto.randomBytes(Math.ceil(length/2)).toString('hex').slice(0,length);
sudam
источник
Ницца! Абсолютно недооцененный раствор. Было бы здорово, если бы вы переименовали «length» в «требуемый длина» и инициировали его значением перед использованием :)
Florian Blum
Для тех , кто интересно, то ceilи sliceвызовов необходимы для желаемой длины, четные. Даже для длины они ничего не меняют.
Сет
6

С асинхронным ожиданием и обещанием .

const crypto = require('crypto')
const randomBytes = Util.promisify(crypto.randomBytes)
const plain = (await randomBytes(24)).toString('base64').replace(/\W/g, '')

Создает что-то похожее на VjocVHdFiz5vGHnlnwqJKN0NdeHcz8eM

Znarkus
источник
4

Посмотрите на real_atesES2016, это более правильно.

ECMAScript 2016 (ES7) путь

import crypto from 'crypto';

function spawnTokenBuf() {
    return function(callback) {
        crypto.randomBytes(48, callback);
    };
}

async function() {
    console.log((await spawnTokenBuf()).toString('base64'));
};

Генератор / Путь Дохода

var crypto = require('crypto');
var co = require('co');

function spawnTokenBuf() {
    return function(callback) {
        crypto.randomBytes(48, callback);
    };
}

co(function* () {
    console.log((yield spawnTokenBuf()).toString('base64'));
});
К - Токсичность в СО растет.
источник
@Jeffpowrs Действительно, Javascript обновляется :) Поиск обещаний и генераторов!
К - Токсичность в СО растет.
попробуй подождать, еще один обработчик обещаний ECMA7
Jain
Я думаю, что вы должны сделать ES 2016 первым примером в этом отношении, поскольку в большинстве случаев он движется к «правильному способу сделать это»
real_ate
Ниже я добавил свой собственный ответ, специфичный для Node (используя require вместо import). Была ли конкретная причина, по которой вы используете импорт? У тебя бегает бабель?
real_ate
@real_ate Да, я возвращался к использованию CommonJS, пока импорт официально не поддерживается.
К - Токсичность в СО растет.
2

Модуль npm anyid предоставляет гибкий API для генерации различных типов идентификаторов / кодов строк.

Чтобы сгенерировать случайную строку в A-Za-z0-9, используя 48 случайных байтов:

const id = anyid().encode('Aa0').bits(48 * 8).random().id();
// G4NtiI9OYbSgVl3EAkkoxHKyxBAWzcTI7aH13yIUNggIaNqPQoSS7SpcalIqX0qGZ

Чтобы сгенерировать только алфавит фиксированной длины, строку, заполненную случайными байтами:

const id = anyid().encode('Aa').length(20).random().id();
// qgQBBtDwGMuFHXeoVLpt

Внутренне он использует crypto.randomBytes()для генерации случайных.

aleung
источник
1

Вот асинхронная версия, взятая дословно сверху @Yves M. ответ

var crypto = require('crypto');

function createCryptoString(length, chars) { // returns a promise which renders a crypto string

    if (!chars) { // provide default dictionary of chars if not supplied

        chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    }

    return new Promise(function(resolve, reject) {

        var charsLength = chars.length;
        if (charsLength > 256) {
            reject('parm chars length greater than 256 characters' +
                        ' masks desired key unpredictability');
        }

        var randomBytes = crypto.randomBytes(length);

        var result = new Array(length);

        var cursor = 0;
        for (var i = 0; i < length; i++) {
            cursor += randomBytes[i];
            result[i] = chars[cursor % charsLength];
        }

        resolve(result.join(''));
    });
}

// --- now generate crypto string async using promise --- /

var wantStringThisLength = 64; // will generate 64 chars of crypto secure string

createCryptoString(wantStringThisLength)
.then(function(newCryptoString) {

    console.log(newCryptoString); // answer here

}).catch(function(err) {

    console.error(err);
});
Скотт Стенсленд
источник
1

Простая функция, которая дает вам токен, который безопасен для URL и имеет кодировку base64! Это комбинация из 2 ответов сверху.

const randomToken = () => {
    crypto.randomBytes(64).toString('base64').replace(/\//g,'_').replace(/\+/g,'-');
}
Томас
источник