Подтвердить что ты не робот

Как мы можем создать довольно безопасный хэш пароля в PHP?

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

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

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

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

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

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

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?

4b9b3361

Ответ 1

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

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

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

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 salt), и вы можете быть уверены, что атака практически невозможна без какой-либо смехотворной суммы средств или оборудования.

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);

Ответ 2

О значениях соли

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

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

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

user1: hash1
user2: hash2
user3: hash3

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

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

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

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

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

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

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

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

Но в вашем случае соль слишком велика. То, что вы хотите предотвратить, - это два разных пользовательских хэша, имеющих одинаковое значение соли. Так, например, соль длиной 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, который делает то, что вам нужно.

Ответ 3

На самом деле не нужно пытаться реализовать свою собственную серию хэшей. Вот простой класс, реализующий 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). Это делает нецелесообразным попытку восстановить пароли всей базы данных с помощью грубой силы.