GAP School Module 06 — Marketplace Syndication Lesson 6.2

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 situation

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.


What I did

Key derivation from wp-config.php

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.

PHP
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 ); }

Encryption and decryption

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:

PHP
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 : ''; }

Typed credential structure per marketplace

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:

PHP
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; }

Admin settings page: never echo stored values

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:

PHP
// 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'] ), ] ); }

Why it matters

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.


The Anchor build

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.


Do this, not that

  • Derive the encryption key from wp-config.php constants, never from a database-stored value. The security property is that database-only access can’t decrypt. If the key is in the database, this property is gone.
  • Use a random IV on every encryption call. A static IV means identical plaintexts produce identical ciphertext. An attacker can see which credentials are the same without decrypting anything.
  • Never echo stored credential values back into the admin form. Show only masked placeholders. The plaintext appears in form value attributes, page source, and browser history if you render it.
  • Use a typed credential shape per marketplace. Knowing the expected fields at save time prevents partial updates and makes the code readable — you can see what a “boat_trader credential” looks like without reading the database.
  • Empty form submission = keep existing credential. Staff updating one of four credentials shouldn’t have to re-enter all four. Empty fields should be no-ops, not overwrites with empty strings.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

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.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

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 →