104 lines
4.0 KiB
Swift
104 lines
4.0 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
enum KeychainService {
|
|
static let service = "net.absdev.SonarDesk"
|
|
|
|
static func saveToken(_ token: String, serverID: UUID) throws {
|
|
try saveLocal(token, account: serverID.uuidString)
|
|
}
|
|
|
|
static func token(serverID: UUID) -> String? {
|
|
readWithLegacyMigration(account: serverID.uuidString)
|
|
}
|
|
|
|
static func saveProjectToken(_ token: String, serverID: UUID, projectKey: String) throws {
|
|
try saveLocal(token, account: "project|\(serverID.uuidString)|\(projectKey)")
|
|
}
|
|
|
|
static func projectToken(serverID: UUID, projectKey: String) -> String? {
|
|
readWithLegacyMigration(account: "project|\(serverID.uuidString)|\(projectKey)")
|
|
}
|
|
|
|
static func deleteToken(serverID: UUID) {
|
|
deleteAll(account: serverID.uuidString)
|
|
}
|
|
|
|
private static func query(account: String, synchronizable: CFTypeRef) -> [String: Any] {
|
|
[kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: account,
|
|
kSecAttrSynchronizable as String: synchronizable]
|
|
}
|
|
|
|
private static func saveLocal(_ value: String, account: String) throws {
|
|
let clean = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !clean.isEmpty else { throw KeychainError.emptyToken }
|
|
let data = Data(clean.utf8)
|
|
let localQuery = query(account: account, synchronizable: kCFBooleanFalse)
|
|
let updateStatus = SecItemUpdate(localQuery as CFDictionary,
|
|
[kSecValueData as String: data] as CFDictionary)
|
|
if updateStatus == errSecSuccess { return }
|
|
guard updateStatus == errSecItemNotFound else { throw KeychainError.status(updateStatus) }
|
|
|
|
var add = localQuery
|
|
add[kSecValueData as String] = data
|
|
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
let status = SecItemAdd(add as CFDictionary, nil)
|
|
guard status == errSecSuccess else { throw KeychainError.status(status) }
|
|
}
|
|
|
|
private static func readWithLegacyMigration(account: String) -> String? {
|
|
if let local = read(account: account, synchronizable: kCFBooleanFalse) { return local }
|
|
guard let legacy = read(account: account, synchronizable: kSecAttrSynchronizableAny) else { return nil }
|
|
// Best-effort migration from older iCloud/synchronizable Keychain entries.
|
|
try? saveLocal(legacy, account: account)
|
|
return legacy
|
|
}
|
|
|
|
private static func read(account: String, synchronizable: CFTypeRef) -> String? {
|
|
var lookup = query(account: account, synchronizable: synchronizable)
|
|
lookup[kSecReturnData as String] = true
|
|
lookup[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
var item: CFTypeRef?
|
|
guard SecItemCopyMatching(lookup as CFDictionary, &item) == errSecSuccess,
|
|
let data = item as? Data,
|
|
let value = String(data: data, encoding: .utf8),
|
|
!value.isEmpty else { return nil }
|
|
return value
|
|
}
|
|
|
|
private static func deleteAll(account: String) {
|
|
SecItemDelete(query(account: account, synchronizable: kSecAttrSynchronizableAny) as CFDictionary)
|
|
}
|
|
}
|
|
|
|
enum KeychainError: LocalizedError {
|
|
case emptyToken
|
|
case status(OSStatus)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .emptyToken: return "The SonarQube token cannot be empty."
|
|
case .status(let status):
|
|
return SecCopyErrorMessageString(status, nil) as String? ?? "Keychain error (\(status))."
|
|
}
|
|
}
|
|
}
|
|
|
|
extension KeychainService {
|
|
private static let licenseTokenAccount = "sonardesk.license-token"
|
|
|
|
static func saveLicenseToken(_ token: String) throws {
|
|
try saveLocal(token, account: licenseTokenAccount)
|
|
}
|
|
|
|
static func licenseToken() -> String? {
|
|
readWithLegacyMigration(account: licenseTokenAccount)
|
|
}
|
|
|
|
static func deleteLicenseToken() {
|
|
deleteAll(account: licenseTokenAccount)
|
|
}
|
|
}
|