SPB Git

spb/zyquo-cloud Public MIT

Native macOS AI chat client for 12 cloud providers — your keys, every cloud model, one beautiful chat.

Swift 97.4% Shell 1.7% Makefile 1%
3.7 KB · 107 lines swift
Raw Blame History
1//2//  SecureKeyStoreTests.swift3//  Zyquo Cloud4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//89import Foundation10import Testing11@testable import ZyquoCloud1213@Suite struct SecureKeyStoreTests {14    private func temporaryVaultURL() -> URL {15        FileManager.default.temporaryDirectory16            .appendingPathComponent("zyquo-tests-\(UUID().uuidString)")17            .appendingPathComponent("vault.zq")18    }1920    @Test func encryptDecryptRoundTrip() throws {21        let url = temporaryVaultURL()22        defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) }23        let store = SecureKeyStore(vaultURL: url)2425        let keys = [26            "openai": "sk-proj-test-1234",27            "anthropic": "sk-ant-test-5678",28            "mistral": "plainkey",29        ]30        try store.saveKeys(keys)31        let loaded = try store.loadKeys()32        #expect(loaded == keys)3334        // Vault file must not contain any key material in plaintext.35        let raw = try Data(contentsOf: url)36        let rawString = String(decoding: raw, as: UTF8.self)37        #expect(!rawString.contains("sk-proj-test-1234"))38        #expect(!rawString.contains("sk-ant-test-5678"))39        #expect(!rawString.contains("openai"))40    }4142    @Test func perProviderSetGetDelete() throws {43        let url = temporaryVaultURL()44        defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) }45        let store = SecureKeyStore(vaultURL: url)4647        try store.setKey("xai-abc", for: .xai)48        try store.setKey("pplx-def", for: .perplexity)49        #expect(try store.key(for: .xai) == "xai-abc")50        #expect(try store.key(for: .perplexity) == "pplx-def")5152        try store.deleteKey(for: .xai)53        #expect(try store.key(for: .xai) == nil)54        #expect(try store.key(for: .perplexity) == "pplx-def")55    }5657    @Test func emptyVaultLoadsEmpty() throws {58        let store = SecureKeyStore(vaultURL: temporaryVaultURL())59        #expect(try store.loadKeys().isEmpty)60    }6162    @Test func tamperedVaultThrowsCorrupted() throws {63        let url = temporaryVaultURL()64        defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) }65        let store = SecureKeyStore(vaultURL: url)66        try store.saveKeys(["openai": "sk-test"])6768        var blob = try Data(contentsOf: url)69        blob[blob.count - 1] ^= 0xFF // flip a tag byte70        try blob.write(to: url)7172        #expect(throws: SecureKeyStore.VaultError.self) {73            _ = try store.loadKeys()74        }75    }7677    @Test func vaultIsMachineBound() throws {78        let url = temporaryVaultURL()79        defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) }8081        let machineA = SecureKeyStore(vaultURL: url, machineEntropy: { Data("machine-A".utf8) })82        try machineA.saveKeys(["openai": "sk-test"])8384        let machineB = SecureKeyStore(vaultURL: url, machineEntropy: { Data("machine-B".utf8) })85        #expect(throws: SecureKeyStore.VaultError.self) {86            _ = try machineB.loadKeys()87        }88    }8990    @Test func saltIsStableAcrossSaves() throws {91        let url = temporaryVaultURL()92        defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) }93        let store = SecureKeyStore(vaultURL: url)9495        try store.saveKeys(["a": "1"])96        let salt1 = try Data(contentsOf: url).prefix(32)97        try store.saveKeys(["a": "1", "b": "2"])98        let salt2 = try Data(contentsOf: url).prefix(32)99        #expect(salt1 == salt2)100        #expect(try store.loadKeys() == ["a": "1", "b": "2"])101    }102103    @Test func redactionShowsOnlyLastFour() {104        #expect(SecureKeyStore.redacted("sk-proj-abcdefgh1234") == "••••1234")105    }106}107