otp
The otp library provides predicates for generating and verifying
HOTP and TOTP one-time passwords following:
https://www.rfc-editor.org/rfc/rfc4226
https://www.rfc-editor.org/rfc/rfc6238
The implementation is built on top of the hmac and base32
libraries.
API documentation
Open the ../../apis/library_index.html#otp link in a web browser.
Loading
To load all entities in this library, load the loader.lgt utility
file:
| ?- logtalk_load(otp(loader)).
Testing
To test this library predicates, load the tester.lgt file:
| ?- logtalk_load(otp(tester)).
Public predicates
The library currently provides the following public predicates:
otp::hotp/5otp::totp/5otp::hotp_verify/7otp::totp_verify/7
The arguments are:
hotp(Hash, Secret, Counter, Digits, OTP)totp(Hash, Secret, UnixTime, Digits, OTP)hotp_verify(Hash, Secret, Counter, Window, Digits, OTP, MatchedCounter)totp_verify(Hash, Secret, UnixTime, Window, Digits, OTP, MatchedTimeStep)
The totp/5 and totp_verify/7 predicates take an explicit Unix
time in seconds. The current implementation uses the standard 30-second
time step and the Unix epoch T0 = 0.
Secrets
The Secret argument can be given either as a raw byte list or using
one of the following Base32 wrapper terms:
base32(atom(Atom))base32(chars(Chars))base32(codes(Codes))
For example:
| ?- otp::hotp(sha1, base32(atom('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ')), 0, 6, OTP).
OTP = '755224'
yes
| ?- atom_codes('12345678901234567890', Secret),
otp::hotp(sha1, Secret, 1, 6, OTP).
OTP = '287082'
Secret = [49,50,51,52,53,54,55,56,57,48,49,50,51,52,53,54,55,56,57,48]
yes
Generation
HOTP generation uses the supplied moving counter directly:
| ?- atom_codes('12345678901234567890', Secret),
otp::hotp(sha1, Secret, 9, 6, OTP).
OTP = '520489'
yes
TOTP generation first converts the explicit Unix time to the corresponding 30-second time step and then computes an HOTP value for that step:
| ?- atom_codes('12345678901234567890', Secret),
otp::totp(sha1, Secret, 59, 8, OTP).
OTP = '94287082'
yes
Generated OTP values are returned as zero-padded atoms so that leading zeros are preserved.
Verification
The otp::hotp_verify/7 predicate searches forward from the supplied
counter through a bounded counter window and returns the matching
counter on success.
| ?- atom_codes('12345678901234567890', Secret),
otp::hotp_verify(sha1, Secret, 0, 9, 6, '520489', MatchedCounter).
MatchedCounter = 9
Secret = [49,50,51,52,53,54,55,56,57,48,49,50,51,52,53,54,55,56,57,48]
yes
The otp::totp_verify/7 predicate searches within a symmetric
time-step window around the current time step and returns the matching
time step on success.
| ?- atom_codes('12345678901234567890', Secret),
otp::totp_verify(sha1, Secret, 29, 1, 8, '94287082', MatchedTimeStep).
MatchedTimeStep = 1
Secret = [49,50,51,52,53,54,55,56,57,48,49,50,51,52,53,54,55,56,57,48]
yes
Hash support
The Hash argument must identify a currently loaded hash object that
provides digest/2, digest_size/1, and block_size/1, and
whose digest size is at least 20 bytes.
In practice, the standard RFC examples in this library use sha1,
sha256, and sha512 on unbounded-integer backends. Hashes such as
md5 are rejected because their digest size is too short for this
library’s validation rules.
Bounded-backend limitation
The current repository hash implementation only defines sha1,
sha256, and sha512 on unbounded-integer backends. On
bounded-integer backends, the hashes library loads hash_32.lgt,
which does not provide those digest objects.
As a consequence:
the
otplibrary itself still loads on bounded backendsstandard HOTP/TOTP generation with
sha1,sha256, orsha512is not available therecalls such as
otp::hotp(sha1, Secret, Counter, Digits, OTP)fail withdomain_error(otp_hash, sha1)on bounded-integer backends
This is a limitation of the currently available hash objects in the repository, not of the OTP control-flow logic itself.