Skip to content

rsyncOSX/SSHCreateKey

Repository files navigation

Hi there 👋

This package is code for assisting users to create SSH identityfile and key in RsyncUI. The user can either let RsyncUI assist in creating SSH identityfile and key in RsyncUI, or create it by commandline.

SSHCreateKey

A Swift package for managing SSH keys, including creation, validation, and deployment to remote servers. Provides a safe, type-safe interface for common SSH key operations.

Features

  • SSH Key Generation: Create RSA key pairs with ssh-keygen
  • Key Deployment: Copy public keys to remote servers using ssh-copy-id
  • Key Validation: Verify public key presence and remote key access
  • Path Management: Handle custom SSH key paths with tilde expansion
  • Security Validation: Input sanitization for server addresses and usernames
  • Port Configuration: Support for custom SSH ports
  • Directory Management: Automatic SSH directory creation
  • Key Discovery: List all SSH keys in the key directory

Requirements

  • Swift 5.9+
  • macOS 13.0+ / iOS 16.0+ (macOS recommended for full SSH functionality)
  • Foundation framework
  • SSH command-line tools (ssh-keygen, ssh-copy-id, ssh)

Usage

Basic Key Creation

import SSHCreateKey

// Initialize with default settings
let sshKey = SSHCreateKey(
    sharedSSHPort: nil,
    sharedSSHKeyPathAndIdentityFile: nil
)

// Create SSH directory if needed
try sshKey.createSSHKeyRootPath()

// Generate key creation arguments
let args = try sshKey.argumentsCreateKey()
// Returns: ["-t", "rsa", "-N", "", "-f", "/Users/username/.ssh/id_rsa"]

// Execute ssh-keygen (using your process execution framework)
// /usr/bin/ssh-keygen -t rsa -N "" -f /Users/username/.ssh/id_rsa

Custom SSH Key Path

// Use custom key location
let sshKey = SSHCreateKey(
    sharedSSHPort: "2222",
    sharedSSHKeyPathAndIdentityFile: "~/.ssh/my_custom_key"
)

// Get full path
if let fullPath = sshKey.sshKeyPathAndIdentityFile {
    print("Key will be created at: \(fullPath)")
    // Output: /Users/username/.ssh/my_custom_key
}

// Get just the identity file name
print("Identity file: \(sshKey.identityFileOnly)")
// Output: my_custom_key

// Get directory path only
if let dirPath = sshKey.sshKeyPath {
    print("SSH directory: \(dirPath)")
    // Output: /Users/username/.ssh
}

Deploy Key to Remote Server

let sshKey = SSHCreateKey(
    sharedSSHPort: "22",
    sharedSSHKeyPathAndIdentityFile: "~/.ssh/id_rsa"
)

// Generate ssh-copy-id arguments
let args = try sshKey.argumentsSSHCopyID(
    offsiteServer: "example.com",
    offsiteUsername: "john"
)
// Returns arguments for: ssh-copy-id -i ~/.ssh/id_rsa -p 22 john@example.com

// Execute ssh-copy-id with these arguments

Verify Remote Key Access

let sshKey = SSHCreateKey(
    sharedSSHPort: "2222",
    sharedSSHKeyPathAndIdentityFile: "~/.ssh/id_rsa"
)

// Generate SSH verification arguments
let args = try sshKey.argumentsVerifyRemotePublicSSHKey(
    offsiteServer: "example.com",
    offsiteUsername: "john"
)
// Returns arguments for: ssh -p 2222 -i ~/.ssh/id_rsa john@example.com

// Test connection with these arguments

Validate Key Presence

let sshKey = SSHCreateKey(
    sharedSSHPort: nil,
    sharedSSHKeyPathAndIdentityFile: "~/.ssh/id_rsa"
)

// Check if public key exists
if sshKey.validatePublicKeyPresent() {
    print("✓ Public key (id_rsa.pub) exists")
} else {
    print("✗ Public key not found - need to create it")
}

List All SSH Keys

let sshKey = SSHCreateKey(
    sharedSSHPort: nil,
    sharedSSHKeyPathAndIdentityFile: nil
)

// Get all files in SSH directory
if let keyFiles = sshKey.allSSHKeyFiles {
    print("SSH Key Files:")
    for file in keyFiles {
        print("  - \(file)")
    }
}
// Example output:
//   - id_rsa
//   - id_rsa.pub
//   - known_hosts
//   - config

Complete Workflow Example

Creating and Deploying an SSH Key

import SSHCreateKey
import ProcessCommand  // Your process execution framework

func setupSSHKey(
    server: String,
    username: String,
    customKeyPath: String? = nil,
    port: String? = nil
) async throws {
    
    // 1. Initialize SSH key manager
    let sshKey = SSHCreateKey(
        sharedSSHPort: port,
        sharedSSHKeyPathAndIdentityFile: customKeyPath
    )
    
    // 2. Create SSH directory if needed
    print("Creating SSH directory...")
    try sshKey.createSSHKeyRootPath()
    
    // 3. Check if key already exists
    if sshKey.validatePublicKeyPresent() {
        print("✓ SSH key already exists")
    } else {
        print("Creating new SSH key...")
        
        // 4. Generate the key
        let keygenArgs = try sshKey.argumentsCreateKey()
        
        // Execute ssh-keygen (pseudo-code)
        let process = ProcessCommand(
            command: "/usr/bin/ssh-keygen",
            arguments: keygenArgs,
            handlers: createHandlers()
        )
        try await process.executeProcess()
        
        print("✓ SSH key created")
    }
    
    // 5. Copy key to remote server
    print("Deploying key to \(server)...")
    let copyArgs = try sshKey.argumentsSSHCopyID(
        offsiteServer: server,
        offsiteUsername: username
    )
    
    // Execute ssh-copy-id (pseudo-code)
    let copyProcess = ProcessCommand(
        command: "/usr/bin/ssh-copy-id",
        arguments: Array(copyArgs.dropFirst()), // Remove command itself
        handlers: createHandlers()
    )
    try await copyProcess.executeProcess()
    
    print("✓ Key deployed to server")
    
    // 6. Verify connection
    print("Verifying SSH connection...")
    let verifyArgs = try sshKey.argumentsVerifyRemotePublicSSHKey(
        offsiteServer: server,
        offsiteUsername: username
    )
    
    // Test SSH connection (pseudo-code)
    let verifyProcess = ProcessCommand(
        command: "/usr/bin/ssh",
        arguments: Array(verifyArgs.dropFirst()) + ["echo", "Connection successful"],
        handlers: createHandlers()
    )
    try await verifyProcess.executeProcess()
    
    print("✓ SSH setup complete!")
}

// Usage
try await setupSSHKey(
    server: "example.com",
    username: "john",
    customKeyPath: "~/.ssh/my_server_key",
    port: "2222"
)

SwiftUI Integration

SSH Key Setup View

import SwiftUI
import SSHCreateKey

struct SSHKeySetupView: View {
    @State private var server = ""
    @State private var username = ""
    @State private var port = "22"
    @State private var customKeyPath = ""
    @State private var useCustomPath = false
    
    @State private var isProcessing = false
    @State private var statusMessage = ""
    @State private var errorMessage: String?
    
    var body: some View {
        Form {
            Section("Server Details") {
                TextField("Server Address", text: $server)
                    .textContentType(.URL)
                
                TextField("Username", text: $username)
                    .textContentType(.username)
                
                TextField("Port", text: $port)
                    .keyboardType(.numberPad)
            }
            
            Section("SSH Key Configuration") {
                Toggle("Use Custom Key Path", isOn: $useCustomPath)
                
                if useCustomPath {
                    TextField("Key Path", text: $customKeyPath)
                        .font(.system(.body, design: .monospaced))
                    Text("Example: ~/.ssh/my_custom_key")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
            
            Section {
                Button(action: setupKey) {
                    if isProcessing {
                        ProgressView()
                    } else {
                        Label("Setup SSH Key", systemImage: "key.fill")
                    }
                }
                .disabled(isProcessing || server.isEmpty || username.isEmpty)
            }
            
            if !statusMessage.isEmpty {
                Section("Status") {
                    Text(statusMessage)
                        .foregroundStyle(.secondary)
                }
            }
            
            if let errorMessage {
                Section("Error") {
                    Text(errorMessage)
                        .foregroundStyle(.red)
                }
            }
        }
        .navigationTitle("SSH Key Setup")
    }
    
    func setupKey() {
        Task {
            isProcessing = true
            errorMessage = nil
            statusMessage = "Initializing..."
            
            do {
                let sshKey = SSHCreateKey(
                    sharedSSHPort: port,
                    sharedSSHKeyPathAndIdentityFile: useCustomPath ? customKeyPath : nil
                )
                
                // Create directory
                statusMessage = "Creating SSH directory..."
                try sshKey.createSSHKeyRootPath()
                
                // Check for existing key
                if sshKey.validatePublicKeyPresent() {
                    statusMessage = "✓ SSH key already exists"
                } else {
                    statusMessage = "Creating new SSH key..."
                    let args = try sshKey.argumentsCreateKey()
                    // Execute key creation here
                    statusMessage = "✓ SSH key created"
                }
                
                // Deploy key
                statusMessage = "Deploying key to server..."
                let copyArgs = try sshKey.argumentsSSHCopyID(
                    offsiteServer: server,
                    offsiteUsername: username
                )
                // Execute ssh-copy-id here
                
                statusMessage = "✓ Setup complete!"
                
            } catch let error as SSHKeyError {
                errorMessage = error.localizedDescription
                statusMessage = "Setup failed"
            } catch {
                errorMessage = error.localizedDescription
                statusMessage = "Setup failed"
            }
            
            isProcessing = false
        }
    }
}

API Reference

SSHCreateKey

Main class for SSH key management.

Initialization

public init(
    sharedSSHPort: String?,
    sharedSSHKeyPathAndIdentityFile: String?
)

Properties

  • createKeyCommand: String - Path to ssh-keygen (default: "/usr/bin/ssh-keygen")
  • allSSHKeyFiles: [String]? - List of all files in SSH directory
  • sshKeyPathAndIdentityFile: String? - Full path including identity file
  • identityFileOnly: String - Just the identity file name
  • sshKeyPath: String? - Directory path without identity file
  • userHomeDirectoryPath: String? - User's home directory

Methods

Directory Management:

func createSSHKeyRootPath() throws

Creates SSH directory if it doesn't exist.

Key Generation:

func argumentsCreateKey() throws -> [String]

Returns arguments for ssh-keygen to create RSA key pair.

Key Deployment:

func argumentsSSHCopyID(
    offsiteServer: String,
    offsiteUsername: String
) throws -> [String]

Returns arguments for ssh-copy-id to deploy public key.

Key Verification:

func argumentsVerifyRemotePublicSSHKey(
    offsiteServer: String,
    offsiteUsername: String
) throws -> [String]

Returns arguments for SSH connection test.

Validation:

func validatePublicKeyPresent() -> Bool

Checks if public key file exists.

Error Types

public enum SSHKeyError: LocalizedError {
    case invalidPath                    // Invalid SSH key path
    case invalidPort                    // Invalid port number
    case keyDirectoryCreationFailed     // Cannot create directory
    case homeDirectoryNotFound          // Cannot find home directory
    case invalidServerAddress           // Invalid server address
    case invalidUsername                // Invalid username
}

LocationKind

Helper enum for file system checks:

public enum LocationKind {
    case file      // Location is a file
    case folder    // Location is a folder
}

Security Features

Input Validation

SSHCreateKey validates all inputs to prevent command injection:

// These characters are blocked in server addresses and usernames
let invalidCharacters = ";|&$`\n\r"

try sshKey.argumentsSSHCopyID(
    offsiteServer: "example.com; rm -rf /",  // ❌ Throws SSHKeyError.invalidServerAddress
    offsiteUsername: "user"
)

Port Validation

// Port must be between 1 and 65535
let sshKey = SSHCreateKey(
    sharedSSHPort: "99999",  // ❌ Will throw SSHKeyError.invalidPort
    sharedSSHKeyPathAndIdentityFile: nil
)

Path Handling

Tilde Expansion

let sshKey = SSHCreateKey(
    sharedSSHPort: nil,
    sharedSSHKeyPathAndIdentityFile: "~/.ssh/custom_key"
)

// Automatically expands ~ to user home directory
if let fullPath = sshKey.sshKeyPathAndIdentityFile {
    print(fullPath)
    // Output: /Users/john/.ssh/custom_key (not ~/.ssh/custom_key)
}

Default Paths

If no custom path is provided, defaults are used:

  • Key directory: ~/.ssh
  • Identity file: id_rsa
  • Full default path: ~/.ssh/id_rsa

Best Practices

  1. Always validate key presence before attempting to create a new one
  2. Use custom key paths for server-specific keys (e.g., ~/.ssh/production_key)
  3. Handle errors appropriately - all methods throw typed errors
  4. Create directory first using createSSHKeyRootPath() before key generation
  5. Validate input - the class handles validation, but check return values
  6. Use specific ports when servers don't use default SSH port (22)
  7. Test connections after deployment using argumentsVerifyRemotePublicSSHKey()

Common Patterns

Multi-Server Setup

struct ServerConfig {
    let name: String
    let address: String
    let username: String
    let port: String
    let keyPath: String
}

let servers = [
    ServerConfig(name: "Production", address: "prod.example.com", 
                 username: "deploy", port: "22", keyPath: "~/.ssh/prod_key"),
    ServerConfig(name: "Staging", address: "staging.example.com", 
                 username: "deploy", port: "2222", keyPath: "~/.ssh/staging_key")
]

for server in servers {
    let sshKey = SSHCreateKey(
        sharedSSHPort: server.port,
        sharedSSHKeyPathAndIdentityFile: server.keyPath
    )
    
    // Setup key for this server
    try sshKey.createSSHKeyRootPath()
    
    if !sshKey.validatePublicKeyPresent() {
        let args = try sshKey.argumentsCreateKey()
        // Execute key creation
    }
    
    // Deploy to server
    let copyArgs = try sshKey.argumentsSSHCopyID(
        offsiteServer: server.address,
        offsiteUsername: server.username
    )
    // Execute deployment
}

Troubleshooting

Key Already Exists Error

// Check before creating
if sshKey.validatePublicKeyPresent() {
    print("Key already exists - skipping creation")
} else {
    let args = try sshKey.argumentsCreateKey()
    // Create key
}

Permission Denied

Ensure the SSH directory has correct permissions (700):

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub

Connection Refused

Verify the port and server address:

let sshKey = SSHCreateKey(
    sharedSSHPort: "22",  // Verify correct port
    sharedSSHKeyPathAndIdentityFile: nil
)

License

MIT

Author

Thomas Evensen

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages