.. _library_otp:

``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 <../../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/5``
- ``otp::totp/5``
- ``otp::hotp_verify/7``
- ``otp::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 ``otp`` library itself still loads on bounded backends
- standard HOTP/TOTP generation with ``sha1``, ``sha256``, or ``sha512``
  is not available there
- calls such as ``otp::hotp(sha1, Secret, Counter, Digits, OTP)`` fail
  with ``domain_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.
