Add key names to authorized key exports
This commit is contained in:
1 parent
ec7be10c6a
commit
3dfaac3ebd
4 files changed
+71
-19
No files matched your search
@@ -1263,16 +1263,16 @@ struct ExportKeyToHostsSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
let publicKeys = keys.map(\.publicKey)
|
||||
let exportKeys = keys.map { (name: $0.label, publicKey: $0.publicKey) }
|
||||
let connectionCoordinator = app.connections
|
||||
Task {
|
||||
await withTaskGroup(of: (Host.ID, String?).self) { group in
|
||||
for (id, configuration) in configurations {
|
||||
group.addTask {
|
||||
do {
|
||||
for publicKey in publicKeys {
|
||||
for key in exportKeys {
|
||||
try await connectionCoordinator.installAuthorizedKey(
|
||||
publicKey, using: configuration
|
||||
key.publicKey, named: key.name, using: configuration
|
||||
)
|
||||
}
|
||||
return (id, nil)
|
||||
|
||||
@@ -52,8 +52,11 @@ nonisolated final class RemoteConnectionCoordinator: Sendable {
|
||||
}
|
||||
|
||||
func installAuthorizedKey(
|
||||
_ publicKey: String, using configuration: SSHConnectionConfiguration
|
||||
_ publicKey: String, named keyName: String,
|
||||
using configuration: SSHConnectionConfiguration
|
||||
) async throws {
|
||||
try await SSHSessionActor.installAuthorizedKey(publicKey, using: configuration)
|
||||
try await SSHSessionActor.installAuthorizedKey(
|
||||
publicKey, named: keyName, using: configuration
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -474,14 +474,15 @@ actor SSHSessionActor {
|
||||
)
|
||||
}
|
||||
|
||||
/// Appends a public key to a remote account's `authorized_keys`, unless the
|
||||
/// exact line is already present. The key travels over the channel's stdin,
|
||||
/// never as shell syntax, so a comment on the key cannot become a command.
|
||||
/// Appends a named public key to a remote account's `authorized_keys`, unless
|
||||
/// the exact key line is already present. The generated comment and key travel
|
||||
/// over the channel's stdin, never as shell syntax.
|
||||
static func installAuthorizedKey(
|
||||
_ publicKey: String, using configuration: SSHConnectionConfiguration
|
||||
_ publicKey: String, named keyName: String,
|
||||
using configuration: SSHConnectionConfiguration
|
||||
) async throws {
|
||||
try await SSHSessionWorker.installAuthorizedKey(
|
||||
publicKey, using: configuration
|
||||
publicKey, named: keyName, using: configuration
|
||||
)
|
||||
}
|
||||
|
||||
@@ -647,19 +648,17 @@ nonisolated final class SSHSessionWorker: @unchecked Sendable {
|
||||
}
|
||||
|
||||
static func installAuthorizedKey(
|
||||
_ publicKey: String, using configuration: SSHConnectionConfiguration
|
||||
_ publicKey: String, named keyName: String,
|
||||
using configuration: SSHConnectionConfiguration
|
||||
) async throws {
|
||||
_ = initialization
|
||||
let normalized = publicKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalized.isEmpty, !normalized.contains("\n"), !normalized.contains("\r") else {
|
||||
throw TermindSSHError.library("The public key is not a single OpenSSH key line.")
|
||||
}
|
||||
let input = try authorizedKeyInstallInput(publicKey: publicKey, keyName: keyName)
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try installAuthorizedKeyBlocking(
|
||||
normalized, using: configuration
|
||||
input, using: configuration
|
||||
)
|
||||
continuation.resume()
|
||||
} catch {
|
||||
@@ -673,7 +672,7 @@ nonisolated final class SSHSessionWorker: @unchecked Sendable {
|
||||
/// the OpenSSH directory with restrictive permissions and performs an exact
|
||||
/// line check before appending, making repeated exports idempotent.
|
||||
private static func installAuthorizedKeyBlocking(
|
||||
_ publicKey: String, using configuration: SSHConnectionConfiguration
|
||||
_ input: Data, using configuration: SSHConnectionConfiguration
|
||||
) throws {
|
||||
let (transport, created) = try openAuthenticatedSession(
|
||||
using: configuration, timeout: 20
|
||||
@@ -702,7 +701,7 @@ nonisolated final class SSHSessionWorker: @unchecked Sendable {
|
||||
channel = opened
|
||||
|
||||
let command =
|
||||
#"umask 077; dir="$HOME/.ssh"; file="$dir/authorized_keys"; mkdir -p "$dir" && touch "$file" && chmod 700 "$dir" && chmod 600 "$file" && IFS= read -r key && { LC_ALL=C grep -Fqx -- "$key" "$file" || printf '%s\n' "$key" >> "$file"; }"#
|
||||
#"umask 077; dir="$HOME/.ssh"; file="$dir/authorized_keys"; mkdir -p "$dir" && touch "$file" && chmod 700 "$dir" && chmod 600 "$file" && IFS= read -r comment && IFS= read -r key && { LC_ALL=C grep -Fqx -- "$key" "$file" || printf '%s\n%s\n' "$comment" "$key" >> "$file"; }"#
|
||||
let started = command.withCString { commandBuffer in
|
||||
libssh2_channel_process_startup(
|
||||
opened, "exec", 4, commandBuffer, UInt32(command.utf8.count)
|
||||
@@ -712,7 +711,6 @@ nonisolated final class SSHSessionWorker: @unchecked Sendable {
|
||||
throw sessionError(created, prefix: "Unable to start authorized_keys update")
|
||||
}
|
||||
|
||||
let input = Data((publicKey + "\n").utf8)
|
||||
try input.withUnsafeBytes { bytes in
|
||||
guard let base = bytes.baseAddress?.assumingMemoryBound(to: CChar.self) else { return }
|
||||
var offset = 0
|
||||
@@ -743,6 +741,25 @@ nonisolated final class SSHSessionWorker: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
static func authorizedKeyInstallInput(publicKey: String, keyName: String) throws -> Data {
|
||||
let normalizedKey = publicKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard
|
||||
!normalizedKey.isEmpty, !normalizedKey.contains("\n"),
|
||||
!normalizedKey.contains("\r")
|
||||
else {
|
||||
throw TermindSSHError.library("The public key is not a single OpenSSH key line.")
|
||||
}
|
||||
|
||||
let printableName = keyName.unicodeScalars
|
||||
.map { CharacterSet.controlCharacters.contains($0) ? " " : String($0) }
|
||||
.joined()
|
||||
let normalizedName = printableName
|
||||
.split(whereSeparator: \Character.isWhitespace)
|
||||
.joined(separator: " ")
|
||||
let safeName = normalizedName.isEmpty ? "Key" : normalizedName
|
||||
return Data(("# \(safeName) Key by Termind\n\(normalizedKey)\n").utf8)
|
||||
}
|
||||
|
||||
private static func readAll(from channel: OpaquePointer, stream: Int32) -> String {
|
||||
var data = Data()
|
||||
var buffer = [CChar](repeating: 0, count: 4 * 1024)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import Termind
|
||||
|
||||
struct AuthorizedKeyExportTests {
|
||||
@Test
|
||||
func exportPlacesTheKeyNameCommentAboveThePublicKey() throws {
|
||||
let input = try SSHSessionWorker.authorizedKeyInstallInput(
|
||||
publicKey: "ssh-ed25519 AAAAC3Nza test@example.com",
|
||||
keyName: "Production"
|
||||
)
|
||||
|
||||
#expect(
|
||||
String(decoding: input, as: UTF8.self)
|
||||
== "# Production Key by Termind\nssh-ed25519 AAAAC3Nza test@example.com\n"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
func exportKeepsTheGeneratedCommentOnOneLine() throws {
|
||||
let input = try SSHSessionWorker.authorizedKeyInstallInput(
|
||||
publicKey: "ssh-ed25519 AAAAC3Nza",
|
||||
keyName: " Production\n\tServer "
|
||||
)
|
||||
|
||||
#expect(
|
||||
String(decoding: input, as: UTF8.self)
|
||||
== "# Production Server Key by Termind\nssh-ed25519 AAAAC3Nza\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user