From afe192a7799e5585344a6a59d57505decde89c43 Mon Sep 17 00:00:00 2001 From: Alexander Lindenstruth Date: Mon, 23 Jun 2025 10:18:53 +0200 Subject: [PATCH] Store api tokens securely in the macOS Keychain Pibar is now storing api tokens securely in the macOS Login Keychain instead of using the UserDefaults for plain text storage. For this purpose an additional SPM dependency for a 3rd party keychain wrapper has been added. --- PiBar.xcodeproj/project.pbxproj | 17 ++++++++ .../xcshareddata/swiftpm/Package.resolved | 11 ++++- PiBar/Data Sources/Structs.swift | 40 ++++++++++++++++--- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/PiBar.xcodeproj/project.pbxproj b/PiBar.xcodeproj/project.pbxproj index 0e0775c..6008b22 100644 --- a/PiBar.xcodeproj/project.pbxproj +++ b/PiBar.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 44E390C72D87C3E2002196DC /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 44E390C62D87C3E2002196DC /* LaunchAtLogin */; }; 44E390C92D87ED53002196DC /* PiholeV6SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E390C82D87ED49002196DC /* PiholeV6SettingsViewController.swift */; }; 44FFB092247627B100DCEDEC /* PiBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FFB091247627B100DCEDEC /* PiBarManager.swift */; }; + 7DFE62342E0938CA0097B220 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 7DFE62332E0938CA0097B220 /* KeychainAccess */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -83,6 +84,7 @@ files = ( 44E390C72D87C3E2002196DC /* LaunchAtLogin in Frameworks */, 44E390C22D877DDE002196DC /* HotKey in Frameworks */, + 7DFE62342E0938CA0097B220 /* KeychainAccess in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -230,6 +232,7 @@ packageProductDependencies = ( 44E390C12D877DDE002196DC /* HotKey */, 44E390C62D87C3E2002196DC /* LaunchAtLogin */, + 7DFE62332E0938CA0097B220 /* KeychainAccess */, ); productName = PiBar; productReference = 449395D22471ABD600FA0C34 /* PiBar.app */; @@ -263,6 +266,7 @@ packageReferences = ( 44E390C02D877DDE002196DC /* XCRemoteSwiftPackageReference "HotKey" */, 44E390C52D87C3E2002196DC /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */, + 7DFE62322E0938CA0097B220 /* XCRemoteSwiftPackageReference "KeychainAccess" */, ); productRefGroup = 449395D32471ABD600FA0C34 /* Products */; projectDirPath = ""; @@ -566,6 +570,14 @@ kind = branch; }; }; + 7DFE62322E0938CA0097B220 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; + requirement = { + branch = master; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -579,6 +591,11 @@ package = 44E390C52D87C3E2002196DC /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */; productName = LaunchAtLogin; }; + 7DFE62332E0938CA0097B220 /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 7DFE62322E0938CA0097B220 /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 449395CA2471ABD600FA0C34 /* Project object */; diff --git a/PiBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PiBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1dd36b7..bc1ea03 100644 --- a/PiBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PiBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ab1eefd7a2b30c55c93e58acb6ad16334c48b48a141518af254356092869f16e", + "originHash" : "9794f416c441490423495df59b3760e5e3ab7d4596346f57bd1991bb5cd6c27f", "pins" : [ { "identity" : "hotkey", @@ -10,6 +10,15 @@ "revision" : "a3cf605d7a96f6ff50e04fcb6dea6e2613cfcbe4" } }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "state" : { + "branch" : "master", + "revision" : "e0c7eebc5a4465a3c4680764f26b7a61f567cdaf" + } + }, { "identity" : "launchatlogin-legacy", "kind" : "remoteSourceControl", diff --git a/PiBar/Data Sources/Structs.swift b/PiBar/Data Sources/Structs.swift index d618335..619a453 100644 --- a/PiBar/Data Sources/Structs.swift +++ b/PiBar/Data Sources/Structs.swift @@ -10,6 +10,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. import Foundation +import KeychainAccess // MARK: - Pi-hole Connections @@ -81,37 +82,66 @@ extension PiholeConnectionV2 { } // PiBar v1.2 format -struct PiholeConnectionV3: Codable { +struct PiholeConnectionV3: Codable, Copyable { let hostname: String let port: Int let useSSL: Bool - let token: String + var token: String let passwordProtected: Bool let adminPanelURL: String let isV6: Bool } extension PiholeConnectionV3 { + + private var keychainIdentifier: String? { + get { + if self.hostname.count > 0 { + return "apitoken.\(self.hostname):\(self.port)" + } + return nil + } + } + init?(data: Data) { let jsonDecoder = JSONDecoder() do { - let object = try jsonDecoder.decode(PiholeConnectionV3.self, from: data) + var object = try jsonDecoder.decode(PiholeConnectionV3.self, from: data) + // load the token from the Keychain and add it to the connection object: + if let bundleIdentifier = Bundle.main.bundleIdentifier, let keychainIdentifier = object.keychainIdentifier { + let keychain = Keychain(service: bundleIdentifier, accessGroup: nil) + if let tokenStr = keychain[keychainIdentifier], !tokenStr.isEmpty { + object.token = tokenStr + } + } self = object } catch { Log.debug("Couldn't decode connection: \(error.localizedDescription)") return nil } } - + func encode() -> Data? { let jsonEncoder = JSONEncoder() - if let data = try? jsonEncoder.encode(self) { + // make sure the token is being securely stored to the Keychain only, and nowhere else + var copySelf = self.copy() as! PiholeConnectionV3 + if let bundleIdentifier = Bundle.main.bundleIdentifier, !copySelf.token.isEmpty, let keychainIdentifier = copySelf.keychainIdentifier { + let keychain = Keychain(service: bundleIdentifier, accessGroup: nil) + keychain[keychainIdentifier] = copySelf.token + copySelf.token = "" + } + if let data = try? jsonEncoder.encode(copySelf) { return data } else { return nil } } + func copy(with zone: NSZone? = nil) -> Any { + let copy = PiholeConnectionV3(hostname: hostname, port: port, useSSL: useSSL, token: token, passwordProtected: passwordProtected, adminPanelURL: adminPanelURL, isV6: isV6) + return copy + } + static func generateAdminPanelURL(hostname: String, port: Int, useSSL: Bool) -> String { let prefix: String = useSSL ? "https" : "http" return "\(prefix)://\(hostname):\(port)/admin/"