1
0
Files
breadcrumb-the-shire/core/Support/Crypto.php
fs 0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00

132 lines
3.9 KiB
PHP

<?php
namespace MintyPHP\Support;
// AES-256-GCM encryption/decryption for sensitive stored values (e.g. SSO secrets).
// Requires APP_CRYPTO_KEY — see EnvValidator for accepted key formats.
class Crypto
{
private const CIPHER = 'aes-256-gcm';
public static function isConfigured(): bool
{
return self::getKey() !== null;
}
public static function encryptString(string $plaintext): string
{
$key = self::getKey();
if ($key === null) {
throw new \RuntimeException('Missing APP_CRYPTO_KEY');
}
$iv = random_bytes(12);
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
self::CIPHER,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if (!is_string($ciphertext) || strlen($tag) !== 16) {
throw new \RuntimeException('Encryption failed');
}
// Outer base64 wraps a JSON object with v (version), iv, auth tag, and ciphertext —
// all inner binary values are base64url-encoded to stay JSON-safe.
$payload = json_encode([
'v' => 1,
'iv' => self::base64UrlEncode($iv),
'tag' => self::base64UrlEncode($tag),
'ct' => self::base64UrlEncode($ciphertext),
]);
if (!is_string($payload)) {
throw new \RuntimeException('Encryption payload encoding failed');
}
return base64_encode($payload);
}
public static function decryptString(string $encoded): string
{
$key = self::getKey();
if ($key === null) {
throw new \RuntimeException('Missing APP_CRYPTO_KEY');
}
$json = base64_decode($encoded, true);
if (!is_string($json) || $json === '') {
throw new \RuntimeException('Encrypted payload is invalid');
}
$payload = json_decode($json, true);
if (!is_array($payload)) {
throw new \RuntimeException('Encrypted payload cannot be decoded');
}
$iv = self::base64UrlDecode((string) ($payload['iv'] ?? ''));
$tag = self::base64UrlDecode((string) ($payload['tag'] ?? ''));
$ciphertext = self::base64UrlDecode((string) ($payload['ct'] ?? ''));
if ($iv === '' || strlen($iv) !== 12 || $tag === '' || strlen($tag) !== 16 || $ciphertext === '') {
throw new \RuntimeException('Encrypted payload is malformed');
}
$plaintext = openssl_decrypt(
$ciphertext,
self::CIPHER,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if (!is_string($plaintext)) {
throw new \RuntimeException('Decryption failed');
}
return $plaintext;
}
private static function getKey(): ?string
{
$raw = defined('APP_CRYPTO_KEY') ? trim((string) APP_CRYPTO_KEY) : '';
if ($raw === '') {
return null;
}
// Accepts 64-char hex (32 bytes) or base64-encoded 32 bytes — both yield a 256-bit key.
$hex = preg_match('/^[0-9a-f]{64}$/i', $raw) ? hex2bin($raw) : false;
if (is_string($hex) && strlen($hex) === 32) {
return $hex;
}
$base64 = base64_decode($raw, true);
if (is_string($base64) && strlen($base64) === 32) {
return $base64;
}
return null;
}
// URL-safe base64: replaces +/ with -_ and strips = padding.
private static function base64UrlEncode(string $value): string
{
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
}
private static function base64UrlDecode(string $value): string
{
$value = strtr($value, '-_', '+/');
$pad = strlen($value) % 4;
if ($pad > 0) {
$value .= str_repeat('=', 4 - $pad);
}
$decoded = base64_decode($value, true);
return is_string($decoded) ? $decoded : '';
}
}