Skip to content

Commit b989015

Browse files
committed
ssh: keyboard-interactive auth #956
1 parent 21ed054 commit b989015

File tree

7 files changed

+99
-26
lines changed

7 files changed

+99
-26
lines changed

CodeApp/Containers/MainScene.swift

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -262,21 +262,25 @@ private struct MainView: View {
262262
authenticationRequestManager.title,
263263
isPresented: $authenticationRequestManager.isShowingAlert,
264264
actions: {
265-
TextField(
266-
authenticationRequestManager.usernameTitleKey ?? "common.username",
267-
text: $authenticationRequestManager.username
268-
)
269-
.textContentType(.username)
270-
.disableAutocorrection(true)
271-
.autocapitalization(.none)
272-
273-
SecureField(
274-
authenticationRequestManager.passwordTitleKey ?? "common.password",
275-
text: $authenticationRequestManager.password
276-
)
277-
.textContentType(.password)
278-
.disableAutocorrection(true)
279-
.autocapitalization(.none)
265+
if let usernameTitleKey = authenticationRequestManager.usernameTitleKey {
266+
TextField(
267+
usernameTitleKey,
268+
text: $authenticationRequestManager.username
269+
)
270+
.textContentType(.username)
271+
.disableAutocorrection(true)
272+
.autocapitalization(.none)
273+
}
274+
275+
if let passwordTitleKey = authenticationRequestManager.passwordTitleKey {
276+
SecureField(
277+
passwordTitleKey,
278+
text: $authenticationRequestManager.password
279+
)
280+
.textContentType(.password)
281+
.disableAutocorrection(true)
282+
.autocapitalization(.none)
283+
}
280284

281285
Button(
282286
"common.cancel", role: .cancel,

CodeApp/Containers/RemoteContainer.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ struct RemoteContainer: View {
103103
guard
104104
try await context.evaluatePolicy(
105105
.deviceOwnerAuthenticationWithBiometrics,
106-
localizedReason: NSLocalizedString(
107-
"remote.authenticate_to \(hostUrl.host ?? "host")", comment: ""))
106+
localizedReason: String(
107+
format: NSLocalizedString(
108+
"remote.authenticate_to %@", comment: ""), hostUrl.host ?? "host"))
108109
else {
109110
throw WorkSpaceStorage.FSError.AuthFailure
110111
}
@@ -185,6 +186,13 @@ struct RemoteContainer: View {
185186
}
186187
}
187188

189+
private func onRequestInteractiveKeyboard(prompt: String) async -> String {
190+
return
191+
(try? await authenticationRequestManager.requestPasswordAuthentication(
192+
title: "\(prompt)", usernameTitleKey: ""
193+
).0) ?? ""
194+
}
195+
188196
private func connectToHostWithCredentialsUsingJumpHost(
189197
host: RemoteHost,
190198
jumpHost: RemoteHost,
@@ -210,7 +218,8 @@ struct RemoteContainer: View {
210218
App.workSpaceStorage.connectToServer(
211219
host: hostUrl, authenticationModeForHost: hostAuthenticationMode,
212220
jumpServer: jumpHostUrl,
213-
authenticationModeForJumpServer: jumpHostAuthenticationMode
221+
authenticationModeForJumpServer: jumpHostAuthenticationMode,
222+
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard
214223
) {
215224
error in
216225
connectionResultHandler(
@@ -250,7 +259,8 @@ struct RemoteContainer: View {
250259
try await withCheckedThrowingContinuation {
251260
(continuation: CheckedContinuation<Void, Error>) in
252261
App.workSpaceStorage.connectToServer(
253-
host: hostUrl, authenticationMode: authenticationMode
262+
host: hostUrl, authenticationMode: authenticationMode,
263+
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard
254264
) {
255265
error in
256266
connectionResultHandler(

CodeApp/Localization/en.lproj/Localizable.strings

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ are licensed under [BSD-3-Clause License](https://en.wikipedia.org/wiki/BSD_lice
439439
"remote.reload_key" = "Reload key";
440440
"remote.passphrase_for_private_key" = "Passphrase for private key";
441441
"remote.credentials_for %@" = "Credentials for %@";
442-
"remote.enter_credentials" = "Enter credentials";
442+
"remote.enter_credentials" = "Enter Credentials";
443443
"remote.authenticate_to %@" = "Authenticate to %@";
444444
"remote.one_or_more_hosts_use_this_host_as_jump_proxy" = "One or more hosts use this host as a jump proxy.";
445445
"remote.confirm_delete_are_you_sure_to_delete" = "Are you sure you want to delete this host?";

CodeApp/Managers/FileSystem/SFTP/SFTPFileSystemProvider.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,19 @@ class SFTPFileSystemProvider: NSObject {
5555
private var didDisconnect: (Error) -> Void
5656
private var onSocketClosed: ((SFTPSocket) -> Void)? = nil
5757
private var onTerminalData: ((Data) -> Void)? = nil
58+
private var onRequestInteractiveKeyboard: ((String) async -> String)
5859
private var session: NMSSHSession!
5960
private let queue = DispatchQueue(label: "sftp.serial.queue")
6061
private var jumpHostFSS: [SFTPFileSystemProvider] = []
6162

6263
init?(
6364
baseURL: URL, username: String, didDisconnect: @escaping (Error) -> Void,
65+
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
6466
onTerminalData: ((Data) -> Void)?
6567
) {
6668
self.didDisconnect = didDisconnect
6769
self.onTerminalData = onTerminalData
70+
self.onRequestInteractiveKeyboard = onRequestInteractiveKeyboard
6871
super.init()
6972

7073
do {
@@ -84,7 +87,8 @@ class SFTPFileSystemProvider: NSObject {
8487

8588
private func configureTerminalSession(baseURL: URL, username: String) throws {
8689
self._terminalServiceProvider = SFTPTerminalServiceProvider(
87-
baseURL: baseURL, username: username)
90+
baseURL: baseURL, username: username,
91+
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard)
8892
guard self._terminalServiceProvider != nil else {
8993
throw SFTPError.InvalidHostURL
9094
}
@@ -118,8 +122,8 @@ class SFTPFileSystemProvider: NSObject {
118122
try configureTerminalSession(baseURL: terminalURL, username: session.username)
119123
}
120124

121-
async let r1: ()? = self._terminalServiceProvider?.connect(authentication: authentication)
122-
async let r2: Void = withCheckedThrowingContinuation {
125+
try await self._terminalServiceProvider?.connect(authentication: authentication)
126+
try await withCheckedThrowingContinuation {
123127
(continuation: CheckedContinuation<Void, Error>) in
124128
queue.async {
125129
self.session.connect()
@@ -147,6 +151,10 @@ class SFTPFileSystemProvider: NSObject {
147151
}
148152
}
149153

154+
if !self.session.isAuthorized {
155+
self.session.authenticateByKeyboardInteractive()
156+
}
157+
150158
guard self.session.isConnected && self.session.isAuthorized else {
151159
continuation.resume(throwing: SFTPError.AuthFailure)
152160
return
@@ -160,7 +168,6 @@ class SFTPFileSystemProvider: NSObject {
160168
self.session.sftp.connect()
161169
}
162170
}
163-
_ = try await (r1, r2)
164171
}
165172

166173
private func configureJumpHost(jumpHost: SFTPJumpHost) async throws -> (URL, URL) {
@@ -169,12 +176,14 @@ class SFTPFileSystemProvider: NSObject {
169176
baseURL: jumpHost.url,
170177
username: jumpHost.username,
171178
didDisconnect: didDisconnect,
179+
onRequestInteractiveKeyboard: self.onRequestInteractiveKeyboard,
172180
onTerminalData: nil
173181
),
174182
let secondaryJumpServerFS = SFTPFileSystemProvider(
175183
baseURL: jumpHost.url,
176184
username: jumpHost.username,
177185
didDisconnect: didDisconnect,
186+
onRequestInteractiveKeyboard: self.onRequestInteractiveKeyboard,
178187
onTerminalData: nil
179188
)
180189
else {
@@ -223,6 +232,12 @@ extension SFTPFileSystemProvider: NMSSHSessionDelegate {
223232
func session(_ session: NMSSHSession, didDisconnectWithError error: Error) {
224233
didDisconnect(error)
225234
}
235+
236+
func session(_ session: NMSSHSession, keyboardInteractiveRequest request: String) -> String {
237+
return UnsafeTask {
238+
await self.onRequestInteractiveKeyboard(request)
239+
}.get()
240+
}
226241
}
227242

228243
extension SFTPFileSystemProvider: NMSSHSocketDelegate {

CodeApp/Managers/FileSystem/SFTP/SFTPTerminalServiceProvider.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@ class SFTPTerminalServiceProvider: NSObject, TerminalServiceProvider {
1414

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

19-
init?(baseURL: URL, username: String) {
20+
init?(
21+
baseURL: URL, username: String,
22+
onRequestInteractiveKeyboard: @escaping ((String) async -> String)
23+
) {
2024
guard baseURL.scheme == "sftp",
2125
let host = baseURL.host,
2226
let port = baseURL.port
2327
else {
2428
return nil
2529
}
30+
self.onRequestInteractiveKeyboard = onRequestInteractiveKeyboard
2631
super.init()
2732

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

4349
if self.session.isConnected {
4450
switch authentication {
@@ -62,6 +68,10 @@ class SFTPTerminalServiceProvider: NSObject, TerminalServiceProvider {
6268
}
6369
}
6470

71+
if !self.session.isAuthorized {
72+
self.session.authenticateByKeyboardInteractive()
73+
}
74+
6575
guard self.session.isConnected && self.session.isAuthorized else {
6676
continuation.resume(throwing: SFTPError.AuthFailure)
6777
return
@@ -118,6 +128,15 @@ class SFTPTerminalServiceProvider: NSObject, TerminalServiceProvider {
118128
}
119129

120130
extension SFTPTerminalServiceProvider: NMSSHSessionDelegate {
131+
func session(_ session: NMSSHSession, didDisconnectWithError error: Error) {
132+
didDisconnect?()
133+
}
134+
135+
func session(_ session: NMSSHSession, keyboardInteractiveRequest request: String) -> String {
136+
return UnsafeTask {
137+
await self.onRequestInteractiveKeyboard(request)
138+
}.get()
139+
}
121140
}
122141

123142
extension SFTPTerminalServiceProvider: NMSSHChannelDelegate {

CodeApp/Managers/FileSystem/WorkSpaceStorage.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class WorkSpaceStorage: ObservableObject {
9797
func connectToServer(
9898
host: URL, authenticationModeForHost: RemoteAuthenticationMode,
9999
jumpServer: URL, authenticationModeForJumpServer: RemoteAuthenticationMode,
100+
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
100101
completionHandler: @escaping (Error?) -> Void
101102
) {
102103
guard host.scheme == "sftp" && jumpServer.scheme == "sftp" else {
@@ -111,6 +112,7 @@ class WorkSpaceStorage: ObservableObject {
111112
_connectToServer(
112113
host: host,
113114
authenticationMode: authenticationModeForHost,
115+
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard,
114116
sftpJumpHost: SFTPJumpHost(
115117
url: jumpServer, username: authenticationModeForJumpServer.credentials.user!,
116118
authentication: authenticationModeForJumpServer),
@@ -122,6 +124,7 @@ class WorkSpaceStorage: ObservableObject {
122124

123125
func connectToServer(
124126
host: URL, authenticationMode: RemoteAuthenticationMode,
127+
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
125128
completionHandler: @escaping (Error?) -> Void
126129
) {
127130
if isConnecting {
@@ -130,7 +133,8 @@ class WorkSpaceStorage: ObservableObject {
130133
}
131134
isConnecting = true
132135
_connectToServer(
133-
host: host, authenticationMode: authenticationMode, sftpJumpHost: nil,
136+
host: host, authenticationMode: authenticationMode,
137+
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard, sftpJumpHost: nil,
134138
completionHandler: { error in
135139
completionHandler(error)
136140
self.isConnecting = false
@@ -139,6 +143,7 @@ class WorkSpaceStorage: ObservableObject {
139143

140144
private func _connectToServer(
141145
host: URL, authenticationMode: RemoteAuthenticationMode,
146+
onRequestInteractiveKeyboard: @escaping ((String) async -> String),
142147
sftpJumpHost: SFTPJumpHost?,
143148
completionHandler: @escaping (Error?) -> Void
144149
) {
@@ -169,6 +174,7 @@ class WorkSpaceStorage: ObservableObject {
169174
didDisconnect: { error in
170175
self.disconnect()
171176
},
177+
onRequestInteractiveKeyboard: onRequestInteractiveKeyboard,
172178
onTerminalData: self.onTerminalDataAction)
173179
else {
174180
completionHandler(FSError.Unknown)

CodeApp/Utilities/Utilities.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,22 @@ extension CodableWrapper: Equatable {
5252
return lhs.rawValue == rhs.rawValue
5353
}
5454
}
55+
56+
// https://stackoverflow.com/questions/74372835/mutation-of-captured-var-in-concurrently-executing-code
57+
58+
class UnsafeTask<T> {
59+
let semaphore = DispatchSemaphore(value: 0)
60+
private var result: T?
61+
init(block: @escaping () async -> T) {
62+
Task {
63+
result = await block()
64+
semaphore.signal()
65+
}
66+
}
67+
68+
func get() -> T {
69+
if let result = result { return result }
70+
semaphore.wait()
71+
return result!
72+
}
73+
}

0 commit comments

Comments
 (0)