feature: Initial functional push

This commit is contained in:
Keith Solomon
2025-12-14 16:58:52 -06:00
parent 15445ad40e
commit 189b32ccff
14 changed files with 3365 additions and 0 deletions

76
includes/class-auth.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
namespace SiteSync;
use WP_Error;
use WP_REST_Request;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles authentication for SiteSync REST requests using shared key and HMAC.
*/
class Auth {
/**
* Verify an incoming REST request using shared key + HMAC.
*
* @param WP_REST_Request $request The REST request object.
* @param Settings $settings The SiteSync settings instance.
*/
public static function verify( WP_REST_Request $request, Settings $settings ) {
$providedKey = $request->get_header( 'X-Site-Sync-Key' );
$signature = $request->get_header( 'X-Site-Sync-Signature' );
$timestamp = $request->get_header( 'X-Site-Sync-Timestamp' );
if ( ! $providedKey || ! $signature || ! $timestamp ) {
return new WP_Error( 'site_sync_missing_headers', __( 'Missing authentication headers.', 'site-sync' ), array( 'status' => 401 ) );
}
$peerKey = $settings->get( 'peer_site_key' );
if ( ! empty( $peerKey ) && ! hash_equals( $peerKey, $providedKey ) ) {
return new WP_Error( 'site_sync_invalid_peer', __( 'Peer key does not match.', 'site-sync' ), array( 'status' => 401 ) );
}
$sharedSecret = $settings->get( 'shared_key' );
if ( empty( $sharedSecret ) ) {
return new WP_Error( 'site_sync_no_secret', __( 'Shared secret is not configured.', 'site-sync' ), array( 'status' => 401 ) );
}
$body = $request->get_body() ?? '';
$expected = self::build_signature( $providedKey, $timestamp, $body, $sharedSecret );
if ( ! hash_equals( $expected, $signature ) ) {
return new WP_Error( 'site_sync_bad_signature', __( 'Signature verification failed.', 'site-sync' ), array( 'status' => 401 ) );
}
$ts = is_numeric( $timestamp ) ? (int) $timestamp : 0;
if ( $ts > 0 && abs( time() - $ts ) > 300 ) {
return new WP_Error( 'site_sync_stale', __( 'Request timestamp is outside the allowed window.', 'site-sync' ), array( 'status' => 401 ) );
}
$cacheKey = 'site_sync_seen_' . md5( $providedKey . $signature . $timestamp );
if ( false !== get_transient( $cacheKey ) ) {
return new WP_Error( 'site_sync_replay', __( 'Replay detected.', 'site-sync' ), array( 'status' => 401 ) );
}
set_transient( $cacheKey, 1, 5 * MINUTE_IN_SECONDS );
return true;
}
/**
* Build an HMAC signature for the given key, timestamp, and body using the shared secret.
*
* @param string $key The peer site key.
* @param string $timestamp The request timestamp.
* @param string $body The request body.
* @param string $secret The shared secret key.
* @return string The generated HMAC signature.
*/
public static function build_signature( string $key, string $timestamp, string $body, string $secret ): string {
$canonical = implode( "\n", array( $key, $timestamp, $body ) );
return hash_hmac( 'sha256', $canonical, $secret );
}
}