security fixes
This commit is contained in:
parent
58b1aa50c2
commit
6ab1333b41
4 changed files with 227 additions and 0 deletions
126
lib/mix/tasks/totp_diagnose.exs
Normal file
126
lib/mix/tasks/totp_diagnose.exs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
defmodule Mix.Tasks.Totp.Diagnose do
|
||||
@shortdoc "Diagnose TOTP verification failures"
|
||||
|
||||
@moduledoc """
|
||||
Diagnostic tool for TOTP verification failures.
|
||||
|
||||
Usage:
|
||||
mix totp.diagnose <user_email> <provided_code> <server_unix_timestamp>
|
||||
|
||||
Example:
|
||||
mix totp.diagnose user@example.com 776330 1769641093
|
||||
|
||||
This will:
|
||||
1. Check if the provided code matches the user's secret at any time within ±24 hours
|
||||
2. Report the exact time offset if found
|
||||
3. Help identify if the issue is clock drift or secret mismatch
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
require Logger
|
||||
|
||||
def run(args) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
case args do
|
||||
[email, provided_code, timestamp_str] ->
|
||||
diagnose(email, provided_code, String.to_integer(timestamp_str))
|
||||
|
||||
_ ->
|
||||
IO.puts("""
|
||||
Usage: mix totp.diagnose <user_email> <provided_code> <server_unix_timestamp>
|
||||
|
||||
Example:
|
||||
mix totp.diagnose user@example.com 776330 1769641093
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
defp diagnose(email, provided_code, server_timestamp) do
|
||||
IO.puts("\n=== TOTP Diagnostic Tool ===\n")
|
||||
IO.puts("Email: #{email}")
|
||||
IO.puts("Provided code: #{provided_code}")
|
||||
IO.puts("Server time: #{DateTime.from_unix!(server_timestamp)} (unix: #{server_timestamp})")
|
||||
|
||||
user = Towerops.Accounts.get_user_by_email(email)
|
||||
|
||||
if is_nil(user) do
|
||||
IO.puts("\n❌ User not found!")
|
||||
exit({:shutdown, 1})
|
||||
end
|
||||
|
||||
if is_nil(user.totp_secret) do
|
||||
IO.puts("\n❌ User does not have TOTP enabled!")
|
||||
exit({:shutdown, 1})
|
||||
end
|
||||
|
||||
IO.puts("\nTOTP enabled: Yes")
|
||||
IO.puts("Secret length: #{byte_size(user.totp_secret)} bytes")
|
||||
|
||||
# Check current expected codes
|
||||
IO.puts("\n--- Expected codes at failure time ---")
|
||||
expected_current = NimbleTOTP.verification_code(user.totp_secret, time: server_timestamp)
|
||||
expected_prev = NimbleTOTP.verification_code(user.totp_secret, time: server_timestamp - 30)
|
||||
expected_next = NimbleTOTP.verification_code(user.totp_secret, time: server_timestamp + 30)
|
||||
|
||||
IO.puts("Current window: #{expected_current}")
|
||||
IO.puts("Previous (-30s): #{expected_prev}")
|
||||
IO.puts("Next (+30s): #{expected_next}")
|
||||
|
||||
# Search for when the provided code would have been valid
|
||||
IO.puts("\n--- Searching for provided code in wider time window ---")
|
||||
IO.puts("Scanning ±24 hours (2880 time steps)...")
|
||||
|
||||
found_time = search_for_code(user.totp_secret, provided_code, server_timestamp)
|
||||
|
||||
if found_time do
|
||||
offset_seconds = found_time - server_timestamp
|
||||
offset_minutes = Float.round(offset_seconds / 60, 1)
|
||||
|
||||
IO.puts("\n✓ FOUND! The provided code was valid at:")
|
||||
IO.puts(" Time: #{DateTime.from_unix!(found_time)}")
|
||||
IO.puts(" Offset: #{offset_seconds} seconds (#{offset_minutes} minutes)")
|
||||
|
||||
cond do
|
||||
abs(offset_seconds) <= 120 ->
|
||||
IO.puts("\n⚠️ This should have been accepted (within ±2 minute window)")
|
||||
IO.puts(" Possible bug in verification logic!")
|
||||
|
||||
abs(offset_seconds) <= 900 ->
|
||||
IO.puts("\n⏰ Clock drift detected: #{abs(offset_minutes)} minutes")
|
||||
IO.puts(" The client's clock is off by more than 2 minutes.")
|
||||
|
||||
true ->
|
||||
IO.puts("\n⏰ Extreme clock drift: #{abs(offset_minutes)} minutes")
|
||||
IO.puts(" The client's clock is significantly incorrect.")
|
||||
end
|
||||
else
|
||||
IO.puts("\n❌ NOT FOUND in ±24 hour window")
|
||||
IO.puts("\nThis strongly suggests a SECRET MISMATCH:")
|
||||
IO.puts(" - The secret in the database differs from what's in the authenticator app")
|
||||
IO.puts(" - The user may have scanned a different QR code")
|
||||
IO.puts(" - The secret may have been regenerated without updating the authenticator")
|
||||
IO.puts("\nRecommended action:")
|
||||
IO.puts(" 1. Have the user disable TOTP")
|
||||
IO.puts(" 2. Re-enable TOTP and scan the new QR code")
|
||||
IO.puts(" 3. Verify the new code works before proceeding")
|
||||
end
|
||||
|
||||
IO.puts("\n")
|
||||
end
|
||||
|
||||
defp search_for_code(secret, provided_code, server_timestamp) do
|
||||
# Check ±24 hours (86400 seconds = 2880 time steps of 30 seconds)
|
||||
time_steps = 2880
|
||||
|
||||
Enum.find_value(-time_steps..time_steps, fn step ->
|
||||
test_time = server_timestamp + step * 30
|
||||
expected = NimbleTOTP.verification_code(secret, time: test_time)
|
||||
|
||||
if expected == provided_code do
|
||||
test_time
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
75
lib/mix/tasks/totp_time_check.exs
Normal file
75
lib/mix/tasks/totp_time_check.exs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
defmodule Mix.Tasks.Totp.TimeCheck do
|
||||
@shortdoc "Check server time and TOTP time synchronization"
|
||||
|
||||
@moduledoc """
|
||||
Diagnostic tool to check if the server's clock is correct.
|
||||
|
||||
Usage:
|
||||
mix totp.time_check
|
||||
|
||||
This will:
|
||||
1. Show the server's current time (BEAM VM time)
|
||||
2. Show the system's current time
|
||||
3. Calculate what TOTP codes should be valid right now for a test secret
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
require Logger
|
||||
|
||||
def run(_args) do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
IO.puts("\n=== Time Synchronization Check ===\n")
|
||||
|
||||
# Get different time sources
|
||||
system_time = System.system_time(:second)
|
||||
os_time = :os.system_time(:second)
|
||||
datetime_now = DateTime.utc_now()
|
||||
|
||||
IO.puts("System.system_time(:second): #{system_time}")
|
||||
IO.puts(" → #{DateTime.from_unix!(system_time)}")
|
||||
IO.puts("")
|
||||
IO.puts(":os.system_time(:second): #{os_time}")
|
||||
IO.puts(" → #{DateTime.from_unix!(os_time)}")
|
||||
IO.puts("")
|
||||
IO.puts("DateTime.utc_now(): #{datetime_now}")
|
||||
IO.puts(" → unix: #{DateTime.to_unix(datetime_now)}")
|
||||
IO.puts("")
|
||||
|
||||
# Check if they match
|
||||
if system_time == os_time and system_time == DateTime.to_unix(datetime_now) do
|
||||
IO.puts("✓ All time sources agree")
|
||||
else
|
||||
IO.puts("⚠️ WARNING: Time sources disagree!")
|
||||
IO.puts(" System vs OS diff: #{system_time - os_time} seconds")
|
||||
IO.puts(" System vs DateTime diff: #{system_time - DateTime.to_unix(datetime_now)} seconds")
|
||||
end
|
||||
|
||||
IO.puts("")
|
||||
|
||||
# Show what TOTP codes would be generated now
|
||||
test_secret = NimbleTOTP.secret()
|
||||
current_code = NimbleTOTP.verification_code(test_secret, time: system_time)
|
||||
prev_code = NimbleTOTP.verification_code(test_secret, time: system_time - 30)
|
||||
next_code = NimbleTOTP.verification_code(test_secret, time: system_time + 30)
|
||||
|
||||
IO.puts("Test TOTP codes (with random secret):")
|
||||
IO.puts(" Current window: #{current_code}")
|
||||
IO.puts(" Previous (-30s): #{prev_code}")
|
||||
IO.puts(" Next (+30s): #{next_code}")
|
||||
IO.puts("")
|
||||
|
||||
# Recommend action
|
||||
IO.puts("=== Recommendations ===")
|
||||
IO.puts("")
|
||||
IO.puts("Compare the server time above with actual UTC time:")
|
||||
IO.puts(" Current actual UTC: Run 'date -u' on your local machine")
|
||||
IO.puts("")
|
||||
IO.puts("If the server time is wrong:")
|
||||
IO.puts(" 1. Check K8s node time: kubectl exec -it <pod> -- date -u")
|
||||
IO.puts(" 2. Check if NTP is running on the node")
|
||||
IO.puts(" 3. Restart the pod to resync time")
|
||||
IO.puts("")
|
||||
end
|
||||
end
|
||||
|
|
@ -78,4 +78,29 @@ defmodule ToweropsWeb.HealthController do
|
|||
defp redis_status_string(:ok), do: "connected"
|
||||
defp redis_status_string(:error), do: "disconnected"
|
||||
defp redis_status_string(:not_configured), do: "not_configured"
|
||||
|
||||
@doc """
|
||||
Time diagnostic endpoint for debugging TOTP issues.
|
||||
Returns server time from multiple sources.
|
||||
"""
|
||||
def time(conn, _params) do
|
||||
system_time = System.system_time(:second)
|
||||
os_time = :os.system_time(:second)
|
||||
datetime_now = DateTime.utc_now()
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(
|
||||
200,
|
||||
Jason.encode!(%{
|
||||
system_time: system_time,
|
||||
system_time_iso: system_time |> DateTime.from_unix!() |> DateTime.to_iso8601(),
|
||||
os_time: os_time,
|
||||
os_time_iso: os_time |> DateTime.from_unix!() |> DateTime.to_iso8601(),
|
||||
datetime_now: DateTime.to_unix(datetime_now),
|
||||
datetime_now_iso: DateTime.to_iso8601(datetime_now),
|
||||
time_sources_agree: system_time == os_time and system_time == DateTime.to_unix(datetime_now)
|
||||
})
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ defmodule ToweropsWeb.Router do
|
|||
# Health check endpoint for Kubernetes probes (no authentication required)
|
||||
scope "/", ToweropsWeb do
|
||||
get "/health", HealthController, :index
|
||||
get "/health/time", HealthController, :time
|
||||
end
|
||||
|
||||
scope "/", ToweropsWeb do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue