Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions includes/class-wpfa-mailconnect-encryption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

/**
* Encryption utility for sensitive data protection.
*
* Provides encryption and decryption methods for sensitive configuration data
* using WordPress salts and OpenSSL for secure credential storage.
*
* @link https://fossasia.org
* @since 1.2.2
* @package Wpfa_Mailconnect
* @subpackage Wpfa_Mailconnect/includes
*/

/**
* Encryption utility class definition.
*
* @since 1.2.2
* @package Wpfa_Mailconnect
* @subpackage Wpfa_Mailconnect/includes
* @author FOSSASIA <info@fossasia.org>
*/
class Wpfa_Mailconnect_Encryption {

/**
* Encryption method to use.
*
* @since 1.2.2
*/
const CIPHER_METHOD = 'AES-256-CBC';

/**
* Prefix to identify encrypted values.
*
* @since 1.2.2
*/
const ENCRYPTED_PREFIX = 'wpfa_enc_';

/**
* Encrypts a string value.
*
* Uses OpenSSL with AES-256-CBC encryption and WordPress salts for the key.
*
* @since 1.2.2
* @param string $value The plain text value to encrypt.
* @return string The encrypted value with prefix, or original value if encryption fails.
*/
public static function encrypt( $value ) {
// Return empty if value is empty
if ( empty( $value ) ) {
return $value;
}

// Don't re-encrypt already encrypted values
if ( self::is_encrypted( $value ) ) {
return $value;
}

// Check if OpenSSL is available
if ( ! function_exists( 'openssl_encrypt' ) ) {
error_log( 'WPFA MailConnect: OpenSSL not available, storing value without encryption.' );
return $value;
}

try {
$key = self::get_encryption_key();
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::CIPHER_METHOD ) );

$encrypted = openssl_encrypt(
$value,
self::CIPHER_METHOD,
$key,
0,
$iv
);

if ( false === $encrypted ) {
error_log( 'WPFA MailConnect: Encryption failed.' );
return $value;
}

// Combine IV and encrypted data, then base64 encode
$result = base64_encode( $iv . $encrypted );

// Add prefix to identify encrypted values
return self::ENCRYPTED_PREFIX . $result;

} catch ( Exception $e ) {
error_log( 'WPFA MailConnect Encryption Error: ' . $e->getMessage() );
return $value;
}
}

/**
* Decrypts an encrypted string value.
*
* @since 1.2.2
* @param string $value The encrypted value (with prefix).
* @return string The decrypted plain text value, or original if not encrypted.
*/
public static function decrypt( $value ) {
// Return empty if value is empty
if ( empty( $value ) ) {
return $value;
}

// If not encrypted, return as-is (backwards compatibility)
if ( ! self::is_encrypted( $value ) ) {
return $value;
}

// Check if OpenSSL is available
if ( ! function_exists( 'openssl_decrypt' ) ) {
error_log( 'WPFA MailConnect: OpenSSL not available for decryption.' );
return '';
}

try {
// Remove prefix
$value = substr( $value, strlen( self::ENCRYPTED_PREFIX ) );

// Decode from base64
$decoded = base64_decode( $value, true );

if ( false === $decoded ) {
error_log( 'WPFA MailConnect: Base64 decode failed.' );
return '';
}

$key = self::get_encryption_key();
$iv_length = openssl_cipher_iv_length( self::CIPHER_METHOD );

// Extract IV and encrypted data
$iv = substr( $decoded, 0, $iv_length );
$encrypted = substr( $decoded, $iv_length );

$decrypted = openssl_decrypt(
$encrypted,
self::CIPHER_METHOD,
$key,
0,
$iv
);

if ( false === $decrypted ) {
error_log( 'WPFA MailConnect: Decryption failed.' );
return '';
}

return $decrypted;

} catch ( Exception $e ) {
error_log( 'WPFA MailConnect Decryption Error: ' . $e->getMessage() );
return '';
}
}

/**
* Checks if a value is encrypted.
*
* @since 1.2.2
* @param string $value The value to check.
* @return bool True if encrypted, false otherwise.
*/
public static function is_encrypted( $value ) {
return is_string( $value ) && strpos( $value, self::ENCRYPTED_PREFIX ) === 0;
}

/**
* Generates an encryption key based on WordPress salts.
*
* Uses WordPress AUTH_KEY and SECURE_AUTH_KEY salts to create a unique,
* site-specific encryption key.
*
* @since 1.2.2
* @return string The encryption key.
*/
private static function get_encryption_key() {
// Use WordPress salts to create a unique key per site
$key = AUTH_KEY . SECURE_AUTH_KEY;

// Hash to ensure consistent key length for AES-256 (32 bytes)
return hash( 'sha256', $key, true );
}
}
117 changes: 68 additions & 49 deletions includes/class-wpfa-mailconnect-smtp.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,26 @@ public function sanitize_smtp_options( $input ) {
$output[ $id ] = $input[ $id ] === '1' ? '1' : '0';
}
} else {
// Handle sanitization for other field types (optional but recommended)
// Handle sanitization for other field types
if ( isset( $input[ $id ] ) ) {
switch ( $args['type'] ) {
case 'text':
case 'password':
case 'select':
$output[ $id ] = sanitize_text_field( $input[ $id ] );
break;
case 'password':
// ENCRYPTION: Only encrypt if password was actually changed
// Check if the submitted value is not empty and different from placeholder
if ( ! empty( $input[ $id ] ) && $input[ $id ] !== '********' ) {
// Encrypt the new password before saving
$output[ $id ] = Wpfa_Mailconnect_Encryption::encrypt( $input[ $id ] );
} elseif ( empty( $input[ $id ] ) ) {
// If empty, keep the existing encrypted value
$existing_options = get_option( 'smtp_options', array() );
$output[ $id ] = isset( $existing_options[ $id ] ) ? $existing_options[ $id ] : '';
}
// If it's '********', it means user didn't change it, keep existing value
break;
case 'number':
$output[ $id ] = absint( $input[ $id ] );
break;
Expand Down Expand Up @@ -243,53 +255,55 @@ public function test_section_callback() {
* @return void
*/
public function render_field( $args ) {
$options = get_option( 'smtp_options', array() );
$id = sanitize_key( $args['id'] );
$options = get_option( 'smtp_options', array() );
$id = sanitize_key( $args['id'] );

// Check type first, then set value once
if ( isset( $args['type'] ) && 'password' === $args['type'] ) {
// Password fields: use saved value or empty string (never show default)
$value = isset( $options[ $id ] ) ? $options[ $id ] : '';
if ( isset( $args['type'] ) && 'password' === $args['type'] ) {
// Password fields: show placeholder if value exists, empty string if not
// NEVER show the actual encrypted value or decrypted password
$value = isset( $options[ $id ] ) && ! empty( $options[ $id ] ) ? '********' : '';
} else {
// All other fields: use saved value or default
$value = isset( $options[ $id ] ) ? $options[ $id ] : $args['default'];
}

if ( isset( $args['type'] ) && 'select' === $args['type'] ) {
echo '<select id="' . esc_attr( $id ) . '" name="smtp_options[' . esc_attr( $id ) . ']">';
foreach ( $args['options'] as $val => $label ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $val ),
selected( $value, $val, false ),
esc_html( $label )
);
}
echo '</select>';
} elseif ( isset( $args['type'] ) && 'checkbox' === $args['type'] ) {
// Checkbox handling: value is 1 if checked, 0 if not set/unchecked
$checked = ( '1' === $value || true === $value );
printf(
'<input type="checkbox" id="%s" name="smtp_options[%s]" value="1" %s />',
esc_attr( $id ),
esc_attr( $id ),
checked( $checked, true, false )
);
} else {
// text/password/number
printf(
'<input type="%s" id="%s" name="smtp_options[%s]" value="%s" class="regular-text" />',
esc_attr( $args['type'] ),
esc_attr( $id ),
esc_attr( $id ),
esc_attr( $value )
);
}
}

if ( isset( $args['description'] ) ) {
printf( '<p class="description">%s</p>', esc_html( $args['description'] ) );
}
}
if ( isset( $args['type'] ) && 'select' === $args['type'] ) {
echo '<select id="' . esc_attr( $id ) . '" name="smtp_options[' . esc_attr( $id ) . ']">';
foreach ( $args['options'] as $val => $label ) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $val ),
selected( $value, $val, false ),
esc_html( $label )
);
}
echo '</select>';
} elseif ( isset( $args['type'] ) && 'checkbox' === $args['type'] ) {
// Checkbox handling: value is 1 if checked, 0 if not set/unchecked
$checked = ( '1' === $value || true === $value );
printf(
'<input type="checkbox" id="%s" name="smtp_options[%s]" value="1" %s />',
esc_attr( $id ),
esc_attr( $id ),
checked( $checked, true, false )
);
} else {
// text/password/number
printf(
'<input type="%s" id="%s" name="smtp_options[%s]" value="%s" class="regular-text" placeholder="%s" />',
esc_attr( $args['type'] ),
esc_attr( $id ),
esc_attr( $id ),
esc_attr( $value ),
( 'password' === $args['type'] && ! empty( $value ) ) ? 'Leave blank to keep current password' : ''
);
}

if ( isset( $args['description'] ) ) {
printf( '<p class="description">%s</p>', esc_html( $args['description'] ) );
}
}

/**
* Renders the plugin settings page.
Expand Down Expand Up @@ -518,17 +532,22 @@ public function handle_test_email() {
* @param PHPMailer $phpmailer The PHPMailer instance to configure.
* @return void
*/
public function phpmailer_override( $phpmailer ) {
$options = get_option( 'smtp_options', array() );
public function phpmailer_override( $phpmailer ) {
$options = get_option( 'smtp_options', array() );

$user = isset( $options['smtp_user'] ) ? trim( $options['smtp_user'] ) : '';
$pass = isset( $options['smtp_pass'] ) ? $options['smtp_pass'] : '';

// DECRYPTION: Decrypt password before use
$pass = isset( $options['smtp_pass'] ) ? Wpfa_Mailconnect_Encryption::decrypt( $options['smtp_pass'] ) : '';

$host = isset( $options['smtp_host'] ) ? trim( $options['smtp_host'] ) : 'localhost';
$port = isset( $options['smtp_port'] ) ? (int) $options['smtp_port'] : 25;

// Validate port range (1-65535)
if ( $port < 1 || $port > 65535 ) {
$port = 25; // fallback to default SMTP port
}

$secure = isset( $options['smtp_secure'] ) ? $options['smtp_secure'] : '';
$auth = isset( $options['smtp_auth'] ) ? (bool) $options['smtp_auth'] : false;
$from = isset( $options['smtp_from'] ) ? trim( $options['smtp_from'] ) : get_option( 'admin_email' );
Expand All @@ -541,7 +560,7 @@ public function phpmailer_override( $phpmailer ) {
$phpmailer->SMTPAuth = $auth;
$phpmailer->Port = $port;
$phpmailer->Username = $user;
$phpmailer->Password = $pass;
$phpmailer->Password = $pass; // Decrypted password
$phpmailer->SMTPSecure = $secure;

// Validate 'From' email address before assignment
Expand All @@ -555,7 +574,7 @@ public function phpmailer_override( $phpmailer ) {
$phpmailer->FromName = $name;
}

// Disable debug output
$phpmailer->SMTPDebug = 0;
// Disable debug output
$phpmailer->SMTPDebug = 0;
}
}
5 changes: 5 additions & 0 deletions includes/class-wpfa-mailconnect.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ private function load_dependencies() {
*/
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-wpfa-mailconnect-smtp.php';

/**
* The class responsible for encryption utilities.
*/
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-wpfa-mailconnect-encryption.php';

$this->loader = new Wpfa_Mailconnect_Loader();

}
Expand Down