Skip to content

Commit

Permalink
new encryption service provider; router deprecation fix
Browse files Browse the repository at this point in the history
  • Loading branch information
donwilson committed Jan 11, 2024
1 parent 66b5961 commit fee5208
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 8 deletions.
4 changes: 2 additions & 2 deletions src/Magnetar/Auth/AuthManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public function logout(): void {
}

/**
* Remember the user by looking up the 'remember me' cookie
* See if we remember the user by looking up the 'remember me' cookie
* @return bool
*
* @throws \Magnetar\Auth\Exceptions\AuthorizationException
Expand All @@ -147,7 +147,7 @@ public function remember(): bool {
// decode and decrypt cookie
$cookie = (new Encryption(
$this->app['config']['app.key'],
null,//$this->app['config']['app.digest'],
$this->app['config']['app.digest'],
$this->app['config']['app.cipher']
))->decrypt($raw_cookie);

Expand Down
195 changes: 195 additions & 0 deletions src/Magnetar/Encryption/Encryption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);

namespace Magnetar\Encryption;

use Magnetar\Encryption\Exceptions\EncryptionException;
use Magnetar\Utilities\Str;

/**
* Encryption utility static class
*/
class Encryption {
/**
* The digest method to use for encryption/decryption.
* @var string
*
* @see https://www.php.net/manual/en/function.openssl-get-md-methods.php
*/
protected string $digest_method = 'SHA256';

/**
* The cipher method to use for encryption/decryption.
* @var string
*
* @see https://www.php.net/manual/en/function.openssl-get-cipher-methods.php
*/
protected string $cipher_method = 'AES-256-CBC';

/**
* Encryption constructor.
* @param string $salt Effectively a 'password' to an encrypted block of text. Encrypting with one salt and decrypting with another will result in garbage data.
* @param string|null $digest_method Optional. A value from openssl_get_md_methods()
* @param string|null $cipher_method Optional. A value from openssl_get_cipher_methods()
*
* @see https://www.php.net/manual/en/function.openssl-get-md-methods.php
* @see https://www.php.net/manual/en/function.openssl-get-cipher-methods.php
*/
public function __construct(
protected string $salt,
string|null $digest_method=null,
string|null $cipher_method=null
) {
if(str_contains($this->salt, 'base64:')) {
$this->salt = base64_decode(substr(trim($this->salt), 7));
}

if(null !== $digest_method) {
$this->digest_method = $digest_method;
}

if(null !== $cipher_method) {
$this->cipher_method = $cipher_method;
}
}

/**
* Encrypt a string with config-based key/settings that can passed on and be decrypted via Encryption->decrypt. Returns false on error (usually from bad digest_method/cipher_method settings)
* @param string $string String to encrypt
* @param bool $serialize Whether to serialize the string before encrypting it
* @return string|false
*
* @throws \Magnetar\Encryption\Exceptions\EncryptionException
*/
public function encrypt(string $string, bool $serialize=true): string|false {
// generate initialization vector
$iv = random_bytes(openssl_cipher_iv_length($this->cipher_method));

// encrypt value
$value = openssl_encrypt(
$serialize ? serialize($string) : $string,
strtolower($this->cipher_method),
openssl_digest($this->salt, $this->digest_method, true),
0,
$iv,
$tag
);

if(false === $value) {
throw new EncryptionException('Failed to encrypt data');
}

// encode iv and tag
$iv = base64_encode($iv);
$tag = base64_encode($tag ?? '');

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

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

if(JSON_ERROR_NONE !== json_last_error()) {
throw new EncryptionException('Failed to generate cargo of encrypted data');
}

return base64_encode($json);
}

/**
* Decrypt a previously encrypted string from Encryption->encrypt. Returns false on error (mostly from wrong encrypted data or a changed encryption key, or sometimes from bad digest_method/cipher_method settings)
* @param string $payload Payload to decrypt
* @param bool $unserialize Whether to unserialize the decrypted string
* @return string|false
*
* @throws \Magnetar\Encryption\Exceptions\EncryptionException
*/
public function decrypt(string $payload, bool $unserialize=true): string|false {
$payload = json_decode(base64_decode($payload), true);

if(JSON_ERROR_NONE !== json_last_error()) {
throw new EncryptionException('Failed to decode encrypted data');
}

if(!isset($payload['iv'], $payload['value'], $payload['mac'], $payload['tag'])) {
throw new EncryptionException('Failed to decode encrypted data');
}

// decode iv and tag
$iv = base64_decode($payload['iv']);
$tag = base64_decode($payload['tag']);

// validate mac
if(!$this->isValidMac($payload)) {
throw new EncryptionException('Failed to validate mac');
}

// decrypt value
$value = openssl_decrypt(
$payload['value'],
strtolower($this->cipher_method),
openssl_digest($this->salt, $this->digest_method, true),
0,
$iv,
$tag
);

if(false === $value) {
throw new EncryptionException('Failed to decrypt data');
}

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

/**
* Hash a string with the salt
* @param string $iv Initialization vector
* @param string $value Value to hash
* @return string
*/
protected function hash(string $iv, string $value): string {
return hash_hmac(
'sha256',
$iv . $value,
$this->salt
);
}

/**
* Validate a decrypted payload
* @param mixed $payload Payload to validate
* @return bool
*/
protected function isValidPayload(mixed $payload): bool {
if(!is_array($payload)) {
return false;
}

foreach(['iv', 'value', 'mac'] as $key) {
if(!isset($payload[ $key ]) || !is_string($payload[ $key ])) {
return false;
}
}

if(isset($payload['tag']) && !is_string($payload['tag'])) {
return false;
}

if(strlen(base64_decode($payload['iv'], true)) !== openssl_cipher_iv_length(strtolower($this->cipher_method))) {
return false;
}

return true;
}

/**
* Validate the mac of a decrypted payload
* @param array $payload Payload to validate
* @return bool
*/
protected function isValidMac(array $payload): bool {
return hash_equals(
$payload['mac'],
$this->hash($payload['iv'], $payload['value'])
);
}
}
35 changes: 35 additions & 0 deletions src/Magnetar/Encryption/EncryptionServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);

namespace Magnetar\Encryption;

use Magnetar\Helpers\ServiceProvider;
use Magnetar\Encryption\Encryption;

/**
* Provider for data encryption services
*/
class EncryptionServiceProvider extends ServiceProvider {
/**
* {@inheritDoc}
*/
public function register(): void {
// register connection services
$this->app->singleton('encryption', function() {
return new Encryption(
$this->app->config['app.key'],
$this->app->config['app.digest'] ?? 'sha256',
$this->app->config['app.cipher']
);
});
}

/**
* {@inheritDoc}
*/
public function provides(): array {
return [
'encryption',
];
}
}
13 changes: 13 additions & 0 deletions src/Magnetar/Encryption/Exceptions/EncryptionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);

namespace Magnetar\Encryption\Exceptions;

use Exception;

/**
* Exception thrown during encryption/decryption
*/
class EncryptionException extends Exception {

}
1 change: 1 addition & 0 deletions src/Magnetar/Helpers/DefaultServiceProviders.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function __construct(
\Magnetar\Cache\CacheServiceProvider::class,
\Magnetar\Http\CookieJar\CookieJarServiceProvider::class,
\Magnetar\Database\DatabaseServiceProvider::class,
\Magnetar\Encryption\EncryptionServiceProvider::class,
\Magnetar\Filesystem\FilesystemServiceProvider::class,
\Magnetar\Hashing\HashingServiceProvider::class,
\Magnetar\Queue\QueueServiceProvider::class,
Expand Down
2 changes: 1 addition & 1 deletion src/Magnetar/Router/RouteCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public function formatNameWithPrefix(string $name): string {
* @return Route
*/
public function makeRoute(
HTTPMethodEnum|array|string|null $method=null,
HTTPMethodEnum|array|string|null $method,
string $pattern
): Route {
// create and add the route to the collection
Expand Down
7 changes: 2 additions & 5 deletions src/Magnetar/Router/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,11 @@ protected function assignRoute(
* @return Route The generated route
*/
protected function makeRoute(
HTTPMethodEnum|array|string|null $method=null,
HTTPMethodEnum|array|string|null $method,
string $pattern
): Route {
// create a route using the route collection
return $this->routeCollection->makeRoute(
$method,
$pattern
);
return $this->routeCollection->makeRoute($method, $pattern);
}

/**
Expand Down
28 changes: 28 additions & 0 deletions src/Magnetar/_autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,20 @@ function data_path(string $rel_path=''): string {
}
}

if(!function_exists('decrypt')) {
/**
* Decrypt a string that was encrypted via Encryption->encrypt
* @param string $string String to decrypt
* @param bool $unserialize Whether to unserialize the decrypted string
* @return string|false
*
* @throws \Magnetar\Encryption\Exceptions\EncryptionException
*/
function decrypt(string $string, bool $unserialize=true): string|false {
return app('encryption')->decrypt($string, $unserialize);
}
}

if(!function_exists('display')) {
/**
* Generate a Response by processing a template from the active theme
Expand All @@ -206,6 +220,20 @@ function display_tpl(string $template_name, mixed $data=[]): void {
}
}

if(!function_exists('encrypt')) {
/**
* Encrypt a string with config-based key/settings that can passed on and be decrypted via Encryption->decrypt. Returns false on error (usually from bad digest_method/cipher_method settings)
* @param string $string String to encrypt
* @param bool $serialize Whether to serialize the string before encrypting it
* @return string|false
*
* @throws \Magnetar\Encryption\Exceptions\EncryptionException
*/
function encrypt(string $string, bool $serialize=true): string|false {
return app('encryption')->encrypt($string, $serialize);
}
}

if(!function_exists('env')) {
/**
* Get an environment variable
Expand Down

0 comments on commit fee5208

Please sign in to comment.