Files
Martin Slachta 0d829845c4 initial
2026-06-11 19:03:29 +02:00

50 lines
1.8 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Symmetric encryption for secrets kept in wp_options (Google OAuth access /
* refresh tokens and the client secret).
*
* Uses libsodium's secretbox with a 32-byte key derived from the site's
* AUTH_KEY / AUTH_SALT. Ciphertext is prefixed with "rsvenc:" so decrypt() can
* transparently pass through values that were written as plaintext before this
* was introduced — existing installs migrate to encrypted storage on the next
* write (e.g. the next token refresh).
*/
final class RsvCrypto {
private const PREFIX = 'rsvenc:';
public static function encrypt(string $plaintext): string {
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = sodium_crypto_secretbox($plaintext, $nonce, self::key());
return self::PREFIX . base64_encode($nonce . $cipher);
}
/** Returns the plaintext, or null if a ciphertext can't be decrypted. */
public static function decrypt(string $stored): ?string {
if (!str_starts_with($stored, self::PREFIX)) {
return $stored; // legacy plaintext — pass through for migration
}
$raw = base64_decode(substr($stored, strlen(self::PREFIX)), true);
if ($raw === false || strlen($raw) <= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) {
return null;
}
$nonce = substr($raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = substr($raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$plain = sodium_crypto_secretbox_open($cipher, $nonce, self::key());
return $plain === false ? null : $plain;
}
/** 32-byte key derived from the site's auth salts. */
private static function key(): string {
$material = (defined('AUTH_KEY') ? AUTH_KEY : '') . '|' . (defined('AUTH_SALT') ? AUTH_SALT : '');
return hash('sha256', $material, true);
}
}