mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 07:05:19 +00:00
Implement message retention, retry mechanism, and app updates
- Add KeychainManager for secure password storage - Implement MessageRetentionService for encrypted local storage of favorite room messages - Add MessageRetryService for automatic retry of failed messages - Add /favorite command to toggle room retention - Add /discover command to find active rooms - Fix all force unwrapped optionals in BluetoothMeshService - Update app info screen with new features - Integrate all services with proper cleanup on panic
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// KeychainManager.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class KeychainManager {
|
||||
static let shared = KeychainManager()
|
||||
|
||||
private let service = "com.bitchat.passwords"
|
||||
private let accessGroup: String? = nil // Set this if using app groups
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Room Passwords
|
||||
|
||||
func saveRoomPassword(_ password: String, for room: String) -> Bool {
|
||||
let key = "room_\(room)"
|
||||
return save(password, forKey: key)
|
||||
}
|
||||
|
||||
func getRoomPassword(for room: String) -> String? {
|
||||
let key = "room_\(room)"
|
||||
return retrieve(forKey: key)
|
||||
}
|
||||
|
||||
func deleteRoomPassword(for room: String) -> Bool {
|
||||
let key = "room_\(room)"
|
||||
return delete(forKey: key)
|
||||
}
|
||||
|
||||
func getAllRoomPasswords() -> [String: String] {
|
||||
var passwords: [String: String] = [:]
|
||||
|
||||
// Query all items
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecReturnData as String: true
|
||||
]
|
||||
|
||||
if let accessGroup = accessGroup {
|
||||
query[kSecAttrAccessGroup as String] = accessGroup
|
||||
}
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
if status == errSecSuccess, let items = result as? [[String: Any]] {
|
||||
for item in items {
|
||||
if let account = item[kSecAttrAccount as String] as? String,
|
||||
account.hasPrefix("room_"),
|
||||
let data = item[kSecValueData as String] as? Data,
|
||||
let password = String(data: data, encoding: .utf8) {
|
||||
let room = String(account.dropFirst(5)) // Remove "room_" prefix
|
||||
passwords[room] = password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return passwords
|
||||
}
|
||||
|
||||
// MARK: - Identity Keys
|
||||
|
||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
||||
return saveData(keyData, forKey: "identity_\(key)")
|
||||
}
|
||||
|
||||
func getIdentityKey(forKey key: String) -> Data? {
|
||||
return retrieveData(forKey: "identity_\(key)")
|
||||
}
|
||||
|
||||
// MARK: - Generic Operations
|
||||
|
||||
private func save(_ value: String, forKey key: String) -> Bool {
|
||||
guard let data = value.data(using: .utf8) else { return false }
|
||||
return saveData(data, forKey: key)
|
||||
}
|
||||
|
||||
private func saveData(_ data: Data, forKey key: String) -> Bool {
|
||||
// First try to update existing
|
||||
let updateQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
|
||||
var mutableUpdateQuery = updateQuery
|
||||
if let accessGroup = accessGroup {
|
||||
mutableUpdateQuery[kSecAttrAccessGroup as String] = accessGroup
|
||||
}
|
||||
|
||||
let updateAttributes: [String: Any] = [
|
||||
kSecValueData as String: data,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
]
|
||||
|
||||
var status = SecItemUpdate(mutableUpdateQuery as CFDictionary, updateAttributes as CFDictionary)
|
||||
|
||||
if status == errSecItemNotFound {
|
||||
// Item doesn't exist, create it
|
||||
var createQuery = mutableUpdateQuery
|
||||
createQuery[kSecValueData as String] = data
|
||||
createQuery[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
|
||||
status = SecItemAdd(createQuery as CFDictionary, nil)
|
||||
}
|
||||
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
private func retrieve(forKey key: String) -> String? {
|
||||
guard let data = retrieveData(forKey: key) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func retrieveData(forKey key: String) -> Data? {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
if let accessGroup = accessGroup {
|
||||
query[kSecAttrAccessGroup as String] = accessGroup
|
||||
}
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
if status == errSecSuccess, let data = result as? Data {
|
||||
return data
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func delete(forKey key: String) -> Bool {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
|
||||
if let accessGroup = accessGroup {
|
||||
query[kSecAttrAccessGroup as String] = accessGroup
|
||||
}
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
return status == errSecSuccess || status == errSecItemNotFound
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
func deleteAllPasswords() -> Bool {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service
|
||||
]
|
||||
|
||||
if let accessGroup = accessGroup {
|
||||
query[kSecAttrAccessGroup as String] = accessGroup
|
||||
}
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
return status == errSecSuccess || status == errSecItemNotFound
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user