# -*- coding: utf-8 -*-
#
# Copyright 2013, 2014 Mark Lee
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Base API for handling one-time passwords.
There are two API types: simple and advanced. The simple API (:class:`HOTP`
and :class:`TOTP`) is based on the two-factor authentication API in the
`Cryptography library`_. The advanced API (:class:`OATH`) is based on the
functional API in OATH Toolkit's liboath_.
When compared to the :class:`HOTP`/:class:`TOTP` classes:
* :class:`OATH` has a more customizable set of parameters.
* :class:`OATH` is more likely to add parameters to its method as OATH Toolkit
gains APIs.
.. _Cryptography library: https://cryptography.io/
.. _liboath: http://oath-toolkit.nongnu.org/liboath-api/liboath-oath.html
"""
from abc import ABCMeta
import base64
import hashlib
import os
if not os.environ.get('READTHEDOCS') and not os.environ.get('SETUP_PY'):
try: # pragma: no cover
from . import impl_cython as oath
except ImportError: # pragma: no cover
from . import impl_cffi as oath
from .exc import OATHError
from .metadata import DESCRIPTION, VERSION
__description__ = DESCRIPTION
__version__ = VERSION
OTP_ALGORITHMS = (
hashlib.sha1,
# hashlib.sha256,
# hashlib.sha512,
)
class OTP(object):
"""Base class for one-time password (OTP) implementations."""
__metaclass__ = ABCMeta
__slots__ = ['_algorithm']
def __init__(self, key, length, algorithm=None):
if not algorithm:
algorithm = hashlib.sha1
self.algorithm = algorithm
self.key = key
self.length = length
@property
def algorithm(self):
"""
The hash algorithm used during OTP generation.
Not currently implemented (requires liboath >= 2.6.0).
"""
return self._algorithm
@algorithm.setter
def algorithm(self, value):
if value not in OTP_ALGORITHMS:
raise ValueError('Unrecognized OTP algorithm')
self._algorithm = value
[docs]class HOTP(OTP):
"""
HMAC-based one-time password (HOTP) convenience implementation.
API based on :class:`cryptography.hazmat.primitives.twofactor.hotp.HOTP`.
:param bytes key: The secret key.
:param int length: The length of generated one-time passwords.
:param algorithm: The hash algorithm used during OTP generation. Not
currently implemented (requires liboath >= 2.6.0).
Defaults to HMAC-SHA1.
"""
__slots__ = ['key', 'length', '_algorithm']
def __init__(self, key, length, algorithm=None):
super(HOTP, self).__init__(key, length, algorithm)
[docs] def generate(self, counter):
"""
Generate an OTP at the specified offset in the OTP stream.
:param counter: The start counter in the OTP stream.
:type counter: :func:`int` or :func:`long`
:rtype: :func:`bytes`
"""
return oath.hotp_generate(self.key, counter, self.length, False, -1)
[docs] def verify(self, hotp, counter, window=0):
"""
Verify that the given one-time password is within the range of
generated OTPs, given ``counter`` and ``window``.
:param bytes hotp: The OTP to verify.
:param counter: The start counter in the OTP stream.
:type counter: :func:`int` or :func:`long`
:param int window: The number of OTPs after the start counter to test.
:return: The position in the OTP window, where ``0`` is the first
position.
:rtype: :func:`int`
:raise: :class:`OATHError` if invalid
"""
return oath.hotp_validate(self.key, counter, window, hotp)
[docs]class TOTP(OTP):
"""
Time-based one-time password (TOTP) convenience implementation.
API based on :class:`cryptography.hazmat.primitives.twofactor.totp.TOTP`.
:param bytes key: The secret key.
:param int length: The length of generated one-time passwords.
:param int time_step: The time step size, which is essentially the
lifetime of a given OTP, in seconds. To be clear,
this does not mean that the start of the lifetime is
the ``time`` value given to a method of this object.
It is recommended to set this value to ``30``.
:param algorithm: The hash algorithm used during OTP generation. Not
currently implemented (requires liboath >= 2.6.0).
Defaults to HMAC-SHA1.
"""
__slots__ = ['key', 'length', '_algorithm', 'time_step']
def __init__(self, key, length, time_step, algorithm=None):
super(TOTP, self).__init__(key, length, algorithm)
self.time_step = time_step
[docs] def generate(self, time):
"""
Generate an OTP for the given time value.
:param time: The UNIX timestamp-encoded time value.
:type time: :func:`int` or :func:`long`
:rtype: :func:`bytes`
"""
return oath.totp_generate(self.key, time, self.time_step, 0,
self.length)
[docs] def verify(self, totp, time, window=0):
"""
Verify that the given one-time password is within the range of
generated OTPs, given ``counter`` and ``window``.
:param bytes totp: The OTP to verify.
:param time: The UNIX timestamp-encoded time value.
:type time: :func:`int` or :func:`long`
:param int window: The number of OTPs before and after the start OTP
to test.
:return: The position in the OTP window, where ``0`` is the first
position.
:rtype: :func:`int`
:raise: :class:`OATHError` if invalid
"""
return oath.totp_validate(self.key, time, self.time_step, 0, window,
totp)
[docs]class OATH(object):
"""
A convenience class that is a direct port of the OATH Toolkit API.
"""
__slots__ = []
@property
def library_version(self):
"""
The version of liboath being used.
:rtype: :func:`bytes`
"""
return oath.library_version
[docs] def check_library_version(self, version):
"""
Determine whether the library version is greater than or equal to the
specified version.
:param bytes version: The dotted version number to check
:rtype: :func:`bool`
"""
return oath.check_library_version(version)
@staticmethod
def _chunk_iterable(iterable, n, fillvalue=None):
"""
Collect data into fixed-length chunks or blocks.
>>> list(OATH._chunk_iterable('ABCDEFG', 3, 'x'))
['ABC', 'DEF', 'Gxx']
Copied from the Python documentation in the itertools module.
"""
from ._compat import zip_longest
args = [iter(iterable)] * n
return zip_longest(fillvalue=fillvalue, *args)
[docs] def base32_encode(self, data, human_readable=False):
"""
Base32-encode data.
:param data: The data to be encoded. Must be castable into a
:func:`bytes` object.
:param bool human_readable: If :data:`True`, transforms the Base32
string into space-separated chunks of 4
characters, removing trailing ``=``.
:rtype: bytes
"""
from ._compat import bytify, to_bytes
if not data:
return b''
encoded = base64.b32encode(to_bytes(data))
if human_readable:
chunked_iterable = self._chunk_iterable(encoded, 4)
encoded = b' '.join([bytify(chunk)
for chunk in chunked_iterable])
encoded = encoded.rstrip(b'=')
return encoded
def _py_base32_decode(self, data):
data = data.replace(b' ', b'').upper()
if len(data) % 8 != 0:
data = data.ljust((int(len(data) / 8) + 1) * 8, b'=')
return base64.b32decode(data)
[docs] def base32_decode(self, data):
"""
Decode Base32 data. Unlike :func:`base64.b32decode`, it handles
human-readable Base32 strings.
:param bytes data: The data to be decoded.
:rtype: bytes
"""
if not data:
raise OATHError('Invalid base32 string')
elif not (data.isupper() or data.islower()):
raise OATHError(
'Base32 string cannot be both upper- and lowercased')
if self.check_library_version(b'2.0.0'): # pragma: no cover
return oath.base32_decode(data)
else: # pragma: no cover
return self._py_base32_decode(data)
[docs] def hotp_generate(self, secret, moving_factor, digits, add_checksum=False,
truncation_offset=-1):
"""
Generate a one-time password using the HOTP algorithm (:rfc:`4226`).
:param bytes secret: The secret string used to generate the one-time
password.
:param int moving_factor: unsigned, can be :func:`long`, in theory. A
counter indicating where in OTP stream to
generate an OTP.
:param int digits: unsigned, the number of digits of the one-time
password.
:param bool add_checksum: Whether to add a checksum digit (depending
on the version of ``liboath`` used, this may
be ignored).
:param int truncation_offset: A truncation offset to use, if not set to
a negative value (which means
``2^32 - 1``).
:return: one-time password
:rtype: :func:`bytes`
"""
return oath.hotp_generate(secret, moving_factor, digits, add_checksum,
truncation_offset)
[docs] def hotp_validate(self, secret, start_moving_factor, window, otp):
"""
Validate a one-time password generated using the HOTP algorithm
(:rfc:`4226`).
:param bytes secret: The secret used to generate the one-time password.
:param int start_moving_factor: Unsigned, can be :func:`long`, in
theory. The start counter in the
OTP stream.
:param int window: The number of OTPs after the start offset OTP
to test.
:param bytes otp: The one-time password to validate.
:return: The position in the OTP window, where ``0`` is the first
position.
:rtype: int
:raise: :class:`OATHError` if invalid
"""
return oath.hotp_validate(secret, start_moving_factor, window, otp)
[docs] def totp_generate(self, secret, now, time_step_size, time_offset, digits):
"""
Generate a one-time password using the TOTP algorithm (:rfc:`6238`).
:param bytes secret: The secret string used to generate the one-time
password.
:param int now: The UNIX timestamp (usually the current one)
:param time_step_size: Unsigned, the time step system parameter. If
set to :data:`None`, defaults to ``30``.
:type time_step_size: :func:`int` or :data:`None`
:param int time_offset: The UNIX timestamp of when to start counting
time steps (usually should be ``0``).
:param int digits: The number of digits of the one-time password.
:return: one-time password
:rtype: :func:`bytes`
"""
return oath.totp_generate(secret, now, time_step_size, time_offset,
digits)
[docs] def totp_validate(self, secret, now, time_step_size, start_offset, window,
otp):
"""
Validate a one-time password generated using the TOTP algorithm
(:rfc:`6238`).
:param bytes secret: The secret used to generate the one-time password.
:param int now: The UNIX timestamp (usually the current one)
:param time_step_size: Unsigned, the time step system parameter. If
set to :data:`None`, defaults to ``30``.
:type time_step_size: :func:`int` or :data:`None`
:param int start_offset: The UNIX timestamp of when to start counting
time steps (usually should be ``0``).
:param int window: The number of OTPs before and after the start OTP
to test.
:param bytes otp: The one-time password to validate.
:return: The absolute and relative positions in the OTP window, where
``0`` is the first position.
:rtype: :class:`oath_toolkit.types.OTPPosition`
:raise: :class:`OATHError` if invalid
"""
return oath.totp_validate(secret, now, time_step_size, start_offset,
window, otp)