Как защитить ключи API и учетные данные сторонних сайтов (LAMP)?

Я создаю сайт, который будет использовать идентификаторы, пароли и ключи API для других сторонних сайтов - для того, чтобы сервер мог получить соответствующую информацию. Для этой беседы позвольте предположить, что это для платежного шлюза, то есть воздействие этой информации, которая хранится в БД, может означать, что злоумышленник может вывести наличные деньги из учетной записи, чьи учетные данные были пропущены.

К сожалению, это не похоже на ситуацию с паролем/хэшированием, поскольку пользователь не вводит учетные данные каждый раз - они вводят его один раз, а затем сохраняются на сервере для дальнейшего использования приложением.

Единственный разумный метод, который я могу придумать (это будет приложение MySQL/PHP), заключается в шифровании учетных данных с помощью жесткого "пароля" в приложении PHP. Единственное преимущество здесь в том, что если злоумышленник/хакер получает доступ к базе данных, но не к PHP-коду, у них все еще ничего нет. Тем не менее, это кажется бессмысленным для меня, потому что я думаю, что мы можем разумно предположить, что хакер получит все, если они получат тот или иной, верно?

Если сообщество решит некоторые хорошие решения, было бы неплохо собрать другие источники в примерах/учебниках/более глубокой информации, чтобы это могло быть реализовано в будущем для всех.

Я был удивлен, что уже не видел этого вопроса с любыми хорошими ответами на стек. Я нашел это, но в моем случае это действительно не применимо: Как я должен этически подойти к хранилищу паролей пользователей для последующего поиска в обычном тексте?

Спасибо всем.

Ответ 1

Основываясь на том, что я вижу в вопросе, ответах и ​​комментариях; Я бы предложил воспользоваться OpenSSL. Это предполагает, что ваш сайт нуждается в периодическом доступе к этой информации (что означает, что это может быть запланировано). Как вы заявили:

Серверу потребуется эта информация для отправки платежей для всех видов ситуаций. Для входа в систему не требуется "владелец" указанных ключей, на самом деле владелец, возможно, никогда не захочет их видеть снова, как только они предоставят их в первый раз.

Из этого комментария, и предположение, что доступ к данным, которые вы хотите сохранить, можно поместить в задание cron. Кроме того, предполагается, что у вас есть SSL (https) на вашем сервере, так как вы будете иметь дело с конфиденциальной информацией пользователя и иметь доступные модули OpenSSL и mcrypt. Кроме того, следующее будет довольно общим для того, "это может быть достигнуто, но на самом деле не детали его выполнения в вашей ситуации. Следует также отметить, что этот" практический" подход является общим, и перед его внедрением следует сделать больше исследований. При этом, давайте начнем.

Во-первых, расскажите о том, что предлагает OpenSSL. OpenSSL дает нам криптографию с открытым ключом: возможность шифрования данных с использованием открытого ключа (который, если он скомпрометирован, не будет нарушать безопасность данные, зашифрованные им.) Во-вторых, он предоставляет способ доступа к этой информации с помощью "Закрытого ключа". Поскольку нам не нужно создавать сертификат (нам нужны только ключи шифрования), их можно получить с помощью простой функции (которую вы будете использовать только один раз.):

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 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 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 decode the base64 data
    $data = base64_decode($data);
    //Now let 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 является жестко запрограммированным, вы можете хранить как переменную в своем классе и обращаться к нему таким образом, чтобы передавать меньше переменных или вводить его из другого места, чтобы каждый раз отправлять метод, Пример использования для шифрования данных:

$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 script, а не в public_html, содержащий закрытый ключ, затем используйте curl, чтобы опубликовать секретный ключ на своей странице, которая его ищет. (убедитесь, что вы вызываете адрес, начинающийся с https)

Я надеюсь, что это поможет ответить, как можно безопасно хранить информацию пользователя в своем приложении, которое не будет скомпрометировано путем доступа к вашему коду или вашей базе данных.

Ответ 2

Позвольте мне посмотреть, могу ли я суммировать проблему, а затем ответить на то, что я понимаю.

Вы хотите, чтобы пользователи вошли в ваше приложение, а затем сохранили сторонние учетные данные. (Неважно, какие эти учетные данные...). Для обеспечения безопасности вы хотите, чтобы не было простого способа дешифрования этих учетных данных в случае получения хакером доступа к базе данных.

Вот что я предлагаю.

  • Создайте систему аутентификации для входа пользователя в ваше приложение. Пользователь ДОЛЖЕН войти в систему при каждом посещении сайта. Когда вы сохраняете доступ ко всем этим другим учетным данным, "помните меня" - это просто ужасная идея. Аутентификация создается путем объединения и хэширования имени пользователя, пароля и соли. Таким образом, ни одна из этих данных не хранится в db.

  • Хешированная версия комбинации имени пользователя и пароля сохраняется в сеансе. Это становится MASTER KEY.

  • Введена информация сторонней стороны. Эта информация зашифровывается с использованием хэша MASTER KEY.

Итак, это означает...

Если пользователь не знает свой пароль, им не повезло. Однако хакеру было бы очень сложно получить информацию. Им нужно будет понять хэширование имени пользователя, пароля, соли, сломать аутентификацию, а затем использовать хешированную версию hte username/password для главного ключа, а затем использовать ее для дешифрования данных.

Можно по-прежнему взломать, но очень сложно - невозможно. Я бы также сказал, что это дает вам относительное отрицание, потому что, согласно этому методу, вы никогда не знаете информацию на сервере, поскольку она зашифрована до ее хранения. Этот метод похож на то, как я предполагаю, что службы, подобные OnePassword, работают.

Ответ 3

Есть несколько возможных решений, если вы думаете из коробки... и можете действовать из коробки, что не обязательно в вашем случае, но я собираюсь предложить их в любом случае.

  • Получить учетные записи с ограниченными разрешениями сторонних сайтов. В примере вашего платежного шлюза - учетная запись, которая позволяет вам разрешать и оплачивать платежи, но не вызывать API TransferToBankAccount ($ accountNumber).

    • Аналогичным образом попросите поставщика ввести ограничения. $accountNumber должен быть одним из нескольких, которые вы предоставили или, возможно, просто должны находиться в той же стране, в которой вы находитесь *.
  • Некоторые защищенные системы полагаются на аппаратный токен для обеспечения аутентификации. (Я в основном думаю о создании ключей и подписании ключей.) Я точно не знаю, как это будет работать в вашей ситуации. Предположим, у вас есть ключ, на который вы запрашиваете аутентификацию, и он будет отвечать только при определенных обстоятельствах. Это сложно, если все, что он может сделать, это дать (или не дать) имя пользователя/пароль. (Vs говорят, подписывая запрос, где он может проверить параметры запроса). Вы могли бы сделать это с помощью сертификата клиента SSL, где запрос третьей стороне требует имени пользователя/пароля/клиента, если ваша сторонняя сторона примет такую ​​вещь.

  • Комбинация # 1 и # 2. Настройте другой сервер, чтобы действовать как промежуточный. Этот сервер будет реализовывать базовую бизнес-логику, которую я предложил вашей третьей стороне сделать в # 1. Он предоставляет API и проверяет, является ли запрос "действительным" (платежи могут быть урегулированы, но только исходящие переводы на ваш аккаунт и т.д.), Прежде чем захватывать данные авторизации и делать запрос напрямую. API может включать SetAuthDetails, но не GetAuthDetails. Преимущество здесь в том, что это еще одна вещь для злоумышленника для компрометации. И, чем проще сервер, тем легче затвердеть. (Нет необходимости запускать SMTP, FTP и любой потенциально глючный PHP-стек вашего основного сервера.... всего несколько PHP-скриптов, порт 443, SSH и, например, экземпляр sqlite. Сохранение HTTPD и PHP в актуальном состоянии должно быть проще, потому что меньше внимания уделяется проблемам совместимости.)

  • Предположим, что вы получите компрометацию и контроль/аудит потенциальных эффектов. Есть еще один сервер где-нибудь (да) есть данные авторизации для входа в систему и проверки журналов auth (или, в идеале, только для чтения). Проверяйте каждую минуту и ​​проверяйте несанкционированные логины и/или транзакции и делайте что-то решительное (возможно, измените пароль на что-то случайное и запишите на страницу).

* Независимо от того, говорим ли мы о платежном шлюзе или нет, любая сторонняя сторона, которой вы обеспокоены тем, что ее взломали, также должна быть обеспокоена их собственной безопасностью, в том числе, если вас (или других клиентов) взломают. Они также несут ответственность в той или иной степени, поэтому они должны быть готовы обеспечить гарантии.

Ответ 4

Если вы используете случайную соль на основе критериев X, которые вы можете предсказать, но что хакер не мог тогда зависеть от того, как вы пишете код, все равно может быть неясно, что есть, даже если хакер получает доступ ко всему.

Например, вы используете текущее время и дату, а также IP-адрес пользователей как соль. Затем вы сохраняете эти значения вместе с хешем в базе данных. Вы запутываете функцию, используемую для создания хэша, и может быть не так очевидно, какая соль. Конечно, любой определенный хакер может сломаться в конечном итоге, но он покупает вам время и некоторый дополнительный уровень защиты.

Ответ 5

Пользователи не очень разбираются в технологиях, но пользователь, попросивший предоставить учетные данные для стороннего сайта, должен убежать с вашего сайта так быстро, как только может. Это просто плохая идея. Вопрос о сохранении паролей с открытым текстом определенно применим и здесь. Не делайте этого.

Вы не предоставляете много контекста о третьих сторонах, о которых идет речь, и о ваших отношениях с ними. Но вы должны поговорить с ними о том, готовы ли они внести некоторые изменения в поддержку вашего прецедента. Их реализация oauth будет хорошим решением.

Если вы хотите посмотреть альтернативы, найдите федеративный идентификатор