Как работает Шифрование в Laravel

Laravel шифрование

«Моя система безопасна, она использует шифрование». Вы часто слышите/говорите это. Но как и почему это безопасно? Вы действительно знаете это?

Основы шифрования/дешифрования

Шифрование/дешифрование в Laravel основано на классе Illuminate\Encryption\Encrypter,
который использует ключ шифрования и шифр (т. е. Алгоритм шифрования):

__construct($key, $cipher = 'AES-128-CBC')

Он поддерживает (среди прочих) следующие основные методы:

  • encrypt($value, $serialize = true)
  • decrypt($payload, $unserialize = true)

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

$encrypter = new Illuminate\Encryption\Encrypter('1234567812345678', 'AES-128-CBC');

$encrypted = $encrypter->encrypt('Hello world');
dump($encrypted);
/* выведет что-то похожее на 
"eyJpdiI6ImdMd2dWcW5jMXBrUDBranRJZXQ5MEE9PSIsInZhbHVlIjoiNnhTODBSclB3ZVp3SFR
RUWFWTHpReFQwYWQ1aXVmTmhXOXV5WHM2TzR1WT0iLCJtYWMiOiIwODQyZDhiMzZlND
QwZTZjYTRiYmI2MGE0MTgzNzk5NGNkZTU1Yzc5NDIyYzdjYmYwNzk2ZTA5MGNjYjc4MGYzIn0="
*/

$decrypted = $encoder->decrypt($encrypted);
dump($decrypted);
// выведет снова "Hello world"

Это круто! И достаточно, для использования наилучшим способом.

Однако, если вы хотите знать, как работает вся эта кухня, то читайте дальше.

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

Как работает шифрование

В настоящее время шифровальщик Laravel использует OpenSSL для шифрования AES-256 и AES-128. Он также использует защиту Имитовставка (MAC — message authentication code), гарантирующую, что данные не будут изменены после шифрования.

Вы можете подумать, что зашифрованная строка из предыдущего примера «eyJpdiI6ImdMd2dWcW5jMXBrUDBranRJZXQ5MEE9PSIsInZhbHVlIjoiNnhTODBSclB3ZVp3SFRRUWFWTHpReFQwYWQ1aXVmTmhXOXV5WHM2TzR1WT0iLCJtYWMiOiIwODQyZDhiMzZlNDQwZTZjYTRiYmI2MGE0MTgzNzk5NGNkZTU1Yzc5NDIyYzdjYmYwNzk2ZTA5MGNjYjc4MGYzIn0=» это зашифрованная версия входных данных. Отчасти это верно, но тут есть кое-что ещё.

В сущности это строка, закодированная в base64. «Какая строка?», спросите вы… И можете получить ответ, просто запустив:

$encrypted = $encrypter->encrypt('Hello world');
$decodedEncrypted = base64_decode($encrypted);

в результате получите json-строку, аналогичную следующей:

{
  "iv":"gLwgVqnc1pkP0kjtIet90A==",
  "value":"6xS80RrPweZwHTQQaVLzQxT0ad5iufNhW9uyXs6O4uY=",
  "mac":"0842d8b36e440e6ca4bbb60a41837994cde55c79422c7cbf0796e090ccb780f3"
}

Стало еще более непонятно, что же это?

Эти данные состоят из трех основных частей шифрования:

  • value: основные зашифрованные данные в формате base64
  • iv: вектор инициализации — это сгенерированная случайная последовательность данных фиксированного размера, подключаемая при каждом запуске, предотвращающая семантические атаки. Также в формате base64.
  • mac: имитовставка — сигнатура, используемая для обнаружения взлома блока value, сгенерированного хеширования value и iv. Представлен в шестнадцатеричном формате

Обратите внимание, что и iv и value закодированы в base64, так как они представляют собой общую последовательность байтов и могут содержать непечатаемые символы.

Код шифрования

Чтобы понять, как генерируется полезная нагрузка, давайте подробнее рассмотрим метод encrypt():

public function encrypt($value, $serialize = true)
{
    $iv = random_bytes (openssl_cipher_iv_length ($this->cipher));

    $value = \openssl_encrypt (
        $serialize ? serialize ($value) : $value,
        $this->cipher,
        $this->key,
        0,
        $iv
    );

    if ($value === false) {
        throw new EncryptException('Could not encrypt the data.');
    }

    $mac = $this->hash ($iv = base64_encode ($iv), $value);

    $json = json_encode (compact ('iv', 'value', 'mac'), JSON_UNESCAPED_SLASHES);

    if (json_last_error () !== JSON_ERROR_NONE) {
        throw new EncryptException('Could not encrypt the data.');
    }

    return base64_encode ($json);
}

В методе выполняется 5 шагов:

  1. Вектор инициализации генерируется в строке 3 — 128 или 256 бит (в зависимости от используемого шифра) случайных данных
  2. Зашифрованные данные создаются в строках 5-8 с помощью OpenSSL на (возможно) сериализованных данных с использованием выбранного шифра, ключа шифрования и IV. Обратите внимание, что результаты закодированы в base64
  3. MAC генерируется методом hash(), к которому добавляются iv в формате base64 и value. Хеширование задаётся как:
    protected function hash($iv, $value)
    {
        return hash_hmac('sha256', $iv.$value, $this->key);
    }

    т. е. SHA256-хеширование объединенных IV и value с использованием предоставленного ключа шифрования.

  4. Генерируется массив , содержащий iv, value и mac и преобразуется в JSON в строке 16
  5. JSON кодируется в base64 и возвращается в строка 22

Код дешифрования

Чтобы понять, как восстанавливаются чистые данные, давайте подробнее рассмотрим метод decrypt():

public function decrypt($payload, $unserialize = true)
{
    $payload = $this->getJsonPayload ($payload);

    $iv = base64_decode ($payload['iv']);

    // Здесь мы расшифровываем value.
    // Если мы сможем его успешно расшифровать, 
    // то десериализуем его и возвращаем вызывающему
    // Если же нет, то выбрасываем исключение
    $decrypted = \openssl_decrypt (
        $payload['value'],
        $this->cipher,
        $this->key,
        0,
        $iv
    );

    if ($decrypted === false) {
        throw new DecryptException('Could not decrypt the data.');
    }

    return $unserialize ? unserialize ($decrypted) : $decrypted;
}

В методе выполняется 3 шага:

  1. Извлекается полезная JSON-нагрузка в строке 3. Во время извлечения происходит валидация:
    • Это массив
    • Он содержит поля iv, value и mac.
    • Длина iv совместима с требованиями шифра
    • mac валиден
  2. Данные расшифровываются с использованием OpenSSL в строках 5-12
  3. Результат (возможно) десериализуется и возвращается

Почему это безопасно?

Эта схема обеспечивает безопасность до тех пор, пока ключ шифрования храниться в секрете. Посмотрим почему:

  • Конфиденциальность: данные могут быть расшифрованы только тем, кто знает секретный ключ
  • Целостность: если данные будут изменены, то расшифровать их не получится. Если iv и value будут модифицированы, то потенциально сообщение можно расшифровать, но защита Имитовставки обнаружит несанкционированный доступ и расшифровать данные не удастся. В любом случае, изменяя любую комбинацию iv и/или value и/или mac, расшифровка завершается неудачно из-за повреждения полезной нагрузки.
  • Единственный способ обмануть защиту Имитовставки — это знание ключа шифрования, который позволяет создавать новые валидные зашифрованные данные.

Давайте попробуем создать другое зашифрованное сообщение:

$encrypted2 = $encrypter->encrypt('Hello hacker');
$decodedEncrypted2 = json_decode(base64_decode($encrypted2), true);
dump('DECODED ENCRYPTED 2: ');
var_dump($decodedEncrypted2);

Теперь попробуем подменить значения и расшифровать результат.

// изменяем зашифрованные данные и пытаемся расшифровать
try {
    $tampered = $decodedEncrypted;
    $tampered['value'] = $decodedEncrypted2['value'];
    $encrypter->decrypt(base64_encode(json_encode($tampered)));
} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
    dump($exception->getMessage());
}

// изменяем iv и пытаемся расшифровать
try {
    $tampered = $decodedEncrypted;
    $tampered['iv'] = $decodedEncrypted2['iv'];
    $encrypter->decrypt(base64_encode(json_encode($tampered)));
} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
    dump($exception->getMessage());
}

// изменяем MAC и пытаемся расшифровать
try {
    $tampered = $decodedEncrypted;
    $tampered['mac'] = $decodedEncrypted2['mac'];
    $encrypter->decrypt(base64_encode(json_encode($tampered)));
} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
    dump($exception->getMessage());
}

В каждом из трех случаев Имитовставка (MAC) предотвратит расшифровку сообщения и выбросит исключение DecryptException.

Выводы

Теперь вы досконально знаете как работает шифрование в Laravel. Для вас никак не изменится способы его использования, но теперь вы сможете доверять этому инструменту. Более того, теперь вы сможете обосновать клиенту, насколько безопасна ваша система.

Автор: Roberto Gallea
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.