您如何保护 API 密钥和第 3 方站点凭据 (LAMP)?

Posted

技术标签:

【中文标题】您如何保护 API 密钥和第 3 方站点凭据 (LAMP)?【英文标题】:How do you protect API keys and 3rd party site credentials (LAMP)? 【发布时间】:2013-02-17 05:39:48 【问题描述】:

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

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

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

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

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

谢谢大家。

【问题讨论】:

您的赏金正在寻求讨论。阅读常见问题; SO 不讨论。 另外,使用 oauth。它是安全的。 @Hiroto 很抱歉关于这个问题:讨论,无论如何,如果有人出现并意识到赏金,我会采取有效的答案。 oauth 声明 OAuth provides a method for users to grant third-party access to their resources without sharing their passwords. It also provides a way to grant limited access (in scope, duration, etc.). 对于所描述的情况(即支付处理),服务器出于各种原因需要始终无限制地访问这些 API。我看不出这会如何改进——共享的密码不一定是问题。 顺便补充一下,我将使用的服务不支持 oAth =),因为许多网站还不支持。 【参考方案1】:

用户并非都非常精通技术,但被要求为第三方网站提供凭据的用户应该尽快逃离您的网站。这简直是​​个坏主意。关于存储明文密码的问题在这里也肯定适用。不要这样做。

您没有提供有关第三方以及您与他们的关系的太多背景信息。但是您应该与他们讨论他们是否愿意进行一些更改以支持您的用例。他们实施 oauth 将是一个很好的解决方案。

如果您想四处寻找替代方案,请查找federated identity

【讨论】:

上下文是支付处理器和商家的上下文。例如:站点有很多商家。用户可以从网站上的任何商家处购买。网站需要每个商家的凭据/API 密钥才能将钱汇到正确的地方。 关于处理支付信息的安全性有大量的规则、检查、审计和法律术语。您无法决定什么足够安全,什么不够安全。银行和信用卡公司制定规则。 这与 PCI 标准无关,只需将这些凭证以明文形式存储在数据库中即可满足要求。 PCI 是关于持卡人安全的。问题是关于存储此 API/商家凭证类型信息的最佳实践(这样我们就不必将其以纯文本形式存储在数据库中)。【参考方案2】:

如果你开箱即用,有几种可能的解决方案......并且可以开箱即用,这不一定是你的情况,但我还是会建议他们。

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

    同样,要求提供商设置限制。 $accountNumber 必须是您提供的几个之一,或者只需与您所在的国家/地区相同*。

    一些安全系统依赖硬件令牌来提供身份验证。 (我主要考虑创建密钥和签署加密狗。)我不确定这在您的情况下究竟是如何工作的。假设您有一个请求身份验证的加密狗,它只会在某些情况下回复。如果它所能做的只是提供(或不提供)用户名/密码,这很困难。 (比方说,签署一个请求,它可以检查请求参数)。您可以使用 SSL 客户端证书来执行此操作,其中对第 3 方的请求需要用户名/通行证/和客户端签名——如果您的第 3 方接受这样的事情。

    #1 和#2 的组合。设置另一台服务器作为中间人。该服务器将实现我建议您的第 3 方可以在 #1 中执行的基本业务逻辑。在获取身份验证详细信息并直接发出请求之前,它公开了一个 API 并检查以确保请求是“有效的”(可以结算付款,但只能向您的帐户 # 发出转帐等)。 API 可以包括 SetAuthDetails,但不能包括 GetAuthDetails。这里的好处是攻击者妥协是另一回事。 而且,服务器越简单,就越容易加固。 (无需运行 SMTP、FTP 和任何可能有问题的 PHP 堆栈,您的主服务器有......只需几个 PHP 脚本、端口 443、SSH 和一个例如 sqlite 实例。保持 HTTPD 和 PHP 是最新的应该更容易,因为对兼容性问题的担忧较少。)

    假设您将受到威胁并监控/审核潜在影响。让另一台服务器(是)具有身份验证详细信息以登录并检查身份验证日志(或者,理想情况下,只读权限)。让它每分钟检查一次,检查未经授权的登录和/或交易,并做一些激烈的事情(也许将密码更改为随机密码并传呼你)。

*无论我们是否真的在谈论支付网关,您担心被黑客入侵的任何第 3 方也应该关注他们自己的安全,包括您(或其他客户)是否被黑客入侵。他们在某种程度上也有责任,所以他们应该愿意采取保护措施。

【讨论】:

【参考方案3】:

根据我在问题、答案和 cmets 中看到的内容;我建议利用 OpenSSL。这是假设您的站点需要定期访问此信息(这意味着可以安排)。正如你所说:

服务器需要这些信息来为各种情况发送付款。它不需要所述密钥的“所有者”登录,事实上,一旦他们第一次提供密钥,所有者可能就再也不想看到它们了。

来自此评论,并且可以将访问要存储的数据的假设放在 cron 作业中。进一步假设您的服务器上有 SSL (https),​​因为您将处理机密用户信息,并且有可用的 OpenSSLmcrypt 模块。此外,关于“如何' 它可以实现,但不是根据你的情况做这件事的细节。还应注意,此“操作方法”是一般性的,您应该在实施之前进行更多研究。话虽如此,让我们开始吧。

首先,让我们谈谈 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;

现在,您有一个 PublicPrivate 密钥。保护私钥,使其远离您的服务器,并将其远离数据库。将其存储在另一台服务器上,一台可以运行 cron 作业的计算机等上。除非您每次需要处理付款并使用 AES 加密或其他东西加密私钥时都可以要求管理员在场,否则就离公众视线很远相似的。但是,公钥将被硬编码到您的应用程序中,并且每次用户输入要存储的信息时都会使用该公钥。

接下来,您需要确定计划如何验证解密数据(这样您就不会开始向支付 API 发送无效请求。)我将假设需要存储多个字段,并且作为我们只想加密一次,它将在一个可以是serialize'd 的 PHP 数组中。根据需要存储的数据量,我们可以直接对其进行加密,也可以生成密码以使用公钥进行加密,然后使用该随机密码对数据本身进行加密。我将在解释中走这条路线。要走这条路,我们将使用 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;
    

查看这两个函数,阅读 cmets 等。弄清楚它们的作用以及它们是如何工作的,这样你就不会错误地实现它们。现在,加密用户数据。我们将使用公钥对其进行加密,并且以下函数假设到目前为止(以及将来)的每个函数都在同一个类中。我将同时提供 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;

为了帮助理解这些功能,我保留了 cmets。现在是我们进入有趣部分的地方,实际使用用户数据。 send 方法中的$data 是序列化数组中的用户数据。请记住 $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.

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

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

【讨论】:

给你赏金。这是我们在这里所拥有的最完整和最好的答案。谢谢乔恩。 谢谢!不客气 ^^ 虽然,从那个措辞来看,我认为这不是你想要的? 嗯,我想我意识到,如果没有第三方服务(双方同意使用)或类似你描述的东西,可能无法非常安全地完成我所说的事情这就是全部。 我同意这种说法。通过存储此类信息,尤其是在您自己的服务器上,您有责任确保如果代码或数据库受到损害,其中的信息不会受到损害。但是,如果购买的产品不需要存储敏感信息,则可以更轻松地处理支付信息的存储。如果您与我联系(个人资料中的链接),我将非常乐意就您可以使用的其他选项进行更多讨论,如果我知道那是什么情况,这些选项可能有助于您的特定情况。 ^^【参考方案4】:

让我看看我是否可以总结问题 - 然后我对我所理解的问题的回答。

您希望用户登录到您的应用程序,然后存储第 3 方凭据。 (这些凭据是什么无关紧要...)为了安全起见,您不希望有一种简单的方法可以在黑客访问数据库的情况下解密这些凭据。

这是我的建议。

    为用户创建一个身份验证系统以登录您的应用程序。用户每次访问该站点时都必须登录。在存储对所有这些其他凭据的访问权限时,“记住我”只是一个可怕的想法。身份验证是通过组合和散列用户名、密码和盐来创建的。这样,这些信息都不会存储在数据库中。

    用户名/密码组合的散列版本存储在会话中。这将成为 MASTER KEY。

    已输入第三方信息。此信息使用 MASTER KEY 哈希加密。

所以这意味着...

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

仍有可能被黑客入侵,但非常困难 - 不太可能。我还要说这给了你相对的否认性,因为根据这种方法,你永远不知道服务器上的信息,因为它在存储之前是加密的。这种方法类似于我假设 OnePassword 等服务的工作方式。

【讨论】:

不完全是。此处的目的是让服务器以编程方式使用这些凭据。例如,它可以是支付 API 用户名/密码/API 密钥。服务器将需要此信息来为各种情况发送付款。它不需要所述密钥的“所有者”登录,事实上,一旦他们第一次提供密钥,所有者可能再也不想看到它们了。 啊-感谢您的澄清。好吧,虽然我不能说“这不能比其他人建议的做得更好”,但这超出了我所能提供的范围。我无法想象加密密钥如此断开以致受感染的服务器和数据库仍然使数据无法使用的情况。这当然是许多服务现在使用 OAuth 的原因。我讨厌成为那个说“改用这个工具!!”的人但也许如果安全是最重要的,您可以考虑只支持提供散列密钥访问的服务。 @Shackrock 用户输入的信息会被安排使用吗? (即,您的情况是否允许需要在一天中一次性支付的所有款项,或增量支付?)或者作为他们与网站直接互动的结果? @jon 他们将基于其他用户输入。 IE。另一个用户为产品付款,被购买产品的人的 API 密钥/用户名/密码立即被访问并发送付款。 @Shackrock 在同一个服务器中没有一种干净的方法可以做到这一点,尽管您可以稍微修改我的答案并将付款放在一个队列中(使用 CC 信息上的公钥加密) 并每隔一分钟左右运行 cron 以检查新的付款,因此它几乎是无缝的,但需要购买用户等到 cron 处理付款,然后更新数据库,以便用户检查并看到付款完成,然后可以从那里(获取产品,进行下载等)。【参考方案5】:

如果您使用基于 X 标准的随机盐,您可以预测但黑客无法预测,则取决于您编写代码的方式,即使黑客获得了对所有内容的访问权限,它仍然可能不清楚是什么。

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

【讨论】:

谢谢。我认为这与在 PHP 中存储某种硬编码的“加密密码”是一样的。就像你说的,黑客可以找出我生成这个的方法,最后它就像一个硬编码的 PHP 密码,但是是的,它确实需要一些时间。 如果安全性是关键任务,我会租用第二台服务器,它的唯一工作是存储数据库。这样,如果他的黑客闯入了您的代码,他们仍然没有数据库信息,反之亦然。 @aguyfromhere:如果您的应用服务器可以访问数据库,但它被黑客入侵了,那么您如何判断黑客无法访问数据库? :-)

以上是关于您如何保护 API 密钥和第 3 方站点凭据 (LAMP)?的主要内容,如果未能解决你的问题,请参考以下文章

从 Google GCP 项目凭据 API 密钥中检索信息

如何保护公共 API(无凭据)不被利用?

未经授权,此IP,站点或移动应用程序无法使用此API密钥

如何在不存储凭据的情况下连接到KeyVault?

验证 Google 凭据

如何在 c# 应用程序中正确保护 api 凭据