关于PHP:如何保护API密钥和第三方站点凭据(LAMP)?

How do you protect API keys and 3rd party site credentials (LAMP)?

我正在创建一个站点,它将使用其他第三方站点的ID、密码和API密钥,以便服务器相应地访问信息。在本次对话中,假设是针对支付网关,这意味着存储在数据库中的信息暴露可能意味着恶意用户可以从其凭证被泄露的帐户中提取现金。

不幸的是,这不像密码/哈希情况,因为用户不会每次都输入凭据-他们只输入一次,然后将其保存在服务器上,以供应用程序将来使用。

我能想到的唯一合理的方法(这将是一个mysql/php应用程序)是通过php应用程序中的硬编码"密码"加密凭证。这里唯一的好处是,如果恶意用户/黑客获得了对数据库的访问权,而不是PHP代码,那么他们仍然没有任何东西。这就是说,这对我来说似乎毫无意义,因为我认为我们可以合理地假设一个黑客只要得到一个或另一个就可以得到所有东西-对吗?

如果社区决定采用一些好的解决方案,那么最好收集其他来源的示例/教程/更深入的信息,以便在将来为每个人实现这一点。

我很惊讶我没有看到这个问题在堆栈上有任何好的答案。我确实找到了这一个,但在我的例子中,这并不真正适用:我应该如何在道德上接近用户密码存储,以便以后进行纯文本检索?

谢谢大家。


基于我在问题、答案和评论中看到的内容,我建议利用OpenSSL。这是假设您的站点需要定期访问此信息(这意味着它可以被调度)。正如你所说:好的。

The server would need this information to send payments for all sorts of situations. It does not require the"owner" of said keys to log in, in fact the owner might never care to see them ever again once they provide them the first time.

Ok.

这是从这个注释中得出的,并且假设访问您想要存储的数据可以放在cron作业中。进一步假设您的服务器上有SSL(HTTPS),因为您将处理机密的用户信息,并且有OpenSSLmcrypt模块可用。此外,下面的内容对于"如何"实现这一目标也相当笼统,但实际上并不是根据您的情况来做这一工作的细节。还应该注意到,这个"操作方法"是通用的,在实现它之前,您应该做更多的研究。这么说,我们开始吧。好的。

首先,我们来谈谈OpenSSL提供了什么。openssl为我们提供了一种公钥加密技术:使用公钥加密数据的能力(如果受到威胁,不会危及用它加密的数据的安全性)。其次,它提供了一种使用"私钥"访问该信息的方法。由于我们不关心创建证书(我们只需要加密密钥),可以通过简单的函数(您只需使用一次)获得这些密钥:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function makeKeyPair()
{
    //Define variables that will be used, set to ''
    $private = '';
    $public = '';
    //Generate the resource for the keys
    $resource = openssl_pkey_new();

    //get the private key
    openssl_pkey_export($resource, $private);

    //get the public key
    $public = openssl_pkey_get_details($resource);
    $public = $public["key"];
    $ret = array('privateKey' => $private, 'publicKey' => $public);
    return $ret;
}

现在,您有了一个公钥和私钥。保护私钥,使其远离服务器,并使其远离数据库。将它存储在另一台服务器上,一台可以运行cron作业的计算机,等等。除非你可以要求管理员在每次需要处理付款时在场,并用aes加密或类似的方法加密私钥。但是,公钥将被硬编码到您的应用程序中,并且将在用户每次输入要存储的信息时使用。好的。

接下来,您需要确定您计划如何验证解密数据(这样您就不会使用无效的请求发布到支付API了)。我假设需要存储多个字段,因为我们只想加密一次,所以它将位于可序列化的PHP数组中。根据需要存储的数据量,我们将l或者可以直接加密,或者生成一个用公钥加密的密码,然后使用该随机密码对数据本身进行加密。我将在解释中走这条路。为了走这条路,我们将使用AES加密,并且需要有一个方便的加密和解密功能——以及一种随机为数据生成合适的一次性PAD的方法。我将提供我使用的密码生成器,尽管我将它移植到了前一段时间编写的代码中,但它将起到作用,或者您可以编写更好的密码生成器。^ ^好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function generatePassword() {
    //create a random password here
    $chars = array( 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E', 'f', 'F', 'g', 'G', 'h', 'H', 'i', 'I', 'j', 'J',  'k', 'K', 'l', 'L', 'm', 'M', 'n', 'N', 'o', 'O', 'p', 'P', 'q', 'Q', 'r', 'R', 's', 'S', 't', 'T',  'u', 'U', 'v', 'V', 'w', 'W', 'x', 'X', 'y', 'Y', 'z', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '?', '<', '>', '.', ',', ';', '-', '@', '!', '#', '$', '%', '^', '&', '*', '(', ')');

    $max_chars = count($chars) - 1;
    srand( (double) microtime()*1000000);

    $rand_str = '';
    for($i = 0; $i < 30; $i++)
    {
            $rand_str .= $chars[rand(0, $max_chars)];
    }
    return $rand_str;

}

这个特殊的函数将生成30位数字,这提供了相当好的熵,但是您可以根据需要修改它。接下来,执行aes加密的函数:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Encrypt AES
 *
 * Will Encrypt data with a password in AES compliant encryption.  It
 * adds built in verification of the data so that the {@link this::decryptAES}
 * can verify that the decrypted data is correct.
 *
 * @param String $data This can either be string or binary input from a file
 * @param String $pass The Password to use while encrypting the data
 * @return String The encrypted data in concatenated base64 form.
 */

public function encryptAES($data, $pass) {
    //First, let's change the pass into a 256bit key value so we get 256bit encryption
    $pass = hash('SHA256', $pass, true);
    //Randomness is good since the Initialization Vector(IV) will need it
    srand();
    //Create the IV (CBC mode is the most secure we get)
    $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
    //Create a base64 version of the IV and remove the padding
    $base64IV = rtrim(base64_encode($iv), '=');
    //Create our integrity check hash
    $dataHash = md5($data);
    //Encrypt the data with AES 128 bit (include the hash at the end of the data for the integrity check later)
    $rawEnc = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $pass, $data . $dataHash, MCRYPT_MODE_CBC, $iv);
    //Transfer the encrypted data from binary form using base64
    $baseEnc = base64_encode($rawEnc);
    //attach the IV to the front of the encrypted data (concatenated IV)
    $ret = $base64IV . $baseEnc;
    return $ret;
}

(我最初将这些函数编写为类的一部分,并建议您将它们实现到自己的类中。)此外,使用此函数还可以使用创建的一次性pad,但是,如果与其他应用程序的用户特定密码一起使用,则您肯定需要在其中添加一些盐来添加密码。接下来,要解密并验证解密的数据是否正确:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
 * Decrypt AES
 *
 * Decrypts data previously encrypted WITH THIS CLASS, and checks the
 * integrity of that data before returning it to the programmer.
 *
 * @param String $data The encrypted data we will work with
 * @param String $pass The password used for decryption
 * @return String|Boolean False if the integrity check doesn't pass, or the raw decrypted data.
 */

public function decryptAES($data, $pass){
    //We used a 256bit key to encrypt, recreate the key now
    $pass = hash('SHA256', $this->salt . $pass, true);
    //We should have a concatenated data, IV in the front - get it now
    //NOTE the IV base64 should ALWAYS be 22 characters in length.
    $base64IV = substr($data, 0, 22) .'=='; //add padding in case PHP changes at some point to require it
    //change the IV back to binary form
    $iv = base64_decode($base64IV);
    //Remove the IV from the data
    $data = substr($data, 22);
    //now convert the data back to binary form
    $data = base64_decode($data);
    //Now we can decrypt the data
    $decData = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $pass, $data, MCRYPT_MODE_CBC, $iv);
    //Now we trim off the padding at the end that php added
    $decData = rtrim($decData,"\0");
    //Get the md5 hash we stored at the end
    $dataHash = substr($decData, -32);
    //Remove the hash from the data
    $decData = substr($decData, 0, -32);
    //Integrity check, return false if it doesn't pass
    if($dataHash != md5($decData)) {
        return false;
    } else {
        //Passed the integrity check, give use their data
        return $decData;
    }
}

查看这两个函数,阅读注释等。找出它们的作用和工作方式,这样您就不会错误地实现它们。现在,对用户数据进行加密。我们将用公钥加密它,下面的函数假定到目前为止(以及将来)的每个函数都在同一类中。我将同时提供openssl加密/解密功能,因为稍后我们将需要第二个。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * Public Encryption
 *
 * Will encrypt data based on the public key
 *
 * @param String $data The data to encrypt
 * @param String $publicKey The public key to use
 * @return String The Encrypted data in base64 coding
 */

public function publicEncrypt($data, $publicKey) {
    //Set up the variable to get the encrypted data
    $encData = '';
    openssl_public_encrypt($data, $encData, $publicKey);
    //base64 code the encrypted data
    $encData = base64_encode($encData);
    //return it
    return $encData;
}

/**
 * Private Decryption
 *
 * Decrypt data that was encrypted with the assigned private
 * key's public key match. (You can't decrypt something with
 * a private key if it doesn't match the public key used.)
 *
 * @param String $data The data to decrypt (in base64 format)
 * @param String $privateKey The private key to decrypt with.
 * @return String The raw decoded data
 */

public function privateDecrypt($data, $privateKey) {
    //Set up the variable to catch the decoded date
    $decData = '';
    //Remove the base64 encoding on the inputted data
    $data = base64_decode($data);
    //decrypt it
    openssl_private_decrypt($data, $decData, $privateKey);
    //return the decrypted data
    return $decData;
}

其中的$data始终是一次性的pad,而不是用户信息。接下来,将公钥加密和一次性PAD的AES结合起来进行加密和解密的功能。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
 * Secure Send
 *
 * OpenSSL and 'public-key' schemes are good for sending
 * encrypted messages to someone that can then use their
 * private key to decrypt it.  However, for large amounts
 * of data, this method is incredibly slow (and limited).
 * This function will take the public key to encrypt the data
 * to, and using that key will encrypt a one-time-use randomly
 * generated password.  That one-time password will be
 * used to encrypt the data that is provided.  So the data
 * will be encrypted with a one-time password that only
 * the owner of the private key will be able to uncover.
 * This method will return a base64encoded serialized array
 * so that it can easily be stored, and all parts are there
 * without modification for the receive function
 *
 * @param String $data The data to encrypt
 * @param String $publicKey The public key to use
 * @return String serialized array of 'password' and 'data'
 */

public function secureSend($data, $publicKey)
{
    //First, we'll create a 30digit random password
    $pass = $this->generatePassword();
    //Now, we will encrypt in AES the data
    $encData = $this->encryptAES($data, $pass);
    //Now we will encrypt the password with the public key
    $pass = $this->publicEncrypt($pass, $publicKey);
    //set up the return array
    $ret = array('password' => $pass, 'data' => $encData);
    //serialize the array and then base64 encode it
    $ret = serialize($ret);
    $ret = base64_encode($ret);
    //send it on its way
    return $ret;
}

/**
 * Secure Receive
 *
 * This is the complement of {@link this::secureSend}.
 * Pass the data that was returned from secureSend, and it
 * will dismantle it, and then decrypt it based on the
 * private key provided.
 *
 * @param String $data the base64 serialized array
 * @param String $privateKey The private key to use
 * @return String the decoded data.
 */

public function secureReceive($data, $privateKey) {
    //Let's decode the base64 data
    $data = base64_decode($data);
    //Now let's put it into array format
    $data = unserialize($data);
    //assign variables for the different parts
    $pass = $data['password'];
    $data = $data['data'];
    //Now we'll get the AES password by decrypting via OpenSSL
    $pass = $this->privateDecrypt($pass, $privateKey);
    //and now decrypt the data with the password we found
    $data = $this->decryptAES($data, $pass);
    //return the data
    return $data;
}

我保留了完整的注释,以帮助理解这些函数。现在我们开始讨论有趣的部分,实际上是使用用户数据。send方法中的$data是序列化数组中的用户数据。记住,对于$publicKey是硬编码的send方法,您可以在类中作为变量存储并以这种方式访问它,以便更少的变量传递给它,或者让它从其他地方输入以每次发送给该方法。加密数据的示例用法:好的。

1
2
3
4
5
6
7
8
9
10
$myCrypt = new encryptClass();
$userData = array(
    'id' => $_POST['id'],
    'password' => $_POST['pass'],
    'api' => $_POST['api_key']
);
$publicKey ="the public key from earlier";
$encData = $myCrypt->secureSend(serialize($userData), $publicKey));
//Now store the $encData in the DB with a way to associate with the user
//it is base64 encoded, so it is safe for DB input.

现在,这是最简单的部分,下一部分是能够使用这些数据。为此,您需要在服务器上有一个接受$_POST['privKey']的页面,然后以您的站点所需的方式循环访问用户等,获取$encData。从中解密的示例用法:好的。

1
2
3
4
5
6
$myCrypt = new encryptClass();
$encData ="pulled from DB";
$privKey = $_POST['privKey'];
$data = unserialize($myCrypt->secureReceive($encData, $privKey));
//$data will now contain the original array of data, or false if
//it failed to decrypt it.  Now do what you need with it.

接下来,使用特定的理论访问带有私钥的安全页面。在另一台服务器上,您将有一个cron作业,它运行一个php脚本,特别是不在包含私钥的public_html中的脚本,然后使用curl将私钥发布到正在查找它的页面。(确保您正在调用以https开头的地址)好的。

我希望这有助于回答如何在应用程序中安全地存储用户信息,而这些信息不会因访问代码或数据库而受到损害。好的。好啊。


有几种可能的解决方案,如果你想的开箱即用…而且可以开箱即用,这不一定是你的情况,但我无论如何都会建议他们。

  • 从第三方网站获取权限有限的帐户。在您的支付网关示例中,允许您授权和结算付款,但不调用TransferToBankAccount($accountNumber)API的帐户。

    • 同样,要求提供者设置限制。$accountNumber必须是您提供的几个帐户中的一个,或者可能只是必须与您所在的国家/地区相同*。
  • 一些安全系统依赖硬件令牌来提供身份验证。(我主要考虑的是密钥创建和加密狗的签名。)我不确定这在您的情况下会如何工作。假设您有一个加密狗向其请求身份验证,它只在某些情况下回复。如果它所能做的就是给出(或不给出)一个用户名/密码,那么这是很困难的。(比如,在请求上签名,它可以检查请求参数)。如果您的第三方接受这样的请求,那么您可以使用一个SSL客户机证书来完成这项工作,在这种情况下,向第三方发出的请求需要用户名/密码/和客户机签名。

  • _1和_2的组合。设置另一个服务器作为中间层。这台服务器将实现基本的业务逻辑,我建议您的第三方可以在1中完成。它公开了一个API和检查,以确保请求"有效"(付款可以结算,但只有传出转账到您的帐户等),然后获取授权详细信息并直接提出请求。API可以包含setAuthDetails,但不能包含getAuthDetails。这里的好处是攻击者还有一件事要做。而且,服务器越简单,就越容易硬化。(不需要运行smtp、ftp,以及您的主服务器可能有问题的php堆栈…。只有几个PHP脚本、端口443、ssh和一个,例如sqlite实例。保持httpd和php的最新应该更容易,因为对兼容性问题的关注更少。)

  • 假设您将受到影响,并监控/审计潜在影响。让另一个服务器在某个地方(是)具有身份验证详细信息以登录并检查身份验证日志(或者,理想情况下,是只读权限)。让它每分钟检查一次,并检查是否有未经授权的登录和/或事务,然后做一些剧烈的事情(可能将密码更改为随机的,然后传呼给您)。

  • *无论我们是否真的在讨论支付网关,任何第三方如果你担心被黑客攻击,也应该关心他们自己的安全,包括你(或其他客户)是否被黑客攻击。他们在某种程度上也有责任,所以他们应该愿意加入保障措施。


    让我看看我是否能总结出这个问题,然后我对我所理解的问题的答案。

    您希望让用户登录到您的应用程序,然后存储第三方凭据。(这些凭证是什么并不重要…)为了安全起见,您不希望有一种简单的方法来解密这些凭证,以防黑客访问数据库。

    这是我的建议。

  • 创建一个身份验证系统供用户登录到您的应用程序。用户每次访问网站时都必须登录。当存储对所有这些其他凭证的访问时,"记住我"只是一个可怕的想法。通过组合和散列用户名、密码和salt来创建身份验证。这样,数据库中就不会存储这些信息。

  • 用户名/密码组合的哈希版本存储在会话中。这将成为主密钥。

  • 输入第三方信息。此信息使用主密钥哈希加密。

  • 所以这意味着…

    如果一个用户不知道他们的密码,他们就走运了。然而,对于黑客来说,获取信息将是一个非常困难的情况。他们需要了解用户名、密码、salt的散列,以破坏身份验证,然后将该散列版本的hte用户名/密码用于主密钥,然后使用该散列版本对数据进行decyrpt。

    仍然有可能被黑客攻击,但很难——无法实现。我也会说这给了你相对的可否认性,因为根据这个方法,你永远不知道服务器上的信息,因为它在存储之前是加密的。这个方法类似于我假设像OnePassword这样的服务的工作方式。


    用户并不都很精通技术,但如果用户被要求提供第三方网站的凭据,他应该尽快离开您的网站。这只是个坏主意。关于存储明文密码的问题在这里也绝对适用。不要这样做。

    你没有提供太多有关第三方的背景资料,也没有提供你与第三方的关系。但是,如果他们愿意做一些更改来支持您的用例,您应该与他们讨论。他们实现OAuth将是一个很好的解决方案。

    如果您想四处寻找替代方案,请查找联邦身份


    如果你使用一个基于x标准的随机salt,你可以预测,但黑客不能那么依赖于你如何写代码,它仍然可能不明显什么是什么,即使黑客获得了访问一切。

    例如,您使用当前时间和日期加上用户IP地址作为salt。然后将这些值连同哈希一起存储在数据库中。您混淆了用于创建散列的函数,可能不太明显salt是什么。当然,任何一个坚定的黑客最终都会打破这一点,但这会给你时间和一些额外的保护。