self::MAX_PLAINTEXT) { throw new RuntimeException('Outbox encryption failed.'); } try { $salt = random_bytes(32); $iv = random_bytes(12); $key = self::key($salt); $tag = ''; $ciphertext = openssl_encrypt($plaintext, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag, self::aad($event_id, $operation), 16); } catch (Throwable $error) { unset($error, $plaintext, $salt, $iv, $key, $tag, $ciphertext); throw new RuntimeException('Outbox encryption failed.'); } if (!is_string($ciphertext) || '' === $ciphertext || 16 !== strlen($tag)) { unset($plaintext, $salt, $iv, $key, $tag, $ciphertext); throw new RuntimeException('Outbox encryption failed.'); } $envelope = implode(':', array(self::VERSION, self::CIPHER, Akkedli_V2_Crypto::base64url_encode($salt), Akkedli_V2_Crypto::base64url_encode($iv), Akkedli_V2_Crypto::base64url_encode($ciphertext), Akkedli_V2_Crypto::base64url_encode($tag))); unset($plaintext, $salt, $iv, $key, $tag, $ciphertext); return $envelope; } public static function open($envelope, $event_id, $operation) { self::validate_context($event_id, $operation); Akkedli_V2_Crypto::assert_available(); if (!is_string($envelope) || strlen($envelope) > 7500000) { throw new RuntimeException('Stored outbox data is invalid.'); } $parts = explode(':', $envelope); if (6 !== count($parts) || self::VERSION !== $parts[0] || self::CIPHER !== $parts[1]) { throw new RuntimeException('Stored outbox data is invalid.'); } try { $salt = Akkedli_V2_Crypto::base64url_decode($parts[2]); $iv = Akkedli_V2_Crypto::base64url_decode($parts[3]); $ciphertext = Akkedli_V2_Crypto::base64url_decode($parts[4]); $tag = Akkedli_V2_Crypto::base64url_decode($parts[5]); if (32 !== strlen($salt) || 12 !== strlen($iv) || '' === $ciphertext || strlen($ciphertext) > self::MAX_PLAINTEXT || 16 !== strlen($tag)) { throw new RuntimeException('Stored outbox data is invalid.'); } $key = self::key($salt); $plaintext = openssl_decrypt($ciphertext, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag, self::aad($event_id, $operation)); } catch (Throwable $error) { unset($error, $salt, $iv, $ciphertext, $tag, $key, $plaintext); throw new RuntimeException('Stored outbox data is invalid.'); } unset($salt, $iv, $ciphertext, $tag, $key); $value = is_string($plaintext) ? json_decode($plaintext, true) : null; $canonical = is_array($value) ? wp_json_encode($value, JSON_UNESCAPED_SLASHES) : false; if (!is_array($value) || JSON_ERROR_NONE !== json_last_error() || !is_string($canonical) || !hash_equals($plaintext, $canonical)) { unset($plaintext, $canonical, $value); throw new RuntimeException('Stored outbox data is invalid.'); } unset($plaintext, $canonical); return $value; } private static function validate_context($event_id, $operation) { if (!is_string($event_id) || 1 !== preg_match('/^[0-9a-f]{64}$/D', $event_id) || !in_array($operation, array('order', 'status'), true)) { throw new RuntimeException('Outbox context is invalid.'); } } private static function aad($event_id, $operation) { return implode(':', array(self::AAD, 'blog', (string) get_current_blog_id(), 'event', $event_id, 'operation', $operation)); } private static function key($salt) { $context = self::PURPOSE . ':blog:' . (string) get_current_blog_id(); $key = hash_hkdf('sha256', wp_salt('auth'), 32, $context, $salt); if (!is_string($key) || 32 !== strlen($key)) { throw new RuntimeException('Outbox encryption failed.'); } return $key; } }