Skip to content

Commit

Permalink
ssh: keyboard-interactive auth #956
Browse files Browse the repository at this point in the history
  • Loading branch information
bummoblizard committed Jun 21, 2024
1 parent 21ed054 commit b989015
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 26 deletions.
34 changes: 19 additions & 15 deletions CodeApp/Containers/MainScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,21 +262,25 @@ private struct MainView: View {
authenticationRequestManager.title,
isPresented: $authenticationRequestManager.isShowingAlert,
actions: {
TextField(
authenticationRequestManager.usernameTitleKey ?? "common.username",
text: $authenticationRequestManager.username
)
.textContentType(.username)
.disableAutocorrection(true)
.autocapitalization(.none)

SecureField(
authenticationRequestManager.passwordTitleKey ?? "common.password",
text: $authenticationRequestManager.password
)
.textContentType(.password)
.disableAutocorrection(true)
.autocapitalization(.none)
if let usernameTitleKey = authenticationRequestManager.usernameTitleKey {
TextField(
usernameTitleKey,
text: $authenticationRequestManager.username
)
.textContentType(.username)
.disableAutocorrection(true)
.autocapitalization(.none)
}

if let passwordTitleKey = authenticationRequestManager.passwordTitleKey {
SecureField(
passwordTitleKey,
text: $authenticationRequestManager.password
)
.textContentType(.password)
.disableAutocorrection(true)
.autocapitalization(.none)
}

Button(
"common.cancel", role: .cancel,
Expand Down
18 changes: 14 additions & 4 deletions CodeApp/Containers/RemoteContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ struct RemoteContainer: View {
guard
try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString(
"remote.authenticate_to \(hostUrl.host ?? "host")", comment: ""))
localizedReason: String(
format: NSLocalizedString(
"remote.authenticate_to %@", comment: ""), hostUrl.host ?? "host"))
else {
throw WorkSpaceStorage.FSError.AuthFailure
}
Expand Down Expand Up @@ -185,6 +186,13 @@ struct RemoteContainer: View {
}
}

private func onRequestInteractiveKeyboard(prompt: String) async -> String {
return
(try? await authenticationRequestManager.requestPasswordAuthentication(
title: "\(prompt)", usernameTitleKey: ""
).0) ?? ""
}

private func connectToHostWithCredentialsUsingJumpHost(
host: RemoteHost,
jumpHost: RemoteHost,
Expand All @@ -210,7 +218,8 @@ struct RemoteContainer: View {
App.workSpaceStorage.connectToServer(
host: hostUrl, authenticationModeForHost: hostAuthenticationMode,
jumpServer: jumpHostUrl,
authenticationModeForJumpServer: jumpHostAuthenticationMode
authenticationModeForJumpServer: jumpHostAuthenticationMode,
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard
) {
error in
connectionResultHandler(
Expand Down Expand Up @@ -250,7 +259,8 @@ struct RemoteContainer: View {
try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Void, Error>) in
App.workSpaceStorage.connectToServer(
host: hostUrl, authenticationMode: authenticationMode
host: hostUrl, authenticationMode: authenticationMode,
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard
) {
error in
connectionResultHandler(
Expand Down
2 changes: 1 addition & 1 deletion CodeApp/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ are licensed under [BSD-3-Clause License](https://en.wikipedia.org/wiki/BSD_lice
"remote.reload_key" = "Reload key";
"remote.passphrase_for_private_key" = "Passphrase for private key";
"remote.credentials_for %@" = "Credentials for %@";
"remote.enter_credentials" = "Enter credentials";
"remote.enter_credentials" = "Enter Credentials";
"remote.authenticate_to %@" = "Authenticate to %@";
"remote.one_or_more_hosts_use_this_host_as_jump_proxy" = "One or more hosts use this host as a jump proxy.";
"remote.confirm_delete_are_you_sure_to_delete" = "Are you sure you want to delete this host?";
Expand Down
23 changes: 19 additions & 4 deletions CodeApp/Managers/FileSystem/SFTP/SFTPFileSystemProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,19 @@ class SFTPFileSystemProvider: NSObject {
private var didDisconnect: (Error) -> Void
private var onSocketClosed: ((SFTPSocket) -> Void)? = nil
private var onTerminalData: ((Data) -> Void)? = nil
private var onRequestInteractiveKeyboard: ((String) async -> String)
private var session: NMSSHSession!
private let queue = DispatchQueue(label: "sftp.serial.queue")
private var jumpHostFSS: [SFTPFileSystemProvider] = []

init?(
baseURL: URL, username: String, didDisconnect: @escaping (Error) -> Void,
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
onTerminalData: ((Data) -> Void)?
) {
self.didDisconnect = didDisconnect
self.onTerminalData = onTerminalData
self.onRequestInteractiveKeyboard = onRequestInteractiveKeyboard
super.init()

do {
Expand All @@ -84,7 +87,8 @@ class SFTPFileSystemProvider: NSObject {

private func configureTerminalSession(baseURL: URL, username: String) throws {
self._terminalServiceProvider = SFTPTerminalServiceProvider(
baseURL: baseURL, username: username)
baseURL: baseURL, username: username,
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard)
guard self._terminalServiceProvider != nil else {
throw SFTPError.InvalidHostURL
}
Expand Down Expand Up @@ -118,8 +122,8 @@ class SFTPFileSystemProvider: NSObject {
try configureTerminalSession(baseURL: terminalURL, username: session.username)
}

async let r1: ()? = self._terminalServiceProvider?.connect(authentication: authentication)
async let r2: Void = withCheckedThrowingContinuation {
try await self._terminalServiceProvider?.connect(authentication: authentication)
try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Void, Error>) in
queue.async {
self.session.connect()
Expand Down Expand Up @@ -147,6 +151,10 @@ class SFTPFileSystemProvider: NSObject {
}
}

if !self.session.isAuthorized {
self.session.authenticateByKeyboardInteractive()
}

guard self.session.isConnected && self.session.isAuthorized else {
continuation.resume(throwing: SFTPError.AuthFailure)
return
Expand All @@ -160,7 +168,6 @@ class SFTPFileSystemProvider: NSObject {
self.session.sftp.connect()
}
}
_ = try await (r1, r2)
}

private func configureJumpHost(jumpHost: SFTPJumpHost) async throws -> (URL, URL) {
Expand All @@ -169,12 +176,14 @@ class SFTPFileSystemProvider: NSObject {
baseURL: jumpHost.url,
username: jumpHost.username,
didDisconnect: didDisconnect,
onRequestInteractiveKeyboard: self.onRequestInteractiveKeyboard,
onTerminalData: nil
),
let secondaryJumpServerFS = SFTPFileSystemProvider(
baseURL: jumpHost.url,
username: jumpHost.username,
didDisconnect: didDisconnect,
onRequestInteractiveKeyboard: self.onRequestInteractiveKeyboard,
onTerminalData: nil
)
else {
Expand Down Expand Up @@ -223,6 +232,12 @@ extension SFTPFileSystemProvider: NMSSHSessionDelegate {
func session(_ session: NMSSHSession, didDisconnectWithError error: Error) {
didDisconnect(error)
}

func session(_ session: NMSSHSession, keyboardInteractiveRequest request: String) -> String {
return UnsafeTask {
await self.onRequestInteractiveKeyboard(request)
}.get()
}
}

extension SFTPFileSystemProvider: NMSSHSocketDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ class SFTPTerminalServiceProvider: NSObject, TerminalServiceProvider {

private var onStdout: ((Data) -> Void)? = nil
private var onStderr: ((Data) -> Void)? = nil
private var onRequestInteractiveKeyboard: ((String) async -> String)
private let queue = DispatchQueue(label: "terminal.serial.queue")

init?(baseURL: URL, username: String) {
init?(
baseURL: URL, username: String,
onRequestInteractiveKeyboard: @escaping ((String) async -> String)
) {
guard baseURL.scheme == "sftp",
let host = baseURL.host,
let port = baseURL.port
else {
return nil
}
self.onRequestInteractiveKeyboard = onRequestInteractiveKeyboard
super.init()

queue.async {
Expand All @@ -39,6 +44,7 @@ class SFTPTerminalServiceProvider: NSObject, TerminalServiceProvider {
(continuation: CheckedContinuation<Void, Error>) in
queue.async {
self.session.connect()
self.session.timeout = 10

if self.session.isConnected {
switch authentication {
Expand All @@ -62,6 +68,10 @@ class SFTPTerminalServiceProvider: NSObject, TerminalServiceProvider {
}
}

if !self.session.isAuthorized {
self.session.authenticateByKeyboardInteractive()
}

guard self.session.isConnected && self.session.isAuthorized else {
continuation.resume(throwing: SFTPError.AuthFailure)
return
Expand Down Expand Up @@ -118,6 +128,15 @@ class SFTPTerminalServiceProvider: NSObject, TerminalServiceProvider {
}

extension SFTPTerminalServiceProvider: NMSSHSessionDelegate {
func session(_ session: NMSSHSession, didDisconnectWithError error: Error) {
didDisconnect?()
}

func session(_ session: NMSSHSession, keyboardInteractiveRequest request: String) -> String {
return UnsafeTask {
await self.onRequestInteractiveKeyboard(request)
}.get()
}
}

extension SFTPTerminalServiceProvider: NMSSHChannelDelegate {
Expand Down
8 changes: 7 additions & 1 deletion CodeApp/Managers/FileSystem/WorkSpaceStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class WorkSpaceStorage: ObservableObject {
func connectToServer(
host: URL, authenticationModeForHost: RemoteAuthenticationMode,
jumpServer: URL, authenticationModeForJumpServer: RemoteAuthenticationMode,
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
completionHandler: @escaping (Error?) -> Void
) {
guard host.scheme == "sftp" && jumpServer.scheme == "sftp" else {
Expand All @@ -111,6 +112,7 @@ class WorkSpaceStorage: ObservableObject {
_connectToServer(
host: host,
authenticationMode: authenticationModeForHost,
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard,
sftpJumpHost: SFTPJumpHost(
url: jumpServer, username: authenticationModeForJumpServer.credentials.user!,
authentication: authenticationModeForJumpServer),
Expand All @@ -122,6 +124,7 @@ class WorkSpaceStorage: ObservableObject {

func connectToServer(
host: URL, authenticationMode: RemoteAuthenticationMode,
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
completionHandler: @escaping (Error?) -> Void
) {
if isConnecting {
Expand All @@ -130,7 +133,8 @@ class WorkSpaceStorage: ObservableObject {
}
isConnecting = true
_connectToServer(
host: host, authenticationMode: authenticationMode, sftpJumpHost: nil,
host: host, authenticationMode: authenticationMode,
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard, sftpJumpHost: nil,
completionHandler: { error in
completionHandler(error)
self.isConnecting = false
Expand All @@ -139,6 +143,7 @@ class WorkSpaceStorage: ObservableObject {

private func _connectToServer(
host: URL, authenticationMode: RemoteAuthenticationMode,
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
sftpJumpHost: SFTPJumpHost?,
completionHandler: @escaping (Error?) -> Void
) {
Expand Down Expand Up @@ -169,6 +174,7 @@ class WorkSpaceStorage: ObservableObject {
didDisconnect: { error in
self.disconnect()
},
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard,
onTerminalData: self.onTerminalDataAction)
else {
completionHandler(FSError.Unknown)
Expand Down
19 changes: 19 additions & 0 deletions CodeApp/Utilities/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,22 @@ extension CodableWrapper: Equatable {
return lhs.rawValue == rhs.rawValue
}
}

// https://stackoverflow.com/questions/74372835/mutation-of-captured-var-in-concurrently-executing-code

class UnsafeTask<T> {
let semaphore = DispatchSemaphore(value: 0)
private var result: T?
init(block: @escaping () async -> T) {
Task {
result = await block()
semaphore.signal()
}
}

func get() -> T {
if let result = result { return result }
semaphore.wait()
return result!
}
}

0 comments on commit b989015

Please sign in to comment.