TLSOTP
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.
✨ 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
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.
🚀 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=500and7xx=700yield an empty string (step 0).4xx=400/6xx=600are 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 |
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 |
📚 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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
761904965e95d1ea28cbfd27c84822b29866e0cf02d6960d8640ada59bd8d7d4
|
|
| MD5 |
470c20e728af500d6f181d9d2d90a587
|
|
| BLAKE2b-256 |
c9e29571cd94cbc29af0aa93685fe57da7281fcd804f69c229d490ad394e6682
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e63dee9affad05a5cc91f61286739bc6836f0fa2c4869f1054e98c858b564f3
|
|
| MD5 |
f3ba91800b0e7c5bbbffc472a1781de0
|
|
| BLAKE2b-256 |
e5c734ef27f6bbea3ee0962323431851027330b608f9ce50da7846a1154d3a5e
|