Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.
Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
- Provider key: s3compatible
- Reads user-configured endpoint URL from settings
- Uses path-style URL access (required by most S3-compatible services)
- Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
- Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
162 lines
6.4 KiB
PHP
162 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace DeliciousBrains\WP_Offload_Media\Aws3\Aws\Crypto\Polyfill;
|
|
|
|
use DeliciousBrains\WP_Offload_Media\Aws3\Aws\Exception\CryptoPolyfillException;
|
|
use InvalidArgumentException;
|
|
use RangeException;
|
|
/**
|
|
* Class AesGcm
|
|
*
|
|
* This provides a polyfill for AES-GCM encryption/decryption, with caveats:
|
|
*
|
|
* 1. Only 96-bit nonces are supported.
|
|
* 2. Only 128-bit authentication tags are supported. (i.e. non-truncated)
|
|
*
|
|
* Supports AES key sizes of 128-bit, 192-bit, and 256-bit.
|
|
*/
|
|
class AesGcm
|
|
{
|
|
use NeedsTrait;
|
|
/** @var Key $aesKey */
|
|
private $aesKey;
|
|
/** @var int $keySize */
|
|
private $keySize;
|
|
/** @var int $blockSize */
|
|
protected $blockSize = 8192;
|
|
/**
|
|
* AesGcm constructor.
|
|
*
|
|
* @param Key $aesKey
|
|
* @param int $keySize
|
|
* @param int $blockSize
|
|
*
|
|
* @throws CryptoPolyfillException
|
|
* @throws InvalidArgumentException
|
|
* @throws RangeException
|
|
*/
|
|
public function __construct(Key $aesKey, $keySize = 256, $blockSize = 8192)
|
|
{
|
|
/* Preconditions: */
|
|
self::needs(\in_array($keySize, [128, 192, 256], \true), "Key size must be 128, 192, or 256 bits; {$keySize} given", InvalidArgumentException::class);
|
|
self::needs(\is_int($blockSize) && $blockSize > 0 && $blockSize <= \PHP_INT_MAX, 'Block size must be a positive integer.', RangeException::class);
|
|
self::needs($aesKey->length() << 3 === $keySize, 'Incorrect key size; expected ' . $keySize . ' bits, got ' . ($aesKey->length() << 3) . ' bits.');
|
|
$this->aesKey = $aesKey;
|
|
$this->keySize = $keySize;
|
|
}
|
|
/**
|
|
* Encryption interface for AES-GCM
|
|
*
|
|
* @param string $plaintext Message to be encrypted
|
|
* @param string $nonce Number to be used ONCE
|
|
* @param Key $key AES Key
|
|
* @param string $aad Additional authenticated data
|
|
* @param string &$tag Reference to variable to hold tag
|
|
* @param int $keySize Key size (bits)
|
|
* @param int $blockSize Block size (bytes) -- How much memory to buffer
|
|
* @return string
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public static function encrypt($plaintext, $nonce, Key $key, $aad, &$tag, $keySize = 256, $blockSize = 8192)
|
|
{
|
|
self::needs(self::strlen($nonce) === 12, 'Nonce must be exactly 12 bytes', InvalidArgumentException::class);
|
|
$encryptor = new AesGcm($key, $keySize, $blockSize);
|
|
list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
|
|
$ciphertext = \openssl_encrypt($plaintext, "aes-{$encryptor->keySize}-ctr", $key->get(), \OPENSSL_NO_PADDING | \OPENSSL_RAW_DATA, $nonce . "\x00\x00\x00\x02");
|
|
/* Calculate auth tag in a streaming fashion to minimize memory usage: */
|
|
$ciphertextLength = self::strlen($ciphertext);
|
|
for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
|
|
$cBlock = new ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
|
|
$gmac->update($cBlock);
|
|
}
|
|
$tag = $gmac->finish($aadLength, $ciphertextLength)->toString();
|
|
return $ciphertext;
|
|
}
|
|
/**
|
|
* Decryption interface for AES-GCM
|
|
*
|
|
* @param string $ciphertext Ciphertext to decrypt
|
|
* @param string $nonce Number to be used ONCE
|
|
* @param Key $key AES key
|
|
* @param string $aad Additional authenticated data
|
|
* @param string $tag Authentication tag
|
|
* @param int $keySize Key size (bits)
|
|
* @param int $blockSize Block size (bytes) -- How much memory to buffer
|
|
* @return string Plaintext
|
|
*
|
|
* @throws CryptoPolyfillException
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public static function decrypt($ciphertext, $nonce, Key $key, $aad, &$tag, $keySize = 256, $blockSize = 8192)
|
|
{
|
|
/* Precondition: */
|
|
self::needs(self::strlen($nonce) === 12, 'Nonce must be exactly 12 bytes', InvalidArgumentException::class);
|
|
$encryptor = new AesGcm($key, $keySize, $blockSize);
|
|
list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
|
|
/* Calculate auth tag in a streaming fashion to minimize memory usage: */
|
|
$ciphertextLength = self::strlen($ciphertext);
|
|
for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
|
|
$cBlock = new ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
|
|
$gmac->update($cBlock);
|
|
}
|
|
/* Validate auth tag in constant-time: */
|
|
$calc = $gmac->finish($aadLength, $ciphertextLength);
|
|
$expected = new ByteArray($tag);
|
|
self::needs($calc->equals($expected), 'Invalid authentication tag');
|
|
/* Return plaintext if auth tag check succeeded: */
|
|
return \openssl_decrypt($ciphertext, "aes-{$encryptor->keySize}-ctr", $key->get(), \OPENSSL_NO_PADDING | \OPENSSL_RAW_DATA, $nonce . "\x00\x00\x00\x02");
|
|
}
|
|
/**
|
|
* Initialize a Gmac object with the nonce and this object's key.
|
|
*
|
|
* @param string $nonce Must be exactly 12 bytes long.
|
|
* @param string|null $aad
|
|
* @return array
|
|
*/
|
|
protected function gmacInit($nonce, $aad = null)
|
|
{
|
|
$gmac = new Gmac($this->aesKey, $nonce . "\x00\x00\x00\x01", $this->keySize);
|
|
$aadBlock = new ByteArray($aad);
|
|
$aadLength = $aadBlock->count();
|
|
$gmac->update($aadBlock);
|
|
$gmac->flush();
|
|
return [$aadLength, $gmac];
|
|
}
|
|
/**
|
|
* Calculate the length of a string.
|
|
*
|
|
* Uses the appropriate PHP function without being brittle to
|
|
* mbstring.func_overload.
|
|
*
|
|
* @param string $string
|
|
* @return int
|
|
*/
|
|
protected static function strlen($string)
|
|
{
|
|
if (\is_callable('\\mb_strlen')) {
|
|
return (int) \mb_strlen($string, '8bit');
|
|
}
|
|
return (int) \strlen($string);
|
|
}
|
|
/**
|
|
* Return a substring of the provided string.
|
|
*
|
|
* Uses the appropriate PHP function without being brittle to
|
|
* mbstring.func_overload.
|
|
*
|
|
* @param string $string
|
|
* @param int $offset
|
|
* @param int|null $length
|
|
* @return string
|
|
*/
|
|
protected static function substr($string, $offset = 0, $length = null)
|
|
{
|
|
if (\is_callable('\\mb_substr')) {
|
|
return \mb_substr($string, $offset, $length, '8bit');
|
|
} elseif (!\is_null($length)) {
|
|
return \substr($string, $offset, $length);
|
|
}
|
|
return \substr($string, $offset);
|
|
}
|
|
}
|