Как мы можем создать довольно безопасный хэш пароля в PHP? - PullRequest
12 голосов
/ 14 июня 2011

Я читал о хешировании паролей, но все форумы, которые я читаю, полны сообщений от людей, обсуждающих теорию, лежащую в основе этого, которые я не совсем понимаю.

У меня есть старый (и предположительно чрезвычайно слабый) скрипт пароля, который выглядит так: $ hash = sha1 ($ pass1);

function createSalt()
{
$string = md5(uniqid(rand(), true));
return substr($string, 0, 3);
}

$salt = createSalt();
$hash = sha1($salt . $hash);

Если я правильно понимаю, чем длиннее соль, тем больше таблица, которую хакер должен сгенерировать, чтобы сломать хеш. Пожалуйста, поправьте меня, если я ошибаюсь.

Я хочу написать новый сценарий, который будет более безопасным, и я думаю, что что-то вроде этого будет хорошо:

function createSalt()
{
$string = hash('sha256', uniqid(rand(), true));
return $string;
}


$hash = hash('sha256', $password);
$salt = createSalt();
$secret_server_hash =     'ac1d81c5f99fdfc6758f21010be4c673878079fdc8f144394030687374f185ad';
$salt2 = hash('sha256', $salt);
$hash = $salt2 . $hash . $secret_server_hash;
$hash = hash('sha512', $hash );

Это более безопасно? Это имеет заметное количество накладных расходов?

Самое главное, есть ли какой-нибудь лучший способ убедиться, что пароли в моей базе данных не могут (реально) быть восстановлены с помощью криптоанализа, таким образом гарантируя, что единственный способ, которым безопасность будет скомпрометирована, - это моя собственная ошибка в кодировании?

EDIT:

Ознакомившись со всеми вашими ответами и проведя дальнейшие исследования, я решил продолжить и внедрить метод bcrypt для защиты моих паролей. При этом, ради любопытства, если бы я взял свой вышеприведенный код и включил в него цикл, скажем, на 100 000 итераций, достиг бы ли это чего-то похожего на прочность / безопасность bcrypt?

Ответы [ 3 ]

11 голосов
/ 14 июня 2011

Соли могут только помочь вам до сих пор.Если используемый вами алгоритм хэширования настолько быстр, что создание радужных таблиц практически не требует затрат, ваша безопасность по-прежнему подвергается риску.

Несколько указателей:

  • Do НЕ используйте одну соль для всех паролей.Используйте случайно сгенерированную соль для каждого пароля.
  • Do NOT перефразировать неизмененный хеш (проблема коллизий, см. Мой предыдущий ответ , вам нужен бесконечный ввод для хеширования).
  • Не НЕ пытаться создать собственный алгоритм хеширования или алгоритмы сопоставления миксов в сложную операцию.
  • Если застряли с поврежденными / незащищенными / быстрыми примитивами хеширования, используйте усиление ключа .Это увеличивает время, необходимое злоумышленнику для вычисления таблицы радуги.Пример:

function strong_hash($input, $salt = null, $algo = 'sha512', $rounds = 20000) {
  if($salt === null) {
    $salt = crypto_random_bytes(16);
  } else {
    $salt = pack('H*', substr($salt, 0, 32));
  }

  $hash = hash($algo, $salt . $input);

  for($i = 0; $i < $rounds; $i++) {
    // $input is appended to $hash in order to create
    // infinite input.
    $hash = hash($algo, $hash . $input);
  }

  // Return salt and hash. To verify, simply
  // passed stored hash as second parameter.
  return bin2hex($salt) . $hash;
}

function crypto_random_bytes($count) {
  static $randomState = null;

  $bytes = '';

  if(function_exists('openssl_random_pseudo_bytes') &&
      (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
    $bytes = openssl_random_pseudo_bytes($count);
  }

  if($bytes === '' && is_readable('/dev/urandom') &&
     ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
    $bytes = fread($hRand, $count);
    fclose($hRand);
  }

  if(strlen($bytes) < $count) {
    $bytes = '';

    if($randomState === null) {
      $randomState = microtime();
      if(function_exists('getmypid')) {
        $randomState .= getmypid();
      }
    }

    for($i = 0; $i < $count; $i += 16) {
      $randomState = md5(microtime() . $randomState);

      if (PHP_VERSION >= '5') {
        $bytes .= md5($randomState, true);
      } else {
        $bytes .= pack('H*', md5($randomState));
      }
    }

    $bytes = substr($bytes, 0, $count);
  }

  return $bytes;
}

Вместо того, чтобы развертывать свой собственный (по своей природе с недостатками) алгоритм хеширования / соли, почему бы не использовать тот, который был разработан специалистами по безопасности?

Использовать bcrypt .Он был разработан именно для этого.Это медлительность и несколько раундов гарантирует, что злоумышленник должен развернуть огромные средства и оборудование, чтобы иметь возможность взломать ваши пароли.Добавьте к этому соли для каждого пароля (bcrypt REQUIRES солей), и вы можете быть уверены, что атака практически невозможна без смехотворного количества средств или оборудования.

Portable PHP Hashing Framework внепереносимый режим позволяет легко генерировать хэши с помощью bcrypt.

Вы также можете использовать функцию crypt() для генерации хеш-кодов bcrypt из входных строк.Если вы идете по этому пути, убедитесь, что вы генерируете одну соль для хеша.

Этот класс может автоматически генерировать соли и проверять существующие хэши по вводу.

class Bcrypt {
  private $rounds;
  public function __construct($rounds = 12) {
    if(CRYPT_BLOWFISH != 1) {
      throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
    }

    $this->rounds = $rounds;
  }

  public function hash($input) {
    $hash = crypt($input, $this->getSalt());

    if(strlen($hash) > 13)
      return $hash;

    return false;
  }

  public function verify($input, $existingHash) {
    $hash = crypt($input, $existingHash);

    return $hash === $existingHash;
  }

  private function getSalt() {
    $salt = sprintf('$2a$%02d$', $this->rounds);

    $bytes = $this->getRandomBytes(16);

    $salt .= $this->encodeBytes($bytes);

    return $salt;
  }

  private $randomState;
  private function getRandomBytes($count) {
    $bytes = '';

    if(function_exists('openssl_random_pseudo_bytes') &&
        (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
      $bytes = openssl_random_pseudo_bytes($count);
    }

    if($bytes === '' && is_readable('/dev/urandom') &&
       ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
      $bytes = fread($hRand, $count);
      fclose($hRand);
    }

    if(strlen($bytes) < $count) {
      $bytes = '';

      if($this->randomState === null) {
        $this->randomState = microtime();
        if(function_exists('getmypid')) {
          $this->randomState .= getmypid();
        }
      }

      for($i = 0; $i < $count; $i += 16) {
        $this->randomState = md5(microtime() . $this->randomState);

        if (PHP_VERSION >= '5') {
          $bytes .= md5($this->randomState, true);
        } else {
          $bytes .= pack('H*', md5($this->randomState));
        }
      }

      $bytes = substr($bytes, 0, $count);
    }

    return $bytes;
  }

  private function encodeBytes($input) {
    return strtr(rtrim(base64_encode($input), '='), '+', '.');
  }
}

Вы можете использовать этот кодкак таковой:

$bcrypt = new Bcrypt(15);

$hash = $bcrypt->hash('password');
$isGood = $bcrypt->verify('password', $hash);
4 голосов
/ 14 июня 2011

о солевых ценностях

Если я правильно понимаю, чем дольше соль, чем больше стол хакер должен генерировать, чтобы сломай хеш Пожалуйста, поправьте меня, если я я неправ.

Да, это правильно. Хотя, если кто-то пытается сломать хеш только одного пользователя, значения соли бесполезны. Соли полезны для предотвращения (замедления) атакующих словарных атак на хэш-значения всех ваших пользователей.

Позвольте мне объяснить это на примере. Предположим, у вас есть 3 пользователя в вашей системе, и вы не используете солт-значение, поэтому ваша база данных будет выглядеть так:

user1: hash1
user2: hash2
user3: hash3

Теперь давайте предположим, что злоумышленник добивается получения копии вашей базы данных. Теперь он мог выполнить атаку по словарю, выполнив:

h = hash(possible_password)
h == hash1?
h == hash2?
h == hash3?

Итак, он мог проверить, есть ли у одного из 3 пользователей пароль possible_password, вызывая хеш-функцию только один раз.

Нет, предположим, что вы сохранили хеш-значения, которые были объединены с солт-значениями, в вашей базе данных следующим образом:

user1: hash1_salted, salt1
user2: hash2_salted, salt2
user3: hash3_salted, salt3

И снова злоумышленник копирует вашу базу данных. Но теперь, чтобы увидеть, используется ли possible_password одним из 3 пользователей, он должен выполнить следующие проверки:

hash(possible_password + salt1) == hash1_salted?
hash(possible_password + salt2) == hash2_salted?
hash(possible_password + salt3) == hash3_salted?

Как вы видите, в этом случае злоумышленник замедляется в 3 раза (количество пользователей в вашей системе), поскольку он должен хэшировать 3 разные строки. Это общая идея, лежащая в основе значений соли, вы можете прочитать больше в wikipedia .

Но в вашем случае соль слишком большая. То, что вы хотите предотвратить, - это 2 разных пользовательских хеша, которые имеют одинаковое значение соли Так, например, соль длиной 2 бита, вероятно, не будет хорошей идеей (для более чем 4 пользователей будет уверен, что 2 имеют одинаковое значение соли). В любом случае, солт-значения более чем 48 бит будет достаточно.

Кроме того, здесь нет смысла хешировать соль $salt2 = hash('sha256', $salt);, это может как-то замедлить процесс, но в целом добавление большей сложности в вашу систему считается плохим при работе с безопасностью.

Общее

Наконец, никогда не стоит иметь конкретные значения в своем коде при работе с безопасностью, например $secret_server_hash, таких постоянных значений всегда следует избегать.

Лучше использовать SHA-2 вместо MD5, потому что в последние годы в MD5 были обнаружены некоторые уязвимости безопасности (хотя они пока не очень практичны).

Так что я бы сделал что-то вроде этого:

function createSalt()
{
  $string = hash('sha256', uniqid(rand(), true));
  return susbstr($string, 0, 8); // 8 characters is more than enough
}

$salt = createSalt();
$hash = hash('sha256', $hash . $password );

А затем сохраните $hash в своей базе данных.

Во всяком случае, как уже отмечали некоторые пользователи. Вместо того, чтобы создавать свои собственные функции безопасности (что является хорошим способом узнать о безопасности), вы должны лучше использовать известные библиотеки, которые тестируются большим количеством людей и, следовательно, возможно, более безопасны. В вашем случае вы должны взглянуть на crypt, который делает то, что вам нужно.

2 голосов
/ 14 июня 2011

Нет необходимости пытаться реализовать собственную серию хэшей. Вот простой класс, который реализует bcrypt:

class Password
{
    # return a hashed version of the plain text password.
    public static function hash($plain_text, $cost_factor = 10)
    {
        if ($cost_factor < 4 || $cost_factor > 31)
            throw new Exception('Invalid cost factor');

        $cost_factor = sprintf('%02d', $cost_factor);           

        $salt = '';
        for ($i = 0; $i < 8; ++$i)
          $salt .= pack('S1', mt_rand(0, 0xffff));

        $salt = strtr(rtrim(base64_encode($salt), '='), '+', '.');

        return crypt($plain_text, '$2a$'.$cost_factor.'$'.$salt);
    }

    # validate that a hashed password is the same as the plain text version
    public static function validate($plain, $hash)
    {
        return crypt($plain, $hash) == $hash;
    }
}

Использование:

$hash = Password::hash('foo');
if (Password::validate('foo', $hash)) echo "valid";

Преимущество bcrypt в том, что вы можете сделать его вычислительно дорогостоящим для хэширования пароля (через $cost_factor). Это делает нецелесообразным пытаться восстановить пароли всей базы данных с помощью грубой силы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...