Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- SSH TOTP/two-factor authentication support (auto-generate and prompt modes)
- SSH host key verification with fingerprint confirmation
- Keyboard Interactive SSH authentication method
- Column visibility: toggle individual columns on/off via "Columns" button in the status bar or right-click header context menu "Hide Column", with per-tab and per-table persistence
- `SQLDialectDescriptor` in TableProPluginKit: plugins can now self-describe their SQL dialect (keywords, functions, data types, identifier quoting), with `SQLDialectFactory` preferring plugin-provided dialect info over built-in structs
- DDL schema generation protocol in TableProPluginKit: plugins can now optionally provide database-specific ALTER TABLE syntax (ADD/MODIFY/DROP COLUMN, ADD/DROP INDEX, ADD/DROP FK, MODIFY PK) via `PluginDatabaseDriver`, with `SchemaStatementGenerator` trying plugin methods first before falling back to built-in logic
Expand Down
1 change: 1 addition & 0 deletions Libs/libssh2.a
3 changes: 3 additions & 0 deletions Libs/libssh2_arm64.a
Git LFS file not shown
3 changes: 3 additions & 0 deletions Libs/libssh2_universal.a
Git LFS file not shown
3 changes: 3 additions & 0 deletions Libs/libssh2_x86_64.a
Git LFS file not shown
28 changes: 25 additions & 3 deletions TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1777,9 +1777,19 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
HEADER_SEARCH_PATHS = "$(SRCROOT)/TablePro/Core/SSH/CLibSSH2/include";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.17.0;
OTHER_LDFLAGS = "-Wl,-w";
OTHER_LDFLAGS = (
"-Wl,-w",
"-force_load",
"$(PROJECT_DIR)/Libs/libssh2.a",
"-force_load",
"$(PROJECT_DIR)/Libs/libssl.a",
"-force_load",
"$(PROJECT_DIR)/Libs/libcrypto.a",
"-lz",
);
PRODUCT_BUNDLE_IDENTIFIER = com.TablePro;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -1796,6 +1806,7 @@
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TablePro/Core/SSH/CLibSSH2";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.9;
XROS_DEPLOYMENT_TARGET = 26.2;
Expand All @@ -1818,7 +1829,8 @@
CURRENT_PROJECT_VERSION = 31;
DEAD_CODE_STRIPPING = YES;
DEPLOYMENT_POSTPROCESSING = YES;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = D7HJ5TFYCU;
HEADER_SEARCH_PATHS = "$(SRCROOT)/TablePro/Core/SSH/CLibSSH2/include";
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand All @@ -1841,7 +1853,16 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.17.0;
OTHER_LDFLAGS = "-Wl,-w";
OTHER_LDFLAGS = (
"-Wl,-w",
"-force_load",
"$(PROJECT_DIR)/Libs/libssh2.a",
"-force_load",
"$(PROJECT_DIR)/Libs/libssl.a",
"-force_load",
"$(PROJECT_DIR)/Libs/libcrypto.a",
"-lz",
);
PRODUCT_BUNDLE_IDENTIFIER = com.TablePro;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -1858,6 +1879,7 @@
SUPPORTS_MACCATALYST = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/TablePro/Core/SSH/CLibSSH2";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.9;
XROS_DEPLOYMENT_TARGET = 26.2;
Expand Down
12 changes: 9 additions & 3 deletions TablePro/Core/Database/DatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,11 @@ final class DatabaseManager {

// Load Keychain credentials off the main thread to avoid blocking UI
let connectionId = connection.id
let (storedSshPassword, keyPassphrase) = await Task.detached {
let (storedSshPassword, keyPassphrase, totpSecret) = await Task.detached {
let pwd = ConnectionStorage.shared.loadSSHPassword(for: connectionId)
let phrase = ConnectionStorage.shared.loadKeyPassphrase(for: connectionId)
return (pwd, phrase)
let totp = ConnectionStorage.shared.loadTOTPSecret(for: connectionId)
return (pwd, phrase, totp)
}.value

let sshPassword = sshPasswordOverride ?? storedSshPassword
Expand All @@ -373,7 +374,12 @@ final class DatabaseManager {
agentSocketPath: connection.sshConfig.agentSocketPath,
remoteHost: connection.host,
remotePort: connection.port,
jumpHosts: connection.sshConfig.jumpHosts
jumpHosts: connection.sshConfig.jumpHosts,
totpMode: connection.sshConfig.totpMode,
totpSecret: totpSecret,
totpAlgorithm: connection.sshConfig.totpAlgorithm,
totpDigits: connection.sshConfig.totpDigits,
totpPeriod: connection.sshConfig.totpPeriod
)

// Adapt SSL config for tunnel: SSH already authenticates the server,
Expand Down
95 changes: 95 additions & 0 deletions TablePro/Core/SSH/Auth/AgentAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// AgentAuthenticator.swift
// TablePro
//

import Foundation
import os

import CLibSSH2

internal struct AgentAuthenticator: SSHAuthenticator {
private static let logger = Logger(subsystem: "com.TablePro", category: "AgentAuthenticator")

/// Protects setenv/unsetenv of SSH_AUTH_SOCK across concurrent tunnel setups
private static let agentSocketLock = NSLock()

let socketPath: String?

func authenticate(session: OpaquePointer, username: String) throws {
// Save original SSH_AUTH_SOCK so we can restore it
let originalSocketPath = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"]
let needsSocketOverride = socketPath != nil

if let overridePath = socketPath, needsSocketOverride {
Self.agentSocketLock.lock()
Self.logger.debug("Using custom SSH agent socket: \(overridePath, privacy: .private)")
setenv("SSH_AUTH_SOCK", overridePath, 1)
}

defer {
if needsSocketOverride {
// Restore original SSH_AUTH_SOCK
if let originalSocketPath {
setenv("SSH_AUTH_SOCK", originalSocketPath, 1)
} else {
unsetenv("SSH_AUTH_SOCK")
}
Self.agentSocketLock.unlock()
}
}

guard let agent = libssh2_agent_init(session) else {
throw SSHTunnelError.tunnelCreationFailed("Failed to initialize SSH agent")
}

defer {
libssh2_agent_disconnect(agent)
libssh2_agent_free(agent)
}

var rc = libssh2_agent_connect(agent)
guard rc == 0 else {
Self.logger.error("Failed to connect to SSH agent (rc=\(rc))")
throw SSHTunnelError.tunnelCreationFailed("Failed to connect to SSH agent")
}

rc = libssh2_agent_list_identities(agent)
guard rc == 0 else {
Self.logger.error("Failed to list SSH agent identities (rc=\(rc))")
throw SSHTunnelError.tunnelCreationFailed("Failed to list SSH agent identities")
}

// Iterate through available identities and try each
var previousIdentity: UnsafeMutablePointer<libssh2_agent_publickey>?
var currentIdentity: UnsafeMutablePointer<libssh2_agent_publickey>?

while true {
rc = libssh2_agent_get_identity(agent, &currentIdentity, previousIdentity)

if rc == 1 {
// End of identity list, none worked
break
}
if rc < 0 {
Self.logger.error("Failed to get SSH agent identity (rc=\(rc))")
throw SSHTunnelError.tunnelCreationFailed("Failed to get SSH agent identity")
}

guard let identity = currentIdentity else {
break
}

let authRc = libssh2_agent_userauth(agent, username, identity)
if authRc == 0 {
Self.logger.info("SSH agent authentication succeeded")
return
}

previousIdentity = identity
}

Self.logger.error("SSH agent authentication failed: no identity accepted")
throw SSHTunnelError.authenticationFailed
}
}
39 changes: 39 additions & 0 deletions TablePro/Core/SSH/Auth/CompositeAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// CompositeAuthenticator.swift
// TablePro
//

import Foundation
import os

import CLibSSH2

/// Authenticator that tries multiple auth methods in sequence.
/// Used for servers requiring e.g. password + keyboard-interactive (TOTP).
internal struct CompositeAuthenticator: SSHAuthenticator {
private static let logger = Logger(subsystem: "com.TablePro", category: "CompositeAuthenticator")

let authenticators: [any SSHAuthenticator]

func authenticate(session: OpaquePointer, username: String) throws {
var lastError: Error?
for (index, authenticator) in authenticators.enumerated() {
Self.logger.debug("Trying authenticator \(index + 1)/\(authenticators.count)")
do {
try authenticator.authenticate(session: session, username: username)
} catch {
Self.logger.debug("Authenticator \(index + 1) failed: \(error)")
lastError = error
}

if libssh2_userauth_authenticated(session) != 0 {
Self.logger.info("Authentication succeeded after \(index + 1) step(s)")
return
}
}

if libssh2_userauth_authenticated(session) == 0 {
throw lastError ?? SSHTunnelError.authenticationFailed
}
}
}
Loading
Loading