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?
- O problema: relógio errado = código errado
- A solução
- Como funciona
- Instalação
- Uso rápido
- Integração com projetos existentes
- Comparar relógio local vs Google
- Referência da API
- Erros comuns
- Testes
- Origem e licença
- Aviso de responsabilidade
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()oupyotplocal ≠ autenticador no telefone.- Diferença de poucos segundos entre
date -ue horário NTP.
A solução
oath-external-totp busca a hora atual por:
- NTP em
time.google.com(padrão, rápido, UDP porta 123) - Fallback HTTP:
HEADemwww.google.com/generate_204e leitura do headerDate
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:
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61c2b3f6f7b5c48849217789a8f3889c8142812cc9732d699bdd818da4afd0b5
|
|
| MD5 |
cc368fd7d01651adefca305977856542
|
|
| BLAKE2b-256 |
c74316bde9eef280002b5d5dd9f509732aca5b2b2396bfe162095e9796bd2514
|
File details
Details for the file oath_external_totp-0.1.1-py3-none-any.whl.
File metadata
- Download URL: oath_external_totp-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a98e1e01a886167ff690e577448295ba207b5968b67e5e9313cde2a299e2c209
|
|
| MD5 |
cbbfc32c6d313dc8da9f3283e61d7a00
|
|
| BLAKE2b-256 |
50175bdb55ea25b572ac92af684a27c8429a45e9b914ed9982a21ee44a937bbb
|