Skip to content

Repository files navigation

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.

About

Zero-dependency Python library for context-aware one-time passwords — bind OTPs to time, GPS location & optional passphrase, with drift tolerance, versioned algorithms, and TLSOTP:// URIs. A flexible, non-RFC alternative to TOTP.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages