php - 您如何保护 API key 和第 3 方站点凭据 (LAMP)?

标签 php mysql api credentials api-key

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

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

我能想到的唯一合理的方法(这将是一个 MySQL/PHP 应用程序)是通过 PHP 应用程序中的硬编码“密码”加密凭据。这里唯一的好处是,如果恶意用户/黑客获得了对数据库的访问权,但不能访问 PHP 代码,他们仍然一无所获。也就是说,这对我来说似乎毫无意义,因为我认为我们可以合理地假设,如果黑客得到一个或另一个,他们就会得到一切——对吗?

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

我很惊讶我没有在堆栈上看到这个问题有任何好的答案。我确实找到了这个,但就我而言,这并不适用:How should I ethically approach user password storage for later plaintext retrieval?

谢谢大家。

最佳答案

根据我在问题、答案和评论中看到的内容;我建议利用 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.



正是来自此评论,并且假设访问要存储的数据可以放在 cron 作业中。进一步假设您的服务器上有 SSL (https),​​因为您将处理 secret 用户信息,并拥有 OpenSSLmcrypt可用的模块..此外,接下来的内容将是关于“如何”实现的相当通用,但并不是根据您的情况进行操作的详细信息。还应该注意的是,此“操作方法”是通用的,您应该在实现之前进行更多研究。话虽如此,让我们开始吧。

首先,让我们谈谈 OpenSSL 提供了什么。 OpenSSL 给了我们一个 Public-Key Cryptography :使用公钥加密数据的能力(如果被泄露,不会危及用它加密的数据的安全性。)其次,它提供了一种使用“私钥”访问该信息的方法。由于我们不关心创建证书(我们只需要加密 key ),因此可以通过一个简单的函数(您只会使用一次)获得这些证书:
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。根据需要存储的数据量,我们可以直接对其进行加密,也可以生成密码以使用公钥加密,然后使用该随 secret 码对数据本身进行加密。我将在解释中走这条路线。为了走这条路,我们将使用 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;
}

我保留了完整的注释以帮助理解这些功能。现在是我们进入有趣部分的地方,实际处理用户数据。 $datasend方法是序列化数组中的用户数据。请记住 $publicKey 的发送方法是硬编码的,您可以将其作为变量存储在您的类中并以这种方式访问​​它,以便将较少的变量传递给它,或者每次都从其他地方输入以发送到该方法。加密数据的示例用法:
$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.

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

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

关于php - 您如何保护 API key 和第 3 方站点凭据 (LAMP)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15179447/

相关文章:

php - 在我的测试/bootstrap/setup/etc 中使用 phpunit 命令行参数

php - 选择字段值出现在至少 4 个其他行中的行

php - 不区分大小写的自定义replace()函数

php - MySQL 仅按特定行排序

api - 我在哪里可以找到我的 Yahoo Developer API key ?

c++ - 如何覆盖 Py_GetPrefix()、Py_GetPath()?

spring - 实现 Spring Controller 时共享一个公共(public)父路径

php - 使用 PHP 上下移动记录

php - Laravel 查询关系模型::有 ('relation' ) 不起作用

javascript - 如何检查请求是否已发出