50 lines
1.8 KiB
PHP
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);
|
|
}
|
|
}
|