Gitfed
bastien-mrq/bt-message / btmessage / Crypto / GroupKeyManager.swift
// GroupKeyManager.swift
// Stores group AES-256 keys in Keychain (secure) and group metadata in UserDefaults.

import Foundation
import CryptoKit
import Security

public class GroupKeyManager: ObservableObject {
    public static let shared = GroupKeyManager()

    // groupID → 32-byte AES key (en mémoire uniquement, persistance dans Keychain)
    private var keys: [String: SymmetricKey] = [:]
    // groupID → GroupModel
    private var groupMap: [String: GroupModel] = [:]
    private let queue = DispatchQueue(label: "btmessage.groupkey", attributes: .concurrent)

    private init() {
        load()
    }

    // MARK: - Read

    public func hasKey(for groupID: String) -> Bool {
        queue.sync { keys[groupID] != nil }
    }

    public func groupKey(for groupID: String) -> SymmetricKey? {
        queue.sync { keys[groupID] }
    }

    public func group(for groupID: String) -> GroupModel? {
        queue.sync { groupMap[groupID] }
    }

    public var allGroups: [GroupModel] {
        queue.sync { Array(groupMap.values) }
    }

    // MARK: - Write

    public func store(group: GroupModel, key: SymmetricKey) {
        queue.async(flags: .barrier) { [weak self] in
            self?.keys[group.id] = key
            self?.groupMap[group.id] = group
            self?.persist()
        }
    }

    public func store(group: GroupModel, keyData: Data) {
        store(group: group, key: SymmetricKey(data: keyData))
    }

    public func remove(groupID: String) {
        queue.async(flags: .barrier) { [weak self] in
            self?.keys.removeValue(forKey: groupID)
            self?.groupMap.removeValue(forKey: groupID)
            self?.persist()
            self?.keychainDelete(groupID: groupID)
        }
    }

    // MARK: - Persistence

    // Clés dans Keychain (sécurisé), métadonnées dans UserDefaults
    private static let keysKeychainPrefix = "btmessage.groupkey."
    private static let groupsUD = "btmessage.groups"

    private func persist() {
        // 1. Sauvegarder les clés dans Keychain
        for (id, key) in keys {
            let keyData = key.withUnsafeBytes { Data($0) }
            keychainSave(groupID: id, data: keyData)
        }

        // 2. Sauvegarder les métadonnées dans UserDefaults (non sensible)
        if let data = try? JSONEncoder().encode(groupMap) {
            UserDefaults.standard.set(data, forKey: GroupKeyManager.groupsUD)
        }
    }

    private func load() {
        // 1. Charger les métadonnées
        if let data = UserDefaults.standard.data(forKey: GroupKeyManager.groupsUD),
           let decoded = try? JSONDecoder().decode([String: GroupModel].self, from: data) {
            groupMap = decoded
        }

        // 2. Charger les clés depuis Keychain pour chaque groupe connu
        for id in groupMap.keys {
            if let keyData = keychainLoad(groupID: id), keyData.count == 32 {
                keys[id] = SymmetricKey(data: keyData)
            }
        }
    }

    // MARK: - Keychain Helpers

    private func keychainSave(groupID: String, data: Data) {
        let key = GroupKeyManager.keysKeychainPrefix + groupID
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        ]
        SecItemDelete(query as CFDictionary)
        SecItemAdd(query as CFDictionary, nil)
    }

    private func keychainLoad(groupID: String) -> Data? {
        let key = GroupKeyManager.keysKeychainPrefix + groupID
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var item: CFTypeRef?
        guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess else { return nil }
        return item as? Data
    }

    private func keychainDelete(groupID: String) {
        let key = GroupKeyManager.keysKeychainPrefix + groupID
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        SecItemDelete(query as CFDictionary)
    }
}