92 lines
2.9 KiB
PHP
92 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Secrets;
|
|
|
|
use Illuminate\Encryption\Encrypter;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* The SECRETS_KEY encrypter, in one place.
|
|
*
|
|
* Its own key rather than APP_KEY, because rotating APP_KEY is ordinary
|
|
* maintenance and would otherwise render every stored credential unreadable —
|
|
* discovered when Stripe stops answering, not when it happens.
|
|
*
|
|
* Extracted from SecretVault so mailbox passwords use the SAME key handling
|
|
* rather than a second copy of it. The copy is what would rot: the two
|
|
* spellings below exist because requiring the `base64:` prefix once made every
|
|
* read and write fail on exactly the setup the config comment documents.
|
|
*/
|
|
final class SecretCipher
|
|
{
|
|
public function encrypt(string $plain): string
|
|
{
|
|
return $this->encrypter()->encryptString($plain);
|
|
}
|
|
|
|
public function decrypt(string $cipher): string
|
|
{
|
|
return $this->encrypter()->decryptString($cipher);
|
|
}
|
|
|
|
/**
|
|
* Is storing credentials possible at all on this installation?
|
|
*
|
|
* Delegates to the exact same resolveKey() that encrypter() below checks
|
|
* its input against, rather than a second, shorter rule that only looks
|
|
* at emptiness. A malformed-but-nonempty key (wrong length, garbage
|
|
* base64) used to pass a bare "!== ''" check while encrypter() rejected
|
|
* it — which let EditMailbox::save() and MailboxTester::run() sail past
|
|
* the very guard they call this method for, straight into the uncaught
|
|
* RuntimeException it exists to prevent.
|
|
*/
|
|
public function isUsable(): bool
|
|
{
|
|
return $this->resolveKey() !== null;
|
|
}
|
|
|
|
/**
|
|
* The raw, exactly-32-byte key — or null if SECRETS_KEY is empty or
|
|
* malformed. The one place that decides "is this key any good", so
|
|
* isUsable() and encrypter() read it from the same rule and cannot
|
|
* disagree about a value either one is given.
|
|
*/
|
|
private function resolveKey(): ?string
|
|
{
|
|
$key = (string) config('admin_access.secrets_key');
|
|
|
|
if ($key === '') {
|
|
return null;
|
|
}
|
|
|
|
if (str_starts_with($key, 'base64:')) {
|
|
$key = substr($key, 7);
|
|
}
|
|
|
|
if (strlen($key) !== 32) {
|
|
$decoded = base64_decode($key, true);
|
|
|
|
if ($decoded !== false && strlen($decoded) === 32) {
|
|
$key = $decoded;
|
|
}
|
|
}
|
|
|
|
return strlen($key) === 32 ? $key : null;
|
|
}
|
|
|
|
private function encrypter(): Encrypter
|
|
{
|
|
$key = $this->resolveKey();
|
|
|
|
if ($key === null) {
|
|
throw new RuntimeException(
|
|
(string) config('admin_access.secrets_key') === ''
|
|
? 'SECRETS_KEY is not set — refusing to store or read credentials.'
|
|
: 'SECRETS_KEY must be 32 bytes (or base64 of 32 bytes).'
|
|
);
|
|
}
|
|
|
|
return new Encrypter($key, 'aes-256-cbc');
|
|
}
|
|
}
|