Skip to content

Instantly share code, notes, and snippets.

@milo
Last active January 28, 2019 09:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milo/6fe76f4085871ad8b19eb9c19a36813b to your computer and use it in GitHub Desktop.
Save milo/6fe76f4085871ad8b19eb9c19a36813b to your computer and use it in GitHub Desktop.
Crypt with OpenSSL

Single purpose class for encrypt and decrypt text with signing. Use at your own risk, I'm not a security guru. But I hope it is strong enough, it is more or less extracted from https://paragonie.com/ blog articles.

I used to use mcrypt on legacy projects, but it is dropped now in PHP 7.2. Until PHP 7.2 came to be mainstream with Sodium, I wrote this simple OpenSSL wrapper to be usable with PHP 5.6 to 7.2.

Usage:

$crypt = new Milo\Crypt;

$secretKey = 'Lkd:LKuweusn,AKjkjhskjmmNBEWKJhs';  # needs to be 32 bytes long
$signKey = 'By Milo lkjaj3yiujkeqr987m,na,msnd';  # this is secret too

$msg = $crypt->encrypt('Nazdar bazar.', $secretKey, $signKey);
echo "Encrypted: $msg\n";
echo "Decrypted: " . $crypt->decrypt($msg, $secretKey, $signKey);

Message encrypted in this way can be transferred via public medium, e.g. via URL. The $secretKey and $signKey are private and must not be published.

<?php
/**
* Copyright (c) 2015 Miloslav Hůla (https://github.com/milo)
*/
namespace Milo;
final class Crypt
{
const
CIPHER_METHOD = 'aes-256-ctr',
CRYPT_KEY_LENGTH = 32,
HASH_ALGORITHM = 'sha256',
HASH_LENGTH = 64;
public function __construct()
{
if (PHP_VERSION_ID < 50600) {
throw new CryptException('PHP 5.6.0 or newer is required.');
} elseif (!extension_loaded('openssl')) {
throw new CryptException('Missing OpenSSL PHP module.');
} elseif (!in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) {
throw new CryptException('Missing OpenSSL encryption method ' . self::CIPHER_METHOD . '.');
}
}
/**
* @param string
* @param string
* @param string
* @param string
* @return string
* @throws CryptException
*/
public function encryptBinary($text, $encryptKey, $signatureKey, $iv = null)
{
if (strlen($encryptKey) !== self::CRYPT_KEY_LENGTH) {
throw new CryptException('Encryption key needs to be ' . self::CRYPT_KEY_LENGTH . ' bytes long, but ' . strlen($encryptKey) . ' given.');
}
if ($iv === null) {
$iv = self::randomBytes(openssl_cipher_iv_length(self::CIPHER_METHOD));
} elseif (($ivLen = strlen($iv)) !== ($ivNeedLen = openssl_cipher_iv_length(self::CIPHER_METHOD))) {
throw new CryptException("IV needs to be exactly $ivNeedLen bytes long, $ivLen given.");
}
$ciphered = $iv . openssl_encrypt(
$text,
self::CIPHER_METHOD,
$encryptKey,
OPENSSL_RAW_DATA,
$iv
);
return hash_hmac(self::HASH_ALGORITHM, $ciphered, $signatureKey) . $ciphered;
}
/**
* @param string
* @param string
* @param string
* @param string
* @return string
* @throws CryptException
*/
public function encrypt($text, $encryptKey, $signatureKey, $iv = null)
{
return base64_encode($this->encryptBinary($text, $encryptKey, $signatureKey, $iv));
}
/**
* @param string
* @param string
* @param string
* @return string
* @throws CryptException
*/
public function decryptBinary($text, $encryptKey, $signatureKey)
{
if (strlen($text) < self::HASH_LENGTH) {
throw new CryptException('Signed encrypted message is only ' . strlen($text) . ' bytes long, expected more.');
}
$hmac = substr($text, 0, self::HASH_LENGTH);
$text = substr($text, self::HASH_LENGTH);
if (!hash_equals(hash_hmac(self::HASH_ALGORITHM, $text, $signatureKey), $hmac)) {
throw new CryptException('Message signature does not match.');
}
$ivLength = openssl_cipher_iv_length(self::CIPHER_METHOD);
if (strlen($text) < $ivLength) {
throw new CryptException('Encrypted message is only ' . strlen($text) . ' bytes long, expected more.');
}
$iv = substr($text, 0, $ivLength);
$text = substr($text, $ivLength);
$decrypted = openssl_decrypt(
$text,
self::CIPHER_METHOD,
$encryptKey,
OPENSSL_RAW_DATA,
$iv
);
if ($decrypted === false) {
throw new CryptException('Message decryption failed.');
}
return $decrypted;
}
/**
* @param string
* @param string
* @param string
* @return string
* @throws CryptException
*/
public function decrypt($text, $encryptKey, $signatureKey)
{
return $this->decryptBinary(base64_decode($text), $encryptKey, $signatureKey);
}
private static function randomBytes($length)
{
if ($length < 1) {
throw new CryptException('Length must be greater then zero.');
}
if (!defined('PHP_WINDOWS_VERSION_BUILD') && is_readable('/dev/urandom')) {
return file_get_contents('/dev/urandom', false, null, -1, $length);
}
$bytes = openssl_random_pseudo_bytes($length, $secure);
if ($secure !== true) {
throw new CryptException('Random bytes are not cryptographically strong.');
}
return $bytes;
}
}
class CryptException extends \RuntimeException
{
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment