php - 双向加密 : I need to store passwords that can be retrieved

标签 php security encryption passwords

我正在创建一个将存储密码的应用程序,用户可以检索和查看密码。密码用于硬件设备,因此检查哈希值是不可能的。

我需要知道的是:

  • 如何在 PHP 中加密和解密密码?
  • 加密密码的最安全算法是什么?
  • 我在哪里存储私钥?
  • 不是存储私钥,而是要求用户在需要解密密码时输入私钥是一个好主意吗? (此应用程序的用户可以信任)
  • 密码可以通过哪些方式被窃取和解密?我需要注意什么?
  • 最佳答案

    就个人而言,我会像其他人发布的一样使用 mcrypt。但是还有很多需要注意的......

  • 如何在 PHP 中加密和解密密码?

    请参阅下面的强大类,为您处理一切:
  • 加密密码的最安全算法是什么?

    最安全?其中任何一个。如果您要加密,最安全的方法是防止信息泄露漏洞(XSS、远程包含等)。如果它出去,攻击者最终可以破解加密(没有 key ,没有加密是 100% 不可逆的 - 正如@NullUserException 指出的那样,这并不完全正确。有一些加密方案是不可能破解的,例如 OneTimePad ) .
  • 我在哪里存储私钥?

    我会做的是使用 3 个键。一个是用户提供的,一个是特定于应用程序的,另一个是特定于用户的(如盐)。应用程序特定的 key 可以存储在任何地方(在 web 根目录之外的配置文件中,在环境变量中,等等)。特定于用户的密码将存储在数据库中加密密码旁边的一列中。用户提供的一个不会被存储。然后,你会做这样的事情:
    $key = $userKey . $serverKey . $userSuppliedKey;
    

    这样做的好处是,可以在不破坏数据的情况下破坏任何 2 个 key 。如果存在 SQL 注入(inject)攻击,他们可以获得 $userKey ,但不能获得其他 2。如果存在本地服务器漏洞,他们可以获得 $userKey$serverKey ,但不能获得第三个 $userSuppliedKey 。如果他们用 Spanner 殴打用户,他们可以获得 $userSuppliedKey ,但不能获得其他 2 个(但话又说回来,如果用户被 Spanner 殴打,无论如何你都为时已晚)。
  • 不是存储私钥,而是要求用户在需要解密密码时输入私钥是个好主意吗? (此应用程序的用户可以信任)

    绝对。事实上,这是我唯一的办法。否则,您需要以持久存储格式(共享内存,如 APC 或 memcached,或 session 文件)存储未加密版本。这会让自己面临额外的妥协。切勿将密码的未加密版本存储在局部变量以外的任何地方。
  • 密码可以通过哪些方式被窃取和解密?我需要注意什么?

    任何形式的系统入侵都会让他们查看加密数据。如果他们可以注入(inject)代码或访问您的文件系统,他们就可以查看解密的数据(因为他们可以编辑解密数据的文件)。任何形式的重放或中间人攻击也将使他们能够完全访问所涉及的 key 。嗅探原始 HTTP 流量也会为他们提供 key 。

    对所有流量使用 SSL。并确保服务器上没有任何类型的漏洞(CSRF、XSS、SQL 注入(inject)、权限提升、远程代码执行等)。

  • 编辑: 这是一个强加密方法的 PHP 类实现:
    /**
     * A class to handle secure encryption and decryption of arbitrary data
     *
     * Note that this is not just straight encryption.  It also has a few other
     *  features in it to make the encrypted data far more secure.  Note that any
     *  other implementations used to decrypt data will have to do the same exact
     *  operations.  
     *
     * Security Benefits:
     *
     * - Uses Key stretching
     * - Hides the Initialization Vector
     * - Does HMAC verification of source data
     *
     */
    class Encryption {
    
        /**
         * @var string $cipher The mcrypt cipher to use for this instance
         */
        protected $cipher = '';
    
        /**
         * @var int $mode The mcrypt cipher mode to use
         */
        protected $mode = '';
    
        /**
         * @var int $rounds The number of rounds to feed into PBKDF2 for key generation
         */
        protected $rounds = 100;
    
        /**
         * Constructor!
         *
         * @param string $cipher The MCRYPT_* cypher to use for this instance
         * @param int    $mode   The MCRYPT_MODE_* mode to use for this instance
         * @param int    $rounds The number of PBKDF2 rounds to do on the key
         */
        public function __construct($cipher, $mode, $rounds = 100) {
            $this->cipher = $cipher;
            $this->mode = $mode;
            $this->rounds = (int) $rounds;
        }
    
        /**
         * Decrypt the data with the provided key
         *
         * @param string $data The encrypted datat to decrypt
         * @param string $key  The key to use for decryption
         * 
         * @returns string|false The returned string if decryption is successful
         *                           false if it is not
         */
        public function decrypt($data, $key) {
            $salt = substr($data, 0, 128);
            $enc = substr($data, 128, -64);
            $mac = substr($data, -64);
    
            list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
    
            if (!hash_equals(hash_hmac('sha512', $enc, $macKey, true), $mac)) {
                 return false;
            }
    
            $dec = mcrypt_decrypt($this->cipher, $cipherKey, $enc, $this->mode, $iv);
    
            $data = $this->unpad($dec);
    
            return $data;
        }
    
        /**
         * Encrypt the supplied data using the supplied key
         * 
         * @param string $data The data to encrypt
         * @param string $key  The key to encrypt with
         *
         * @returns string The encrypted data
         */
        public function encrypt($data, $key) {
            $salt = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
            list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
    
            $data = $this->pad($data);
    
            $enc = mcrypt_encrypt($this->cipher, $cipherKey, $data, $this->mode, $iv);
    
            $mac = hash_hmac('sha512', $enc, $macKey, true);
            return $salt . $enc . $mac;
        }
    
        /**
         * Generates a set of keys given a random salt and a master key
         *
         * @param string $salt A random string to change the keys each encryption
         * @param string $key  The supplied key to encrypt with
         *
         * @returns array An array of keys (a cipher key, a mac key, and a IV)
         */
        protected function getKeys($salt, $key) {
            $ivSize = mcrypt_get_iv_size($this->cipher, $this->mode);
            $keySize = mcrypt_get_key_size($this->cipher, $this->mode);
            $length = 2 * $keySize + $ivSize;
    
            $key = $this->pbkdf2('sha512', $key, $salt, $this->rounds, $length);
    
            $cipherKey = substr($key, 0, $keySize);
            $macKey = substr($key, $keySize, $keySize);
            $iv = substr($key, 2 * $keySize);
            return array($cipherKey, $macKey, $iv);
        }
    
        /**
         * Stretch the key using the PBKDF2 algorithm
         *
         * @see http://en.wikipedia.org/wiki/PBKDF2
         *
         * @param string $algo   The algorithm to use
         * @param string $key    The key to stretch
         * @param string $salt   A random salt
         * @param int    $rounds The number of rounds to derive
         * @param int    $length The length of the output key
         *
         * @returns string The derived key.
         */
        protected function pbkdf2($algo, $key, $salt, $rounds, $length) {
            $size   = strlen(hash($algo, '', true));
            $len    = ceil($length / $size);
            $result = '';
            for ($i = 1; $i <= $len; $i++) {
                $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true);
                $res = $tmp;
                for ($j = 1; $j < $rounds; $j++) {
                     $tmp  = hash_hmac($algo, $tmp, $key, true);
                     $res ^= $tmp;
                }
                $result .= $res;
            }
            return substr($result, 0, $length);
        }
    
        protected function pad($data) {
            $length = mcrypt_get_block_size($this->cipher, $this->mode);
            $padAmount = $length - strlen($data) % $length;
            if ($padAmount == 0) {
                $padAmount = $length;
            }
            return $data . str_repeat(chr($padAmount), $padAmount);
        }
    
        protected function unpad($data) {
            $length = mcrypt_get_block_size($this->cipher, $this->mode);
            $last = ord($data[strlen($data) - 1]);
            if ($last > $length) return false;
            if (substr($data, -1 * $last) !== str_repeat(chr($last), $last)) {
                return false;
            }
            return substr($data, 0, -1 * $last);
        }
    }
    

    请注意,我使用的是 PHP 5.6 中添加的函数: hash_equals 。如果您的版本低于 5.6,则可以使用此替代函数,该函数使用 timing-safe comparison 实现 double HMAC verification 函数:
    function hash_equals($a, $b) {
        $key = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
        return hash_hmac('sha512', $a, $key) === hash_hmac('sha512', $b, $key);
    }
    

    用法:
    $e = new Encryption(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC);
    $encryptedData = $e->encrypt($data, $key);
    

    然后,解密:
    $e2 = new Encryption(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC);
    $data = $e2->decrypt($encryptedData, $key);
    

    请注意,我第二次使用 $e2 来向您展示不同的实例仍然可以正确解密数据。

    现在,它是如何工作的/为什么在另一个解决方案上使用它:

  • 不直接使用 key 。相反, key 由标准 PBKDF2 派生扩展。
  • 用于加密的 key 对于每个加密的文本块都是唯一的。因此,提供的 key 成为“主 key ”。因此,此类为密码和身份验证 key 提供 key 轮换。
  • 重要说明 $rounds 参数配置为具有足够强度的真随 secret 钥(至少 128 位加密安全随机)。如果您要使用密码或非随 secret 钥(或随机性低于 128 位 CS 随机数),则 必须 增加此参数。我建议密码至少为 10000(您负担得起的越多越好,但它会增加运行时)...
  • 数据完整性
  • 更新版本使用ENCRYPT-THEN-MAC,这是一种更好的确保加密数据真实性的方法。
  • 加密:
  • 它使用 mcrypt 来实际执行加密。我建议使用 MCRYPT_BLOWFISHMCRYPT_RIJNDAEL_128 密码和 MCRYPT_MODE_CBC 作为模式。它足够强大,而且仍然相当快(在我的机器上加密和解密周期大约需要 1/2 秒)。

  • 现在,至于第一个列表中的第 3 点,它会给你一个这样的函数:
    function makeKey($userKey, $serverKey, $userSuppliedKey) {
        $key = hash_hmac('sha512', $userKey, $serverKey);
        $key = hash_hmac('sha512', $key, $userSuppliedKey);
        return $key;
    }
    

    您可以在 makeKey() 函数中拉伸(stretch)它,但由于它稍后会被拉伸(stretch),因此这样做并没有太大意义。

    至于存储大小,它取决于纯文本。 Blowfish 使用 8 字节的块大小,因此您将拥有:
  • 16 字节用于盐
  • hmac
  • 64 字节
  • 数据长度
  • 填充使数据长度 % 8 == 0

  • 因此,对于 16 个字符的数据源,将有 16 个字符的数据需要加密。因此,这意味着由于填充,实际加密数据大小为 16 字节。然后为 salt 添加 16 个字节,为 hmac 添加 64 个字节,总存储大小为 96 个字节。所以最多有 80 个字符的开销,最坏的有 87 个字符的开销......

    我希望这有帮助...

    注意: 12/11/12:我刚刚用更好的加密方法更新了这个类,使用更好的派生 key ,并修复了 MAC 生成......

    关于php - 双向加密 : I need to store passwords that can be retrieved,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5089841/

    相关文章:

    php - 分页可以只用一个 sql 查询吗?

    Android AES 基于密码的加密,对每条消息使用一个 key 和随机 IV

    java - keystore 类型 : which one to use?

    c# - 将数据放在 cookie 中安全吗?

    php - 在 php 中检测到 'flush tables with read lock'

    php - Laravel 查询生成器中的 MySQL YEAR() 等效项

    php - $_Session "complication"登录和注销 php

    android - 验证移动设备

    javascript - 客户端 Web 应用程序中的 GPG (PGP) 解密

    java - AES CBC PKCS5Padding Java 到 Ruby