Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c27210f4f4 |
@@ -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.";
|
||||
|
||||
@@ -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) ?? ""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CFReadStream>?
|
||||
var write: Unmanaged<CFWriteStream>?
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user