我正在创建一个网站,该网站将使用ID,密码和API密钥到其他第三方网站 - 以便服务器相应地访问信息。出于此对话的目的,我们假设它是针对支付网关的 - 意味着存储在数据库中的此信息的暴露可能意味着恶意用户可以从其凭据泄露的帐户中提取现金。
不幸的是,这不像密码/散列情况,因为用户每次都不输入凭据 - 他们输入一次然后将其保存在服务器上供将来使用应用
我能想出的唯一合理的方法(这将是一个MySQL / PHP应用程序),是通过PHP应用程序中的硬编码“密码”加密凭证。这里唯一的好处是,如果恶意用户/黑客获得对数据库的访问权限,而不是PHP代码,他们仍然没有任何东西。也就是说,这对我来说似乎毫无意义,因为我认为我们可以合理地假设一个黑客会得到一切,如果他们得到一个 - 对吧?
如果社区决定采用一些好的解决方案,最好将其他来源收集到示例/教程/更深入的信息中,以便将来可以为所有人实施。
我很惊讶我没有在堆栈上看到任何好的答案这个问题。我找到了这个,但在我的情况下,这并不适用:How should I ethically approach user password storage for later plaintext retrieval?
谢谢大家。
答案 0 :(得分:8)
基于我在问题,答案和评论中可以看到的内容;我建议利用OpenSSL。这假设您的站点需要定期访问此信息(这意味着可以安排)。如你所说:
服务器需要此信息才能为各种情况发送付款。它不需要"所有者"所述密钥登录,事实上,一旦他们第一次提供密钥,所有者可能永远不会再关心它们。
来自此评论,并且假设访问要存储的数据可以放在cron作业中。进一步假设您的服务器上有SSL(https),因为您将处理机密的用户信息,并且OpenSSL
和mcrypt
模块可用。此外,下面将是相当通用的关于'如何'它可以实现,但不是真的根据你的情况做这件事的细节。还应该注意的是,这个“怎么样”'是一般的,你应该在实施之前做更多的研究。话虽这么说,让我们开始吧。
首先,让我们谈谈OpenSSL提供的内容。 OpenSSL为我们提供了一个Public-Key Cryptography:使用公钥加密数据的能力(如果受到损害,将会破坏用它加密的数据的安全性。)其次,它提供了一种访问该方法的方法。带有私钥的信息。由于我们不关心创建证书(我们只需要加密密钥),因此可以使用简单的功能(您只需使用一次)来获取这些证书。
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数组中serialize' d。根据需要存储的数据量,我们要么能够直接对其进行加密,要么生成密码以使用公钥进行加密,并使用该随机密码来加密该数据本身。我将在解释中走这条路。要走这条路线,我们将使用AES加密,并且需要一个方便的加密和解密功能 - 以及为数据随机生成一个合适的一次性填充的方法。我将提供我使用的密码生成器,虽然我从我稍后编写的代码中移植它,它将用于此目的,或者您可以编写更好的密码生成器。 ^^
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加密的功能:
/**
* 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;
}
(我最初写这些函数是一个类的一部分,并建议你将它们实现到你自己的类中。)另外,使用这个函数可以使用创建的一次性填充,但是,如果与其他应用程序的用户专用密码一起使用,那么你肯定需要一些盐来添加密码。接下来,解密并验证解密数据是否正确:
/**
* 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加密/解密功能,因为我们稍后需要第二个。
/**
* 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
始终是一次性填充,而不是用户信息。接下来,将公钥加密和一次性密码的AES结合起来进行加密和解密的功能。
/**
* 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;
}
我完整地留下了评论,以帮助理解这些功能。现在我们开始讨论有趣的部分,实际上是在处理用户数据。 $data
方法中的send
是序列化数组中的用户数据。请记住,对于$publicKey
是硬编码的send方法,您可以将其作为变量存储在类中,并以这种方式访问它,以便将较少的变量传递给它,或者从其他地方输入它以发送给方法每次。加密数据的示例用法:
$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
。用于解密的示例用法:
$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 开头)
我希望这有助于回答如何在您的应用程序中安全地存储用户信息,这些信息不会因访问您的代码或数据库而受到损害。
答案 1 :(得分:2)
让我看看我能否总结一下这个问题 - 然后回答我对这个问题的理解。
您希望用户登录您的应用程序,然后存储第三方凭据。 (这些凭据是什么并不重要......)为了安全起见,您希望在黑客获得对数据库的访问权限时,不能轻易解密这些凭据。
这是我的建议。
为用户创建一个身份验证系统,以登录您的应用程序。用户每次访问该站点时都必须登录。当存储对所有这些其他凭证的访问时,“记住我”只是一个可怕的想法。通过组合和散列用户名,密码和salt来创建身份验证。这样,这些信息都不会存储在数据库中。
用户名/密码组合的散列版本存储在会话中。这将成为MASTER KEY。
输入第三方信息。此信息使用MASTER KEY哈希加密。
所以这意味着......
如果用户不知道他们的密码,他们就不走运了。但是,黑客获取信息将是一个非常困难的情况。他们需要了解用户名,密码,盐的哈希,以破坏身份验证,然后为主密钥使用哈希用户名/密码的哈希版本,然后使用它来解密数据。
仍然可能被黑客攻击,但非常困难 - 不可能。我还说这会给你相对的否定性,因为根据这种方法,你永远不会知道服务器上的信息,因为它在存储之前是加密的。这种方法类似于我假设像OnePassword这样的服务。
答案 2 :(得分:2)
如果您开箱即用,有几种可能的解决方案......并且可以开箱即用,这不一定是您的情况,但我会建议他们。
从第三方网站获取具有有限权限的帐户。在您的支付网关示例中,该帐户允许您授权和结算付款,但不能调用TransferToBankAccount($ accountNumber)API。
某些安全系统依靠硬件令牌来提供身份验证。 (我主要考虑密钥创建和签名加密狗。)我不确定这在你的情况下究竟是如何工作的。假设您有一个加密狗,您要求进行身份验证,它只会在某些情况下回复。如果所能做的只是给(或不给)用户名/密码,这很难。 (比如说,签署一个请求,它可以检查请求参数)。您可以使用SSL客户端证书执行此操作,其中对第三方的请求需要用户名/密码/和客户端签名 - 如果您的第三方接受此类事件。
#1和#2的组合。设置另一台服务器作为中间人。该服务器将实现我建议您的第三方可以在#1中执行的基本业务逻辑。它暴露了一个API并检查以确保请求是“有效”(付款可以结算,但只能传输到您的帐户的传输#等),然后才能获取auth详细信息并直接发出请求。 API可以包含SetAuthDetails,但不包括GetAuthDetails。这样做的好处是攻击者可以妥协的另一件事。 而且,服务器越简单就越容易硬化。 (无需运行SMTP,FTP,以及您的主服务器所具有的任何可能错误的PHP堆栈....只需几个PHP脚本,端口443,SSH和一个例如sqlite实例。保持HTTPD和PHP最新应该更容易,因为对兼容性问题的关注较少。)
假设您将受到损害并监控/审核潜在影响。在某个地方安装另一台服务器(是)具有登录的身份验证详细信息并检查身份验证日志(或理想情况下,只读权限)。让它检查每一分钟并检查未经授权的登录和/或交易,并做一些激烈的事情(可能会将密码更改为随机的内容并将您分页)。
*无论我们是否真的在谈论支付网关,任何您担心被黑客入侵的第三方也应关注自己的安全性,包括您(或其他客户)是否被黑客入侵。他们也可能在某种程度上承担责任,所以他们应该愿意提供保障措施。
答案 3 :(得分:0)
如果您使用基于X标准的随机盐,您可以预测,但黑客不能根据您编写代码的方式,即使黑客获得对所有内容的访问权限,仍然可能不明显是什么。
例如,您使用当前时间和日期以及用户IP地址作为salt。然后,将这些值与哈希值一起存储在数据库中。你混淆了用于创建哈希的函数,盐可能不那么明显。当然,任何坚定的黑客最终都可以打破这种局面,但它会为你带来时间和一些额外的保护。
答案 4 :(得分:0)
用户并非都非常精通技术,但要求为第三方网站提供凭据的用户应尽快离开您的网站。这简直是一个坏主意。关于存储明文密码的问题也适用于此。不要这样做。
您没有提供有关第三方的背景信息,也没有提供您与他们的关系。但是你应该和他们谈谈他们是否愿意做出一些改变来支持你的用例。他们实施oauth将是一个很好的解决方案。
如果您想四处寻找替代方案,请查看federated identity