Time + Location + String One-Time-Password
_________ _ _______ _______ _________ _______
\__ __/( \ ( ____ \( ___ )\__ __/( ____ )
) ( | ( | ( \/| ( ) | ) ( | ( )|
| | | | | (_____ | | | | | | | (____)|
| | | | (_____ )| | | | | | | _____)
| | | | ) || | | | | | | (
| | | (____/Y\____) || (___) | | | | )
)_( (_______|_______)(_______) )_( |/
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.
| 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 |
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__)"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.
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.1Version 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
legacymodule reproduces the algorithms shipped in thev0.1.0andv0.2.0releases — 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 requireiso3_code, and positive location precision is not capped at 6 decimals as in v1.0.
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")# 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)# Full passphrase in the seed
otp = get_OTP(main_key=key, password_str_mode=-1, password_string="hunter2")from tlsotp import verify_otp
verify_otp(main_key=key, user_otp=otp, drift_windows=1) # checks -1, 0, +1 windows| 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) |
| 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=500and7xx=700yield an empty string (step 0).4xx=400/6xx=600are valid and round to 0 decimals.
| Mode | Behaviour | Example |
|---|---|---|
0 |
Disabled | (omitted) |
> 0 |
First N chars |
pwd="DelhiSecure", mode=4 → -Delh- |
< 0 |
Full password | pwd="MyPass" → -MyPass- |
| 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.
| Parameter | Valid range | Default |
|---|---|---|
n_chars |
1–128 (validated by get_data_dict/get_OTP_uri) |
6 |
time_binning |
positive integer (seconds) | 30 |
latitude |
-90…90 |
None |
longitude |
-180…180 |
None |
| Function | Signature | Description |
|---|---|---|
key_gen |
key_gen(length=32) -> str |
Cryptographically secure URL‑safe key (secrets.token_urlsafe); output ≈ 4/3 × length chars |
| 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) |
| 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 |
| 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) |
| 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) |
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 |
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 |
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 neededpip install -e .[dev]
pytest255+ 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).
| 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 |
Licensed under the Apache License 2.0 — see LICENSE.
Found a bug or want a feature? Open an issue
or submit a PR. Documentation
lives in docs/ and is built with MkDocs.