Руководство по шифрованию и хэшированию в Laravel

Источник: «A Guide to Encryption and Hashing in Laravel»
Хеширование и шифрование — важнейшие концепции безопасности, о которых должен знать каждый веб-разработчик. В этой статье мы рассмотрим, что такое хеширование и шифрование, в чем разница между ними и как их использовать в своих приложениях Laravel.

Каждый веб-разработчик должен знать о хешировании и шифровании, поскольку они являются важнейшими концепциями безопасности. При правильном использовании они позволяют повысить безопасность веб-приложений и обеспечить конфиденциальность персональной информации пользователей (Personally Identifiable Information — PII).

В этой статье мы рассмотрим хеширование и шифрование, а также их различия. Кроме того, мы узнаем, как использовать их в проектах Laravel для повышения безопасности приложений. Рассмотрим, как хэшировать данные и сравнивать их с обычным текстом. Расскажем о различных способах шифрования и дешифрования данных в Laravel.

Что такое хэширование и шифрование

Как веб-разработчик, вы наверняка сталкивались с терминами шифрование и хеширование. Возможно, вы даже использовали их в своих проектах, но что они означают и чем отличаются?

Иногда эти термины используются как взаимозаменяемые, однако это разные техники со своими собственными случаями применения.

Шифрование и хеширование являются разновидностями криптографической защиты и заключаются в использовании математических алгоритмов для преобразования данных в форму, нечитаемую человеком и (в основном) безопасную.

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

Шифрование

Для начала рассмотрим шифрование.

Шифрование — это процесс кодирования информации (обычно называемой открытым текстом/plaintext) в альтернативную форму (обычно называемую шифртекстом/ciphertext). Как правило, шифртекст создаётся с помощью ключа и алгоритма шифрования. В зависимости от типа шифрования расшифровать и прочитать его может только тот, кто обладает правильным ключом.

Шифрование может осуществляться в двух формах: симметричное и асимметричное.

При симметричном шифровании один и тот же ключ используется как для шифрования, так и для дешифрования. Это означает, что для шифрования и дешифрования данных используется один и тот же ключ. Примерами этого типа шифрования являются Advanced Encryption Standard (AES) и Data Encryption Standard (DES). По умолчанию в Laravel используется шифрование AES-256-CBC.

Асимметричное шифрование несколько сложнее и использует пару ключей: открытый ключ и закрытый ключ. Открытый ключ используется для шифрования данных, а закрытый — для их расшифровки. Таким образом, открытый ключ может быть передан любому человеку, но закрытый ключ должен храниться в секрете, поскольку любой человек, обладающий им, может успешно расшифровать данные. Примерами такого типа шифрования являются алгоритмы Ривеста-Шамира-Адлемана (RSA) и Диффи-Хеллмана.

В качестве примера того, как может выглядеть зашифрованная строка, предположим, что у нас есть строка текста, которую мы хотим зашифровать. В качестве примера мы используем следующую строку:

This is a secret message.

Если зашифровать этот текст с помощью шифрования AES-256-CBC, используя в качестве ключа U2x2QdvosFTtk5nL0ejrKqLFP1tUDtSt, то мы получим следующий шифротекст:

eyJpdiI6IjF1cmF0YU5TMkRnR3NUMVRMMm1udFE9PSIsInZhbHVlIjoieGloYW5VVWtXV2hjcVRVY3hGTkZ1bDdoOVBSZEo1VkVWNE1LSlB5S0lhNkF3SloxeWhRejNwbjN5SEgxeUJXayIsIm1hYyI6IjljNzY0MTBmMGJlZmRjNzcwMjFiMmFjYmJhNTNkNWVhODkxMTgzYmYwMjA3N2YzMjM1YmVhZWU4NDRiOTYzZWQiLCJ0YWciOiIifQ==

Если затем расшифровать приведённый выше шифротекст с помощью того же ключа, то мы получим исходный открытый текст:

This is a secret message.

Хеширование

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

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

Например, допустим, что пароль пользователя — «hello«. Если использовать алгоритм bcrypt для хэширования этого пароля, то он будет выглядеть следующим образом:

$2y$10$XY2DMYKvLrMj7yqYwyvK5OSvKTA6HOoPTpe3gVpVP5.Y4kN1nbOLq

Важной частью хеширования является использование salt/соль. Соль — случайная строка символов, используемая для того, чтобы хэшированные значения одних и тех же данных отличались друг от друга. Это означает, что если два пользователя имеют одинаковые пароли, то их хэшированные пароли будут храниться в базе данных по-разному. Это важно, так как не позволит хакерам вычислить пользователей с одинаковыми паролями. Например, следующие строки представляют собой хэшированные версии пароля "hello":

$2y$10$NUgYbLrzxn471GzcIN10wedXEcltcbAasHqU7hCeMFv4aCTl/6bVW
$2y$10$AvBxO6HCRwYPNPZmeERIEOzLAJP7ZkcjrekdzaRLwY8YX4m9VJiFy
$2y$10$YQ3lzNx8h0tDgw4K3dzJAOxycZhDhTAnueSugbmoo3NDTuq1OT8KW

Вы, вероятно, думаете: Если пароль хэширован и не может быть восстановлен, то как мы узнаем, что он верный?. Это можно сделать, сравнив хэш-значение пароля, введённого пользователем, с хэш-значением пароля, хранящегося в базе данных, с помощью функции password_verify(), которую предоставляет PHP. Если они совпадают, то мы знаем, что пароль верен. Если они не совпадают, то мы знаем, что пароль неверен. Далее в этой статье мы рассмотрим, как можно выполнить такое сравнение в Laravel.

Существует множество алгоритмов хеширования, таких как bcrypt, argon2i, argon2id, md5, sha1, sha256, sha512 и многие другие. Однако при работе с паролями всегда следует использовать алгоритм хеширования, рассчитанный на медленную работу, например bcrypt, поскольку в этом случае хакерам сложнее перебирать пароли.

Хеширование в Laravel

Теперь, когда мы имеем базовое представление, что такое хеширование, давайте рассмотрим, как работает хеширование в рамках фреймворка Laravel.

Хеширование паролей

Как уже говорилось, не стоит хранить пароли пользователей в виде обычного текста в базе данных. Вместо этого необходимо хранить пароли в хэшированном виде. Именно в этом случае в дело вступает фасад Hash.

По умолчанию Laravel поддерживает три различных алгоритма хеширования: "Bcrypt", "Argon2i" и "Argon2id". По умолчанию Laravel использует алгоритм "Bcrypt", но если вы хотите использовать другой алгоритм, то Laravel позволяет его изменить. Более подробно мы рассмотрим эту тему далее в статье.

Для начала работы с хэшированием значения можно использовать фасад Hash. Например, допустим, мы хотим хэшировать пароль "hello":

use Illuminate\Support\Facades\Hash;

$hashedValue = Hash::make('hello');

// $hashedValue = $2y$10$XY2DMYKvLrMj7yqYwyvK5OSvKTA6HOoPTpe3gVpVP5.Y4kN1nbOLq

Как видите, хэшировать значение очень просто.

Для того чтобы немного пояснить ситуацию, давайте рассмотрим, как это может работать в контроллере вашего приложения. Представьте, что есть контроллер PasswordController, который позволяет аутентифицированным пользователям обновлять свой пароль. Контроллер может выглядеть примерно так:

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

final class PasswordController extends Controller
{
public function update(Request $request)
{
// Validate the new password here...

$request->user()->fill([
'password' => Hash::make($request->newPassword)
])->save();
}
}

Сравнение хэшированных значений с обычным текстом

Как уже говорилось, отменить хэшированное значение невозможно. Поэтому, если мы хотим определить хэшированное значение, мы можем сделать это только путём проверки хэшированного значения по сравнению с открытым текстом. Именно здесь на помощь приходит метод Hash::check().

Представим, что мы хотим определить, совпадает ли обычный текстовый пароль "hello" с созданным ранее хэшированным паролем. Для этого можно воспользоваться методом Hash::check():

$plainTextPassword = 'hello';
$hashedPassword = "$2y$10$XY2DMYKvLrMj7yqYwyvK5OSvKTA6HOoPTpe3gVpVP5.Y4kN1nbOLq";

if (Hash::check($plainTextPassword, $hashedPassword)) {
// Пароли совпадают...
} else {
// Пароли не совпадают...
}

Метод Hash::check() возвращает true, если пароль в открытом виде совпадает с хэшированным паролем. В противном случае он вернёт false.

Такой подход обычно используется при входе пользователя в приложение. Если пренебречь дополнительными мерами безопасности (например, ограничением скорости) для формы входа в систему, то процесс входа в систему может происходить по следующей схеме:

Драйверы хеширования в Laravel

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

Чтобы изменить алгоритм хеширования, используемый во всем приложении, можно изменить значение драйвера в конфигурационном файле config/hashing.php. По умолчанию используется значение bcrypt, но можно изменить его на argon или argon2id, например, так:

return [

// ...

'driver' => 'bcrypt',

// ...

];

В качестве альтернативы, если вы предпочитаете явно определять используемый алгоритм, вы можете использовать метод driver фасада Hash для определения используемого драйвера хеширования. Например, если вы хотите использовать алгоритм "Argon2i", то можно поступить следующим образом:

$hashedValue = Hash::driver('argon')->make('hello');

Шифрование в Laravel

Теперь рассмотрим, как использовать шифрование в Laravel.

Шифрование и дешифрование значений

Для начала работы с шифрованием значений в Laravel можно воспользоваться вспомогательными функциями encrypt() и decrypt() или фасадом Crypt, который предоставляет аналогичную функциональность. Например, предположим, что мы хотим зашифровать строку "hello":

$encryptedValue = encrypt('hello');

В результате выполнения вышеописанных действий переменная encryptedValue теперь будет равна примерно следующему значению:

eyJpdiI6IitBcjVRanJTN3hTdnV6REdScVZFMFE9PSIsInZhbHVlIjoiZGcycC9pTmNKRjU3RWpmeW1GdFErdz09IiwibWFjIjoiODg2N2U0ZTQ1NDM3YjhhNTFjMjFmNmE4OTA2NDI0NzRhZmI2YTg5NzEwYjdmY2VlMjFhMGZhYzE5MGI2NDA3NCIsInRhZyI6IiJ9

Теперь, когда у нас есть зашифрованное значение, мы можем расшифровать его с помощью вспомогательной функции decrypt():

$encryptedValue = 'eyJpdiI6IitBcjVRanJTN3hTdnV6REdScVZFMFE9PSIsInZhbHVlIjoiZGcycC9pTmNKRjU3RWpmeW1GdFErdz09IiwibWFjIjoiODg2N2U0ZTQ1NDM3YjhhNTFjMjFmNmE4OTA2NDI0NzRhZmI2YTg5NzEwYjdmY2VlMjFhMGZhYzE5MGI2NDA3NCIsInRhZyI6IiJ9';

$decryptedValue = decrypt($encryptedValue);

В результате выполнения вышеописанной операции переменная decryptedValue теперь будет равна "hello".

Если бы во вспомогательную функцию decrypt() были переданы некорректные данные, то возникло бы исключение Illuminate\Contracts\Encryption\DecryptException.

Как видите, шифровать и расшифровывать данные в Laravel довольно просто. Это позволяет упростить процесс хранения конфиденциальных данных в базе данных и их последующей расшифровки при необходимости, и использования.

Смена ключа и алгоритма шифрования

Как уже упоминалось в статье, в Laravel по умолчанию используется симметричный алгоритм шифрования "AES-256", то есть для шифрования и расшифровки данных используется один ключ. По умолчанию таким ключом является значение APP_KEY, заданное в файле .env. Это очень важно помнить, поскольку если вы измените APP_KEY вашего приложения, то все зашифрованные данные, хранящиеся в нем, уже не смогут быть расшифрованы (без внесения изменений в код для явного использования старого ключа).

Если вы хотите изменить ключ шифрования, не меняя APP_KEY, то это можно сделать, изменив значение ключа в конфигурационном файле config/app.php. Аналогичным образом можно изменить значение cipher, чтобы указать используемый алгоритм шифрования. По умолчанию используется алгоритм AES-256-CBC, но его можно изменить на AES-128-CBC, AES-128-GCM или AES-256-GCM.

Автоматическое шифрование атрибутов модели

Если ваше приложение хранит в базе данных конфиденциальную информацию, например, ключи API или PII, вы можете воспользоваться кастом encrypted модели Laravel. Эта модель автоматически шифрует данные перед их сохранением в базе данных и затем расшифровывает их при получении.

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

Например, чтобы зашифровать поле my_secret_field в модели User, можно обновить модель следующим образом:

class User extends Model
{
protected $casts = [
'my_secret_field' => 'encrypted',
];
}

Теперь, когда он определён как аксессор, вы можете продолжать использовать поле как обычно. Таким образом, если бы мы захотели обновить значение, хранящееся в поле my_secret_field, мы могли бы использовать метод update следующим образом:

$user->update(['my_secret_field' => 'hello123']);

Обратите внимание, что нам не требуется шифровать данные перед передачей их в метод update.

Если теперь просмотреть строку в базе данных, то в поле my_secret_field пользователя не будет написано "hello123". Вместо этого вы увидите его зашифрованную версию, например, следующую:

eyJpdiI6IjM3MUxuV0lKc2RjSGNYT2dXanhKeXc9PSIsInZhbHVlIjoiNmxPZjUray9ZV21Ba1RnRkFNdHRTZz09IiwibWFjIjoiNTNlNmU0YTY5OGFjZWU2OGJiYzY4OWYzYzExYjMzNTI0MDQ2YTJiM2M4YWZkMjkyMGQxNmQ2MmYwNzQyNGFjYSIsInRhZyI6IiJ9

Благодаря касту encrypted модели мы все равно сможем использовать предполагаемое значение поля. Например,

$result = $user->my_secret_field;

// $result равен: "hello123"

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

Однако она имеет ряд ограничений, о которых следует знать. Во-первых, поскольку шифрование и дешифрование выполняются при хранении и получении Модели, вызовы, использующие фасад DB, не будут автоматически выполнять аналогичные действия. Поэтому, если вы собираетесь выполнять какие-либо запросы к базе данных с помощью фасада DB (а не с помощью таких функций, как Model::find() или Model::get()), то вам придётся вручную обрабатывать шифрование.

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

Использование пользовательского ключа шифрования при ручном шифровании данных

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

Если вы вручную шифруете и расшифровываете данные, то для определения собственного ключа шифрования вы можете вызвать класс \Illuminate\Encryption\Encrypter и передать ключ в конструктор. Например, представим, что мы хотим зашифровать некоторые данные, используя другой ключ шифрования. Наш код может выглядеть следующим образом:

use Illuminate\Encryption\Encrypter;

// Наш ключ шифрования:
$key = 'U2x2QdvosFTtk5nL0ejrKqLFP1tUDtSt';

$encrypter = new Encrypter(
key: $key,
cipher: config('app.cipher'),
);

$encryptedValue = $encrypter->encrypt('hello');

Как видите, это несложно сделать и добавляет гибкости в использовании пользовательского ключа шифрования. Теперь, если попытаемся расшифровать переменную $encryptedValue, нам потребуется использовать тот же ключ, который мы использовали для её шифрования, и мы не сможем запустить вспомогательную функцию decrypt(), поскольку она не будет использовать правильный ключ.

Использование пользовательского ключа шифрования при использовании кастов Модели

Если вы используете каст ecrypted модели, о чем мы рассказывали ранее в этой статье, и хотите использовать пользовательский ключ для зашифрованных полей, вы можете определить собственный шифровальщик для Laravel с помощью метода Model::encryptUsing.

Обычно мы хотим сделать это в рамках сервис-провайдера (например, App\Providers\AppServiceProvider), чтобы пользовательский шифровальщик был определён и готов к использованию при запуске приложения.

Рассмотрим пример использования метода Model::encryptUsing в нашем AppServiceProvider:

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Encryption\Encrypter;

final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->defineCustomModelEncrypter();
}

private function defineCustomModelEncrypter(): void
{
// Наш ключ шифрования:
$key = 'U2x2QdvosFTtk5nL0ejrKqLFP1tUDtSt';

$encrypter = new Encrypter(
key: $key,
cipher: config('app.cipher'),
);

Model::encryptUsing($encrypter);
}

// ...
}

Как видно из приведённого примера, определение пользовательского шифровальщика очень похоже на то, как мы определяли его вручную ранее в этой статье. Единственное отличие заключается в том, что мы передаём объект Encrypter в метод Model::encryptUsing, чтобы Laravel мог использовать его за сценой.

Шифрование файла .env

Начиная с версии Laravel 9.32.0, файл .env приложения также может быть зашифрован. Это полезно, если вы хотите хранить конфиденциальную информацию в файле .env в системе контроля исходного кода (например, git), но не хотите хранить её в виде открытого текста. Это также полезно, поскольку позволяет предоставлять локальные версии переменных конфигурации .env другим разработчикам в вашей команде для локальной разработки.

Для шифрования файла окружения необходимо выполнить следующую команду в корне проекта:

php artisan env:encrypt

В результате будет получен вывод, аналогичный этому:

INFO  Environment successfully encrypted.
Key ........................ base64:amNvB/EvaX1xU+5R9Z37MeKR8gyeIRxh1Ku0pqNlK1Y
Cipher ............................................................ AES-256-CBC
Encrypted file ................................................. .env.encrypted

Как видно из вывода, команда использовала ключ base64:amNvB/EvaX1xU+5R9Z37MeKR8gyeIRxh1Ku0pqNlK1Y для шифрования файла .env с помощью шифра AES-256-CBC, а затем сохранила зашифрованное значение в файле .env.encrypted.

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

Для расшифровки файла необходимо выполнить команду php artisan env:decrypt и передать ключ, использованный для шифрования файла. Например,

php artisan env:decrypt base64:amNvB/EvaX1xU+5R9Z37MeKR8gyeIRxh1Ku0pqNlK1Y

Это приведёт к расшифровке файла .env.encrypted и сохранению расшифрованных значений в файле .env.

Заключение

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

Дополнительные материалы

Предыдущая Статья

Понимание логических свойств CSS

Следующая Статья

12 лучших практик безопасности Laravel на 2023 год