126 lines
4.3 KiB
Elixir
126 lines
4.3 KiB
Elixir
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
|