Upgrade to 3.9.8

This commit is contained in:
Bastian Allgeier
2023-11-14 12:19:47 +01:00
parent 474ecd14c7
commit f96b96af76
53 changed files with 1383 additions and 943 deletions

View File

@@ -59,6 +59,77 @@ class A
return count($array);
}
/**
* Checks if every element in the array passes the test
*
* <code>
* $array = [1, 30, 39, 29, 10, 13];
*
* $isBelowThreshold = fn($value) => $value < 40;
* echo A::every($array, $isBelowThreshold) ? 'true' : 'false';
* // output: 'true'
*
* $isIntegerKey = fn($value, $key) => is_int($key);
* echo A::every($array, $isIntegerKey) ? 'true' : 'false';
* // output: 'true'
* </code>
*
* @since 3.9.8
* @param array $array
* @param callable(mixed $value, int|string $key, array $array):bool $test
* @return bool
*/
public static function every(array $array, callable $test): bool
{
foreach ($array as $key => $value) {
if (!$test($value, $key, $array)) {
return false;
}
}
return true;
}
/**
* Finds the first element matching the given callback
*
* <code>
* $array = [1, 30, 39, 29, 10, 13];
*
* $isAboveThreshold = fn($value) => $value > 30;
* echo A::find($array, $isAboveThreshold);
* // output: '39'
*
* $array = [
* 'cat' => 'miao',
* 'cow' => 'moo',
* 'colibri' => 'humm',
* 'dog' => 'wuff',
* 'chicken' => 'cluck',
* 'bird' => 'tweet'
* ];
*
* $keyNotStartingWithC = fn($value, $key) => $key[0] !== 'c';
* echo A::find($array, $keyNotStartingWithC);
* // output: 'wuff'
* </code>
*
* @since 3.9.8
* @param array $array
* @param callable(mixed $value, int|string $key, array $array):bool $callback
* @return mixed
*/
public static function find(array $array, callable $callback): mixed
{
foreach ($array as $key => $value) {
if ($callback($value, $key, $array)) {
return $value;
}
}
return null;
}
/**
* Gets an element of an array by key
*
@@ -407,6 +478,37 @@ class A
return array_slice($array, $offset, $length, $preserveKeys);
}
/**
* Checks if at least one element in the array passes the test
*
* <code>
* $array = [1, 30, 39, 29, 10, 'foo' => 12, 13];
*
* $isAboveThreshold = fn($value) => $value > 30;
* echo A::some($array, $isAboveThreshold) ? 'true' : 'false';
* // output: 'true'
*
* $isStringKey = fn($value, $key) => is_string($key);
* echo A::some($array, $isStringKey) ? 'true' : 'false';
* // output: 'true'
* </code>
*
* @since 3.9.8
* @param array $array
* @param callable(mixed $value, int|string $key, array $array):bool $test
* @return bool
*/
public static function some(array $array, callable $test): bool
{
foreach ($array as $key => $value) {
if ($test($value, $key, $array)) {
return true;
}
}
return false;
}
/**
* Sums an array
*

View File

@@ -123,6 +123,10 @@ class Collection extends Iterator implements Countable
*/
public function __unset($key)
{
if ($this->caseSensitive !== true) {
$key = strtolower($key);
}
unset($this->data[$key]);
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Kirby\Toolkit;
use Kirby\Data\Json;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use SensitiveParameter;
/**
* User-friendly and safe abstraction for symmetric
* authenticated encryption and decryption using the
* PHP `sodium` extension
* @since 3.9.8
*
* @package Kirby Toolkit
* @author Lukas Bestle <lukas@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class SymmetricCrypto
{
/**
* Cache for secret keys derived from the password
* indexed by the used salt and limits
*/
protected array $secretKeysByOptions = [];
/**
* Initializes the keys used for crypto, both optional
*
* @param string|null $password Password to be derived into a `$secretKey`
* @param string|null $secretKey 256-bit key, alternatively a `$password` can be used
*/
public function __construct(
#[SensitiveParameter]
protected string|null $password = null,
#[SensitiveParameter]
protected string|null $secretKey = null,
) {
if ($password !== null && $secretKey !== null) {
throw new InvalidArgumentException('Passing both a secret key and a password is not supported');
}
if ($secretKey !== null && strlen($secretKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new InvalidArgumentException('Invalid secret key length, expected ' . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes');
}
}
/**
* Hide values of secrets when printing the object
*/
public function __debugInfo(): array
{
return [
'hasPassword' => isset($this->password),
'hasSecretKey' => isset($this->secretKey),
];
}
/**
* Wipes the secrets from memory when they are no longer needed
*/
public function __destruct()
{
$this->memzero($this->password);
$this->memzero($this->secretKey);
foreach ($this->secretKeysByOptions as $key => &$value) {
$this->memzero($value);
unset($this->secretKeysByOptions[$key]);
}
}
/**
* Decrypts JSON data encrypted by `SymmetricCrypto::encrypt()` using the secret key or password
*
* <code>
* // decryption with a password
* $crypto = new SymmetricCrypto(password: 'super secure');
* $plaintext = $crypto->decrypt('a very confidential string');
*
* // decryption with a previously generated key
* $crypto = new SymmetricCrypto(secretKey: $secretKey);
* $plaintext = $crypto->decrypt('{"mode":"secretbox"...}');
* </code>
*/
public function decrypt(string $json): string
{
$props = Json::decode($json);
if (($props['mode'] ?? null) !== 'secretbox') {
throw new InvalidArgumentException('Unsupported encryption mode "' . ($props['mode'] ?? '') . '"');
}
if (
isset($props['data']) !== true ||
isset($props['nonce']) !== true ||
isset($props['salt']) !== true ||
isset($props['limits']) !== true
) {
throw new InvalidArgumentException('Input data does not contain all required props');
}
$data = base64_decode($props['data']);
$nonce = base64_decode($props['nonce']);
$salt = base64_decode($props['salt']);
$limits = $props['limits'];
$plaintext = sodium_crypto_secretbox_open($data, $nonce, $this->secretKey($salt, $limits));
if (is_string($plaintext) !== true) {
throw new LogicException('Encrypted string was tampered with');
}
return $plaintext;
}
/**
* Encrypts a string using the secret key or password
*
* <code>
* // encryption with a password
* $crypto = new SymmetricCrypto(password: 'super secure');
* $ciphertext = $crypto->encrypt('a very confidential string');
*
* // encryption with a random key
* $crypto = new SymmetricCrypto();
* $ciphertext = $crypto->encrypt('a very confidential string');
* $secretKey = $crypto->secretKey();
* </code>
*/
public function encrypt(
#[SensitiveParameter]
string $string
): string {
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES);
$limits = [SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE];
$key = $this->secretKey($salt, $limits);
$ciphertext = sodium_crypto_secretbox($string, $nonce, $key);
// bundle all necessary information in a JSON object;
// always include the salt and limits to hide whether a key or password was used
return Json::encode([
'mode' => 'secretbox',
'data' => base64_encode($ciphertext),
'nonce' => base64_encode($nonce),
'salt' => base64_encode($salt),
'limits' => $limits,
]);
}
/**
* Checks if the required PHP `sodium` extension is available
*/
public static function isAvailable(): bool
{
return defined('SODIUM_LIBRARY_MAJOR_VERSION') === true && SODIUM_LIBRARY_MAJOR_VERSION >= 10;
}
/**
* Returns the binary secret key, optionally derived from the password
* or randomly generated
*
* @param string|null $salt Salt for password-based key derivation
* @param array|null $limits Processing limits for password-based key derivation
*/
public function secretKey(
#[SensitiveParameter]
string|null $salt = null,
array|null $limits = null
): string {
if (isset($this->secretKey) === true) {
return $this->secretKey;
}
// derive from password
if (isset($this->password) === true) {
if ($salt === null || $limits === null) {
throw new InvalidArgumentException('Salt and limits are required when deriving a secret key from a password');
}
// access from cache
$options = $salt . ':' . implode(',', $limits);
if (isset($this->secretKeysByOptions[$options]) === true) {
return $this->secretKeysByOptions[$options];
}
return $this->secretKeysByOptions[$options] = sodium_crypto_pwhash(
SODIUM_CRYPTO_SECRETBOX_KEYBYTES,
$this->password,
$salt,
$limits[0],
$limits[1],
SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
);
}
// generate a random key
return $this->secretKey = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
}
/**
* Wipes a variable from memory if it is a string
*/
protected function memzero(mixed &$value): void
{
if (is_string($value) === true) {
sodium_memzero($value);
$value = '';
}
}
}