v1.2.0
This commit is contained in:
parent
693e5ae2e6
commit
7d3fc1969d
52 changed files with 1776 additions and 295 deletions
|
|
@ -10,21 +10,39 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the AES-256-CBC key for token encryption using HKDF-SHA256.
|
||||
*
|
||||
* Uses wp_salt('auth') as input keying material and derives a 32-byte key
|
||||
* scoped to token encryption. Replaces the <= 1.1.1 practice of feeding
|
||||
* wp_salt('auth') directly to openssl_encrypt().
|
||||
*
|
||||
* @since 1.2.0
|
||||
*
|
||||
* @return string 32 raw key bytes.
|
||||
*/
|
||||
function robotstxt_docmd_token_key(): string {
|
||||
return hash_hkdf( 'sha256', wp_salt( 'auth' ), 32, 'robotstxt-docmd-github-token' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt GitHub token for storage
|
||||
*
|
||||
* Stored format: `v2:` followed by base64( IV || ciphertext ), encrypted with
|
||||
* the HKDF-derived key. The `v2:` prefix distinguishes v2 tokens from the
|
||||
* legacy (<= 1.1.1) format written by earlier versions.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $token Plain token.
|
||||
* @return string Encrypted token, or empty string on failure.
|
||||
* @return string Encrypted token (v2-prefixed), or empty string on failure.
|
||||
*/
|
||||
function robotstxt_docmd_encrypt_token( string $token ): string {
|
||||
if ( empty( $token ) ) {
|
||||
if ( '' === $token ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use WordPress salts for encryption key.
|
||||
$key = wp_salt( 'auth' );
|
||||
$key = robotstxt_docmd_token_key();
|
||||
$iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
|
||||
$iv = openssl_random_pseudo_bytes( $iv_length );
|
||||
$encrypted = openssl_encrypt( $token, 'aes-256-cbc', $key, 0, $iv );
|
||||
|
|
@ -33,34 +51,97 @@ function robotstxt_docmd_encrypt_token( string $token ): string {
|
|||
return '';
|
||||
}
|
||||
|
||||
return base64_encode( $iv . $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
return 'v2:' . base64_encode( $iv . $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt GitHub token from storage
|
||||
*
|
||||
* Handles two storage formats:
|
||||
* - `v2:`-prefixed tokens are decrypted with the HKDF-derived key.
|
||||
* - Legacy (unprefixed) tokens are decrypted with wp_salt('auth') directly,
|
||||
* then transparently re-encrypted with the v2 scheme and persisted (one-time
|
||||
* lazy migration). This keeps the cron path working without an admin request.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $encrypted_token Encrypted token.
|
||||
* @param string $encrypted_token Encrypted token (v2 or legacy).
|
||||
* @return string Plain token, or empty string on failure.
|
||||
*/
|
||||
function robotstxt_docmd_decrypt_token( string $encrypted_token ): string {
|
||||
if ( empty( $encrypted_token ) ) {
|
||||
if ( '' === $encrypted_token ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$key = wp_salt( 'auth' );
|
||||
$iv_length = openssl_cipher_iv_length( 'aes-256-cbc' );
|
||||
|
||||
// v2 tokens use the HKDF-derived key.
|
||||
if ( str_starts_with( $encrypted_token, 'v2:' ) ) {
|
||||
$decoded = base64_decode( substr( $encrypted_token, 3 ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
if ( false === $decoded || strlen( $decoded ) <= $iv_length ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$iv = substr( $decoded, 0, $iv_length );
|
||||
$ciphertext = substr( $decoded, $iv_length );
|
||||
$decrypted = openssl_decrypt( $ciphertext, 'aes-256-cbc', robotstxt_docmd_token_key(), 0, $iv );
|
||||
|
||||
return false !== $decrypted ? $decrypted : '';
|
||||
}
|
||||
|
||||
// Legacy tokens (<= 1.1.1) used wp_salt('auth') directly as the key.
|
||||
$decoded = base64_decode( $encrypted_token, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
if ( false === $decoded || strlen( $decoded ) <= $iv_length ) {
|
||||
return '';
|
||||
}
|
||||
$iv = substr( $decoded, 0, $iv_length );
|
||||
$encrypted = substr( $decoded, $iv_length );
|
||||
|
||||
$decrypted = openssl_decrypt( $encrypted, 'aes-256-cbc', $key, 0, $iv );
|
||||
return false !== $decrypted ? $decrypted : '';
|
||||
$iv = substr( $decoded, 0, $iv_length );
|
||||
$ciphertext = substr( $decoded, $iv_length );
|
||||
$decrypted = openssl_decrypt( $ciphertext, 'aes-256-cbc', wp_salt( 'auth' ), 0, $iv );
|
||||
|
||||
if ( false === $decrypted || '' === $decrypted ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Lazy one-time migration to the v2 scheme.
|
||||
$reencrypted = robotstxt_docmd_encrypt_token( $decrypted );
|
||||
if ( '' !== $reencrypted ) {
|
||||
robotstxt_docmd_persist_migrated_token( $reencrypted );
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a migrated (re-encrypted) token back into plugin settings.
|
||||
*
|
||||
* Runs at most once: after the first migration the stored value carries the
|
||||
* `v2:` prefix and the legacy branch in robotstxt_docmd_decrypt_token() is no
|
||||
* longer taken. Safe under concurrency because the re-encrypted value is
|
||||
* deterministic for a given plaintext + key.
|
||||
*
|
||||
* @since 1.2.0
|
||||
*
|
||||
* @param string $reencrypted Re-encrypted token (v2 scheme).
|
||||
* @return void
|
||||
*/
|
||||
function robotstxt_docmd_persist_migrated_token( string $reencrypted ): void {
|
||||
$raw_settings = get_option( 'robotstxt_docmd_settings', array() );
|
||||
if ( ! is_array( $raw_settings ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current = array_key_exists( 'github_token', $raw_settings ) && is_string( $raw_settings['github_token'] )
|
||||
? $raw_settings['github_token']
|
||||
: '';
|
||||
|
||||
// Only update while the stored token is still the legacy (unprefixed) one.
|
||||
if ( '' === $current || str_starts_with( $current, 'v2:' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$raw_settings['github_token'] = $reencrypted;
|
||||
update_option( 'robotstxt_docmd_settings', $raw_settings, false );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue