mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-05-09 20:47:22 +00:00
* Don't delete public cert keys corrresponding to secretive-tracked keys * First pass * Fix fallback for name * Split out into dedicated handler * Stub add identities * .
121 lines
5.3 KiB
Swift
121 lines
5.3 KiB
Swift
import Foundation
|
|
import OSLog
|
|
|
|
/// Manages storage and lookup for OpenSSH certificates.
|
|
public class OpenSSHCertificateHandler {
|
|
|
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
|
private let logger = Logger()
|
|
private let writer = OpenSSHKeyWriter()
|
|
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
|
|
|
|
/// Initializes an OpenSSHCertificateHandler.
|
|
public init() {
|
|
}
|
|
|
|
/// Reloads any certificates in the PublicKeys folder.
|
|
/// - Parameter secrets: the secrets to look up corresponding certificates for.
|
|
public func reloadCertificates(for secrets: [AnySecret]) {
|
|
guard publicKeyFileStoreController.hasAnyCertificates else {
|
|
logger.log("No certificates, short circuiting")
|
|
return
|
|
}
|
|
keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in
|
|
partialResult[next] = try? loadKeyblobAndName(for: next)
|
|
}
|
|
}
|
|
|
|
/// Whether or not the certificate handler has a certifiicate associated with a given secret.
|
|
/// - Parameter secret: The secret to check for a certificate.
|
|
/// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret
|
|
public func hasCertificate<SecretType: Secret>(for secret: SecretType) -> Bool {
|
|
keyBlobsAndNames[AnySecret(secret)] != nil
|
|
}
|
|
|
|
|
|
/// Reconstructs a public key from a ``Data``, if that ``Data`` contains an OpenSSH certificate hash. Currently only ecdsa certificates are supported
|
|
/// - Parameter certBlock: The openssh certificate to extract the public key from
|
|
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
|
|
public func publicKeyHash(from hash: Data) -> Data? {
|
|
let reader = OpenSSHReader(data: hash)
|
|
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
|
|
|
|
switch certType {
|
|
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
|
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
|
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
|
_ = reader.readNextChunk() // nonce
|
|
let curveIdentifier = reader.readNextChunk()
|
|
let publicKey = reader.readNextChunk()
|
|
|
|
let curveType = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").data(using: .utf8)!
|
|
return writer.lengthAndData(of: curveType) +
|
|
writer.lengthAndData(of: curveIdentifier) +
|
|
writer.lengthAndData(of: publicKey)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
|
/// - Parameter secret: The secret to search for a certificate with
|
|
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
|
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
|
keyBlobsAndNames[AnySecret(secret)]
|
|
}
|
|
|
|
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
|
/// - Parameter secret: The secret to search for a certificate with
|
|
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
|
private func loadKeyblobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
|
let certificatePath = publicKeyFileStoreController.sshCertificatePath(for: secret)
|
|
guard FileManager.default.fileExists(atPath: certificatePath) else {
|
|
return nil
|
|
}
|
|
|
|
logger.debug("Found certificate for \(secret.name)")
|
|
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
|
|
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
|
|
|
|
guard certElements.count >= 2 else {
|
|
logger.warning("Certificate found for \(secret.name) but failed to load")
|
|
throw OpenSSHCertificateError.parsingFailed
|
|
}
|
|
guard let certDecoded = Data(base64Encoded: certElements[1] as String) else {
|
|
logger.warning("Certificate found for \(secret.name) but failed to decode base64 key")
|
|
throw OpenSSHCertificateError.parsingFailed
|
|
}
|
|
|
|
if certElements.count >= 3, let certName = certElements[2].data(using: .utf8) {
|
|
return (certDecoded, certName)
|
|
} else if let certName = secret.name.data(using: .utf8) {
|
|
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
|
|
return (certDecoded, certName)
|
|
} else {
|
|
throw OpenSSHCertificateError.parsingFailed
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension OpenSSHCertificateHandler {
|
|
|
|
enum OpenSSHCertificateError: LocalizedError {
|
|
case unsupportedType
|
|
case parsingFailed
|
|
case doesNotExist
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .unsupportedType:
|
|
return "The key type was unsupported"
|
|
case .parsingFailed:
|
|
return "Failed to properly parse the SSH certificate"
|
|
case .doesNotExist:
|
|
return "Certificate does not exist"
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|