Files
ovpn/Sources/CampusNASMonitor/KeychainStore.swift
T
romanvandClaude Opus 5 cd53b6a1a1 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>
2026-07-31 13:19:31 +06:00

64 lines
2.4 KiB
Swift

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)
}
}