RFC 4226 implementation in Python

24-03-2011 19:51:06 CET Postato in: nerdate, python | Commenta

Straight from RFC 4226, Appendix C:

import hmac
import hashlib
import struct

# Copyright (C) 2011, Matteo Panella. All rights reserved.
#
# This software is licensed under the same terms as the original
# Java reference code in RFC 4226.
#
# This is a work derived from OATH HOTP Algorithm.
#
# The author makes no representations concerning either
# the merchantability of this software or the suitability of this
# software for any particular purpose.
#
# It is provided "as is" without express or implied warranty
# of any kind and THE AUTHOR EXPRESSLY DISCLAIMS ANY
# WARRANTY OR LIABILITY OF ANY KIND relating to this software.
#
# These notices must be retained in any copies of any part of this
# documentation and/or software.

# Copyright notice for original Java code:
# Copyright (C) 2004, OATH.  All rights reserved.
#
# License to copy and use this software is granted provided that it
# is identified as the "OATH HOTP Algorithm" in all material
# mentioning or referencing this software or this function.
#
# License is also granted to make and use derivative works provided
# that such works are identified as
#  "derived from OATH HOTP algorithm"
# in all material mentioning or referencing the derived work.
#
# OATH (Open AuTHentication) and its members make no
# representations concerning either the merchantability of this
# software or the suitability of this software for any particular
# purpose.
#
# It is provided "as is" without express or implied warranty
# of any kind and OATH AND ITS MEMBERS EXPRESSaLY DISCLAIMS
# ANY WARRANTY OR LIABILITY OF ANY KIND relating to this software.
#
# These notices must be retained in any copies of any part of this
# documentation and/or software.

__all__ = ['hotp']

# Checksum algorithm defined in RFC4226
_doubleDigits = (0, 2, 4, 6, 8, 1, 3, 5, 7, 9)
def _calcChecksum(num, digits):
    doubleDigit = True
    total = 0
    while digits > 0:
        digits -= 1
        digit = num % 10
        num /= 10
        if doubleDigit:
            digit = _doubleDigits[digit]
        total += digit
        doubleDigit = not doubleDigit
    result = total % 10
    if result > 0:
        result = 10 - result
    return result

def hotp(secret, movingFactor, codeDigits=6, addChecksum=False, truncationOffset=None):
    """
    Perform RFC4226 HOTP generation from given secret (shared secret) and movingFactor.

    secret: the shared secret
    movingFactor: a counter, current time or other value that changes on a per-use basis (64 bit integer)
    codeDigits: number of digits in the OTP, not including the checksum (if any)
    addChecksum: True if a checksum digit should be appended to the OTP, False otherwise (default: False)
    truncationOffset: the offset into the MAC output to begin truncation. If this value is out of the
                      range 0 .. 15 or is None, then dynamic truncation will be used.
    Returns a numeric string in base 10 (the OTP).

    Test vectors (RFC4226, Appendix D):
    >>> hotp("12345678901234567890", 0)
    '755224'
    >>> hotp("12345678901234567890", 1)
    '287082'
    >>> hotp("12345678901234567890", 2)
    '359152'
    >>> hotp("12345678901234567890", 3)
    '969429'
    >>> hotp("12345678901234567890", 4)
    '338314'
    >>> hotp("12345678901234567890", 5)
    '254676'
    >>> hotp("12345678901234567890", 6)
    '287922'
    >>> hotp("12345678901234567890", 7)
    '162583'
    >>> hotp("12345678901234567890", 8)
    '399871'
    >>> hotp("12345678901234567890", 9)
    '520489'
    """
    digits = codeDigits + 1 if addChecksum else codeDigits
    movingFactor = struct.pack('!q', movingFactor)
    hs = hmac.new(secret, movingFactor, hashlib.sha1).digest()
    if truncationOffset is None or truncationOffset < 0 or truncationOffset > 15:
        # Perform dynamic truncation (per RFC4226)
        # The offset is taken from the lowest 4 bits of hs[19]
        truncationOffset = ord(hs[19]) & 0xf

    # Starting from the offset, 4 bytes are extracted and converted to an
    # unsigned 32 bit integer (big endian) and then masked with 7fffffff
    bin_code = struct.unpack('!I', hs[truncationOffset:truncationOffset+4])[0] & 0x7fffffff
    # OTP is the value modulo 10^codeDigits
    otp = bin_code % 10**codeDigits
    if addChecksum:
        otp = (otp * 10) + _calcChecksum(otp, codeDigits)
    result = '%d' % (otp,)
    return '0' * (len(result) - digits) + result

Commenti

blog comments powered by Disqus