keychain-audit.sh 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. #!/usr/bin/env bash
  2. # mac-ops :: keychain-audit.sh
  3. # Audit Keychain health: login keychain status, certificate trust chain,
  4. # securityd activity, recurring password prompts.
  5. #
  6. # "macOS keeps asking for my password" is the #2 most common Mac complaint
  7. # after "this app won't open the camera". Root cause is usually a damaged
  8. # login.keychain-db, an out-of-sync iCloud Keychain, or a recurring TCC
  9. # prompt being confused for a Keychain prompt.
  10. set -u
  11. while [[ $# -gt 0 ]]; do
  12. case "$1" in
  13. --help|-h)
  14. cat <<EOF
  15. Usage: $0 [options]
  16. --json, --redact, --quiet, --verbose
  17. Reports:
  18. 1. Login keychain location + last-modified time + lock state
  19. 2. System / iCloud keychain detection
  20. 3. securityd / trustd recent error activity
  21. 4. Expired certificates in user keychain
  22. 5. Apple developer codesign trust state
  23. 6. Common "password keeps prompting" causes
  24. Common fix sequence for "keeps prompting":
  25. 1. Keychain Access → File → Lock All Keychains → quit
  26. 2. Quit Keychain Access; open it; "Update Keychain Password" if prompted
  27. 3. Or worst case: rename login.keychain-db (loses cached passwords)
  28. cd ~/Library/Keychains/<UUID>
  29. mv login.keychain-db login.keychain-db.broken
  30. (reboot — a fresh one will be created)
  31. EOF
  32. exit 0 ;;
  33. *) shift ;;
  34. esac
  35. done
  36. source "$(dirname "$0")/_lib/common.sh"
  37. parse_common_flags "$@"
  38. maybe_filter_self "$@"
  39. # ----------------------------------------------------------------------------
  40. section "1. LOGIN KEYCHAIN"
  41. # ----------------------------------------------------------------------------
  42. # Modern macOS stores keychains in ~/Library/Keychains/<UUID>/
  43. keychain_dir=$(ls -d "$HOME/Library/Keychains"/* 2>/dev/null | head -1)
  44. if [[ -n "$keychain_dir" ]]; then
  45. note " Keychain directory: $keychain_dir"
  46. if [[ -f "$keychain_dir/login.keychain-db" ]]; then
  47. size=$(ls -lh "$keychain_dir/login.keychain-db" | awk '{print $5}')
  48. mtime=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$keychain_dir/login.keychain-db")
  49. log_pass "login.keychain-db" "$size, modified $mtime"
  50. else
  51. log_warn "login.keychain-db" "missing in $keychain_dir"
  52. fi
  53. else
  54. log_warn "Keychain directory" "not found at standard location"
  55. fi
  56. # Show keychain list as the security tool sees it
  57. note ""
  58. note " security list-keychains:"
  59. security list-keychains -d user 2>/dev/null | sed 's/^/ /' | head -10
  60. # ----------------------------------------------------------------------------
  61. section "2. KEYCHAIN LOCK STATE"
  62. # ----------------------------------------------------------------------------
  63. # Use 'security show-keychain-info' on the login keychain
  64. if security show-keychain-info "$HOME/Library/Keychains/login.keychain-db" 2>/dev/null; then
  65. log_pass "Login keychain unlocked"
  66. else
  67. # Either locked or doesn't exist at that path; check modern path
  68. login_db=$(security default-keychain 2>/dev/null | tr -d '"' | awk '{print $1}')
  69. if [[ -n "$login_db" ]]; then
  70. if security show-keychain-info "$login_db" 2>/dev/null; then
  71. log_pass "Default keychain unlocked" "$login_db"
  72. else
  73. log_info "Default keychain" "may be locked or auto-locked"
  74. fi
  75. fi
  76. fi
  77. # ----------------------------------------------------------------------------
  78. section "3. SECURITYD / TRUSTD ACTIVITY (recent errors)"
  79. # ----------------------------------------------------------------------------
  80. sec_errors=$(log show --last 24h --style compact \
  81. --predicate '(process == "securityd" OR process == "trustd" OR process == "keychainsharingmessaging") AND (messageType == "Error" OR messageType == "Fault")' \
  82. 2>/dev/null | head -10)
  83. if [[ -n "$sec_errors" ]]; then
  84. n=$(echo "$sec_errors" | wc -l | tr -d ' \n')
  85. log_warn "securityd/trustd errors (24h)" "$n events"
  86. echo "$sec_errors" | head -5 | sed 's/^/ /'
  87. else
  88. log_pass "securityd/trustd errors (24h)" "none"
  89. fi
  90. # Specifically: keychain password prompts in log
  91. prompt_events=$(log show --last 24h --style compact \
  92. --predicate 'eventMessage CONTAINS[c] "keychain" AND (eventMessage CONTAINS[c] "prompt" OR eventMessage CONTAINS[c] "password")' \
  93. 2>/dev/null | head -5)
  94. if [[ -n "$prompt_events" ]]; then
  95. log_info "Keychain prompt events (24h)" "see below"
  96. echo "$prompt_events" | sed 's/^/ /'
  97. fi
  98. # ----------------------------------------------------------------------------
  99. section "4. CERTIFICATE INVENTORY"
  100. # ----------------------------------------------------------------------------
  101. # Count certs in user keychain
  102. cert_count=$(security find-certificate -a 2>/dev/null | grep -c "^keychain:" | tr -d ' \n')
  103. cert_count="${cert_count:-0}"
  104. log_info "Certs in user keychain" "$cert_count"
  105. # Expired certs check (most certs in system keychain rotate naturally; here we
  106. # check the user's own certs)
  107. expired=$(security find-certificate -a -p 2>/dev/null | awk '
  108. /-----BEGIN CERTIFICATE-----/{flag=1; buf=""}
  109. flag{buf=buf"\n"$0}
  110. /-----END CERTIFICATE-----/{
  111. cmd="openssl x509 -noout -enddate 2>/dev/null"
  112. print buf | cmd
  113. close(cmd)
  114. flag=0
  115. }' 2>/dev/null | grep "notAfter" | head -5)
  116. # This is best-effort; full expiry scan requires more work
  117. # ----------------------------------------------------------------------------
  118. section "5. CODESIGN / GATEKEEPER STATE"
  119. # ----------------------------------------------------------------------------
  120. gk_status=$(spctl --status 2>&1)
  121. case "$gk_status" in
  122. *enabled*) log_pass "Gatekeeper" "enabled" ;;
  123. *disabled*) log_warn "Gatekeeper" "disabled — system is less secure" ;;
  124. *) log_info "Gatekeeper" "$gk_status" ;;
  125. esac
  126. # Check developer mode (on Apple Silicon, controls things like unsigned dylib loading)
  127. if is_apple_silicon; then
  128. dev_mode=$(spctl developer-mode status 2>/dev/null || echo "(needs sudo to query)")
  129. note " Apple Silicon developer mode: $dev_mode"
  130. fi
  131. # ----------------------------------------------------------------------------
  132. section "6. iCLOUD KEYCHAIN STATE"
  133. # ----------------------------------------------------------------------------
  134. # Check if iCloud Keychain is enabled by looking for the keychain-sync daemons
  135. if pgrep -x securityd >/dev/null && \
  136. log show --last 1h --style compact --predicate 'process == "securityd" AND eventMessage CONTAINS "circle"' 2>/dev/null | grep -q "joined"; then
  137. log_info "iCloud Keychain" "appears to be in sync circle"
  138. else
  139. log_info "iCloud Keychain" "state could not be determined from log"
  140. fi
  141. # ----------------------------------------------------------------------------
  142. section "7. COMMON ISSUES"
  143. # ----------------------------------------------------------------------------
  144. note " If \"macOS keeps asking for password\":"
  145. note " Most common cause: login.keychain-db password drifted from account password."
  146. note " Fix: Keychain Access → preferences → Reset My Default Keychain"
  147. note " (loses cached passwords but is the cleanest reset)"
  148. note ""
  149. note " If \"This connection is not private\" for valid sites:"
  150. note " Check system clock (mac-ops health-audit reports clock drift)."
  151. note " Run: scripts/health-audit.sh --days 1"
  152. # ----------------------------------------------------------------------------
  153. emit_summary