Add CampusNAS menu bar agent
Native macOS agent that keeps the office VPN and the NAS shares working without the shell wrapper it replaces. - VPN: full port of the `ovpn` script — host-route pinning, gateway discovery via networksetup, tunnel liveness by address plus pushed route, startup grace, restart of a wedged daemon. Root steps are batched into one system authorization prompt. - SMB: mounts via NetFS, discovers shares from the server, detects mounts event-driven through NSWorkspace, matches the host by resolved alias so externally created mounts are recognised. - Credentials never pass through the process: the VPN auth file is written by `security` straight to disk, and the password is stored via SecItemAdd rather than a command line visible in ps. - UI: status window, settings editor, ⌘+/⌘− zoom that re-lays out rather than scaling a bitmap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
/// Writes the VPN password into the login Keychain.
|
||||
///
|
||||
/// Uses the Security framework directly rather than shelling out to
|
||||
/// `security add-generic-password -w <password>`: command-line arguments are
|
||||
/// visible in `ps` to every process on the machine, so a password must never
|
||||
/// travel that way.
|
||||
///
|
||||
/// This is the one place the app touches the secret at all, and only because
|
||||
/// the user typed it here on purpose. It goes straight into the Keychain and is
|
||||
/// never stored, logged or echoed anywhere else — reading it back is left to
|
||||
/// `security` at connect time.
|
||||
enum KeychainStore {
|
||||
|
||||
static func exists(service: String, account: String) -> Bool {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecReturnData as String: false,
|
||||
]
|
||||
return SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess
|
||||
}
|
||||
|
||||
/// Adds the item, or replaces the password if it is already there.
|
||||
static func save(service: String, account: String, password: String) -> String? {
|
||||
guard let data = password.data(using: .utf8) else {
|
||||
return "Не удалось закодировать пароль"
|
||||
}
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
]
|
||||
|
||||
let status: OSStatus
|
||||
if exists(service: service, account: account) {
|
||||
status = SecItemUpdate(
|
||||
query as CFDictionary,
|
||||
[kSecValueData as String: data] as CFDictionary)
|
||||
} else {
|
||||
var attributes = query
|
||||
attributes[kSecValueData as String] = data
|
||||
status = SecItemAdd(attributes as CFDictionary, nil)
|
||||
}
|
||||
|
||||
guard status != errSecSuccess else { return nil }
|
||||
let message = SecCopyErrorMessageString(status, nil) as String?
|
||||
return message ?? "Ошибка Keychain \(status)"
|
||||
}
|
||||
|
||||
static func delete(service: String, account: String) {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user