Skip to main content

TLSOTP

Time + Location + String One-Time-Password

_________ _       _______  _______ _________ _______ 
\__   __/( \     (  ____ \(  ___  )\__   __/(  ____ )
   ) (   | (     | (    \/| (   ) |   ) (   | (    )|
   | |   | |     | (_____ | |   | |   | |   | (____)|
   | |   | |     (_____  )| |   | |   | |   |  _____)
   | |   | |           ) || |   | |   | |   | (      
   | |   | (____/Y\____) || (___) |   | |   | )      
   )_(   (_______|_______)(_______)   )_(   |/       

PyPI - Version PyPI - Python Version License GitHub Repo stars

A tiny, dependency‑free Python library for context‑aware, deterministic OTPs — bind a one‑time password to time, geographic location, and an optional shared passphrase, then verify it with built‑in drift tolerance.

from tlsotp import key_gen, get_OTP, verify_otp

key = key_gen()                       # 32-byte URL-safe secret
otp = get_OTP(main_key=key)           # "784412"
assert verify_otp(main_key=key, user_otp=otp, drift_windows=1)  # True

⚠️ Not RFC‑6238/TOTP compatible. TLSOTP is a custom, deterministic scheme. OTPs cannot be generated by Google Authenticator or other standard TOTP apps.


✨ Features

Capability TLSOTP
Time‑windowed OTPs ✅ every time_binning seconds (default 30 s)
Location binding ✅ country (ISO3), coordinate precision, or grid cell
Password layer ✅ full or truncated shared passphrase in the seed
Output formats ✅ digits (0–9), alpha (A–Z/a–z), alphanumeric (62)
OTP length ✅ 1–128 chars
Hash algorithms ✅ SHA‑1/224/256/384/512, SHA3‑512
Version‑aware OTPs use_version= selects current or legacy algorithm
Legacy compatibility ✅ v0.1 / v0.2 mostly compatible via legacy module
Config‑as‑URI TLSOTP:// (runtime secrets excluded, algorithm version recorded)
Drift tolerance verify_otp(..., drift_windows=N)2N+1 windows
Constant‑time verify hmac.compare_digest
Dependencies none (stdlib only)
Compliance ❌ not RFC‑compatible by design

📦 Installation

Requires Python ≥ 3.8. No dependencies.

pip install tlsotp                # from PyPI
pip install tlsotp[dev]           # + dev tools (pytest, hypothesis, ruff, mkdocs)

From source:

git clone https://github.com/ASH-SuperUser/tlsotp.git
cd tlsotp
pip install .

Verify:

python -c "import tlsotp; print(tlsotp.__version__)"

🧠 How it works

TLSOTP is an HMAC‑based construction. The message is a UTF‑8 string made of three independent components, HMAC‑signed with your secret, then mapped to an OTP:

HMAC(main_key, time_str + location_str + password_str)
                        │
        OTP_gen(digest) ──► OTP string
Component Function Optional
Time time_str() — binned window -{T//bin*bin}- no
Location location_str() — ISO3 / coordinates / grid yes
Password password_str() — full or truncated secret yes

OTP_gen consumes the digest in 4‑byte big‑endian chunks mod the alphabet size, and chains SHA-256 to derive fresh bytes when the digest is exhausted (no cyclic reuse; per‑char modulo bias of ~10⁻⁹–10⁻⁸, ≈10⁵× smaller than RFC 4226 truncation).

Every OTP call is version‑aware: the use_version argument (or the version recorded in a URI) selects which algorithm family runs. None and any v1.x request use the live algorithm above; v0.1 / v0.2 route to the legacy implementation so old OTPs keep working after upgrades.


🕰️ Algorithm versions & legacy compatibility

TLSOTP records an explicit algorithm version and aims to reproduce older releases, so existing v0.1/v0.2 OTPs generally remain valid after upgrading. Backwards compatibility is not guaranteed — it is mostly compatible.

Version Status Notes
v1.0 Current (default) Live algorithm; 4‑byte chunked OTP_gen with SHA‑256 chaining, capped location precision, full validation
v0.2 Legacy Cyclic per‑byte OTP_gen, ISO3 embedded in 4xx/5xx location modes, uncapped positive precision
v0.1 Legacy Identical core to v0.2 (verified — same core.py SHA‑256)

Selecting a version:

from tlsotp import get_OTP

otp_current = get_OTP(main_key=key)                    # v1.0 (default)
otp_v02     = get_OTP(main_key=key, use_version="v0.2")  # legacy algorithm
otp_v01     = get_OTP(main_key=key, use_version="0.1.0") # normalized to v0.1

Version strings are normalized ("v0.2", "0.2", "0.2.0" are equivalent). Legacy versions are dispatched through the legacy module:

from tlsotp.legacy import get_otp_legacy, supported_legacy_versions

supported_legacy_versions()   # ("v0.1", "v0.2")
otp = get_otp_legacy(key, version="v0.2", otp_mode=2, n_chars=8)

Compatibility note. The legacy module reproduces the algorithms shipped in the v0.1.0 and v0.2.0 releases — validated against those releases across millions of configurations (all algorithms, modes, lengths, location/password combos) with zero OTP differences. Backwards compatibility is not guaranteed; it is mostly compatible. Note that legacy 4xx/5xx location modes require iso3_code, and positive location precision is not capped at 6 decimals as in v1.0.


🚀 Quick start

Basic time‑based OTP

from tlsotp import key_gen, get_OTP

key = key_gen()                       # 32-byte URL-safe secret
otp = get_OTP(main_key=key)           # 6-digit numeric OTP
otp = get_OTP(main_key=key, otp_mode=2, n_chars=8, algorithm="sha256")

Location‑aware OTP

# Country binding
otp = get_OTP(main_key=key, location_mode=301, iso3_code="IND")

# Coordinate precision (4 decimal places)
otp = get_OTP(main_key=key, location_mode=404, latitude=28.6139, longitude=77.2090)

Password‑enhanced OTP

# Full passphrase in the seed
otp = get_OTP(main_key=key, password_str_mode=-1, password_string="hunter2")

Verify with drift tolerance

from tlsotp import verify_otp

verify_otp(main_key=key, user_otp=otp, drift_windows=1)  # checks -1, 0, +1 windows

⚙️ Configuration reference

otp_mode — output charset

Mode Charset Alphabet size
0 Digits 0–9 (10)
1 Letters A–Z, a–z (52)
2 Alphanumeric 0–9, A–Z, a–z (62)

location_mode — spatial binding

Mode Behaviour Format Requires
0 Disabled (empty)
301 Country code XXX iso3_code (≥3 chars)
4xx Precision rounding (xx decimals, ≤6) lat-lon latitude, longitude
5xx Grid snap — floor to multiple of xx lat-lon latitude, longitude
6xx Precision rounding + country XXX-lat-lon iso3_code, latitude, longitude
7xx Grid snap + country XXX-lat-lon iso3_code, latitude, longitude
< 0 Grid snap — step abs(mode) lat-lon latitude, longitude
> 0 (else) Precision rounding (min(mode, 6) decimals) lat-lon latitude, longitude

5xx=500 and 7xx=700 yield an empty string (step 0). 4xx=400 / 6xx=600 are valid and round to 0 decimals.

password_str_mode

Mode Behaviour Example
0 Disabled (omitted)
> 0 First N chars pwd="DelhiSecure", mode=4-Delh-
< 0 Full password pwd="MyPass"-MyPass-

algorithm

ID Name algorithm param
0 SHA‑1 (default) 0 or "sha1"
1 SHA‑224 1 or "sha224"
2 SHA‑256 2 or "sha256"
3 SHA‑384 3 or "sha384"
4 SHA‑512 4 or "sha512"
5 SHA3‑512 5 or "sha3-512"

Recommendation: SHA‑256 or stronger for production.

Limits & defaults

Parameter Valid range Default
n_chars 1128 (validated by get_data_dict/get_OTP_uri) 6
time_binning positive integer (seconds) 30
latitude -9090 None
longitude -180180 None

📚 API reference

Key generation

Function Signature Description
key_gen key_gen(length=32) -> str Cryptographically secure URL‑safe key (secrets.token_urlsafe); output ≈ 4/3 × length chars

OTP generation

Function Signature Description
get_OTP get_OTP(main_key, otp_mode=0, n_chars=6, time_binning=30, location_mode=0, latitude=None, longitude=None, iso3_code=None, password_str_mode=0, password_string=None, algorithm=0, now=None, use_version=None) -> str Full OTP from all components; use_version selects legacy algorithm
get_OTP_from_dict get_OTP_from_dict(data_dict, use_version=None) -> str OTP from a validated config dict (unknown keys ignored, defaults applied)

Configuration & validation

Function Signature Description
get_data_dict get_data_dict(main_key, otp_mode=0, n_chars=6, time_binning=30, location_mode=0, latitude=None, longitude=None, iso3_code=None, password_str_mode=0, password_string=None, algorithm=0) -> dict Validate all arguments, return a normalized dict

URI sharing (TLSOTP://)

Function Signature Description
get_OTP_uri get_OTP_uri(main_key, otp_mode=0, n_chars=6, time_binning=30, location_mode=0, password_str_mode=0, algorithm=0, version=None) -> str Serialize static config + algorithm version to a URI
get_OTP_uri_from_dict get_OTP_uri_from_dict(data_dict, version=None) -> str Same, from a config dict (honors a version key)
get_dict_from_uri get_dict_from_uri(uri) -> dict Parse a URI back to a config dict (runtime fields None, includes version)
get_otp_from_uri get_otp_from_uri(uri, *, latitude=None, longitude=None, iso3_code=None, password_string=None, override_dict=None, adder_dict=None) -> str OTP from URI + runtime values (honors URI version)

Verification

Function Signature Description
verify_otp verify_otp(main_key, user_otp, drift_windows=1, use_version=None, **kwargs) -> bool Verify OTP across 2N+1 time windows (optionally legacy version)
verify_otp_from_dict verify_otp_from_dict(data_dict, user_otp, drift_windows=1, use_version=None) -> bool Verify from a config dict (honors a version key)
verify_otp_from_uri verify_otp_from_uri(user_otp, uri, drift_windows=1, *, latitude=None, longitude=None, iso3_code=None, password_string=None) -> bool Verify from a URI + runtime values (honors URI version)

Legacy (tlsotp.legacy)

from tlsotp.legacy import ... — mostly compatible reproduction of older releases.

Function Signature Description
get_otp_legacy get_otp_legacy(main_key, version="v0.2", otp_mode=0, n_chars=6, time_binning=30, location_mode=0, latitude=None, longitude=None, iso3_code=None, password_str_mode=0, password_string=None, algorithm=0, now=None) -> str Master legacy dispatcher (v0.1 / v0.2)
supported_legacy_versions supported_legacy_versions() -> tuple Registered legacy lines, e.g. ("v0.1", "v0.2")
normalize_version normalize_version(version) -> str Normalize any version string to v<major>.<minor>
is_current_version is_current_version(version) -> bool True if the version is on the current major line

Low‑level (core)

from tlsotp.core import ... — primitives used to build the higher‑level API.

Function Signature Description
get_time_OTP get_time_OTP(main_key, otp_mode=0, n_chars=6, time_binning=30, algorithm=0, now=None) -> str Time‑only HMAC OTP
OTP_gen OTP_gen(input_bytes, otp_mode=0, n_chars=6) -> str Map raw bytes to an OTP string (SHA‑256 chaining when exhausted)
time_str time_str(time_binning=30, now=None) -> str Binned time component
location_str location_str(location_mode=0, latitude=None, longitude=None, iso3_code=None) -> str Location component
password_str password_str(password_str_mode=0, pwd_string=None) -> str Password component

🔄 Sharing configs via URI

Runtime‑only values (latitude, longitude, iso3_code, password_string) are never stored in the URI — they are supplied at generation time. A shared URI leaks the key but not the password factor.

The URI also records the algorithm version (version=v1.0 by default). Create a legacy URI with version="v0.2" and anyone with the key can reproduce v0.2 OTPs from it — even on a newer TLSOTP install.

from tlsotp import get_OTP_uri, get_otp_from_uri, verify_otp_from_uri

uri = get_OTP_uri(main_key=key, otp_mode=2, n_chars=8, location_mode=301, algorithm="sha256")
# "TLSOTP://main_key=…&otp_mode=2&n_chars=8&time_binning=30&location_mode=301&password_str_mode=0&algorithm=sha256&version=v1.0"

otp = get_otp_from_uri(uri, iso3_code="IND")                  # supply runtime value
ok  = verify_otp_from_uri(otp, uri, iso3_code="IND", drift_windows=1)

# Legacy URI — reproduces the v0.2 algorithm automatically.
uri_v02 = get_OTP_uri(main_key=key, version="v0.2")
otp_v02 = get_otp_from_uri(uri_v02)                           # v0.2 OTP

# Dict helpers: unknown keys are silently ignored, missing keys get defaults.
parsed = get_dict_from_uri(uri)               # runtime fields → None, includes "version"
same   = get_OTP_from_dict(parsed)            # only if no runtime values needed

🧪 Testing

pip install -e .[dev]
pytest

255+ tests covering determinism, collisions, entropy, URI round‑trips, drift verification, Hypothesis‑driven fuzzing, and legacy‑version compatibility (including v0.1/v0.2 fixtures verified against the original releases).


🔒 Production checklist

Area Recommendation
Key key_gen(32) or larger; store in env vars / vault / HSM — never hardcode
OTP length 6+ characters
Algorithm SHA‑256 (2) or stronger
Time binning 30 s or 60 s
Verification drift_windows=1 to tolerate clock skew; enforce rate‑limiting/lockout server‑side
Transport Always serve over TLS
Compliance TLSOTP is custom, not RFC‑6238 — use TOTP if authenticator‑app interop is required

📄 License

Licensed under the Apache License 2.0 — see LICENSE.


🤝 Contributing

Found a bug or want a feature? Open an issue or submit a PR. Documentation lives in docs/ and is built with MkDocs.

Download files

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

Source Distribution

tlsotp-1.0.0.tar.gz (67.8 kB view details)

Uploaded Source

Built Distribution

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

tlsotp-1.0.0-py3-none-any.whl (27.7 kB view details)

Uploaded Python 3

File details

Details for the file tlsotp-1.0.0.tar.gz.

File metadata

  • Download URL: tlsotp-1.0.0.tar.gz
  • Upload date:
  • Size: 67.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for tlsotp-1.0.0.tar.gz
Algorithm Hash digest
SHA256 761904965e95d1ea28cbfd27c84822b29866e0cf02d6960d8640ada59bd8d7d4
MD5 470c20e728af500d6f181d9d2d90a587
BLAKE2b-256 c9e29571cd94cbc29af0aa93685fe57da7281fcd804f69c229d490ad394e6682

See more details on using hashes here.

File details

Details for the file tlsotp-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: tlsotp-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 27.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for tlsotp-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8e63dee9affad05a5cc91f61286739bc6836f0fa2c4869f1054e98c858b564f3
MD5 f3ba91800b0e7c5bbbffc472a1781de0
BLAKE2b-256 e5c734ef27f6bbea3ee0962323431851027330b608f9ce50da7846a1154d3a5e

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