AES-256-CBC encryption with a key derived from wp-config.php constants. Why the encryption key must never be in the database.
Marketplace API credentials are third-party keys that represent real API access. Storing them as plain text in the WordPress database means any SQL injection, any database backup with loose permissions, or any plugin that reads arbitrary options can extract them. Encrypting them with a key derived from wp-config.php constants ties the credentials to the server’s config file — the database alone isn’t enough to decrypt them.
The Anchor build needed to store API keys and secrets for four marketplaces. The naive approach — update_option('boat_trader_api_key', $key) — stores the value in plain text in the wp_options table. Anyone with database read access (direct, via phpMyAdmin, via an export) gets the keys. The encryption approach requires the database plus the server config. Neither alone is sufficient.
The encryption key is derived from WordPress security constants that live in wp-config.php — never in the database. This is the property that makes the encryption useful: database dump + no wp-config.php = undecryptable ciphertext.
function [client]_get_encryption_key(): string {
if ( ! defined( 'AUTH_KEY' ) || ! defined( 'AUTH_SALT' ) ) {
wp_die( 'AUTH_KEY and AUTH_SALT must be defined in wp-config.php' );
}
// Hash the concatenated constants to a 256-bit key
return substr( hash( 'sha256', AUTH_KEY . AUTH_SALT ), 0, 32 );
}
AES-256-CBC with a random IV on every encryption call. The IV is prepended to the ciphertext before base64-encoding so it’s stored alongside the data it protects:
function [client]_encrypt_credential( string $value ): string {
$key = [client]_get_encryption_key();
$iv = random_bytes( 16 ); // New IV every time
$cipher = openssl_encrypt( $value, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
return base64_encode( $iv . $cipher );
}
function [client]_decrypt_credential( string $stored ): string {
$key = [client]_get_encryption_key();
$data = base64_decode( $stored );
$iv = substr( $data, 0, 16 );
$cipher = substr( $data, 16 );
$plain = openssl_decrypt( $cipher, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
return $plain !== false ? $plain : '';
}
Each marketplace has a known credential shape. Rather than storing keys as individual options, the credentials are stored and retrieved as a typed structure. This makes the expected shape explicit and prevents partial-save bugs:
function [client]_save_marketplace_credentials( string $marketplace, array $credentials ): void {
$allowed_shapes = [
'boat_trader' => [ 'api_key', 'dealer_id' ],
'yachtworld' => [ 'api_key', 'api_secret', 'dealer_id' ],
'boatus' => [ 'username', 'password', 'dealer_code' ],
'aggregator' => [ 'api_token' ],
];
if ( ! isset( $allowed_shapes[ $marketplace ] ) ) {
return;
}
$encrypted = [];
foreach ( $allowed_shapes[ $marketplace ] as $field ) {
if ( ! empty( $credentials[ $field ] ) ) {
$encrypted[ $field ] = [client]_encrypt_credential( $credentials[ $field ] );
}
}
update_option( '[client]_marketplace_creds_' . $marketplace, $encrypted );
}
function [client]_get_marketplace_credentials( string $marketplace ): array {
$stored = get_option( '[client]_marketplace_creds_' . $marketplace, [] );
$decrypted = [];
foreach ( $stored as $field => $value ) {
$decrypted[ $field ] = [client]_decrypt_credential( $value );
}
return $decrypted;
}
The settings form that lets admins update credentials must never render the stored values back into the page. If the page source contains the API key, it’s exposed to anyone who can view source — browser history, screen recordings, shoulder surfing. Show only a masked placeholder:
// In the admin settings form render:
$creds = [client]_get_marketplace_credentials( 'boat_trader' );
// CORRECT: mask stored value, show placeholder
$api_key_display = ! empty( $creds['api_key'] )
? '••••••••' . substr( $creds['api_key'], -4 )
: '';
echo '<input type="password" name="boat_trader_api_key"
placeholder="' . esc_attr( $api_key_display ?: 'Enter API key' ) . '"
value="" autocomplete="off">';
// If the field is submitted empty, keep the existing stored value
if ( isset( $_POST['boat_trader_api_key'] ) && $_POST['boat_trader_api_key'] !== '' ) {
[client]_save_marketplace_credentials( 'boat_trader', [
'api_key' => sanitize_text_field( $_POST['boat_trader_api_key'] ),
'dealer_id' => sanitize_text_field( $_POST['boat_trader_dealer_id'] ),
] );
}
The threat model for marketplace credentials isn’t sophisticated attackers — it’s routine exposure: a database backup that gets left on an FTP server, a plugin that exports options for debugging, a shared hosting environment where the DB is accessible to multiple accounts. Encryption at rest with a key that’s not in the database eliminates the one-step compromise.
The random IV per encryption call means two identical credentials produce different ciphertext every time. This prevents pattern-matching across the database — an attacker can’t tell whether two encrypted values are the same plaintext.
All four marketplace credential sets stored encrypted. The settings page shows masked values on load. Submitting a blank field preserves the existing stored credential — a staff member updating only one credential doesn’t inadvertently clear the others.
One incident: a hosting provider offered a database restore from backup as part of a migration. The restored credentials were unreadable because the wp-config.php constants on the new server were different. Correct behavior — the old credentials should have been re-entered anyway. The incident confirmed the architecture works.
Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →