Files
Termind/TFITests/TailscaleKeychainTests.swift

69 lines
2.9 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import CTailscale
import Foundation
import Security
import TermindRemoteInfrastructure
import Testing
@Suite @MainActor
struct TailscaleKeychainTests {
@Test func nativeStateUsesDataProtectionAndCleanupErasesOnlyTheSelectedProfile() async throws {
let profiles = [UUID(), UUID()]
let scopes = profiles.map { "com.akinokaede.termind.tailscale.\($0.uuidString)" }
let keys = [
"_machinekey", "profile-\(UUID().uuidString.prefix(4).lowercased())", "_current-profile", "_profiles",
]
defer {
for scope in scopes { SecItemDelete(query(scope) as CFDictionary) }
}
for scope in scopes {
for key in keys {
let status = scope.withCString { scope in
key.withCString { key in
"synthetic-state".withCString { lt_store_write(scope, key, $0, Int32(strlen($0))) }
}
}
try #require(status == errSecSuccess)
var attributesQuery = query(scope)
attributesQuery[kSecAttrAccount as String] = key
attributesQuery[kSecReturnAttributes as String] = true
var raw: CFTypeRef?
try #require(SecItemCopyMatching(attributesQuery as CFDictionary, &raw) == errSecSuccess)
let attributes = try #require(raw as? [String: Any])
#expect(
attributes[kSecAttrAccessible as String] as? String
== kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String)
}
}
try await LibTailscaleNodeSession.profileStorage.erase(profiles[0])
for (index, scope) in scopes.enumerated() {
for key in keys {
var bytes: UnsafeMutableRawPointer?
var count: Int32 = 0
let status = scope.withCString { scope in
key.withCString { lt_store_read(scope, $0, &bytes, &count) }
}
defer { free(bytes) }
#expect(status == (index == 0 ? errSecItemNotFound : errSecSuccess))
if index == 1, let bytes {
#expect(Data(bytes: bytes, count: Int(count)) == Data("synthetic-state".utf8))
}
}
}
try await LibTailscaleNodeSession.profileStorage.erase(profiles[0])
try await LibTailscaleNodeSession.profileStorage.erase(profiles[1])
#expect(SecItemCopyMatching(query(scopes[1]) as CFDictionary, nil) == errSecItemNotFound)
}
private func query(_ scope: String) -> [String: Any] {
[
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: scope,
kSecUseDataProtectionKeychain as String: true,
kSecAttrSynchronizable as String: false,
]
}
}