diff --git a/README.md b/README.md index 4008da7..8eacfcc 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,8 @@ LEMANA_VPN_NO_EMOJI=1 sh uninstall.sh TOTP seed — это постоянный секрет 2FA. Сам одноразовый TOTP-код меняется каждые 30 секунд и генерируется `openconnect-lite` в момент входа. +Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password/TOTP seed из item `LM LDAP` и переложить их в macOS Keychain. + Отключить: ```sh @@ -212,6 +214,8 @@ sh install.sh --without-bitwarden vpn-lemanapro.sh --configure-keychain ``` +Если credentials уже лежат в Keychain, подключение без Bitwarden не будет спрашивать пароль заново. CLI явно напишет, что Bitwarden отключён и используются сохранённые LDAP password/TOTP seed из macOS Keychain. + ### Если Bitwarden нет Bitwarden не обязателен. Без него установка работает как обычный `openconnect-lite` profile с секретами в macOS Keychain. @@ -226,11 +230,17 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \ Что понадобится: - LDAP username; -- LDAP password; +- LDAP password: корпоративный LDAP/domain пароль, не мастер-пароль Bitwarden; - TOTP secret из корпоративной 2FA настройки. Важно: вводить нужно не текущие 6 цифр из authenticator-приложения, а постоянный secret. Обычно он есть в QR-коде как `secret=BASE32...` или может быть показан при ручной настройке TOTP. +Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для ввода LDAP/TOTP. Если Keychain пустой, приложение покажет ошибку. В этом случае один раз выполни в Terminal: + +```sh +vpn --configure-keychain +``` + Если secret есть только в QR-коде: 1. Открой QR-код в приложении/на портале, где настраивалась 2FA. @@ -416,7 +426,7 @@ uninstall-lemana-vpn.sh - удаляет sudoers rules и DNS cleanup wrapper; - удаляет блок `lemana-vpn` из `~/.zshrc`; - удаляет `~/.config/openconnect-lite/config.toml`; -- удаляет `~/Applications/LemanaVPN.app` и LaunchAgent автозапуска; +- останавливает уже запущенный процесс `LemanaVPN`, удаляет `~/Applications/LemanaVPN.app` и LaunchAgent автозапуска; - удаляет `~/.config/lemana-vpn`, если не передан `--keep-config`. Опциональные режимы: diff --git a/app/Sources/LemanaVPN/VPNManager.swift b/app/Sources/LemanaVPN/VPNManager.swift index a6f648a..3f2f802 100644 --- a/app/Sources/LemanaVPN/VPNManager.swift +++ b/app/Sources/LemanaVPN/VPNManager.swift @@ -361,6 +361,12 @@ class VPNManager: ObservableObject { case "bw_synced": log(" Credentials synced to Keychain") return + case "keychain_ready": + log(" LDAP credentials are ready in macOS Keychain") + return + case "keychain_required": + log(" LDAP credentials are missing or incomplete") + return case "connecting": state = .connecting case "connected": diff --git a/bin/vpn-lemanapro.sh b/bin/vpn-lemanapro.sh index dd0a0df..503d82b 100755 --- a/bin/vpn-lemanapro.sh +++ b/bin/vpn-lemanapro.sh @@ -355,18 +355,75 @@ if totp_secret: PY } +_can_prompt() { + [[ -t 0 ]] +} + _configure_keychain() { local password totp_secret - read -rsp "LDAP password: " password + local password_present=false totp_present=false + _keychain_has openconnect-lite "$KC_USERNAME" && password_present=true + _keychain_has openconnect-lite "totp/$KC_USERNAME" && totp_present=true + + printf 'Manual LDAP credentials setup for Lemana VPN\n' + printf 'User: %s\n' "$KC_USERNAME" + printf 'LDAP password: your corporate LDAP/domain password, not the Bitwarden master password.\n' + printf 'TOTP seed: permanent BASE32 secret from 2FA setup, not the current 6-digit code.\n' + printf 'Saved values go to macOS Keychain service openconnect-lite.\n\n' + + if $password_present; then + read -rsp "Corporate LDAP password for $KC_USERNAME [leave empty to keep saved password]: " password + else + read -rsp "Corporate LDAP password for $KC_USERNAME: " password + fi printf '\n' - read -rsp "TOTP secret (BASE32, optional if already stored): " totp_secret + if $totp_present; then + read -rsp "TOTP seed BASE32 [leave empty to keep saved seed]: " totp_secret + else + read -rsp "TOTP seed BASE32 from 2FA setup: " totp_secret + fi printf '\n' - if [[ -z "$password" ]]; then - printf 'Empty password, nothing was saved.\n' >&2 + + if [[ -z "$password" && "$password_present" != "true" ]]; then + printf 'LDAP password is required because no saved password was found.\n' >&2 return 1 fi + if [[ -z "$totp_secret" && "$totp_present" != "true" ]]; then + printf 'TOTP seed is required because no saved seed was found. Use the BASE32 setup secret, not the current 6-digit code.\n' >&2 + return 1 + fi + _store_keychain "$password" "$totp_secret" - printf 'Credentials saved to macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME" + printf 'Credentials are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME" +} + +_ensure_keychain_credentials() { + local password_present=false totp_present=false + _keychain_has openconnect-lite "$KC_USERNAME" && password_present=true + _keychain_has openconnect-lite "totp/$KC_USERNAME" && totp_present=true + + if [[ "$password_present" == "true" && "$totp_present" == "true" ]]; then + if [[ "$USE_BITWARDEN" == "1" ]]; then + _emit '{"event":"keychain_ready","source":"keychain"}' "LDAP credentials are ready in macOS Keychain for $KC_USERNAME." + else + _emit '{"event":"keychain_ready","source":"keychain","bitwarden":false}' "Bitwarden is disabled. Using saved LDAP password and TOTP seed from macOS Keychain for $KC_USERNAME." + fi + return 0 + fi + + if [[ "$USE_BITWARDEN" == "1" ]]; then + _emit '{"event":"keychain_required","bitwarden":true}' "Bitwarden sync did not produce complete Keychain credentials." + else + _emit '{"event":"keychain_required","bitwarden":false}' "Bitwarden is disabled and saved LDAP credentials are incomplete." + fi + + if ! _can_prompt; then + _emit '{"event":"error","message":"LDAP credentials are missing. Run vpn --configure-keychain in Terminal, or reinstall with --configure-keychain."}' \ + "LDAP credentials are missing. Run: vpn --configure-keychain" + return 1 + fi + + _configure_keychain } _bw_cache_session() { @@ -410,8 +467,13 @@ _bw_unlock() { fi local manual_pw - _emit '{"event":"bw_manual"}' "Unlocking Bitwarden vault..." - read -rsp "Bitwarden master password: " manual_pw + if ! _can_prompt; then + printf 'Bitwarden vault is locked and no interactive terminal is available. Using existing Keychain credentials.\n' >&2 + return 1 + fi + + _emit '{"event":"bw_manual"}' "Bitwarden vault is locked. Enter Bitwarden master password to sync LDAP credentials." + read -rsp "Bitwarden master password (not LDAP password): " manual_pw printf '\n' if [[ -z "$manual_pw" ]]; then printf 'Empty Bitwarden password. Using existing Keychain credentials.\n' >&2 @@ -428,7 +490,7 @@ _bw_unlock() { if [[ "$USE_TOUCHID" == "1" && -x "$KC_FP" ]]; then local save_choice - read -rp "Save Bitwarden master password behind Touch ID? [Y/n] " save_choice + read -rp "Save Bitwarden master password behind Touch ID for next VPN unlock? [Y/n] " save_choice if [[ "${save_choice:-y}" =~ ^[Yy]?$ ]]; then printf '%s' "$manual_pw" | "$KC_FP" set "$BW_KC_SERVICE" "$BW_KC_ACCOUNT_MASTER" >/dev/null 2>&1 \ && printf 'Saved. Next unlock can use Touch ID.\n' \ @@ -544,19 +606,19 @@ if $CONFIGURE_KEYCHAIN_MODE; then exit 0 fi -trap '_dns_cleanup; _clear_status' EXIT - if ! $JSON_MODE; then _module_status_human else printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)" fi -_patch_oc _sync_bitwarden +_ensure_keychain_credentials +_patch_oc _emit '{"event":"connecting"}' "Connecting to VPN (lemanapro)..." _write_status "{\"pid\":$$,\"state\":\"connecting\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" +trap '_dns_cleanup; _clear_status' EXIT display_mode="hidden" log_level="" diff --git a/tests/smoke.sh b/tests/smoke.sh index a70c299..dd06a49 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -52,6 +52,7 @@ uninstall_output="$( printf '%s\n' "$uninstall_output" | grep -q 'Начинаю удаление Lemana VPN' printf '%s\n' "$uninstall_output" | grep -q 'Проверяю runtime-патчи openconnect-lite' printf '%s\n' "$uninstall_output" | grep -q 'Удаляю sudoers и DNS cleanup wrapper' +printf '%s\n' "$uninstall_output" | grep -q 'killall LemanaVPN # if running' printf '%s\n' "$uninstall_output" | grep -q 'Удаляю VPN-записи из macOS Keychain' if printf '%s\n' "$uninstall_output" | grep -q "$esc"; then @@ -59,6 +60,25 @@ if printf '%s\n' "$uninstall_output" | grep -q "$esc"; then exit 1 fi +missing_user="lemana-smoke-missing-$$" +set +e +manual_output="$( + HOME="$HOME" \ + LEMANA_VPN_USERNAME="$missing_user" \ + LEMANA_VPN_USE_BITWARDEN=0 \ + bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1 +)" +manual_code=$? +set -e + +[ "$manual_code" -ne 0 ] +printf '%s\n' "$manual_output" | grep -q '"event":"keychain_required"' +printf '%s\n' "$manual_output" | grep -q 'vpn --configure-keychain' +if printf '%s\n' "$manual_output" | grep -q 'Cleaning up VPN DNS'; then + echo "missing manual credentials should fail before VPN cleanup trap is installed" >&2 + exit 1 +fi + fake_pwd="$TMP_DIR/fake-pwd" mkdir -p "$fake_pwd/bin" printf 'stale local cli\n' > "$fake_pwd/bin/vpn-lemanapro.sh" diff --git a/uninstall.sh b/uninstall.sh index c3aebad..2c93968 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -247,10 +247,16 @@ main() { if [ "$KEEP_APP" -eq 0 ]; then log_step "Удаляю Menu Bar app" - log_detail "Сначала отключаю LaunchAgent, затем удаляю $APP_DIR." + log_detail "Сначала останавливаю уже запущенное LemanaVPN, затем отключаю LaunchAgent и удаляю $APP_DIR." if [ "$DRY_RUN" -eq 0 ]; then + if pgrep -x LemanaVPN >/dev/null 2>&1; then + log_info "Stopping running LemanaVPN process" + killall LemanaVPN >/dev/null 2>&1 || true + sleep 1 + fi launchctl unload "$LAUNCH_AGENT" >/dev/null 2>&1 || true else + printf '+ killall LemanaVPN # if running\n' printf '+ launchctl unload %s\n' "$LAUNCH_AGENT" fi run rm -f "$LAUNCH_AGENT"