diff --git a/Tests/Mock/Driver.php b/Tests/Mock/Driver.php index 44f44d3ca..b5aec54c1 100644 --- a/Tests/Mock/Driver.php +++ b/Tests/Mock/Driver.php @@ -48,6 +48,7 @@ public static function create(TestCase $test, $nullDate = '0000-00-00 00:00:00', 'getAffectedRows', 'getCollation', 'getConnectionCollation', + 'getConnectionEncryption', 'getConnectors', 'getDateFormat', 'getInstance', diff --git a/Tests/Stubs/nosqldriver.php b/Tests/Stubs/nosqldriver.php index b4115c2d1..e8384c87c 100644 --- a/Tests/Stubs/nosqldriver.php +++ b/Tests/Stubs/nosqldriver.php @@ -214,6 +214,19 @@ public function getConnectionCollation() return false; } + /** + * Method to get the database encryption details (cipher and protocol) in use. + * + * @return string The database encryption details. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getConnectionEncryption(): string + { + return ''; + } + /** * Get the number of returned rows for the previous executed SQL statement. * diff --git a/src/DatabaseInterface.php b/src/DatabaseInterface.php index 11c6b35d2..f09850c06 100644 --- a/src/DatabaseInterface.php +++ b/src/DatabaseInterface.php @@ -140,6 +140,15 @@ public function getConnection(); */ public function getConnectionCollation(); + /** + * Method to get the database encryption details (cipher and protocol) in use. + * + * @return string The database encryption details. + * + * @since __DEPLOY_VERSION__ + */ + public function getConnectionEncryption(): string; + /** * Get the total number of SQL statements executed by the database driver. * diff --git a/src/Mysql/MysqlDriver.php b/src/Mysql/MysqlDriver.php index 057190a84..b8223f72a 100644 --- a/src/Mysql/MysqlDriver.php +++ b/src/Mysql/MysqlDriver.php @@ -79,6 +79,20 @@ class MysqlDriver extends PdoDriver implements UTF8MB4SupportInterface */ protected static $dbMinMariadb = '10.0'; + /** + * The default cipher suite for TLS connections. + * + * @var array + * @since __DEPLOY_VERSION__ + */ + protected static $defaultCipherSuite = [ + 'AES128-GCM-SHA256', + 'AES256-GCM-SHA384', + 'AES128-CBC-SHA256', + 'AES256-CBC-SHA384', + 'DES-CBC3-SHA', + ]; + /** * Constructor. * @@ -132,6 +146,34 @@ public function connect() return; } + // For SSL/TLS connection encryption. + if ($this->options['ssl'] !== [] && $this->options['ssl']['enable'] === true) + { + $sslContextIsNull = true; + + // If customised, add cipher suite, ca file path, ca path, private key file path and certificate file path to PDO driver options. + foreach (['cipher', 'ca', 'capath', 'key', 'cert'] as $key => $value) + { + if ($this->options['ssl'][$value] !== null) + { + $this->options['driverOptions'][constant('\PDO::MYSQL_ATTR_SSL_' . strtoupper($value))] = $this->options['ssl'][$value]; + $sslContextIsNull = false; + } + } + + // PDO, if no cipher, ca, capath, cert and key are set, can't start TLS one-way connection, set a common ciphers suite to force it. + if ($sslContextIsNull === true) + { + $this->options['driverOptions'][\PDO::MYSQL_ATTR_SSL_CIPHER] = implode(':', static::$defaultCipherSuite); + } + + // If customised, for capable systems (PHP 7.0.14+ and 7.1.4+) verify certificate chain and Common Name to driver options. + if ($this->options['ssl']['verify_server_cert'] !== null && defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) + { + $this->options['driverOptions'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->options['ssl']['verify_server_cert']; + } + } + try { // Try to connect to MySQL @@ -296,6 +338,29 @@ public function getConnectionCollation() return $this->setQuery('SELECT @@collation_connection;')->loadResult(); } + /** + * Method to get the database encryption details (cipher and protocol) in use. + * + * @return string The database encryption details. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getConnectionEncryption(): string + { + $this->connect(); + + $variables = $this->setQuery('SHOW SESSION STATUS WHERE `Variable_name` IN (\'Ssl_version\', \'Ssl_cipher\')') + ->loadObjectList('Variable_name'); + + if (!empty($variables['Ssl_cipher']->Value)) + { + return $variables['Ssl_version']->Value . ' (' . $variables['Ssl_cipher']->Value . ')'; + } + + return ''; + } + /** * Return the query string to create new Database. * diff --git a/src/Mysqli/MysqliDriver.php b/src/Mysqli/MysqliDriver.php index c4451f932..1267d12df 100644 --- a/src/Mysqli/MysqliDriver.php +++ b/src/Mysqli/MysqliDriver.php @@ -124,6 +124,18 @@ public function __construct(array $options) $options['socket'] = $options['socket'] ?? null; $options['utf8mb4'] = isset($options['utf8mb4']) ? (bool) $options['utf8mb4'] : false; $options['sqlModes'] = isset($options['sqlModes']) ? (array) $options['sqlModes'] : $sqlModes; + $options['ssl'] = isset($options['ssl']) ? $options['ssl'] : []; + + if ($options['ssl'] !== []) + { + $options['ssl']['enable'] = isset($options['ssl']['enable']) ? $options['ssl']['enable'] : false; + $options['ssl']['cipher'] = isset($options['ssl']['cipher']) ? $options['ssl']['cipher'] : null; + $options['ssl']['ca'] = isset($options['ssl']['ca']) ? $options['ssl']['ca'] : null; + $options['ssl']['capath'] = isset($options['ssl']['capath']) ? $options['ssl']['capath'] : null; + $options['ssl']['key'] = isset($options['ssl']['key']) ? $options['ssl']['key'] : null; + $options['ssl']['cert'] = isset($options['ssl']['cert']) ? $options['ssl']['cert'] : null; + $options['ssl']['verify_server_cert'] = isset($options['ssl']['verify_server_cert']) ? $options['ssl']['verify_server_cert'] : null; + } // Finalize initialisation. parent::__construct($options); @@ -211,9 +223,50 @@ public function connect() $this->connection = mysqli_init(); + $connectionFlags = 0; + + // For SSL/TLS connection encryption. + if ($this->options['ssl'] !== [] && $this->options['ssl']['enable'] === true) + { + $connectionFlags += MYSQLI_CLIENT_SSL; + + // Verify server certificate is only availble in PHP 5.6.16+. See https://www.php.net/ChangeLog-5.php#5.6.16 + if (isset($this->options['ssl']['verify_server_cert'])) + { + // New constants in PHP 5.6.16+. See https://www.php.net/ChangeLog-5.php#5.6.16 + if ($this->options['ssl']['verify_server_cert'] === true && defined('MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT')) + { + $connectionFlags += MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT; + } + elseif ($this->options['ssl']['verify_server_cert'] === false && defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT')) + { + $connectionFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; + } + elseif (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT')) + { + $this->connection->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, $this->options['ssl']['verify_server_cert']); + } + } + + // Add SSL/TLS options only if changed. + $this->connection->ssl_set( + $this->options['ssl']['key'], + $this->options['ssl']['cert'], + $this->options['ssl']['ca'], + $this->options['ssl']['capath'], + $this->options['ssl']['cipher'] + ); + } + // Attempt to connect to the server, use error suppression to silence warnings and allow us to throw an Exception separately. $connected = @$this->connection->real_connect( - $this->options['host'], $this->options['user'], $this->options['password'], null, $this->options['port'], $this->options['socket'] + $this->options['host'], + $this->options['user'], + $this->options['password'], + null, + $this->options['port'], + $this->options['socket'], + $connectionFlags ); if (!$connected) @@ -408,6 +461,29 @@ public function getConnectionCollation() return $this->setQuery('SELECT @@collation_connection;')->loadResult(); } + /** + * Method to get the database encryption details (cipher and protocol) in use. + * + * @return string The database encryption details. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getConnectionEncryption(): string + { + $this->connect(); + + $variables = $this->setQuery('SHOW SESSION STATUS WHERE `Variable_name` IN (\'Ssl_version\', \'Ssl_cipher\')') + ->loadObjectList('Variable_name'); + + if (!empty($variables['Ssl_cipher']->Value)) + { + return $variables['Ssl_version']->Value . ' (' . $variables['Ssl_cipher']->Value . ')'; + } + + return ''; + } + /** * Return the query string to create new Database. * diff --git a/src/Pdo/PdoDriver.php b/src/Pdo/PdoDriver.php index 1c5fe0959..d24e664ee 100644 --- a/src/Pdo/PdoDriver.php +++ b/src/Pdo/PdoDriver.php @@ -78,6 +78,18 @@ public function __construct(array $options) $options['port'] = isset($options['port']) ? (int) $options['port'] : null; $options['password'] = $options['password'] ?? ''; $options['driverOptions'] = $options['driverOptions'] ?? []; + $options['ssl'] = isset($options['ssl']) ? $options['ssl'] : []; + + if ($options['ssl'] !== []) + { + $options['ssl']['enable'] = isset($options['ssl']['enable']) ? $options['ssl']['enable'] : false; + $options['ssl']['cipher'] = isset($options['ssl']['cipher']) ? $options['ssl']['cipher'] : null; + $options['ssl']['ca'] = isset($options['ssl']['ca']) ? $options['ssl']['ca'] : null; + $options['ssl']['capath'] = isset($options['ssl']['capath']) ? $options['ssl']['capath'] : null; + $options['ssl']['key'] = isset($options['ssl']['key']) ? $options['ssl']['key'] : null; + $options['ssl']['cert'] = isset($options['ssl']['cert']) ? $options['ssl']['cert'] : null; + $options['ssl']['verify_server_cert'] = isset($options['ssl']['verify_server_cert']) ? $options['ssl']['verify_server_cert'] : null; + } // Finalize initialisation parent::__construct($options); @@ -253,6 +265,36 @@ public function connect() $replace = ['#HOST#', '#PORT#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['database']]; + // For data in transit TLS encryption. + if ($this->options['ssl'] !== [] && $this->options['ssl']['enable'] === true) + { + if (isset($this->options['ssl']['verify_server_cert']) && $this->options['ssl']['verify_server_cert'] === true) + { + $format .= ';sslmode=verify-full'; + } + else + { + $format .= ';sslmode=require'; + } + + $sslKeysMapping = [ + 'cipher' => null, + 'ca' => 'sslrootcert', + 'capath' => null, + 'key' => 'sslkey', + 'cert' => 'sslcert', + ]; + + // If customised, add cipher suite, ca file path, ca path, private key file path and certificate file path to PDO driver options. + foreach ($sslKeysMapping as $key => $value) + { + if ($value !== null && $this->options['ssl'][$key] !== null) + { + $format .= ';' . $value . '=' . $this->options['ssl'][$key]; + } + } + } + break; case 'sqlite': diff --git a/src/Pgsql/PgsqlDriver.php b/src/Pgsql/PgsqlDriver.php index 04b165b3e..72f5ce34e 100644 --- a/src/Pgsql/PgsqlDriver.php +++ b/src/Pgsql/PgsqlDriver.php @@ -133,6 +133,31 @@ public function getConnectionCollation() return $array[0]['lc_collate']; } + /** + * Method to get the database encryption details (cipher and protocol) in use. + * + * @return string The database encryption details. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getConnectionEncryption(): string + { + $query = $this->getQuery(true) + ->select($this->quoteName(['version', 'cipher'])) + ->from($this->quoteName('pg_stat_ssl')) + ->where($this->quoteName('pid') . ' = pg_backend_pid()'); + + $variables = $this->setQuery($query)->loadAssoc(); + + if (!empty($variables['cipher'])) + { + return $variables['version'] . ' (' . $variables['cipher'] . ')'; + } + + return ''; + } + /** * Shows the table CREATE statement that creates the given tables. * diff --git a/src/Sqlite/SqliteDriver.php b/src/Sqlite/SqliteDriver.php index f7c4f5bde..f2d89681d 100644 --- a/src/Sqlite/SqliteDriver.php +++ b/src/Sqlite/SqliteDriver.php @@ -168,6 +168,20 @@ public function getConnectionCollation() return $this->charset; } + /** + * Method to get the database encryption details (cipher and protocol) in use. + * + * @return string The database encryption details. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getConnectionEncryption(): string + { + // TODO: Not fake this + return ''; + } + /** * Shows the table CREATE statement that creates the given tables. * diff --git a/src/Sqlsrv/SqlsrvDriver.php b/src/Sqlsrv/SqlsrvDriver.php index aaabea81f..ea5f2b92c 100644 --- a/src/Sqlsrv/SqlsrvDriver.php +++ b/src/Sqlsrv/SqlsrvDriver.php @@ -354,6 +354,20 @@ public function getConnectionCollation() return 'MSSQL UTF-8 (UCS2)'; } + /** + * Method to get the database encryption details (cipher and protocol) in use. + * + * @return string The database encryption details. + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function getConnectionEncryption(): string + { + // TODO: Not fake this + return ''; + } + /** * Retrieves field information about the given tables. *