Upgrade to 3.9.8
This commit is contained in:
@@ -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
|
||||
*
|
||||
|
@@ -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]);
|
||||
}
|
||||
|
||||
|
216
kirby/src/Toolkit/SymmetricCrypto.php
Normal file
216
kirby/src/Toolkit/SymmetricCrypto.php
Normal 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 = '';
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user