From 60cbf790db3fa63b08614a32dc8d57894288b53d Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Wed, 21 Oct 2026 09:47:53 +0100 Subject: [PATCH] row editing --- DBSyn.xcodeproj/project.pbxproj | 8 ++++---- DBSyn/DatabaseModel.swift | 8 +++++--- DBSyn/MainView.swift | 8 ++++++-- DBSyn/MySQLProtocol.swift | 13 ++++++++----- docs/DBSyn-1.1.0-build82-release-notes.md | 12 ++++++++++++ 5 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 docs/DBSyn-1.1.0-build82-release-notes.md diff --git a/DBSyn.xcodeproj/project.pbxproj b/DBSyn.xcodeproj/project.pbxproj index c7bb92b..7956ee4 100644 --- a/DBSyn.xcodeproj/project.pbxproj +++ b/DBSyn.xcodeproj/project.pbxproj @@ -221,8 +221,8 @@ ENABLE_APP_SANDBOX = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "DBSyn"; - MARKETING_VERSION = 1.0.8; - CURRENT_PROJECT_VERSION = 81; + MARKETING_VERSION = 1.1.0; + CURRENT_PROJECT_VERSION = 82; ENABLE_TESTABILITY = YES; ONLY_ACTIVE_ARCH = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved."; @@ -244,8 +244,8 @@ ENABLE_APP_SANDBOX = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "DBSyn"; - MARKETING_VERSION = 1.0.8; - CURRENT_PROJECT_VERSION = 81; + MARKETING_VERSION = 1.1.0; + CURRENT_PROJECT_VERSION = 82; ENABLE_TESTABILITY = YES; ONLY_ACTIVE_ARCH = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved."; diff --git a/DBSyn/DatabaseModel.swift b/DBSyn/DatabaseModel.swift index bbb27b1..3998f99 100644 --- a/DBSyn/DatabaseModel.swift +++ b/DBSyn/DatabaseModel.swift @@ -12,15 +12,16 @@ struct ConnectionProfile: Identifiable, Codable, Hashable { // Optional SSH transport. Existing saved profiles decode with these defaults. var useSSHTunnel = false var useTLS = true + var verifyTLS = true var sshHost = "" var sshPort = 22 var sshUser = "" var sshIdentityFile = "" - enum CodingKeys: String, CodingKey { case id, name, host, port, username, database, useSSHTunnel, useTLS, sshHost, sshPort, sshUser, sshIdentityFile } - init(id: UUID = UUID(), name: String = "Local MySQL", host: String = "127.0.0.1", port: Int = 3306, username: String = "root", database: String = "", useSSHTunnel: Bool = false, useTLS: Bool = true, sshHost: String = "", sshPort: Int = 22, sshUser: String = "", sshIdentityFile: String = "") { + enum CodingKeys: String, CodingKey { case id, name, host, port, username, database, useSSHTunnel, useTLS, verifyTLS, sshHost, sshPort, sshUser, sshIdentityFile } + init(id: UUID = UUID(), name: String = "Local MySQL", host: String = "127.0.0.1", port: Int = 3306, username: String = "root", database: String = "", useSSHTunnel: Bool = false, useTLS: Bool = true, verifyTLS: Bool = true, sshHost: String = "", sshPort: Int = 22, sshUser: String = "", sshIdentityFile: String = "") { self.id = id; self.name = name; self.host = host; self.port = port; self.username = username; self.database = database - self.useSSHTunnel = useSSHTunnel; self.useTLS = useTLS; self.sshHost = sshHost; self.sshPort = sshPort; self.sshUser = sshUser; self.sshIdentityFile = sshIdentityFile + self.useSSHTunnel = useSSHTunnel; self.useTLS = useTLS; self.verifyTLS = verifyTLS; self.sshHost = sshHost; self.sshPort = sshPort; self.sshUser = sshUser; self.sshIdentityFile = sshIdentityFile } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) @@ -32,6 +33,7 @@ struct ConnectionProfile: Identifiable, Codable, Hashable { database = try c.decode(String.self, forKey: .database) useSSHTunnel = try c.decodeIfPresent(Bool.self, forKey: .useSSHTunnel) ?? false useTLS = try c.decodeIfPresent(Bool.self, forKey: .useTLS) ?? true + verifyTLS = try c.decodeIfPresent(Bool.self, forKey: .verifyTLS) ?? true sshHost = try c.decodeIfPresent(String.self, forKey: .sshHost) ?? "" sshPort = try c.decodeIfPresent(Int.self, forKey: .sshPort) ?? 22 sshUser = try c.decodeIfPresent(String.self, forKey: .sshUser) ?? "" diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index 1f3f1b6..f038c82 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -2550,8 +2550,12 @@ struct ConnectionEditor: View { TextField("Default Schema", text: $profile.database) } Section("Secure Transport") { - Toggle("Require verified TLS for direct remote connections", isOn: $profile.useTLS) + Toggle("Verify server identity (recommended)", isOn: $profile.verifyTLS) .disabled(profile.useSSHTunnel) + if !profile.verifyTLS && !profile.useSSHTunnel { + Text("Encrypted TLS (MySQL REQUIRED): traffic is encrypted, but the server identity is NOT verified. Only use with servers you trust.") + .font(.caption).foregroundStyle(.orange) + } Toggle("Connect through SSH tunnel", isOn: $profile.useSSHTunnel) if profile.useSSHTunnel { TextField("SSH Host", text: $profile.sshHost) @@ -2561,7 +2565,7 @@ struct ConnectionEditor: View { Text("Uses your system SSH keys and known_hosts. MySQL hostname is resolved from the SSH server.") .font(.caption).foregroundStyle(.secondary) } else { - Text("Direct remote MySQL uses certificate-verified TLS. The server certificate must match its hostname and be trusted by macOS.") + Text(profile.verifyTLS ? "Direct remote MySQL uses verified TLS." : "Direct remote MySQL uses encrypted TLS without server identity verification.") .font(.caption).foregroundStyle(.secondary) } } diff --git a/DBSyn/MySQLProtocol.swift b/DBSyn/MySQLProtocol.swift index 2745da7..b37eeff 100644 --- a/DBSyn/MySQLProtocol.swift +++ b/DBSyn/MySQLProtocol.swift @@ -205,11 +205,13 @@ final class MySQLWireConnection { private let maxPacket = 16_777_215 private let serverHost: String private let requireTLS: Bool + private let verifyTLS: Bool private var tlsActive = false - init(host: String, port: Int, requireTLS: Bool = false) throws { + init(host: String, port: Int, requireTLS: Bool = false, verifyTLS: Bool = true) throws { self.serverHost = host self.requireTLS = requireTLS + self.verifyTLS = verifyTLS guard (1...65535).contains(port) else { throw MySQLWireError(message: "Invalid port") } var read: Unmanaged? var write: Unmanaged? @@ -271,8 +273,8 @@ final class MySQLWireConnection { private func upgradeToTLS() throws { let settings: [String: Any] = [ kCFStreamSSLLevel as String: kCFStreamSocketSecurityLevelNegotiatedSSL, - kCFStreamSSLValidatesCertificateChain as String: true, - kCFStreamSSLPeerName as String: serverHost + kCFStreamSSLValidatesCertificateChain as String: verifyTLS, + kCFStreamSSLPeerName as String: verifyTLS ? serverHost : kCFNull as Any ] let key = Stream.PropertyKey(rawValue: kCFStreamPropertySSLSettings as String) guard input.setProperty(settings, forKey: key), @@ -509,8 +511,9 @@ private final class PersistentMySQLSessions { let endpoint = try DBSynSSHTunnels.shared.endpoint(for: profile) let connection = try MySQLWireConnection( host: endpoint.0, port: endpoint.1, - requireTLS: !profile.useSSHTunnel && profile.useTLS && - !["127.0.0.1", "localhost", "::1"].contains(endpoint.0.lowercased()) + requireTLS: !profile.useSSHTunnel && + !["127.0.0.1", "localhost", "::1"].contains(endpoint.0.lowercased()), + verifyTLS: profile.verifyTLS ) connection.markHost(endpoint.0) try connection.authenticate(user: profile.username, diff --git a/docs/DBSyn-1.1.0-build82-release-notes.md b/docs/DBSyn-1.1.0-build82-release-notes.md new file mode 100644 index 0000000..089c0a5 --- /dev/null +++ b/docs/DBSyn-1.1.0-build82-release-notes.md @@ -0,0 +1,12 @@ +# DBSyn 1.1.0 (Build 82) + +## Connection security +- Added a per-connection **Verify server identity** option. Enabled by default, including for existing profiles. +- When disabled, direct remote connections still negotiate encrypted MySQL TLS, equivalent to `--ssl-mode=REQUIRED`, without validating the server identity. +- SSH tunnel behaviour is unchanged. +- The connection form warns when identity verification is disabled. + +**Security:** Unverified TLS protects against passive interception but not an active impersonation attack. Use verified TLS whenever possible. + +## Validation +Source changes inspected; macOS/Xcode build and live database connection testing must be performed on a Mac.