ManagedSlskdCredentials.swift 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import Foundation
  2. import Security
  3. // MARK: - Managed slskd API Credentials
  4. /// Auto-generated credentials for the managed slskd instance.
  5. /// Separate Keychain service from SlskdCredentials (which stores Soulseek P2P login).
  6. enum ManagedSlskdKeychainService {
  7. private static let service = "com.mixboard.slskd-managed"
  8. static func save(account: String, value: String) throws {
  9. guard let data = value.data(using: .utf8) else {
  10. throw KeychainService.KeychainError.saveFailed(errSecParam)
  11. }
  12. let deleteQuery: [String: Any] = [
  13. kSecClass as String: kSecClassGenericPassword,
  14. kSecAttrService as String: service,
  15. kSecAttrAccount as String: account,
  16. ]
  17. SecItemDelete(deleteQuery as CFDictionary)
  18. let addQuery: [String: Any] = [
  19. kSecClass as String: kSecClassGenericPassword,
  20. kSecAttrService as String: service,
  21. kSecAttrAccount as String: account,
  22. kSecValueData as String: data,
  23. kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,
  24. ]
  25. let status = SecItemAdd(addQuery as CFDictionary, nil)
  26. guard status == errSecSuccess else {
  27. throw KeychainService.KeychainError.saveFailed(status)
  28. }
  29. }
  30. static func load(account: String) -> String? {
  31. let query: [String: Any] = [
  32. kSecClass as String: kSecClassGenericPassword,
  33. kSecAttrService as String: service,
  34. kSecReturnData as String: true,
  35. kSecMatchLimit as String: kSecMatchLimitOne,
  36. kSecAttrAccount as String: account,
  37. ]
  38. var result: AnyObject?
  39. let status = SecItemCopyMatching(query as CFDictionary, &result)
  40. guard status == errSecSuccess, let data = result as? Data else { return nil }
  41. return String(data: data, encoding: .utf8)
  42. }
  43. }
  44. /// Manages auto-generated API credentials for the managed slskd subprocess.
  45. /// On first access, generates random credentials and persists them in Keychain.
  46. @MainActor
  47. final class ManagedSlskdCredentials {
  48. static let shared = ManagedSlskdCredentials()
  49. /// Returns existing credentials or generates new ones.
  50. func ensureCredentials() throws -> (username: String, password: String) {
  51. if let u = ManagedSlskdKeychainService.load(account: "username"),
  52. let p = ManagedSlskdKeychainService.load(account: "password"),
  53. !u.isEmpty, !p.isEmpty {
  54. return (u, p)
  55. }
  56. let username = "mixboard"
  57. let password = UUID().uuidString
  58. try ManagedSlskdKeychainService.save(account: "username", value: username)
  59. try ManagedSlskdKeychainService.save(account: "password", value: password)
  60. return (username, password)
  61. }
  62. var username: String? { ManagedSlskdKeychainService.load(account: "username") }
  63. var password: String? { ManagedSlskdKeychainService.load(account: "password") }
  64. }