105 lines
4.5 KiB
Swift
105 lines
4.5 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
|
|
public nonisolated enum KeyStorage: String, Codable, Hashable, Sendable, CaseIterable, Identifiable {
|
|
case thisDevice
|
|
case iCloud
|
|
case secureEnclave
|
|
/// Private key material is held by an authenticator outside this device, such as a USB or NFC security key.
|
|
case external
|
|
|
|
public var id: String { rawValue }
|
|
|
|
}
|
|
|
|
public nonisolated enum SSHKeyAlgorithm: String, Codable, Hashable, Sendable, CaseIterable, Identifiable {
|
|
case unknown
|
|
case securityKey = "sk-ecdsa-sha2-nistp256@openssh.com"
|
|
case ed25519 = "ssh-ed25519"
|
|
case ecdsa256 = "ecdsa-sha2-nistp256"
|
|
case ecdsa384 = "ecdsa-sha2-nistp384"
|
|
case ecdsa521 = "ecdsa-sha2-nistp521"
|
|
case rsa = "ssh-rsa"
|
|
|
|
public nonisolated var id: Self { self }
|
|
}
|
|
|
|
public nonisolated struct SSHKeyRef: Identifiable, Codable, Hashable, Sendable {
|
|
public var id: UUID = UUID()
|
|
public var label: String
|
|
public var algorithm: SSHKeyAlgorithm = .ed25519
|
|
public var publicKey: String = ""
|
|
public var certificate: String = ""
|
|
public var fingerprint: String = ""
|
|
public var securityKey: SecurityKeyCredential?
|
|
public var credentialRef: String = ""
|
|
public var storage: KeyStorage = .iCloud
|
|
public var keySize: Int?
|
|
public var hasPassphrase = false
|
|
public var requiresUserPresence = false
|
|
public var createdAt: Date = Date(timeIntervalSince1970: 0)
|
|
public var colorTag: TagColor = .none
|
|
public var sortIndex: Double = 0
|
|
|
|
public var requiresPassphraseEntry: Bool { hasPassphrase }
|
|
public init(
|
|
id: UUID = UUID(), label: String, algorithm: SSHKeyAlgorithm = .ed25519, publicKey: String = "",
|
|
certificate: String = "", fingerprint: String = "", credentialRef: String = "", storage: KeyStorage = .iCloud,
|
|
securityKey: SecurityKeyCredential? = nil, keySize: Int? = nil, hasPassphrase: Bool = false,
|
|
requiresUserPresence: Bool = false,
|
|
createdAt: Date = Date(timeIntervalSince1970: 0), colorTag: TagColor = .none, sortIndex: Double = 0
|
|
) {
|
|
self.id = id
|
|
self.label = label
|
|
self.algorithm = algorithm
|
|
self.publicKey = publicKey
|
|
self.certificate = certificate
|
|
self.fingerprint = fingerprint
|
|
self.securityKey = securityKey
|
|
self.credentialRef = credentialRef
|
|
self.storage = storage
|
|
self.keySize = keySize
|
|
self.hasPassphrase = hasPassphrase
|
|
self.requiresUserPresence = requiresUserPresence
|
|
self.createdAt = createdAt
|
|
self.colorTag = colorTag
|
|
self.sortIndex = sortIndex
|
|
}
|
|
|
|
}
|
|
|
|
nonisolated extension SSHKeyRef {
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id, label, algorithm, publicKey, certificate, fingerprint
|
|
case securityKey, credentialRef, storage, keySize, hasPassphrase, requiresUserPresence
|
|
case createdAt, colorTag, sortIndex
|
|
}
|
|
|
|
/// Vault records written before key colors existed do not contain
|
|
/// `colorTag`. Treating that absence as `.none` keeps those records valid
|
|
/// while preserving synthesized encoding for new records.
|
|
public init(from decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.init(
|
|
id: try container.decode(UUID.self, forKey: .id),
|
|
label: try container.decode(String.self, forKey: .label),
|
|
algorithm: try container.decode(SSHKeyAlgorithm.self, forKey: .algorithm),
|
|
publicKey: try container.decode(String.self, forKey: .publicKey),
|
|
certificate: try container.decode(String.self, forKey: .certificate),
|
|
fingerprint: try container.decode(String.self, forKey: .fingerprint),
|
|
credentialRef: try container.decode(String.self, forKey: .credentialRef),
|
|
storage: try container.decode(KeyStorage.self, forKey: .storage),
|
|
securityKey: try container.decodeIfPresent(SecurityKeyCredential.self, forKey: .securityKey),
|
|
keySize: try container.decodeIfPresent(Int.self, forKey: .keySize),
|
|
hasPassphrase: try container.decode(Bool.self, forKey: .hasPassphrase),
|
|
requiresUserPresence: try container.decode(Bool.self, forKey: .requiresUserPresence),
|
|
createdAt: try container.decode(Date.self, forKey: .createdAt),
|
|
colorTag: (try? container.decode(TagColor.self, forKey: .colorTag)) ?? .none,
|
|
sortIndex: try container.decode(Double.self, forKey: .sortIndex)
|
|
)
|
|
if algorithm == .securityKey { storage = .external }
|
|
}
|
|
}
|