Skip to main content

TOTP generation with external time sync (NTP / Google HTTP), derived from oath-toolkit

Project description

oath-external-totp

TOTP com sincronização de horário externo (Google / NTP) para Python

Biblioteca Python que gera códigos de autenticação em dois fatores (TOTP) usando um relógio confiável na internet, em vez de depender apenas do relógio do sistema operacional — o mesmo comportamento do oathtool --google-time do GNU OATH Toolkit modificado.


Índice


Por que este projeto existe?

Autenticadores como Google Authenticator, Microsoft Authenticator e apps corporativos geram códigos TOTP com base no horário universal (UTC) sincronizado via internet.

Muitos scripts e servidores em Python, porém, usam bibliotecas que leem apenas o relógio local do computador (time.time()). Em máquinas com:

  • relógio atrasado ou adiantado;
  • NTP desativado;
  • VMs ou containers sem sync;
  • dual boot ou BIOS com drift;

o código gerado não bate com o do celular — e o login falha mesmo com o segredo correto.

Extensões de navegador oferecem opções do tipo “Sincronizar relógio com o Google” exatamente por isso. Este pacote traz essa mesma ideia para automação em Python (Selenium, APIs, bots de aprovação, etc.), sem alterar o relógio do sistema.


O problema: relógio errado = código errado

TOTP (RFC 6238) não é um código fixo: ele muda a cada 30 segundos e depende de:

contador = floor((unix_timestamp - t0) / 30)
código   = HMAC-SHA1(segredo, contador) → 6 dígitos

Se o relógio do PC estiver 30 segundos ou mais fora da janela correta, o contador cai em outro intervalo e o código fica completamente diferente — não é “quase igual”, é outro número.

Exemplo real

Fonte Código gerado
PC com relógio local desalinhado 629185
Mesmo segredo, horário Google/NTP 236999
App no celular 236999

O PC estava errado; o celular e o --google-time concordaram.

Sintomas no dia a dia

  • Login OTP falha só em um servidor ou VM.
  • Código “funciona às vezes” (quando o drift cai dentro da mesma janela de 30s).
  • oathtool.generate_otp() ou pyotp local ≠ autenticador no telefone.
  • Diferença de poucos segundos entre date -u e horário NTP.

A solução

oath-external-totp busca a hora atual por:

  1. NTP em time.google.com (padrão, rápido, UDP porta 123)
  2. Fallback HTTP: HEAD em www.google.com/generate_204 e leitura do header Date

Com esse instante UTC, calcula o TOTP — sem mudar o relógio do sistema.

Equivalente CLI no oathtool modificado:

oathtool --totp -b SEU_SEGREDO_BASE32 --google-time

Equivalente Python neste pacote:

from oath_external_totp import generate_otp

generate_otp("SEU_SEGREDO_BASE32", google_time=True)

Como funciona

flowchart TB
  subgraph app [Sua aplicação Python]
    call["generate_otp(secret, google_time=True)"]
  end

  subgraph sync [Sincronização externa]
    ntp["NTP → time.google.com"]
    http["HTTP Date → www.google.com"]
  end

  subgraph totp [TOTP RFC 6238]
    pyotp["pyotp · SHA1 · 30s · 6 dígitos"]
  end

  call --> ntp
  ntp -->|falha| http
  ntp -->|sucesso| pyotp
  http -->|sucesso| pyotp
  http -->|falha| err["TimeSyncError"]
  pyotp --> code["Código OTP"]
Etapa Detalhe
Segredo Base32 (mesmo formato do QR code / oathtool -b)
Algoritmo HMAC-SHA1, janela de 30s, 6 dígitos (padrão oathtool)
Relógio externo NTP primeiro; HTTP só se NTP falhar (google_time=True)
Relógio local Comportamento padrão se google_time=False
Sistema Não executa timedatectl nem altera o relógio do SO

Instalação

PyPI (após publicação)

pip install oath-external-totp

Testar antes em ambiente de staging:

pip install -i https://test.pypi.org/simple/ oath-external-totp

Desenvolvimento local

git clone <seu-repositorio>
cd oath_external_totp
pip install -e ".[dev]"

Em outro projeto (requirements.txt)

oath-external-totp>=0.1.1

Ou path local:

oath-external-totp @ file:///caminho/para/oath_external_totp

Requisitos: Python 3.9+, acesso de rede (UDP 123 e/ou HTTP 80) quando usar sync externa.


Uso rápido

from oath_external_totp import (
    generate_otp,
    totp_now,
    get_external_time,
    TimeSyncError,
)

secret = "IJKW6UDLNJRUQNCIK5DDGZRSG42EOVLX"  # segredo base32

# Relógio local (como pyotp / oathtool PyPI antigo)
print(generate_otp(secret))

# Horário Google/NTP — recomendado para bater com o celular
print(generate_otp(secret, google_time=True))

# Servidor NTP explícito
print(generate_otp(secret, ntp_server="time.google.com"))

# Só consultar a hora externa (UTC)
print(get_external_time(google_time=True))

Tratamento de erro (não gera código silenciosamente com hora errada):

try:
    code = generate_otp(secret, google_time=True)
except TimeSyncError:
    print("Sem rede ou sync falhou — corrija conectividade")

Integração com projetos existentes

Substituir oathtool.generate_otp (TicketServer, Selenium, etc.)

Muitos projetos usam o pacote PyPI oathtool, que só lê o relógio local:

import oathtool
otp_code = oathtool.generate_otp(otp_secret)

Migração mínima — mesma assinatura, com sync Google:

import oath_external_totp as oathtool

otp_code = oathtool.generate_otp(otp_secret, google_time=True)

Variável de ambiente (sem mudar cada chamada)

No .env:

OATH_GOOGLE_TIME=1

No código:

import oath_external_totp as oathtool

# google_time=True é aplicado automaticamente
otp_code = oathtool.generate_otp(otp_secret)

Automação com Selenium (fluxo típico)

import oath_external_totp as oathtool

def preencher_otp(driver, otp_secret: str) -> None:
    code = oathtool.generate_otp(otp_secret, google_time=True)
    for i, ch in enumerate(code, start=1):
        driver.find_element(By.ID, f"camp{i}").send_keys(ch)

Comparar relógio local vs Google

Útil para diagnosticar se o problema é o relógio do PC:

# oathtool C (build oath-toolkit modificado)
export LD_LIBRARY_PATH=/path/to/oath-toolkit-2.6.13/liboath/.libs
OT=/path/to/oath-toolkit-2.6.13/oathtool/.libs/oathtool
SECRET="SEU_SEGREDO_BASE32"

echo "Local (PC):    $($OT --totp -b "$SECRET")"
echo "Google (NTP):  $($OT --totp -b "$SECRET" --google-time)"
from oath_external_totp import generate_otp

SECRET = "SEU_SEGREDO_BASE32"
print("Local (PC):   ", generate_otp(SECRET))
print("Google (NTP): ", generate_otp(SECRET, google_time=True))
Resultado Significado
Os dois iguais Relógio local OK; sync externa é opcional
Os dois diferentes PC desalinhado — use google_time=True
Igual ao celular Sync correta; pode automatizar com confiança

Ver horários em UTC:

from datetime import datetime, timezone
from oath_external_totp import get_external_time

print("Local:   ", datetime.now(timezone.utc))
print("Externo:", get_external_time(google_time=True))

Referência da API

Função Descrição
generate_otp(key, hotp_value=None, *, google_time=False, ntp_server=None) API compatível com oathtool PyPI + sync externa
totp_now(secret, *, google_time=False, ntp_server=None) TOTP atual (base32)
totp_at(secret, when) TOTP para um datetime UTC específico
get_external_time(*, google_time=False, ntp_server=None) datetime UTC da rede
fetch_ntp(host) Cliente NTP de baixo nível
fetch_google_http_date() HTTP Date do Google
TimeSyncError Falha de sincronização (mensagem alinhada ao oathtool C)

Parâmetros importantes

Parâmetro Efeito
google_time=True NTP time.google.com → fallback HTTP Google
ntp_server="host" Apenas NTP no host informado
hotp_value=N Contador TOTP fixo (avançado; ignora relógio)
OATH_GOOGLE_TIME=1 Default global para generate_otp()

Não combine google_time=True com ntp_server na mesma chamada.


Erros comuns

TimeSyncError: could not fetch external time; check network connection

  • Sem internet, firewall bloqueando UDP 123 ou HTTP 80
  • Proxy corporativo sem saída para NTP/Google
  • Comportamento intencional: não usa relógio local como fallback silencioso (evita OTP inválido sem aviso)

Código diferente do celular mesmo com google_time=True

  • Confirme que o segredo base32 é o mesmo do autenticador
  • Verifique se o serviço usa SHA1 / 30s (padrão desta lib)
  • Pode estar no limite da janela de 30s — aguarde e gere de novo

ImportError: No module named 'oath_external_totp'

pip install oath-external-totp
# ou no projeto:
pip install -e /caminho/oath_external_totp

Testes

pip install -e ".[dev]"
pytest -v                 # unitários (rede mockada)
pytest -m network -v      # integração com internet real

Validação cruzada com oathtool C:

# Deve imprimir o mesmo código na mesma janela de 30s
OT=.../oathtool/.libs/oathtool
python -c "from oath_external_totp import generate_otp; print(generate_otp('SECRET', google_time=True))"
$OT --totp -b SECRET --google-time

Origem e licença

Este pacote é software livre (GPL-3.0-or-later).

Componente Origem
Lógica NTP + HTTP Portada de google_time.c e ntp_time.c (modificações ao oath-toolkit 2.6.13)
TOTP pyotp (MIT)
Projeto original Copyright 2009-2025 Simon Josefsson

Arquivos legais:

  • COPYING — texto completo da GPL-3
  • NOTICE — atribuições e trabalho derivado

Projetos proprietários: distribuir este pacote junto com código fechado exige conformidade com a GPL (em geral, disponibilizar o código-fonte correspondente). Para produtos fechados, avalie reimplementação a partir das RFCs públicas ou consulte assessoria jurídica.


Aviso de responsabilidade

Este software é fornecido “no estado em que se se encontra” (AS IS), sem garantias de qualquer tipo, nos termos da GPL-3.0, seções 15 e 16 (isenção de garantia e limitação de responsabilidade). Veja COPYING.

  • Não substitui autenticadores oficiais nem auditoria de segurança.
  • Depende de serviços de terceiros (Google/NTP) quando google_time=True.
  • O mantenedor não se responsabiliza por falhas de login, bloqueios de conta ou danos decorrentes de uso em produção.
  • Em ambientes críticos, valide o comportamento no seu ambiente antes de automatizar logins.

Créditos

Desenvolvido a partir de modificações ao GNU OATH Toolkit 2.6.13 para suportar --google-time e --ntp-server no oathtool, portadas para Python para uso em projetos reais de automação e integração.

Versão: 0.1.1

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oath_external_totp-0.1.1.tar.gz (28.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oath_external_totp-0.1.1-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

File details

Details for the file oath_external_totp-0.1.1.tar.gz.

File metadata

  • Download URL: oath_external_totp-0.1.1.tar.gz
  • Upload date:
  • Size: 28.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for oath_external_totp-0.1.1.tar.gz
Algorithm Hash digest
SHA256 61c2b3f6f7b5c48849217789a8f3889c8142812cc9732d699bdd818da4afd0b5
MD5 cc368fd7d01651adefca305977856542
BLAKE2b-256 c74316bde9eef280002b5d5dd9f509732aca5b2b2396bfe162095e9796bd2514

See more details on using hashes here.

File details

Details for the file oath_external_totp-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for oath_external_totp-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a98e1e01a886167ff690e577448295ba207b5968b67e5e9313cde2a299e2c209
MD5 cbbfc32c6d313dc8da9f3283e61d7a00
BLAKE2b-256 50175bdb55ea25b572ac92af684a27c8429a45e9b914ed9982a21ee44a937bbb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page