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.
Metadata
Release files for tlsotp 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tlsotp-1.0.0.tar.gz | 67.8 kB | Without provenance | Details for tlsotp-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tlsotp-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for tlsotp-1.0.0-py3-none-any.whl |
Total release size: 95.5 kB
Release files / tlsotp-1.0.0.tar.gz
| Download URL | tlsotp-1.0.0.tar.gz |
|---|---|
| Size | 67.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
761904965e95d1ea28cbfd27c84822b29866e0cf02d6960d8640ada59bd8d7d4
|
|
BLAKE2b-256 checksum How to use checksums |
c9e29571cd94cbc29af0aa93685fe57da7281fcd804f69c229d490ad394e6682
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Release files / tlsotp-1.0.0-py3-none-any.whl
| Download URL | tlsotp-1.0.0-py3-none-any.whl |
|---|---|
| Size | 27.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8e63dee9affad05a5cc91f61286739bc6836f0fa2c4869f1054e98c858b564f3
|
|
BLAKE2b-256 checksum How to use checksums |
e5c734ef27f6bbea3ee0962323431851027330b608f9ce50da7846a1154d3a5e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Security for tlsotp 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.