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
6 changes: 6 additions & 0 deletions mft.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
570DF26CE3885F940F090C90 /* MFTKeyGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C24E153DFC07E24B3B2A85 /* MFTKeyGenerator.swift */; };
A7C81C84D7DA6BFD1E7B9CEE /* MFTKeyGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C24E153DFC07E24B3B2A85 /* MFTKeyGenerator.swift */; };
C10B3E0B27A6EC7B00A19857 /* NSString+iconv.h in Headers */ = {isa = PBXBuildFile; fileRef = C10B3E0927A6EC7B00A19857 /* NSString+iconv.h */; };
C10B3E0C27A6EC7B00A19857 /* NSString+iconv.m in Sources */ = {isa = PBXBuildFile; fileRef = C10B3E0A27A6EC7B00A19857 /* NSString+iconv.m */; };
C10B3E0E27A6ECA900A19857 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C10B3E0D27A6EC9C00A19857 /* libiconv.tbd */; };
Expand Down Expand Up @@ -45,6 +47,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
19C24E153DFC07E24B3B2A85 /* MFTKeyGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MFTKeyGenerator.swift; path = MFTKeyGenerator.swift; sourceTree = "<group>"; };
C10B3E0927A6EC7B00A19857 /* NSString+iconv.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+iconv.h"; sourceTree = "<group>"; };
C10B3E0A27A6EC7B00A19857 /* NSString+iconv.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+iconv.m"; sourceTree = "<group>"; };
C10B3E0D27A6EC9C00A19857 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -141,6 +144,7 @@
C10B3E3327A7006300A19857 /* module.modulemap */,
C116CC2C2CC182200053B71A /* Localizable.xcstrings */,
C1E0728C2DCD2E0900FDADF5 /* PrivacyInfo.xcprivacy */,
19C24E153DFC07E24B3B2A85 /* MFTKeyGenerator.swift */,
);
path = mft;
sourceTree = "<group>";
Expand Down Expand Up @@ -335,6 +339,7 @@
C10B3EB227A7D05200A19857 /* NSString+iconv.m in Sources */,
C10B3EB327A7D05200A19857 /* MFTSftpItem.swift in Sources */,
C10B3EB427A7D05200A19857 /* MFTSftpConnection.swift in Sources */,
A7C81C84D7DA6BFD1E7B9CEE /* MFTKeyGenerator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -345,6 +350,7 @@
C10B3E0C27A6EC7B00A19857 /* NSString+iconv.m in Sources */,
C1D2B33127A42788004D9ACE /* MFTSftpItem.swift in Sources */,
C1D2B2D727A3EE58004D9ACE /* MFTSftpConnection.swift in Sources */,
570DF26CE3885F940F090C90 /* MFTKeyGenerator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
128 changes: 128 additions & 0 deletions mft/MFTKeyGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// MFTKeyGenerator.swift
// mft
//
// Copyright © 2026 Marcin Labenski. All rights reserved.
//

import Foundation
@_implementationOnly import libssh

/// A newly generated SSH key pair.
///
/// The private half is what the client keeps and must be protected. The
/// public half is what goes in the server's authorized_keys.
@objcMembers public class MFTKeyPair: NSObject {
/// The private key, PEM wrapped and encrypted when a passphrase was given.
/// Ed25519 keys come out in OpenSSH format, RSA and ECDSA keys in PKCS#8.
public let privateKey: String
/// One authorized_keys line, without a trailing comment.
public let publicKey: String
/// The algorithm, as named on the wire, such as "ssh-ed25519".
public let keyType: String

init(privateKey: String, publicKey: String, keyType: String) {
self.privateKey = privateKey
self.publicKey = publicKey
self.keyType = keyType
}
}

/// Which kind of key to generate.
@objc public enum MFTKeyType: Int {
/// Small, fast, and the sensible default for a new key.
case ed25519
case rsa4096
case ecdsa256

var sshType: ssh_keytypes_e {
switch self {
case .ed25519: return SSH_KEYTYPE_ED25519
case .rsa4096: return SSH_KEYTYPE_RSA
case .ecdsa256: return SSH_KEYTYPE_ECDSA_P256
}
}

/// The RSA modulus size to ask for, where the type does not already imply
/// the key size. libssh defaults to 3072 bits, so 4096 has to be requested.
var rsaKeySize: Int32? {
switch self {
case .rsa4096: return 4096
case .ed25519, .ecdsa256: return nil
}
}
}

@objcMembers public class MFTKeyGenerator: NSObject {

/// Generates an SSH key pair.
///
/// Needs no connection: this is local key material, made before there is
/// anything to connect to.
/// - Parameters:
/// - type: The algorithm to use.
/// - passphrase: Encrypts the private half. Pass nil or a blank string
/// to leave it unencrypted.
/// - Returns: The generated pair.
/// - Throws: NSError on error.
public static func generate(type: MFTKeyType, passphrase: String? = nil) throws -> MFTKeyPair {
var context: ssh_pki_ctx?
if var bits = type.rsaKeySize {
context = ssh_pki_ctx_new()
guard let context,
ssh_pki_ctx_options_set(context, SSH_PKI_OPTION_RSA_KEY_SIZE, &bits) == SSH_OK
else {
throw NSError(
domain: "mft",
code: MFTErrorCode.wrong_keyfile.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Could not set the key size."]
)
}
}
defer { if let context { ssh_pki_ctx_free(context) } }

var key: ssh_key?
guard ssh_pki_generate_key(type.sshType, context, &key) == SSH_OK, let key else {
throw NSError(
domain: "mft",
code: MFTErrorCode.wrong_keyfile.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Could not generate a key."]
)
}
defer { ssh_key_free(key) }

var privateBuffer: UnsafeMutablePointer<CChar>?
let secret = (passphrase?.isEmpty == false) ? passphrase : nil
guard ssh_pki_export_privkey_base64(key, secret, nil, nil, &privateBuffer) == SSH_OK,
let privateBuffer
else {
throw NSError(
domain: "mft",
code: MFTErrorCode.wrong_keyfile.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Could not export the generated private key."]
)
}
defer { ssh_string_free_char(privateBuffer) }

var publicBuffer: UnsafeMutablePointer<CChar>?
guard ssh_pki_export_pubkey_base64(key, &publicBuffer) == SSH_OK, let publicBuffer else {
throw NSError(
domain: "mft",
code: MFTErrorCode.wrong_keyfile.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Could not export the generated public key."]
)
}
defer { ssh_string_free_char(publicBuffer) }

let typeName = String(cString: ssh_key_type_to_char(ssh_key_type(key)))
// ssh_pki_export_pubkey_base64 returns the payload alone, while
// authorized_keys wants the algorithm in front of it.
let publicLine = "\(typeName) \(String(cString: publicBuffer))"

return MFTKeyPair(
privateKey: String(cString: privateBuffer),
publicKey: publicLine,
keyType: typeName
)
}
}
Loading