// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import Foundation import SwiftData import TermindApplication import TermindDomain import TermindPersistence import Testing @Suite @MainActor struct WebAuthnVaultMetadataTests { private func key() -> SSHKeyRef { SSHKeyRef( label: "", algorithm: .securityKey, publicKey: "public-key", fingerprint: "SHA256:metadata", storage: .external, securityKey: SecurityKeyCredential( credentialID: Data([1, 2, 3]), publicKeyX963: Data([4]) + Data(repeating: 7, count: 64), userVerification: .preferred), keySize: 256, requiresUserPresence: true) } @Test func connectionPreparationResolvesDestinationAndJumpWithoutKeychain() async throws { var draft = key() draft.label = "Personal YubiKey" let identity = Identity(name: "Hardware", username: "root", keyID: draft.id) let preparation = SSHConnectionPreparation( nodes: ["jump.invalid", "server.invalid"].map { hostname in SSHConnectionPreparation.Node( host: Host(name: hostname, host: hostname, identityID: identity.id), identity: identity, key: draft, submission: nil, trustedKeys: [], proxyIdentity: nil) }, secrets: NoKeychain()) let configuration = try await preparation.resolve(trustPolicy: .allowChallenge) #expect(configuration.username == "root") guard case .securityKey(let destination, let destinationName) = configuration.credentials.first, case .securityKey(let jump, let jumpName) = configuration.jumps.first?.credentials.first else { Issue.record("Expected metadata-backed hardware credentials for both route nodes") return } #expect(destination == draft.securityKey && jump == draft.securityKey) #expect(destinationName == draft.label && jumpName == draft.label) } @Test func metadataPersistsSyncsAndResolvesWithoutKeychain() throws { let container = try TermindPersistence.makeContainer(inMemory: true) let persistence = HostPersistence(context: container.mainContext) let library = HostLibraryModel(persistence: persistence, secrets: NoKeychain()) let draft = key() try library.upsert(draft) let saved = try #require(library.key(draft.id)) #expect(saved.label == "Security Key" && saved.credentialRef.isEmpty) #expect(saved.storage == .external) #expect(library.keyAvailability(for: saved) == .available) #expect(try library.keyCredential(for: saved) == nil) let reloaded = try #require(try persistence.load().keys.first) #expect(reloaded.securityKey == draft.securityKey) #expect(reloaded.storage == .external) let payload = VaultRecordPayload.sshKey(reloaded) let envelope = VaultEnvelope(key: payload.key, version: .baseline(writerID: UUID()), payload: payload) #expect(envelope.formatVersion == SyncRecordFormats.vault.writingVersion(for: "sshKey")) let decoded = try VaultSyncCodec.decode(VaultSyncCodec.encode(envelope)) #expect(decoded.payload == payload) let remoteContainer = try TermindPersistence.makeContainer(inMemory: true) let remote = HostPersistence(context: remoteContainer.mainContext) _ = try remote.applyRemote([decoded]) let remoteLibrary = HostLibraryModel(persistence: remote, secrets: NoKeychain()) var synced = try #require(remoteLibrary.key(saved.id)) #expect(synced.storage == .external) guard case .securityKey(let resolved)? = try remoteLibrary.keySecretCredential(for: synced) else { Issue.record("Expected a Vault hardware reference") return } #expect(resolved == draft.securityKey) synced.securityKey?.userVerification = .required try remoteLibrary.upsert(synced) #expect(try remote.load().keys.first?.securityKey?.userVerification == .required) try remoteLibrary.delete(keyID: synced.id) #expect(try remote.load().keys.isEmpty) } @Test(arguments: [KeyStorage.thisDevice, .iCloud]) func legacyHardwareStorageLoadsAsExternal(_ storage: KeyStorage) throws { var legacy = key() legacy.storage = storage let container = try TermindPersistence.makeContainer(inMemory: true) let row = TermindSchemaV45KeyModels.PersistentSSHKey(legacy) container.mainContext.insert(row) try container.mainContext.save() let persistence = HostPersistence(context: container.mainContext) let restored = try #require(try persistence.load().keys.first) #expect(restored.storage == .external) #expect(restored.securityKey == legacy.securityKey) #expect(try JSONDecoder().decode(SSHKeyRef.self, from: JSONEncoder().encode(legacy)).storage == .external) let library = HostLibraryModel(secrets: NoKeychain()) try library.upsert(legacy) #expect(library.key(legacy.id)?.storage == .external) } @Test func externalHardwareRetainsCompatibleVaultWireStorage() throws { let payload = VaultRecordPayload.sshKey(key()) let envelope = VaultEnvelope(key: payload.key, version: .baseline(writerID: UUID()), payload: payload) let data = try VaultSyncCodec.encode(envelope) let json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) let payloadJSON = try #require(json["payload"] as? [String: Any]) let keyJSON = try #require(payloadJSON["sshKey"] as? [String: Any]) let fields = try #require(keyJSON["_0"] as? [String: Any]) #expect(fields["storage"] as? String == "thisDevice") #expect(try VaultSyncCodec.decode(data).payload == payload) } @Test func softwareKeysCannotUseExternalStorage() throws { let software = SSHKeyRef(label: "Invalid", storage: .external) let library = HostLibraryModel(secrets: NoKeychain()) #expect(throws: (any Error).self) { try library.upsert(software) } #expect(library.keys.isEmpty) let payload = VaultRecordPayload.sshKey(software) for version in [1, 2] { let envelope = VaultEnvelope( key: payload.key, version: .baseline(writerID: UUID()), payload: payload, formatVersion: version) #expect(throws: (any Error).self) { try VaultSyncCodec.encode(envelope) } } } @Test func failedMetadataWritesAndDeletesRollBackWithoutKeychain() throws { let persistence = MetadataPersistence() let library = HostLibraryModel(persistence: persistence, secrets: NoKeychain()) let draft = key() persistence.fails = true #expect(throws: (any Error).self) { try library.upsert(draft) } #expect(library.keys.isEmpty) persistence.fails = false try library.upsert(draft) let saved = try #require(library.key(draft.id)) persistence.fails = true var update = saved update.securityKey?.userVerification = .required #expect(throws: (any Error).self) { try library.upsert(update) } #expect(library.key(saved.id) == saved) #expect(throws: (any Error).self) { try library.delete(keyID: saved.id) } #expect(library.key(saved.id) == saved) } @Test func version44SoftwareKeysSurviveSchemaUpgradeAndReopen() throws { let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: directory) } let url = directory.appendingPathComponent("Vault.store") let software = SSHKeyRef(label: "Existing", credentialRef: "termind.key.existing") try writeVersion44(software, to: url) for _ in 0..<2 { let container = try TermindPersistence.makeContainer(url: url) let persistence = HostPersistence(context: container.mainContext) #expect(try persistence.load().keys == [software]) } } private func writeVersion44(_ key: SSHKeyRef, to url: URL) throws { let schema = Schema(versionedSchema: TermindSchemaV44.self) let container = try ModelContainer( for: schema, configurations: [ModelConfiguration("Termind", schema: schema, url: url, cloudKitDatabase: .none)]) container.mainContext.insert(TermindSchemaV7Models.PersistentSSHKey(key)) try container.mainContext.save() } } private final class NoKeychain: SecretStoring, @unchecked Sendable { func string(for account: String, storage: PasswordStorage) throws -> String? { throw CocoaError(.fileReadNoPermission) } func setString(_ value: String?, for account: String, storage: PasswordStorage) throws { throw CocoaError(.fileWriteNoPermission) } func keyCredential(for account: String, storage: KeyStorage) throws -> StoredSSHKeyCredential? { throw CocoaError(.fileReadNoPermission) } func setKeyCredential(_ value: StoredSSHKeyCredential?, for account: String, storage: KeyStorage) throws { throw CocoaError(.fileWriteNoPermission) } func removePassword(_ account: String, storage: PasswordStorage) throws { throw CocoaError(.fileWriteNoPermission) } func removeKeyCredential(_ account: String, storage: KeyStorage) throws { throw CocoaError(.fileWriteNoPermission) } } @MainActor private final class MetadataPersistence: VaultPersisting { var snapshot = VaultSnapshot() var fails = false func load() throws -> VaultSnapshot { snapshot } func save(_ snapshot: VaultSnapshot, touching keys: Set) throws -> VaultMutationSet { if fails { throw CocoaError(.fileWriteUnknown) } self.snapshot = snapshot return .empty } }