// 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)
}
}