Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
face625f23 | ||
|
|
1d8e8702a8 | ||
|
|
2aaeff3774 | ||
|
|
3f9f572434 | ||
|
|
2229238e52 | ||
|
|
60cbf790db |
@@ -221,8 +221,8 @@
|
|||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "DBSyn";
|
INFOPLIST_KEY_CFBundleDisplayName = "DBSyn";
|
||||||
MARKETING_VERSION = 1.1.0;
|
MARKETING_VERSION = 1.1.1;
|
||||||
CURRENT_PROJECT_VERSION = 82;
|
CURRENT_PROJECT_VERSION = 83;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved.";
|
||||||
@@ -244,8 +244,8 @@
|
|||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "DBSyn";
|
INFOPLIST_KEY_CFBundleDisplayName = "DBSyn";
|
||||||
MARKETING_VERSION = 1.1.0;
|
MARKETING_VERSION = 1.1.1;
|
||||||
CURRENT_PROJECT_VERSION = 82;
|
CURRENT_PROJECT_VERSION = 83;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved.";
|
||||||
|
|||||||
@@ -74,17 +74,53 @@ enum PasswordStore {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
static func save(_ password: String, id: UUID) {
|
@discardableResult
|
||||||
|
static func save(_ password: String, id: UUID) -> Bool {
|
||||||
let query: [String: Any] = [
|
let query: [String: Any] = [
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
kSecAttrService as String: service,
|
kSecAttrService as String: service,
|
||||||
kSecAttrAccount as String: id.uuidString
|
kSecAttrAccount as String: id.uuidString
|
||||||
]
|
]
|
||||||
SecItemDelete(query as CFDictionary)
|
|
||||||
guard !password.isEmpty else { return }
|
if password.isEmpty {
|
||||||
var attributes = query
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
attributes[kSecValueData as String] = Data(password.utf8)
|
return status == errSecSuccess || status == errSecItemNotFound
|
||||||
SecItemAdd(attributes as CFDictionary, nil)
|
}
|
||||||
|
|
||||||
|
let data = Data(password.utf8)
|
||||||
|
|
||||||
|
let update: [String: Any] = [
|
||||||
|
kSecValueData as String: data
|
||||||
|
]
|
||||||
|
|
||||||
|
let updateStatus = SecItemUpdate(
|
||||||
|
query as CFDictionary,
|
||||||
|
update as CFDictionary
|
||||||
|
)
|
||||||
|
|
||||||
|
if updateStatus == errSecSuccess {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateStatus == errSecItemNotFound {
|
||||||
|
var attributes = query
|
||||||
|
attributes[kSecValueData as String] = data
|
||||||
|
|
||||||
|
let addStatus = SecItemAdd(
|
||||||
|
attributes as CFDictionary,
|
||||||
|
nil
|
||||||
|
)
|
||||||
|
|
||||||
|
if addStatus == errSecSuccess {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog("DBSyn Keychain add failed: %d", addStatus)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog("DBSyn Keychain update failed: %d", updateStatus)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
static func delete(_ id: UUID) {
|
static func delete(_ id: UUID) {
|
||||||
|
|||||||
+183
-45
@@ -3,6 +3,7 @@ import CommonCrypto
|
|||||||
import CoreFoundation
|
import CoreFoundation
|
||||||
import CFNetwork
|
import CFNetwork
|
||||||
import Security
|
import Security
|
||||||
|
import Darwin
|
||||||
|
|
||||||
// Native MySQL wire protocol client. No external mysql executable is used.
|
// Native MySQL wire protocol client. No external mysql executable is used.
|
||||||
// Initial scope: TCP, mysql_native_password and caching_sha2_password fast authentication.
|
// Initial scope: TCP, mysql_native_password and caching_sha2_password fast authentication.
|
||||||
@@ -135,8 +136,14 @@ private func authResponse(password: String, salt: [UInt8], plugin: String) throw
|
|||||||
return zip(first, last).map { $0 ^ $1 }
|
return zip(first, last).map { $0 ^ $1 }
|
||||||
}
|
}
|
||||||
if plugin == "caching_sha2_password" {
|
if plugin == "caching_sha2_password" {
|
||||||
let first = sha256(pwd), second = sha256(sha256(pwd))
|
// MySQL caching_sha2_password authentication:
|
||||||
let last = sha256(second + salt)
|
// XOR(SHA256(password),
|
||||||
|
// SHA256(SHA256(SHA256(password)) + challenge))
|
||||||
|
let first = sha256(pwd)
|
||||||
|
let second = sha256(first)
|
||||||
|
let third = sha256(second)
|
||||||
|
let last = sha256(third + salt)
|
||||||
|
|
||||||
return zip(first, last).map { $0 ^ $1 }
|
return zip(first, last).map { $0 ^ $1 }
|
||||||
}
|
}
|
||||||
throw MySQLWireError(message: "Authentication plugin \(plugin) is not yet supported")
|
throw MySQLWireError(message: "Authentication plugin \(plugin) is not yet supported")
|
||||||
@@ -198,9 +205,141 @@ private func mysqlRSAPublicKey(_ packet: [UInt8]) throws -> SecKey {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecureTransport operates on the same socket before and after MySQL SSLRequest.
|
||||||
|
// CFStream SSL properties cannot reliably retrofit TLS onto already-open streams.
|
||||||
|
private final class MySQLSocketTransport {
|
||||||
|
let fd: Int32
|
||||||
|
private var ssl: SSLContext?
|
||||||
|
|
||||||
|
init(host: String, port: Int) throws {
|
||||||
|
var hints = addrinfo()
|
||||||
|
hints.ai_family = AF_UNSPEC
|
||||||
|
hints.ai_socktype = Int32(SOCK_STREAM)
|
||||||
|
hints.ai_protocol = Int32(IPPROTO_TCP)
|
||||||
|
var addresses: UnsafeMutablePointer<addrinfo>?
|
||||||
|
let result = getaddrinfo(host, String(port), &hints, &addresses)
|
||||||
|
guard result == 0 else { throw MySQLWireError(message: "Cannot resolve MySQL host: \(String(cString: gai_strerror(result)))") }
|
||||||
|
defer { freeaddrinfo(addresses) }
|
||||||
|
var candidate = addresses
|
||||||
|
var connected: Int32 = -1
|
||||||
|
while let item = candidate {
|
||||||
|
let info = item.pointee
|
||||||
|
let sock = Darwin.socket(info.ai_family, info.ai_socktype, info.ai_protocol)
|
||||||
|
if sock >= 0 {
|
||||||
|
if Darwin.connect(sock, info.ai_addr, info.ai_addrlen) == 0 {
|
||||||
|
connected = sock
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = Darwin.close(sock)
|
||||||
|
}
|
||||||
|
candidate = info.ai_next
|
||||||
|
}
|
||||||
|
guard connected >= 0 else { throw MySQLWireError(message: "Cannot connect to MySQL at \(host):\(port): \(String(cString: strerror(errno)))") }
|
||||||
|
fd = connected
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let ssl { _ = SSLClose(ssl) }
|
||||||
|
_ = Darwin.close(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(_ buffer: UnsafeMutablePointer<UInt8>, count: Int) throws -> Int {
|
||||||
|
if let ssl {
|
||||||
|
var processed = 0
|
||||||
|
let status = SSLRead(ssl, buffer, count, &processed)
|
||||||
|
if status == errSecSuccess || (status == errSSLWouldBlock && processed > 0) { return processed }
|
||||||
|
throw MySQLWireError(message: "MySQL TLS read failed (OSStatus \(status))")
|
||||||
|
}
|
||||||
|
let n = Darwin.recv(fd, buffer, count, 0)
|
||||||
|
guard n > 0 else { throw MySQLWireError(message: n == 0 ? "MySQL connection closed" : "MySQL read failed: \(String(cString: strerror(errno)))") }
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ buffer: UnsafePointer<UInt8>, count: Int) throws -> Int {
|
||||||
|
if let ssl {
|
||||||
|
var processed = 0
|
||||||
|
let status = SSLWrite(ssl, buffer, count, &processed)
|
||||||
|
if status == errSecSuccess || (status == errSSLWouldBlock && processed > 0) { return processed }
|
||||||
|
throw MySQLWireError(message: "MySQL TLS write failed (OSStatus \(status))")
|
||||||
|
}
|
||||||
|
let n = Darwin.send(fd, buffer, count, 0)
|
||||||
|
guard n > 0 else { throw MySQLWireError(message: "MySQL write failed: \(String(cString: strerror(errno)))") }
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTLS(host: String, verify: Bool) throws {
|
||||||
|
NSLog("DBSyn TLS: host=%@ verify=%@", host, verify ? "ON" : "OFF")
|
||||||
|
guard let context = SSLCreateContext(nil, .clientSide, .streamType) else {
|
||||||
|
throw MySQLWireError(message: "Unable to create MySQL TLS context")
|
||||||
|
}
|
||||||
|
// SecureTransport callback connections refer to this retained transport.
|
||||||
|
let connection = Unmanaged.passUnretained(self).toOpaque()
|
||||||
|
var status = SSLSetIOFuncs(context, { connection, data, length in
|
||||||
|
|
||||||
|
let transport = Unmanaged<MySQLSocketTransport>.fromOpaque(connection).takeUnretainedValue()
|
||||||
|
let requested = length.pointee
|
||||||
|
let n = Darwin.recv(transport.fd, data, requested, 0)
|
||||||
|
if n > 0 { length.pointee = n; return errSecSuccess }
|
||||||
|
length.pointee = 0
|
||||||
|
return n == 0 ? errSSLClosedGraceful : errSecIO
|
||||||
|
}, { connection, data, length in
|
||||||
|
|
||||||
|
let transport = Unmanaged<MySQLSocketTransport>.fromOpaque(connection).takeUnretainedValue()
|
||||||
|
let n = Darwin.send(transport.fd, data, length.pointee, 0)
|
||||||
|
if n > 0 { length.pointee = n; return errSecSuccess }
|
||||||
|
length.pointee = 0
|
||||||
|
return errSecIO
|
||||||
|
})
|
||||||
|
guard status == errSecSuccess else { throw MySQLWireError(message: "TLS I/O setup failed (\(status))") }
|
||||||
|
status = SSLSetConnection(context, connection)
|
||||||
|
guard status == errSecSuccess else { throw MySQLWireError(message: "TLS socket setup failed (\(status))") }
|
||||||
|
status = host.withCString { SSLSetPeerDomainName(context, $0, host.utf8.count) }
|
||||||
|
guard status == errSecSuccess else { throw MySQLWireError(message: "TLS peer setup failed (\(status))") }
|
||||||
|
// REQUIRED encrypts but does not authenticate the server. Keep verification
|
||||||
|
// enabled for the verified mode; never silently fall back to REQUIRED.
|
||||||
|
// Certificate verification is handled after the TLS handshake.
|
||||||
|
status = SSLSetSessionOption(context, .breakOnServerAuth, true)
|
||||||
|
guard status == errSecSuccess else {
|
||||||
|
throw MySQLWireError(message: "TLS authentication setup failed (\(status))")
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat {
|
||||||
|
status = SSLHandshake(context)
|
||||||
|
} while status == errSSLWouldBlock
|
||||||
|
|
||||||
|
if status == OSStatus(-9841) {
|
||||||
|
if verify {
|
||||||
|
var trust: SecTrust?
|
||||||
|
let trustStatus = SSLCopyPeerTrust(context, &trust)
|
||||||
|
|
||||||
|
guard trustStatus == errSecSuccess, let trust else {
|
||||||
|
throw MySQLWireError(message: "Unable to obtain MySQL server trust")
|
||||||
|
}
|
||||||
|
|
||||||
|
var trustError: CFError?
|
||||||
|
|
||||||
|
guard SecTrustEvaluateWithError(trust, &trustError) else {
|
||||||
|
throw MySQLWireError(
|
||||||
|
message: "MySQL server verification failed (verifyTLS=\(verify)): \(trustError?.localizedDescription ?? "untrusted server")"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat {
|
||||||
|
status = SSLHandshake(context)
|
||||||
|
} while status == errSSLWouldBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
guard status == errSecSuccess else {
|
||||||
|
throw MySQLWireError(message: "MySQL TLS handshake failed (OSStatus \(status))")
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class MySQLWireConnection {
|
final class MySQLWireConnection {
|
||||||
private let input: InputStream
|
private let transport: MySQLSocketTransport
|
||||||
private let output: OutputStream
|
|
||||||
private var sequence: UInt8 = 0
|
private var sequence: UInt8 = 0
|
||||||
private let maxPacket = 16_777_215
|
private let maxPacket = 16_777_215
|
||||||
private let serverHost: String
|
private let serverHost: String
|
||||||
@@ -213,32 +352,16 @@ final class MySQLWireConnection {
|
|||||||
self.requireTLS = requireTLS
|
self.requireTLS = requireTLS
|
||||||
self.verifyTLS = verifyTLS
|
self.verifyTLS = verifyTLS
|
||||||
guard (1...65535).contains(port) else { throw MySQLWireError(message: "Invalid port") }
|
guard (1...65535).contains(port) else { throw MySQLWireError(message: "Invalid port") }
|
||||||
var read: Unmanaged<CFReadStream>?
|
transport = try MySQLSocketTransport(host: host, port: port)
|
||||||
var write: Unmanaged<CFWriteStream>?
|
|
||||||
CFStreamCreatePairWithSocketToHost(nil, host as CFString, UInt32(port), &read, &write)
|
|
||||||
guard let r = read?.takeRetainedValue(), let w = write?.takeRetainedValue() else {
|
|
||||||
throw MySQLWireError(message: "Cannot create connection")
|
|
||||||
}
|
|
||||||
input = unsafeBitCast(r, to: InputStream.self)
|
|
||||||
output = unsafeBitCast(w, to: OutputStream.self)
|
|
||||||
input.open(); output.open()
|
|
||||||
// A CFStream may still be .opening immediately after open(). The
|
|
||||||
// first blocking read below waits for the server greeting.
|
|
||||||
if input.streamStatus == .error || output.streamStatus == .error {
|
|
||||||
throw MySQLWireError(message: input.streamError?.localizedDescription ?? output.streamError?.localizedDescription ?? "Could not open MySQL connection")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
deinit { input.close(); output.close() }
|
|
||||||
|
|
||||||
private func readExact(_ count: Int) throws -> [UInt8] {
|
private func readExact(_ count: Int) throws -> [UInt8] {
|
||||||
var buffer = [UInt8](repeating: 0, count: count)
|
var buffer = [UInt8](repeating: 0, count: count)
|
||||||
var done = 0
|
var done = 0
|
||||||
while done < count {
|
while done < count {
|
||||||
let n = buffer.withUnsafeMutableBufferPointer { ptr -> Int in
|
let n = try buffer.withUnsafeMutableBufferPointer { ptr -> Int in
|
||||||
guard let base = ptr.baseAddress else { return -1 }
|
try transport.read(ptr.baseAddress!.advanced(by: done), count: count - done)
|
||||||
return input.read(base.advanced(by: done), maxLength: count - done)
|
|
||||||
}
|
}
|
||||||
guard n > 0 else { throw MySQLWireError(message: input.streamError?.localizedDescription ?? "MySQL connection closed") }
|
|
||||||
done += n
|
done += n
|
||||||
}
|
}
|
||||||
return buffer
|
return buffer
|
||||||
@@ -247,9 +370,7 @@ final class MySQLWireConnection {
|
|||||||
let header = try readExact(4)
|
let header = try readExact(4)
|
||||||
let count = Int(header[0]) | Int(header[1]) << 8 | Int(header[2]) << 16
|
let count = Int(header[0]) | Int(header[1]) << 8 | Int(header[2]) << 16
|
||||||
sequence = header[3] &+ 1
|
sequence = header[3] &+ 1
|
||||||
guard count <= maxPacket else {
|
guard count <= maxPacket else { throw MySQLWireError(message: "MySQL packet exceeds protocol limit") }
|
||||||
throw MySQLWireError(message: "MySQL packet exceeds protocol limit")
|
|
||||||
}
|
|
||||||
return try readExact(count)
|
return try readExact(count)
|
||||||
}
|
}
|
||||||
private func send(_ payload: [UInt8]) throws {
|
private func send(_ payload: [UInt8]) throws {
|
||||||
@@ -259,30 +380,14 @@ final class MySQLWireConnection {
|
|||||||
sequence = sequence &+ 1
|
sequence = sequence &+ 1
|
||||||
var written = 0
|
var written = 0
|
||||||
while written < packet.count {
|
while written < packet.count {
|
||||||
let n = packet.withUnsafeBufferPointer { ptr -> Int in
|
let n = try packet.withUnsafeBufferPointer { ptr -> Int in
|
||||||
guard let base = ptr.baseAddress else { return -1 }
|
try transport.write(ptr.baseAddress!.advanced(by: written), count: packet.count - written)
|
||||||
return output.write(base.advanced(by: written), maxLength: packet.count - written)
|
|
||||||
}
|
}
|
||||||
guard n > 0 else { throw MySQLWireError(message: output.streamError?.localizedDescription ?? "MySQL write failed") }
|
|
||||||
written += n
|
written += n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MySQL TLS is negotiated AFTER the server greeting: first send CLIENT_SSL,
|
|
||||||
// then upgrade the existing TCP streams, then send the authentication packet.
|
|
||||||
// Never send credentials if TLS negotiation or certificate verification fails.
|
|
||||||
private func upgradeToTLS() throws {
|
private func upgradeToTLS() throws {
|
||||||
let settings: [String: Any] = [
|
try transport.startTLS(host: serverHost, verify: verifyTLS)
|
||||||
kCFStreamSSLLevel as String: kCFStreamSocketSecurityLevelNegotiatedSSL,
|
|
||||||
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),
|
|
||||||
output.setProperty(settings, forKey: key) else {
|
|
||||||
throw MySQLWireError(message: "TLS stream upgrade unavailable; no credentials sent. Use SSH tunnelling for this server.")
|
|
||||||
}
|
|
||||||
// CFStream performs the TLS handshake before delivering encrypted data.
|
|
||||||
// Writing authentication triggers the handshake; any failure aborts it.
|
|
||||||
tlsActive = true
|
tlsActive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +414,21 @@ final class MySQLWireConnection {
|
|||||||
_ = try c.take(10)
|
_ = try c.take(10)
|
||||||
let salt2 = try c.take(max(13, authLen - 8))
|
let salt2 = try c.take(max(13, authLen - 8))
|
||||||
let salt = salt1 + salt2.prefix(max(0, authLen - 9))
|
let salt = salt1 + salt2.prefix(max(0, authLen - 9))
|
||||||
|
|
||||||
|
// MySQL authentication uses the 20-byte server challenge.
|
||||||
|
guard salt.count == 20 else {
|
||||||
|
throw MySQLWireError(
|
||||||
|
message: "Invalid MySQL authentication challenge length: \(salt.count)"
|
||||||
|
)
|
||||||
|
}
|
||||||
let plugin = (try? c.nulString()) ?? "mysql_native_password"
|
let plugin = (try? c.nulString()) ?? "mysql_native_password"
|
||||||
|
|
||||||
|
NSLog(
|
||||||
|
"DBSyn MySQL authentication: plugin=%@ saltLength=%d tlsRequired=%@",
|
||||||
|
plugin,
|
||||||
|
salt.count,
|
||||||
|
requireTLS ? "YES" : "NO"
|
||||||
|
)
|
||||||
if requireTLS {
|
if requireTLS {
|
||||||
let clientSSL = 0x00000800
|
let clientSSL = 0x00000800
|
||||||
guard capabilities & clientSSL != 0 else {
|
guard capabilities & clientSSL != 0 else {
|
||||||
@@ -331,6 +450,13 @@ final class MySQLWireConnection {
|
|||||||
body += Array(plugin.utf8) + [0]
|
body += Array(plugin.utf8) + [0]
|
||||||
try send(body)
|
try send(body)
|
||||||
var reply = try readPacket()
|
var reply = try readPacket()
|
||||||
|
|
||||||
|
NSLog(
|
||||||
|
"DBSyn MySQL initial authentication response: type=0x%02X length=%d",
|
||||||
|
Int(reply.first ?? 0),
|
||||||
|
reply.count
|
||||||
|
)
|
||||||
|
|
||||||
try checkError(reply)
|
try checkError(reply)
|
||||||
var currentPlugin = plugin
|
var currentPlugin = plugin
|
||||||
var currentSalt = Array(salt)
|
var currentSalt = Array(salt)
|
||||||
@@ -349,6 +475,12 @@ final class MySQLWireConnection {
|
|||||||
guard currentPlugin == "caching_sha2_password", reply.count >= 2 else {
|
guard currentPlugin == "caching_sha2_password", reply.count >= 2 else {
|
||||||
throw MySQLWireError(message: "Unsupported authentication continuation")
|
throw MySQLWireError(message: "Unsupported authentication continuation")
|
||||||
}
|
}
|
||||||
|
NSLog(
|
||||||
|
"DBSyn MySQL authentication continuation: code=0x%02X TLS=%@",
|
||||||
|
Int(reply[1]),
|
||||||
|
tlsActive ? "YES" : "NO"
|
||||||
|
)
|
||||||
|
|
||||||
switch reply[1] {
|
switch reply[1] {
|
||||||
case 0x03: // Fast authentication accepted; wait for OK.
|
case 0x03: // Fast authentication accepted; wait for OK.
|
||||||
reply = try readPacket()
|
reply = try readPacket()
|
||||||
@@ -384,6 +516,12 @@ final class MySQLWireConnection {
|
|||||||
throw MySQLWireError(message: "Unsupported caching_sha2_password response: \(reply[1])")
|
throw MySQLWireError(message: "Unsupported caching_sha2_password response: \(reply[1])")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NSLog(
|
||||||
|
"DBSyn MySQL final authentication response: type=0x%02X length=%d",
|
||||||
|
Int(reply.first ?? 0),
|
||||||
|
reply.count
|
||||||
|
)
|
||||||
|
|
||||||
try checkError(reply)
|
try checkError(reply)
|
||||||
guard reply.first == 0x00 else { throw MySQLWireError(message: "Authentication did not complete") }
|
guard reply.first == 0x00 else { throw MySQLWireError(message: "Authentication did not complete") }
|
||||||
if !database.isEmpty { _ = try query("USE `\(database.replacingOccurrences(of: "`", with: "``"))`") }
|
if !database.isEmpty { _ = try query("USE `\(database.replacingOccurrences(of: "`", with: "``"))`") }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# DBSyn 1.1.1 (Build 83)
|
||||||
|
|
||||||
|
- Replace CFStream's post-open TLS property upgrade with a socket-backed SecureTransport handshake after MySQL SSLRequest.
|
||||||
|
- Preserve verified TLS and explicitly selected encrypted-only TLS modes.
|
||||||
|
- Do not change SSH tunnelling, saved connections, or database operations.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
This source patch has not been compiled on macOS or tested against Venus. Build and test in Xcode before release. SecureTransport is a deprecated Apple API and should be migrated to a maintained TLS implementation in a later release.
|
||||||
Reference in New Issue
Block a user