You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1909 lines
71 KiB

  1. """passlib.totp -- TOTP / RFC6238 / Google Authenticator utilities."""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import absolute_import, division, print_function
  6. from passlib.utils.compat import PY3
  7. # core
  8. import base64
  9. import calendar
  10. import json
  11. import logging; log = logging.getLogger(__name__)
  12. import math
  13. import struct
  14. import sys
  15. import time as _time
  16. import re
  17. if PY3:
  18. from urllib.parse import urlparse, parse_qsl, quote, unquote
  19. else:
  20. from urllib import quote, unquote
  21. from urlparse import urlparse, parse_qsl
  22. from warnings import warn
  23. # site
  24. try:
  25. # TOTP encrypted keys only supported if cryptography (https://cryptography.io) is installed
  26. from cryptography.hazmat.backends import default_backend as _cg_default_backend
  27. import cryptography.hazmat.primitives.ciphers.algorithms
  28. import cryptography.hazmat.primitives.ciphers.modes
  29. from cryptography.hazmat.primitives import ciphers as _cg_ciphers
  30. del cryptography
  31. except ImportError:
  32. log.debug("can't import 'cryptography' package, totp encryption disabled")
  33. _cg_ciphers = _cg_default_backend = None
  34. # pkg
  35. from passlib import exc
  36. from passlib.exc import TokenError, MalformedTokenError, InvalidTokenError, UsedTokenError
  37. from passlib.utils import (to_unicode, to_bytes, consteq,
  38. getrandbytes, rng, SequenceMixin, xor_bytes, getrandstr)
  39. from passlib.utils.binary import BASE64_CHARS, b32encode, b32decode
  40. from passlib.utils.compat import (u, unicode, native_string_types, bascii_to_str, int_types, num_types,
  41. irange, byte_elem_value, UnicodeIO, suppress_cause)
  42. from passlib.utils.decor import hybrid_method, memoized_property
  43. from passlib.crypto.digest import lookup_hash, compile_hmac, pbkdf2_hmac
  44. from passlib.hash import pbkdf2_sha256
  45. # local
  46. __all__ = [
  47. # frontend classes
  48. "AppWallet",
  49. "TOTP",
  50. # errors (defined in passlib.exc, but exposed here for convenience)
  51. "TokenError",
  52. "MalformedTokenError",
  53. "InvalidTokenError",
  54. "UsedTokenError",
  55. # internal helper classes
  56. "TotpToken",
  57. "TotpMatch",
  58. ]
  59. #=============================================================================
  60. # HACK: python < 2.7.4's urlparse() won't parse query strings unless the url scheme
  61. # is one of the schemes in the urlparse.uses_query list. 2.7 abandoned
  62. # this, and parses query if present, regardless of the scheme.
  63. # as a workaround for older versions, we add "otpauth" to the known list.
  64. # this was fixed by https://bugs.python.org/issue9374, in 2.7.4 release.
  65. #=============================================================================
  66. if sys.version_info < (2,7,4):
  67. from urlparse import uses_query
  68. if "otpauth" not in uses_query:
  69. uses_query.append("otpauth")
  70. log.debug("registered 'otpauth' scheme with urlparse.uses_query")
  71. del uses_query
  72. #=============================================================================
  73. # internal helpers
  74. #=============================================================================
  75. #-----------------------------------------------------------------------------
  76. # token parsing / rendering helpers
  77. #-----------------------------------------------------------------------------
  78. #: regex used to clean whitespace from tokens & keys
  79. _clean_re = re.compile(u(r"\s|[-=]"), re.U)
  80. _chunk_sizes = [4,6,5]
  81. def _get_group_size(klen):
  82. """
  83. helper for group_string() --
  84. calculates optimal size of group for given string size.
  85. """
  86. # look for exact divisor
  87. for size in _chunk_sizes:
  88. if not klen % size:
  89. return size
  90. # fallback to divisor with largest remainder
  91. # (so chunks are as close to even as possible)
  92. best = _chunk_sizes[0]
  93. rem = 0
  94. for size in _chunk_sizes:
  95. if klen % size > rem:
  96. best = size
  97. rem = klen % size
  98. return best
  99. def group_string(value, sep="-"):
  100. """
  101. reformat string into (roughly) evenly-sized groups, separated by **sep**.
  102. useful for making tokens & keys easier to read by humans.
  103. """
  104. klen = len(value)
  105. size = _get_group_size(klen)
  106. return sep.join(value[o:o+size] for o in irange(0, klen, size))
  107. #-----------------------------------------------------------------------------
  108. # encoding helpers
  109. #-----------------------------------------------------------------------------
  110. def _decode_bytes(key, format):
  111. """
  112. internal TOTP() helper --
  113. decodes key according to specified format.
  114. """
  115. if format == "raw":
  116. if not isinstance(key, bytes):
  117. raise exc.ExpectedTypeError(key, "bytes", "key")
  118. return key
  119. # for encoded data, key must be either unicode or ascii-encoded bytes,
  120. # and must contain a hex or base32 string.
  121. key = to_unicode(key, param="key")
  122. key = _clean_re.sub("", key).encode("utf-8") # strip whitespace & hypens
  123. if format == "hex" or format == "base16":
  124. return base64.b16decode(key.upper())
  125. elif format == "base32":
  126. return b32decode(key)
  127. # XXX: add base64 support?
  128. else:
  129. raise ValueError("unknown byte-encoding format: %r" % (format,))
  130. #=============================================================================
  131. # OTP management
  132. #=============================================================================
  133. #: flag for detecting if encrypted totp support is present
  134. AES_SUPPORT = bool(_cg_ciphers)
  135. #: regex for validating secret tags
  136. _tag_re = re.compile("(?i)^[a-z0-9][a-z0-9_.-]*$")
  137. class AppWallet(object):
  138. """
  139. This class stores application-wide secrets that can be used
  140. to encrypt & decrypt TOTP keys for storage.
  141. It's mostly an internal detail, applications usually just need
  142. to pass ``secrets`` or ``secrets_path`` to :meth:`TOTP.using`.
  143. .. seealso::
  144. :ref:`totp-storing-instances` for more details on this workflow.
  145. Arguments
  146. =========
  147. :param secrets:
  148. Dict of application secrets to use when encrypting/decrypting
  149. stored TOTP keys. This should include a secret to use when encrypting
  150. new keys, but may contain additional older secrets to decrypt
  151. existing stored keys.
  152. The dict should map tags -> secrets, so that each secret is identified
  153. by a unique tag. This tag will be stored along with the encrypted
  154. key in order to determine which secret should be used for decryption.
  155. Tag should be string that starts with regex range ``[a-z0-9]``,
  156. and the remaining characters must be in ``[a-z0-9_.-]``.
  157. It is recommended to use something like a incremental counter
  158. ("1", "2", ...), an ISO date ("2016-01-01", "2016-05-16", ...),
  159. or a timestamp ("19803495", "19813495", ...) when assigning tags.
  160. This mapping be provided in three formats:
  161. * A python dict mapping tag -> secret
  162. * A JSON-formatted string containing the dict
  163. * A multiline string with the format ``"tag: value\\ntag: value\\n..."``
  164. (This last format is mainly useful when loading from a text file via **secrets_path**)
  165. .. seealso:: :func:`generate_secret` to create a secret with sufficient entropy
  166. :param secrets_path:
  167. Alternately, callers can specify a separate file where the
  168. application-wide secrets are stored, using either of the string
  169. formats described in **secrets**.
  170. :param default_tag:
  171. Specifies which tag in **secrets** should be used as the default
  172. for encrypting new keys. If omitted, the tags will be sorted,
  173. and the largest tag used as the default.
  174. if all tags are numeric, they will be sorted numerically;
  175. otherwise they will be sorted alphabetically.
  176. this permits tags to be assigned numerically,
  177. or e.g. using ``YYYY-MM-DD`` dates.
  178. :param encrypt_cost:
  179. Optional time-cost factor for key encryption.
  180. This value corresponds to log2() of the number of PBKDF2
  181. rounds used.
  182. .. warning::
  183. The application secret(s) should be stored in a secure location by
  184. your application, and each secret should contain a large amount
  185. of entropy (to prevent brute-force attacks if the encrypted keys
  186. are leaked).
  187. :func:`generate_secret` is provided as a convenience helper
  188. to generate a new application secret of suitable size.
  189. Best practice is to load these values from a file via **secrets_path**,
  190. and then have your application give up permission to read this file
  191. once it's running.
  192. Public Methods
  193. ==============
  194. .. autoattribute:: has_secrets
  195. .. autoattribute:: default_tag
  196. Semi-Private Methods
  197. ====================
  198. The following methods are used internally by the :class:`TOTP`
  199. class in order to encrypt & decrypt keys using the provided application
  200. secrets. They will generally not be publically useful, and may have their
  201. API changed periodically.
  202. .. automethod:: get_secret
  203. .. automethod:: encrypt_key
  204. .. automethod:: decrypt_key
  205. """
  206. #========================================================================
  207. # instance attrs
  208. #========================================================================
  209. #: default salt size for encrypt_key() output
  210. salt_size = 12
  211. #: default cost (log2 of pbkdf2 rounds) for encrypt_key() output
  212. #: NOTE: this is relatively low, since the majority of the security
  213. #: relies on a high entropy secret to pass to AES.
  214. encrypt_cost = 14
  215. #: map of secret tag -> secret bytes
  216. _secrets = None
  217. #: tag for default secret
  218. default_tag = None
  219. #========================================================================
  220. # init
  221. #========================================================================
  222. def __init__(self, secrets=None, default_tag=None, encrypt_cost=None,
  223. secrets_path=None):
  224. # TODO: allow a lot more things to be customized from here,
  225. # e.g. setting default TOTP constructor options.
  226. #
  227. # init cost
  228. #
  229. if encrypt_cost is not None:
  230. if isinstance(encrypt_cost, native_string_types):
  231. encrypt_cost = int(encrypt_cost)
  232. assert encrypt_cost >= 0
  233. self.encrypt_cost = encrypt_cost
  234. #
  235. # init secrets map
  236. #
  237. # load secrets from file (if needed)
  238. if secrets_path is not None:
  239. if secrets is not None:
  240. raise TypeError("'secrets' and 'secrets_path' are mutually exclusive")
  241. secrets = open(secrets_path, "rt").read()
  242. # parse & store secrets
  243. secrets = self._secrets = self._parse_secrets(secrets)
  244. #
  245. # init default tag/secret
  246. #
  247. if secrets:
  248. if default_tag is not None:
  249. # verify that tag is present in map
  250. self.get_secret(default_tag)
  251. elif all(tag.isdigit() for tag in secrets):
  252. default_tag = max(secrets, key=int)
  253. else:
  254. default_tag = max(secrets)
  255. self.default_tag = default_tag
  256. def _parse_secrets(self, source):
  257. """
  258. parse 'secrets' parameter
  259. :returns:
  260. Dict[tag:str, secret:bytes]
  261. """
  262. # parse string formats
  263. # to make this easy to pass in configuration from a separate file,
  264. # 'secrets' can be string using two formats -- json & "tag:value\n"
  265. check_type = True
  266. if isinstance(source, native_string_types):
  267. if source.lstrip().startswith(("[", "{")):
  268. # json list / dict
  269. source = json.loads(source)
  270. elif "\n" in source and ":" in source:
  271. # multiline string containing series of "tag: value\n" rows;
  272. # empty and "#\n" rows are ignored
  273. def iter_pairs(source):
  274. for line in source.splitlines():
  275. line = line.strip()
  276. if line and not line.startswith("#"):
  277. tag, secret = line.split(":", 1)
  278. yield tag.strip(), secret.strip()
  279. source = iter_pairs(source)
  280. check_type = False
  281. else:
  282. raise ValueError("unrecognized secrets string format")
  283. # ensure we have iterable of (tag, value) pairs
  284. if source is None:
  285. return {}
  286. elif isinstance(source, dict):
  287. source = source.items()
  288. # XXX: could support iterable of (tag,value) pairs, but not yet needed...
  289. # elif check_type and (isinstance(source, str) or not isinstance(source, Iterable)):
  290. elif check_type:
  291. raise TypeError("'secrets' must be mapping, or list of items")
  292. # parse into final dict, normalizing contents
  293. return dict(self._parse_secret_pair(tag, value)
  294. for tag, value in source)
  295. def _parse_secret_pair(self, tag, value):
  296. if isinstance(tag, native_string_types):
  297. pass
  298. elif isinstance(tag, int):
  299. tag = str(tag)
  300. else:
  301. raise TypeError("tag must be unicode/string: %r" % (tag,))
  302. if not _tag_re.match(tag):
  303. raise ValueError("tag contains invalid characters: %r" % (tag,))
  304. if not isinstance(value, bytes):
  305. value = to_bytes(value, param="secret %r" % (tag,))
  306. if not value:
  307. raise ValueError("tag contains empty secret: %r" % (tag,))
  308. return tag, value
  309. #========================================================================
  310. # accessing secrets
  311. #========================================================================
  312. @property
  313. def has_secrets(self):
  314. """whether at least one application secret is present"""
  315. return self.default_tag is not None
  316. def get_secret(self, tag):
  317. """
  318. resolve a secret tag to the secret (as bytes).
  319. throws a KeyError if not found.
  320. """
  321. secrets = self._secrets
  322. if not secrets:
  323. raise KeyError("no application secrets configured")
  324. try:
  325. return secrets[tag]
  326. except KeyError:
  327. raise suppress_cause(KeyError("unknown secret tag: %r" % (tag,)))
  328. #========================================================================
  329. # encrypted key helpers -- used internally by TOTP
  330. #========================================================================
  331. @staticmethod
  332. def _cipher_aes_key(value, secret, salt, cost, decrypt=False):
  333. """
  334. Internal helper for :meth:`encrypt_key` --
  335. handles lowlevel encryption/decryption.
  336. Algorithm details:
  337. This function uses PBKDF2-HMAC-SHA256 to generate a 32-byte AES key
  338. and a 16-byte IV from the application secret & random salt.
  339. It then uses AES-256-CTR to encrypt/decrypt the TOTP key.
  340. CTR mode was chosen over CBC because the main attack scenario here
  341. is that the attacker has stolen the database, and is trying to decrypt a TOTP key
  342. (the plaintext value here). To make it hard for them, we want every password
  343. to decrypt to a potentially valid key -- thus need to avoid any authentication
  344. or padding oracle attacks. While some random padding construction could be devised
  345. to make this work for CBC mode, a stream cipher mode is just plain simpler.
  346. OFB/CFB modes would also work here, but seeing as they have malleability
  347. and cyclic issues (though remote and barely relevant here),
  348. CTR was picked as the best overall choice.
  349. """
  350. # make sure backend AES support is available
  351. if _cg_ciphers is None:
  352. raise RuntimeError("TOTP encryption requires 'cryptography' package "
  353. "(https://cryptography.io)")
  354. # use pbkdf2 to derive both key (32 bytes) & iv (16 bytes)
  355. # NOTE: this requires 2 sha256 blocks to be calculated.
  356. keyiv = pbkdf2_hmac("sha256", secret, salt=salt, rounds=(1 << cost), keylen=48)
  357. # use AES-256-CTR to encrypt/decrypt input value
  358. cipher = _cg_ciphers.Cipher(_cg_ciphers.algorithms.AES(keyiv[:32]),
  359. _cg_ciphers.modes.CTR(keyiv[32:]),
  360. _cg_default_backend())
  361. ctx = cipher.decryptor() if decrypt else cipher.encryptor()
  362. return ctx.update(value) + ctx.finalize()
  363. def encrypt_key(self, key):
  364. """
  365. Helper used to encrypt TOTP keys for storage.
  366. :param key:
  367. TOTP key to encrypt, as raw bytes.
  368. :returns:
  369. dict containing encrypted TOTP key & configuration parameters.
  370. this format should be treated as opaque, and potentially subject
  371. to change, though it is designed to be easily serialized/deserialized
  372. (e.g. via JSON).
  373. .. note::
  374. This function requires installation of the external
  375. `cryptography <https://cryptography.io>`_ package.
  376. To give some algorithm details: This function uses AES-256-CTR to encrypt
  377. the provided data. It takes the application secret and randomly generated salt,
  378. and uses PBKDF2-HMAC-SHA256 to combine them and generate the AES key & IV.
  379. """
  380. if not key:
  381. raise ValueError("no key provided")
  382. salt = getrandbytes(rng, self.salt_size)
  383. cost = self.encrypt_cost
  384. tag = self.default_tag
  385. if not tag:
  386. raise TypeError("no application secrets configured, can't encrypt OTP key")
  387. ckey = self._cipher_aes_key(key, self.get_secret(tag), salt, cost)
  388. # XXX: switch to base64?
  389. return dict(v=1, c=cost, t=tag, s=b32encode(salt), k=b32encode(ckey))
  390. def decrypt_key(self, enckey):
  391. """
  392. Helper used to decrypt TOTP keys from storage format.
  393. Consults configured secrets to decrypt key.
  394. :param source:
  395. source object, as returned by :meth:`encrypt_key`.
  396. :returns:
  397. ``(key, needs_recrypt)`` --
  398. **key** will be the decrypted key, as bytes.
  399. **needs_recrypt** will be a boolean flag indicating
  400. whether encryption cost or default tag is too old,
  401. and henace that key needs re-encrypting before storing.
  402. .. note::
  403. This function requires installation of the external
  404. `cryptography <https://cryptography.io>`_ package.
  405. """
  406. if not isinstance(enckey, dict):
  407. raise TypeError("'enckey' must be dictionary")
  408. version = enckey.get("v", None)
  409. needs_recrypt = False
  410. if version == 1:
  411. _cipher_key = self._cipher_aes_key
  412. else:
  413. raise ValueError("missing / unrecognized 'enckey' version: %r" % (version,))
  414. tag = enckey['t']
  415. cost = enckey['c']
  416. key = _cipher_key(
  417. value=b32decode(enckey['k']),
  418. secret=self.get_secret(tag),
  419. salt=b32decode(enckey['s']),
  420. cost=cost,
  421. )
  422. if cost != self.encrypt_cost or tag != self.default_tag:
  423. needs_recrypt = True
  424. return key, needs_recrypt
  425. #=============================================================================
  426. # eoc
  427. #=============================================================================
  428. #=============================================================================
  429. # TOTP class
  430. #=============================================================================
  431. #: helper to convert HOTP counter to bytes
  432. _pack_uint64 = struct.Struct(">Q").pack
  433. #: helper to extract value from HOTP digest
  434. _unpack_uint32 = struct.Struct(">I").unpack
  435. #: dummy bytes used as temp key for .using() method
  436. _DUMMY_KEY = b"\x00" * 16
  437. class TOTP(object):
  438. """
  439. Helper for generating and verifying TOTP codes.
  440. Given a secret key and set of configuration options, this object
  441. offers methods for token generation, token validation, and serialization.
  442. It can also be used to track important persistent TOTP state,
  443. such as the last counter used.
  444. This class accepts the following options
  445. (only **key** and **format** may be specified as positional arguments).
  446. :arg str key:
  447. The secret key to use. By default, should be encoded as
  448. a base32 string (see **format** for other encodings).
  449. Exactly one of **key** or ``new=True`` must be specified.
  450. :arg str format:
  451. The encoding used by the **key** parameter. May be one of:
  452. ``"base32"`` (base32-encoded string),
  453. ``"hex"`` (hexadecimal string), or ``"raw"`` (raw bytes).
  454. Defaults to ``"base32"``.
  455. :param bool new:
  456. If ``True``, a new key will be generated using :class:`random.SystemRandom`.
  457. Exactly one ``new=True`` or **key** must be specified.
  458. :param str label:
  459. Label to associate with this token when generating a URI.
  460. Displayed to user by most OTP client applications (e.g. Google Authenticator),
  461. and typically has format such as ``"John Smith"`` or ``"jsmith@webservice.example.org"``.
  462. Defaults to ``None``.
  463. See :meth:`to_uri` for details.
  464. :param str issuer:
  465. String identifying the token issuer (e.g. the domain name of your service).
  466. Used internally by some OTP client applications (e.g. Google Authenticator) to distinguish entries
  467. which otherwise have the same label.
  468. Optional but strongly recommended if you're rendering to a URI.
  469. Defaults to ``None``.
  470. See :meth:`to_uri` for details.
  471. :param int size:
  472. Number of bytes when generating new keys. Defaults to size of hash algorithm (e.g. 20 for SHA1).
  473. .. warning::
  474. Overriding the default values for ``digits``, ``period``, or ``alg`` may
  475. cause problems with some OTP client programs (such as Google Authenticator),
  476. which may have these defaults hardcoded.
  477. :param int digits:
  478. The number of digits in the generated / accepted tokens. Defaults to ``6``.
  479. Must be in range [6 .. 10].
  480. .. rst-class:: inline-title
  481. .. caution::
  482. Due to a limitation of the HOTP algorithm, the 10th digit can only take on values 0 .. 2,
  483. and thus offers very little extra security.
  484. :param str alg:
  485. Name of hash algorithm to use. Defaults to ``"sha1"``.
  486. ``"sha256"`` and ``"sha512"`` are also accepted, per :rfc:`6238`.
  487. :param int period:
  488. The time-step period to use, in integer seconds. Defaults to ``30``.
  489. ..
  490. See the passlib documentation for a full list of attributes & methods.
  491. """
  492. #=============================================================================
  493. # class attrs
  494. #=============================================================================
  495. #: minimum number of bytes to allow in key, enforced by passlib.
  496. # XXX: see if spec says anything relevant to this.
  497. _min_key_size = 10
  498. #: minimum & current serialization version (may be set independently by subclasses)
  499. min_json_version = json_version = 1
  500. #: AppWallet that this class will use for encrypting/decrypting keys.
  501. #: (can be overwritten via the :meth:`TOTP.using()` constructor)
  502. wallet = None
  503. #: function to get system time in seconds, as needed by :meth:`generate` and :meth:`verify`.
  504. #: defaults to :func:`time.time`, but can be overridden on a per-instance basis.
  505. now = _time.time
  506. #=============================================================================
  507. # instance attrs
  508. #=============================================================================
  509. #---------------------------------------------------------------------------
  510. # configuration attrs
  511. #---------------------------------------------------------------------------
  512. #: [private] secret key as raw :class:`!bytes`
  513. #: see .key property for public access.
  514. _key = None
  515. #: [private] cached copy of encrypted secret,
  516. #: so .to_json() doesn't have to re-encrypt on each call.
  517. _encrypted_key = None
  518. #: [private] cached copy of keyed HMAC function,
  519. #: so ._generate() doesn't have to rebuild this each time
  520. #: ._find_match() invokes it.
  521. _keyed_hmac = None
  522. #: number of digits in the generated tokens.
  523. digits = 6
  524. #: name of hash algorithm in use (e.g. ``"sha1"``)
  525. alg = "sha1"
  526. #: default label for :meth:`to_uri`
  527. label = None
  528. #: default issuer for :meth:`to_uri`
  529. issuer = None
  530. #: number of seconds per counter step.
  531. #: *(TOTP uses an internal time-derived counter which
  532. #: increments by 1 every* :attr:`!period` *seconds)*.
  533. period = 30
  534. #---------------------------------------------------------------------------
  535. # state attrs
  536. #---------------------------------------------------------------------------
  537. #: Flag set by deserialization methods to indicate the object needs to be re-serialized.
  538. #: This can be for a number of reasons -- encoded using deprecated format,
  539. #: or encrypted using a deprecated key or too few rounds.
  540. changed = False
  541. #=============================================================================
  542. # prototype construction
  543. #=============================================================================
  544. @classmethod
  545. def using(cls, digits=None, alg=None, period=None,
  546. issuer=None, wallet=None, now=None, **kwds):
  547. """
  548. Dynamically create subtype of :class:`!TOTP` class
  549. which has the specified defaults set.
  550. :parameters: **digits, alg, period, issuer**:
  551. All these options are the same as in the :class:`TOTP` constructor,
  552. and the resulting class will use any values you specify here
  553. as the default for all TOTP instances it creates.
  554. :param wallet:
  555. Optional :class:`AppWallet` that will be used for encrypting/decrypting keys.
  556. :param secrets, secrets_path, encrypt_cost:
  557. If specified, these options will be passed to the :class:`AppWallet` constructor,
  558. allowing you to directly specify the secret keys that should be used
  559. to encrypt & decrypt stored keys.
  560. :returns:
  561. subclass of :class:`!TOTP`.
  562. This method is useful for creating a TOTP class configured
  563. to use your application's secrets for encrypting & decrypting
  564. keys, as well as create new keys using it's desired configuration defaults.
  565. As an example::
  566. >>> # your application can create a custom class when it initializes
  567. >>> from passlib.totp import TOTP, generate_secret
  568. >>> TotpFactory = TOTP.using(secrets={"1": generate_secret()})
  569. >>> # subsequent TOTP objects created from this factory
  570. >>> # will use the specified secrets to encrypt their keys...
  571. >>> totp = TotpFactory.new()
  572. >>> totp.to_dict()
  573. {'enckey': {'c': 14,
  574. 'k': 'H77SYXWORDPGVOQTFRR2HFUB3C45XXI7',
  575. 's': 'G5DOQPIHIBUM2OOHHADQ',
  576. 't': '1',
  577. 'v': 1},
  578. 'type': 'totp',
  579. 'v': 1}
  580. .. seealso:: :ref:`totp-creation` and :ref:`totp-storing-instances` tutorials for a usage example
  581. """
  582. # XXX: could add support for setting default match 'window' and 'reuse' policy
  583. # :param now:
  584. # Optional callable that should return current time for generator to use.
  585. # Default to :func:`time.time`. This optional is generally not needed,
  586. # and is mainly present for examples & unit-testing.
  587. subcls = type("TOTP", (cls,), {})
  588. def norm_param(attr, value):
  589. """
  590. helper which uses constructor to validate parameter value.
  591. it returns corresponding attribute, so we use normalized value.
  592. """
  593. # NOTE: this creates *subclass* instance,
  594. # so normalization takes into account any custom params
  595. # already stored.
  596. kwds = dict(key=_DUMMY_KEY, format="raw")
  597. kwds[attr] = value
  598. obj = subcls(**kwds)
  599. return getattr(obj, attr)
  600. if digits is not None:
  601. subcls.digits = norm_param("digits", digits)
  602. if alg is not None:
  603. subcls.alg = norm_param("alg", alg)
  604. if period is not None:
  605. subcls.period = norm_param("period", period)
  606. # XXX: add default size as configurable parameter?
  607. if issuer is not None:
  608. subcls.issuer = norm_param("issuer", issuer)
  609. if kwds:
  610. subcls.wallet = AppWallet(**kwds)
  611. if wallet:
  612. raise TypeError("'wallet' and 'secrets' keywords are mutually exclusive")
  613. elif wallet is not None:
  614. if not isinstance(wallet, AppWallet):
  615. raise exc.ExpectedTypeError(wallet, AppWallet, "wallet")
  616. subcls.wallet = wallet
  617. if now is not None:
  618. assert isinstance(now(), num_types) and now() >= 0, \
  619. "now() function must return non-negative int/float"
  620. subcls.now = staticmethod(now)
  621. return subcls
  622. #=============================================================================
  623. # init
  624. #=============================================================================
  625. @classmethod
  626. def new(cls, **kwds):
  627. """
  628. convenience alias for creating new TOTP key, same as ``TOTP(new=True)``
  629. """
  630. return cls(new=True, **kwds)
  631. def __init__(self, key=None, format="base32",
  632. # keyword only...
  633. new=False, digits=None, alg=None, size=None, period=None,
  634. label=None, issuer=None, changed=False,
  635. **kwds):
  636. super(TOTP, self).__init__(**kwds)
  637. if changed:
  638. self.changed = changed
  639. # validate & normalize alg
  640. info = lookup_hash(alg or self.alg)
  641. self.alg = info.name
  642. digest_size = info.digest_size
  643. if digest_size < 4:
  644. raise RuntimeError("%r hash digest too small" % alg)
  645. # parse or generate new key
  646. if new:
  647. # generate new key
  648. if key:
  649. raise TypeError("'key' and 'new=True' are mutually exclusive")
  650. if size is None:
  651. # default to digest size, per RFC 6238 Section 5.1
  652. size = digest_size
  653. elif size > digest_size:
  654. # not forbidden by spec, but would just be wasted bytes.
  655. # maybe just warn about this?
  656. raise ValueError("'size' should be less than digest size "
  657. "(%d)" % digest_size)
  658. self.key = getrandbytes(rng, size)
  659. elif not key:
  660. raise TypeError("must specify either an existing 'key', or 'new=True'")
  661. elif format == "encrypted":
  662. # NOTE: this handles decrypting & setting '.key'
  663. self.encrypted_key = key
  664. elif key:
  665. # use existing key, encoded using specified <format>
  666. self.key = _decode_bytes(key, format)
  667. # enforce min key size
  668. if len(self.key) < self._min_key_size:
  669. # only making this fatal for new=True,
  670. # so that existing (but ridiculously small) keys can still be used.
  671. msg = "for security purposes, secret key must be >= %d bytes" % self._min_key_size
  672. if new:
  673. raise ValueError(msg)
  674. else:
  675. warn(msg, exc.PasslibSecurityWarning, stacklevel=1)
  676. # validate digits
  677. if digits is None:
  678. digits = self.digits
  679. if not isinstance(digits, int_types):
  680. raise TypeError("digits must be an integer, not a %r" % type(digits))
  681. if digits < 6 or digits > 10:
  682. raise ValueError("digits must in range(6,11)")
  683. self.digits = digits
  684. # validate label
  685. if label:
  686. self._check_label(label)
  687. self.label = label
  688. # validate issuer
  689. if issuer:
  690. self._check_issuer(issuer)
  691. self.issuer = issuer
  692. # init period
  693. if period is not None:
  694. self._check_serial(period, "period", minval=1)
  695. self.period = period
  696. #=============================================================================
  697. # helpers to verify value types & ranges
  698. #=============================================================================
  699. @staticmethod
  700. def _check_serial(value, param, minval=0):
  701. """
  702. check that serial value (e.g. 'counter') is non-negative integer
  703. """
  704. if not isinstance(value, int_types):
  705. raise exc.ExpectedTypeError(value, "int", param)
  706. if value < minval:
  707. raise ValueError("%s must be >= %d" % (param, minval))
  708. @staticmethod
  709. def _check_label(label):
  710. """
  711. check that label doesn't contain chars forbidden by KeyURI spec
  712. """
  713. if label and ":" in label:
  714. raise ValueError("label may not contain ':'")
  715. @staticmethod
  716. def _check_issuer(issuer):
  717. """
  718. check that issuer doesn't contain chars forbidden by KeyURI spec
  719. """
  720. if issuer and ":" in issuer:
  721. raise ValueError("issuer may not contain ':'")
  722. #=============================================================================
  723. # key attributes
  724. #=============================================================================
  725. #------------------------------------------------------------------
  726. # raw key
  727. #------------------------------------------------------------------
  728. @property
  729. def key(self):
  730. """
  731. secret key as raw bytes
  732. """
  733. return self._key
  734. @key.setter
  735. def key(self, value):
  736. # set key
  737. if not isinstance(value, bytes):
  738. raise exc.ExpectedTypeError(value, bytes, "key")
  739. self._key = value
  740. # clear cached properties derived from key
  741. self._encrypted_key = self._keyed_hmac = None
  742. #------------------------------------------------------------------
  743. # encrypted key
  744. #------------------------------------------------------------------
  745. @property
  746. def encrypted_key(self):
  747. """
  748. secret key, encrypted using application secret.
  749. this match the output of :meth:`AppWallet.encrypt_key`,
  750. and should be treated as an opaque json serializable object.
  751. """
  752. enckey = self._encrypted_key
  753. if enckey is None:
  754. wallet = self.wallet
  755. if not wallet:
  756. raise TypeError("no application secrets present, can't encrypt TOTP key")
  757. enckey = self._encrypted_key = wallet.encrypt_key(self.key)
  758. return enckey
  759. @encrypted_key.setter
  760. def encrypted_key(self, value):
  761. wallet = self.wallet
  762. if not wallet:
  763. raise TypeError("no application secrets present, can't decrypt TOTP key")
  764. self.key, needs_recrypt = wallet.decrypt_key(value)
  765. if needs_recrypt:
  766. # mark as changed so it gets re-encrypted & written to db
  767. self.changed = True
  768. else:
  769. # cache encrypted key for re-use
  770. self._encrypted_key = value
  771. #------------------------------------------------------------------
  772. # pretty-printed / encoded key helpers
  773. #------------------------------------------------------------------
  774. @property
  775. def hex_key(self):
  776. """
  777. secret key encoded as hexadecimal string
  778. """
  779. return bascii_to_str(base64.b16encode(self.key)).lower()
  780. @property
  781. def base32_key(self):
  782. """
  783. secret key encoded as base32 string
  784. """
  785. return b32encode(self.key)
  786. def pretty_key(self, format="base32", sep="-"):
  787. """
  788. pretty-print the secret key.
  789. This is mainly useful for situations where the user cannot get the qrcode to work,
  790. and must enter the key manually into their TOTP client. It tries to format
  791. the key in a manner that is easier for humans to read.
  792. :param format:
  793. format to output secret key. ``"hex"`` and ``"base32"`` are both accepted.
  794. :param sep:
  795. separator to insert to break up key visually.
  796. can be any of ``"-"`` (the default), ``" "``, or ``False`` (no separator).
  797. :return:
  798. key as native string.
  799. Usage example::
  800. >>> t = TOTP('s3jdvb7qd2r7jpxx')
  801. >>> t.pretty_key()
  802. 'S3JD-VB7Q-D2R7-JPXX'
  803. """
  804. if format == "hex" or format == "base16":
  805. key = self.hex_key
  806. elif format == "base32":
  807. key = self.base32_key
  808. else:
  809. raise ValueError("unknown byte-encoding format: %r" % (format,))
  810. if sep:
  811. key = group_string(key, sep)
  812. return key
  813. #=============================================================================
  814. # time & token parsing
  815. #=============================================================================
  816. @classmethod
  817. def normalize_time(cls, time):
  818. """
  819. Normalize time value to unix epoch seconds.
  820. :arg time:
  821. Can be ``None``, :class:`!datetime`,
  822. or unix epoch timestamp as :class:`!float` or :class:`!int`.
  823. If ``None``, uses current system time.
  824. Naive datetimes are treated as UTC.
  825. :returns:
  826. unix epoch timestamp as :class:`int`.
  827. """
  828. if isinstance(time, int_types):
  829. return time
  830. elif isinstance(time, float):
  831. return int(time)
  832. elif time is None:
  833. return int(cls.now())
  834. elif hasattr(time, "utctimetuple"):
  835. # coerce datetime to UTC timestamp
  836. # NOTE: utctimetuple() assumes naive datetimes are in UTC
  837. # NOTE: we explicitly *don't* want microseconds.
  838. return calendar.timegm(time.utctimetuple())
  839. else:
  840. raise exc.ExpectedTypeError(time, "int, float, or datetime", "time")
  841. def _time_to_counter(self, time):
  842. """
  843. convert timestamp to HOTP counter using :attr:`period`.
  844. """
  845. return time // self.period
  846. def _counter_to_time(self, counter):
  847. """
  848. convert HOTP counter to timestamp using :attr:`period`.
  849. """
  850. return counter * self.period
  851. @hybrid_method
  852. def normalize_token(self_or_cls, token):
  853. """
  854. Normalize OTP token representation:
  855. strips whitespace, converts integers to a zero-padded string,
  856. validates token content & number of digits.
  857. This is a hybrid method -- it can be called at the class level,
  858. as ``TOTP.normalize_token()``, or the instance level as ``TOTP().normalize_token()``.
  859. It will normalize to the instance-specific number of :attr:`~TOTP.digits`,
  860. or use the class default.
  861. :arg token:
  862. token as ascii bytes, unicode, or an integer.
  863. :raises ValueError:
  864. if token has wrong number of digits, or contains non-numeric characters.
  865. :returns:
  866. token as :class:`!unicode` string, containing only digits 0-9.
  867. """
  868. digits = self_or_cls.digits
  869. if isinstance(token, int_types):
  870. token = u("%0*d") % (digits, token)
  871. else:
  872. token = to_unicode(token, param="token")
  873. token = _clean_re.sub(u(""), token)
  874. if not token.isdigit():
  875. raise MalformedTokenError("Token must contain only the digits 0-9")
  876. if len(token) != digits:
  877. raise MalformedTokenError("Token must have exactly %d digits" % digits)
  878. return token
  879. #=============================================================================
  880. # token generation
  881. #=============================================================================
  882. # # debug helper
  883. # def generate_range(self, size, time=None):
  884. # counter = self._time_to_counter(time) - (size + 1) // 2
  885. # end = counter + size
  886. # while counter <= end:
  887. # token = self._generate(counter)
  888. # yield TotpToken(self, token, counter)
  889. # counter += 1
  890. def generate(self, time=None):
  891. """
  892. Generate token for specified time
  893. (uses current time if none specified).
  894. :arg time:
  895. Can be ``None``, a :class:`!datetime`,
  896. or class:`!float` / :class:`!int` unix epoch timestamp.
  897. If ``None`` (the default), uses current system time.
  898. Naive datetimes are treated as UTC.
  899. :returns:
  900. A :class:`TotpToken` instance, which can be treated
  901. as a sequence of ``(token, expire_time)`` -- see that class
  902. for more details.
  903. Usage example::
  904. >>> # generate a new token, wrapped in a TotpToken instance...
  905. >>> otp = TOTP('s3jdvb7qd2r7jpxx')
  906. >>> otp.generate(1419622739)
  907. <TotpToken token='897212' expire_time=1419622740>
  908. >>> # when you just need the token...
  909. >>> otp.generate(1419622739).token
  910. '897212'
  911. """
  912. time = self.normalize_time(time)
  913. counter = self._time_to_counter(time)
  914. if counter < 0:
  915. raise ValueError("timestamp must be >= 0")
  916. token = self._generate(counter)
  917. return TotpToken(self, token, counter)
  918. def _generate(self, counter):
  919. """
  920. base implementation of HOTP token generation algorithm.
  921. :arg counter: HOTP counter, as non-negative integer
  922. :returns: token as unicode string
  923. """
  924. # generate digest
  925. assert isinstance(counter, int_types), "counter must be integer"
  926. assert counter >= 0, "counter must be non-negative"
  927. keyed_hmac = self._keyed_hmac
  928. if keyed_hmac is None:
  929. keyed_hmac = self._keyed_hmac = compile_hmac(self.alg, self.key)
  930. digest = keyed_hmac(_pack_uint64(counter))
  931. digest_size = keyed_hmac.digest_info.digest_size
  932. assert len(digest) == digest_size, "digest_size: sanity check failed"
  933. # derive 31-bit token value
  934. assert digest_size >= 20, "digest_size: sanity check 2 failed" # otherwise 0xF+4 will run off end of hash.
  935. offset = byte_elem_value(digest[-1]) & 0xF
  936. value = _unpack_uint32(digest[offset:offset+4])[0] & 0x7fffffff
  937. # render to decimal string, return last <digits> chars
  938. # NOTE: the 10'th digit is not as secure, as it can only take on values 0-2, not 0-9,
  939. # due to 31-bit mask on int ">I". But some servers / clients use it :|
  940. # if 31-bit mask removed (which breaks spec), would only get values 0-4.
  941. digits = self.digits
  942. assert 0 < digits < 11, "digits: sanity check failed"
  943. return (u("%0*d") % (digits, value))[-digits:]
  944. #=============================================================================
  945. # token verification
  946. #=============================================================================
  947. @classmethod
  948. def verify(cls, token, source, **kwds):
  949. r"""
  950. Convenience wrapper around :meth:`TOTP.from_source` and :meth:`TOTP.match`.
  951. This parses a TOTP key & configuration from the specified source,
  952. and tries and match the token.
  953. It's designed to parallel the :meth:`passlib.ifc.PasswordHash.verify` method.
  954. :param token:
  955. Token string to match.
  956. :param source:
  957. Serialized TOTP key.
  958. Can be anything accepted by :meth:`TOTP.from_source`.
  959. :param \\*\\*kwds:
  960. All additional keywords passed to :meth:`TOTP.match`.
  961. :return:
  962. A :class:`TotpMatch` instance, or raises a :exc:`TokenError`.
  963. """
  964. return cls.from_source(source).match(token, **kwds)
  965. def match(self, token, time=None, window=30, skew=0, last_counter=None):
  966. """
  967. Match TOTP token against specified timestamp.
  968. Searches within a window before & after the provided time,
  969. in order to account for transmission delay and small amounts of skew in the client's clock.
  970. :arg token:
  971. Token to validate.
  972. may be integer or string (whitespace and hyphens are ignored).
  973. :param time:
  974. Unix epoch timestamp, can be any of :class:`!float`, :class:`!int`, or :class:`!datetime`.
  975. if ``None`` (the default), uses current system time.
  976. *this should correspond to the time the token was received from the client*.
  977. :param int window:
  978. How far backward and forward in time to search for a match.
  979. Measured in seconds. Defaults to ``30``. Typically only useful if set
  980. to multiples of :attr:`period`.
  981. :param int skew:
  982. Adjust timestamp by specified value, to account for excessive
  983. client clock skew. Measured in seconds. Defaults to ``0``.
  984. Negative skew (the common case) indicates transmission delay,
  985. and/or that the client clock is running behind the server.
  986. Positive skew indicates the client clock is running ahead of the server
  987. (and by enough that it cancels out any negative skew added by
  988. the transmission delay).
  989. You should ensure the server clock uses a reliable time source such as NTP,
  990. so that only the client clock's inaccuracy needs to be accounted for.
  991. This is an advanced parameter that should usually be left at ``0``;
  992. The **window** parameter is usually enough to account
  993. for any observed transmission delay.
  994. :param last_counter:
  995. Optional value of last counter value that was successfully used.
  996. If specified, verify will never search earlier counters,
  997. no matter how large the window is.
  998. Useful when client has previously authenticated,
  999. and thus should never provide a token older than previously
  1000. verified value.
  1001. :raises ~passlib.exc.TokenError:
  1002. If the token is malformed, fails to match, or has already been used.
  1003. :returns TotpMatch:
  1004. Returns a :class:`TotpMatch` instance on successful match.
  1005. Can be treated as tuple of ``(counter, time)``.
  1006. Raises error if token is malformed / can't be verified.
  1007. Usage example::
  1008. >>> totp = TOTP('s3jdvb7qd2r7jpxx')
  1009. >>> # valid token for this time period
  1010. >>> totp.match('897212', 1419622729)
  1011. <TotpMatch counter=47320757 time=1419622729 cache_seconds=60>
  1012. >>> # token from counter step 30 sec ago (within allowed window)
  1013. >>> totp.match('000492', 1419622729)
  1014. <TotpMatch counter=47320756 time=1419622729 cache_seconds=60>
  1015. >>> # invalid token -- token from 60 sec ago (outside of window)
  1016. >>> totp.match('760389', 1419622729)
  1017. Traceback:
  1018. ...
  1019. InvalidTokenError: Token did not match
  1020. """
  1021. time = self.normalize_time(time)
  1022. self._check_serial(window, "window")
  1023. client_time = time + skew
  1024. if last_counter is None:
  1025. last_counter = -1
  1026. start = max(last_counter, self._time_to_counter(client_time - window))
  1027. end = self._time_to_counter(client_time + window) + 1
  1028. # XXX: could pass 'expected = _time_to_counter(client_time + TRANSMISSION_DELAY)'
  1029. # to the _find_match() method, would help if window set to very large value.
  1030. counter = self._find_match(token, start, end)
  1031. assert counter >= last_counter, "sanity check failed: counter went backward"
  1032. if counter == last_counter:
  1033. raise UsedTokenError(expire_time=(last_counter + 1) * self.period)
  1034. # NOTE: By returning match tied to <time>, not <client_time>, we're
  1035. # causing .skipped to reflect the observed skew, independent of
  1036. # the 'skew' param. This is deliberately done so that caller
  1037. # can use historical .skipped values to estimate future skew.
  1038. return TotpMatch(self, counter, time, window)
  1039. def _find_match(self, token, start, end, expected=None):
  1040. """
  1041. helper for verify() --
  1042. returns counter value within specified range that matches token.
  1043. :arg token:
  1044. token value to match (will be normalized internally)
  1045. :arg start:
  1046. starting counter value to check
  1047. :arg end:
  1048. check up to (but not including) this counter value
  1049. :arg expected:
  1050. optional expected value where search should start,
  1051. to help speed up searches.
  1052. :raises ~passlib.exc.TokenError:
  1053. If the token is malformed, or fails to verify.
  1054. :returns:
  1055. counter value that matched
  1056. """
  1057. token = self.normalize_token(token)
  1058. if start < 0:
  1059. start = 0
  1060. if end <= start:
  1061. raise InvalidTokenError()
  1062. generate = self._generate
  1063. if not (expected is None or expected < start) and consteq(token, generate(expected)):
  1064. return expected
  1065. # XXX: if (end - start) is very large (e.g. for resync purposes),
  1066. # could start with expected value, and work outward from there,
  1067. # alternately checking before & after it until match is found.
  1068. # XXX: can't use irange(start, end) here since py2x/win32
  1069. # throws error on values >= (1<<31), which 'end' can be.
  1070. counter = start
  1071. while counter < end:
  1072. if consteq(token, generate(counter)):
  1073. return counter
  1074. counter += 1
  1075. raise InvalidTokenError()
  1076. #-------------------------------------------------------------------------
  1077. # TODO: resync(self, tokens, time=None, min_tokens=10, window=100)
  1078. # helper to re-synchronize using series of sequential tokens,
  1079. # all of which must validate; per RFC recommendation.
  1080. # NOTE: need to make sure this function is constant time
  1081. # (i.e. scans ALL tokens, and doesn't short-circuit after first mismatch)
  1082. #-------------------------------------------------------------------------
  1083. #=============================================================================
  1084. # generic parsing
  1085. #=============================================================================
  1086. @classmethod
  1087. def from_source(cls, source):
  1088. """
  1089. Load / create a TOTP object from a serialized source.
  1090. This acts as a wrapper for the various deserialization methods:
  1091. * TOTP URIs are handed off to :meth:`from_uri`
  1092. * Any other strings are handed off to :meth:`from_json`
  1093. * Dicts are handed off to :meth:`from_dict`
  1094. :param source:
  1095. Serialized TOTP object.
  1096. :raises ValueError:
  1097. If the key has been encrypted, but the application secret isn't available;
  1098. or if the string cannot be recognized, parsed, or decoded.
  1099. See :meth:`TOTP.using()` for how to configure application secrets.
  1100. :returns:
  1101. a :class:`TOTP` instance.
  1102. """
  1103. if isinstance(source, TOTP):
  1104. # return object unchanged if they share same wallet.
  1105. # otherwise make a new one that's bound to expected wallet.
  1106. if cls.wallet == source.wallet:
  1107. return source
  1108. source = source.to_dict(encrypt=False)
  1109. if isinstance(source, dict):
  1110. return cls.from_dict(source)
  1111. # NOTE: letting to_unicode() raise TypeError in this case
  1112. source = to_unicode(source, param="totp source")
  1113. if source.startswith("otpauth://"):
  1114. return cls.from_uri(source)
  1115. else:
  1116. return cls.from_json(source)
  1117. #=============================================================================
  1118. # uri parsing
  1119. #=============================================================================
  1120. @classmethod
  1121. def from_uri(cls, uri):
  1122. """
  1123. create an OTP instance from a URI (such as returned by :meth:`to_uri`).
  1124. :returns:
  1125. :class:`TOTP` instance.
  1126. :raises ValueError:
  1127. if the uri cannot be parsed or contains errors.
  1128. .. seealso:: :ref:`totp-configuring-clients` tutorial for a usage example
  1129. """
  1130. # check for valid uri
  1131. uri = to_unicode(uri, param="uri").strip()
  1132. result = urlparse(uri)
  1133. if result.scheme != "otpauth":
  1134. raise cls._uri_parse_error("wrong uri scheme")
  1135. # validate netloc, and hand off to helper
  1136. cls._check_otp_type(result.netloc)
  1137. return cls._from_parsed_uri(result)
  1138. @classmethod
  1139. def _check_otp_type(cls, type):
  1140. """
  1141. validate otp URI type is supported.
  1142. returns True or raises appropriate error.
  1143. """
  1144. if type == "totp":
  1145. return True
  1146. if type == "hotp":
  1147. raise NotImplementedError("HOTP not supported")
  1148. raise ValueError("unknown otp type: %r" % type)
  1149. @classmethod
  1150. def _from_parsed_uri(cls, result):
  1151. """
  1152. internal from_uri() helper --
  1153. handles parsing a validated TOTP URI
  1154. :param result:
  1155. a urlparse() instance
  1156. :returns:
  1157. cls instance
  1158. """
  1159. # decode label from uri path
  1160. label = result.path
  1161. if label.startswith("/") and len(label) > 1:
  1162. label = unquote(label[1:])
  1163. else:
  1164. raise cls._uri_parse_error("missing label")
  1165. # extract old-style issuer prefix
  1166. if ":" in label:
  1167. try:
  1168. issuer, label = label.split(":")
  1169. except ValueError: # too many ":"
  1170. raise cls._uri_parse_error("malformed label")
  1171. else:
  1172. issuer = None
  1173. if label:
  1174. # NOTE: KeyURI spec says there may be leading spaces
  1175. label = label.strip() or None
  1176. # parse query params
  1177. params = dict(label=label)
  1178. for k, v in parse_qsl(result.query):
  1179. if k in params:
  1180. raise cls._uri_parse_error("duplicate parameter (%r)" % k)
  1181. params[k] = v
  1182. # synchronize issuer prefix w/ issuer param
  1183. if issuer:
  1184. if "issuer" not in params:
  1185. params['issuer'] = issuer
  1186. elif params['issuer'] != issuer:
  1187. raise cls._uri_parse_error("conflicting issuer identifiers")
  1188. # convert query params to constructor kwds, and call constructor
  1189. return cls(**cls._adapt_uri_params(**params))
  1190. @classmethod
  1191. def _adapt_uri_params(cls, label=None, secret=None, issuer=None,
  1192. digits=None, algorithm=None, period=None,
  1193. **extra):
  1194. """
  1195. from_uri() helper --
  1196. converts uri params into constructor args.
  1197. """
  1198. assert label, "from_uri() failed to provide label"
  1199. if not secret:
  1200. raise cls._uri_parse_error("missing 'secret' parameter")
  1201. kwds = dict(label=label, issuer=issuer, key=secret, format="base32")
  1202. if digits:
  1203. kwds['digits'] = cls._uri_parse_int(digits, "digits")
  1204. if algorithm:
  1205. kwds['alg'] = algorithm
  1206. if period:
  1207. kwds['period'] = cls._uri_parse_int(period, "period")
  1208. if extra:
  1209. # malicious uri, deviation from spec, or newer revision of spec?
  1210. # in either case, we issue warning and ignore extra params.
  1211. warn("%s: unexpected parameters encountered in otp uri: %r" %
  1212. (cls, extra), exc.PasslibRuntimeWarning)
  1213. return kwds
  1214. @staticmethod
  1215. def _uri_parse_error(reason):
  1216. """uri parsing helper -- creates preformatted error message"""
  1217. return ValueError("Invalid otpauth uri: %s" % (reason,))
  1218. @classmethod
  1219. def _uri_parse_int(cls, source, param):
  1220. """uri parsing helper -- int() wrapper"""
  1221. try:
  1222. return int(source)
  1223. except ValueError:
  1224. raise cls._uri_parse_error("Malformed %r parameter" % param)
  1225. #=============================================================================
  1226. # uri rendering
  1227. #=============================================================================
  1228. def to_uri(self, label=None, issuer=None):
  1229. """
  1230. Serialize key and configuration into a URI, per
  1231. Google Auth's `KeyUriFormat <http://code.google.com/p/google-authenticator/wiki/KeyUriFormat>`_.
  1232. :param str label:
  1233. Label to associate with this token when generating a URI.
  1234. Displayed to user by most OTP client applications (e.g. Google Authenticator),
  1235. and typically has format such as ``"John Smith"`` or ``"jsmith@webservice.example.org"``.
  1236. Defaults to **label** constructor argument. Must be provided in one or the other location.
  1237. May not contain ``:``.
  1238. :param str issuer:
  1239. String identifying the token issuer (e.g. the domain or canonical name of your service).
  1240. Optional but strongly recommended if you're rendering to a URI.
  1241. Used internally by some OTP client applications (e.g. Google Authenticator) to distinguish entries
  1242. which otherwise have the same label.
  1243. Defaults to **issuer** constructor argument, or ``None``.
  1244. May not contain ``:``.
  1245. :raises ValueError:
  1246. * if a label was not provided either as an argument, or in the constructor.
  1247. * if the label or issuer contains invalid characters.
  1248. :returns:
  1249. all the configuration information for this OTP token generator,
  1250. encoded into a URI.
  1251. These URIs are frequently converted to a QRCode for transferring
  1252. to a TOTP client application such as Google Auth.
  1253. Usage example::
  1254. >>> from passlib.totp import TOTP
  1255. >>> tp = TOTP('s3jdvb7qd2r7jpxx')
  1256. >>> uri = tp.to_uri("user@example.org", "myservice.another-example.org")
  1257. >>> uri
  1258. 'otpauth://totp/user@example.org?secret=S3JDVB7QD2R7JPXX&issuer=myservice.another-example.org'
  1259. .. versionchanged:: 1.7.2
  1260. This method now prepends the issuer URI label. This is recommended by the KeyURI
  1261. specification, for compatibility with older clients.
  1262. """
  1263. # encode label
  1264. if label is None:
  1265. label = self.label
  1266. if not label:
  1267. raise ValueError("a label must be specified as argument, or in the constructor")
  1268. self._check_label(label)
  1269. # NOTE: reference examples in spec seem to indicate the '@' in a label
  1270. # shouldn't be escaped, though spec doesn't explicitly address this.
  1271. # XXX: is '/' ok to leave unencoded?
  1272. label = quote(label, '@')
  1273. # encode query parameters
  1274. params = self._to_uri_params()
  1275. if issuer is None:
  1276. issuer = self.issuer
  1277. if issuer:
  1278. self._check_issuer(issuer)
  1279. # NOTE: per KeyURI spec, including issuer as part of label is deprecated,
  1280. # in favor of adding it to query params. however, some QRCode clients
  1281. # don't recognize the 'issuer' query parameter, so spec recommends (as of 2018-7)
  1282. # to include both.
  1283. label = "%s:%s" % (quote(issuer, '@'), label)
  1284. params.append(("issuer", issuer))
  1285. # NOTE: not using urllib.urlencode() because it encodes ' ' as '+';
  1286. # but spec says to use '%20', and not sure how fragile
  1287. # the various totp clients' parsers are.
  1288. param_str = u("&").join(u("%s=%s") % (key, quote(value, '')) for key, value in params)
  1289. assert param_str, "param_str should never be empty"
  1290. # render uri
  1291. return u("otpauth://totp/%s?%s") % (label, param_str)
  1292. def _to_uri_params(self):
  1293. """return list of (key, param) entries for URI"""
  1294. args = [("secret", self.base32_key)]
  1295. if self.alg != "sha1":
  1296. args.append(("algorithm", self.alg.upper()))
  1297. if self.digits != 6:
  1298. args.append(("digits", str(self.digits)))
  1299. if self.period != 30:
  1300. args.append(("period", str(self.period)))
  1301. return args
  1302. #=============================================================================
  1303. # json rendering / parsing
  1304. #=============================================================================
  1305. @classmethod
  1306. def from_json(cls, source):
  1307. """
  1308. Load / create an OTP object from a serialized json string
  1309. (as generated by :meth:`to_json`).
  1310. :arg json:
  1311. Serialized output from :meth:`to_json`, as unicode or ascii bytes.
  1312. :raises ValueError:
  1313. If the key has been encrypted, but the application secret isn't available;
  1314. or if the string cannot be recognized, parsed, or decoded.
  1315. See :meth:`TOTP.using()` for how to configure application secrets.
  1316. :returns:
  1317. a :class:`TOTP` instance.
  1318. .. seealso:: :ref:`totp-storing-instances` tutorial for a usage example
  1319. """
  1320. source = to_unicode(source, param="json source")
  1321. return cls.from_dict(json.loads(source))
  1322. def to_json(self, encrypt=None):
  1323. """
  1324. Serialize configuration & internal state to a json string,
  1325. mainly useful for persisting client-specific state in a database.
  1326. All keywords passed to :meth:`to_dict`.
  1327. :returns:
  1328. json string containing serializes configuration & state.
  1329. """
  1330. state = self.to_dict(encrypt=encrypt)
  1331. return json.dumps(state, sort_keys=True, separators=(",", ":"))
  1332. #=============================================================================
  1333. # dict rendering / parsing
  1334. #=============================================================================
  1335. @classmethod
  1336. def from_dict(cls, source):
  1337. """
  1338. Load / create a TOTP object from a dictionary
  1339. (as generated by :meth:`to_dict`)
  1340. :param source:
  1341. dict containing serialized TOTP key & configuration.
  1342. :raises ValueError:
  1343. If the key has been encrypted, but the application secret isn't available;
  1344. or if the dict cannot be recognized, parsed, or decoded.
  1345. See :meth:`TOTP.using()` for how to configure application secrets.
  1346. :returns:
  1347. A :class:`TOTP` instance.
  1348. .. seealso:: :ref:`totp-storing-instances` tutorial for a usage example
  1349. """
  1350. if not isinstance(source, dict) or "type" not in source:
  1351. raise cls._dict_parse_error("unrecognized format")
  1352. return cls(**cls._adapt_dict_kwds(**source))
  1353. @classmethod
  1354. def _adapt_dict_kwds(cls, type, **kwds):
  1355. """
  1356. Internal helper for .from_json() --
  1357. Adapts serialized json dict into constructor keywords.
  1358. """
  1359. # default json format is just serialization of constructor kwds.
  1360. # XXX: just pass all this through to _from_json / constructor?
  1361. # go ahead and mark as changed (needs re-saving) if the version is too old
  1362. assert cls._check_otp_type(type)
  1363. ver = kwds.pop("v", None)
  1364. if not ver or ver < cls.min_json_version or ver > cls.json_version:
  1365. raise cls._dict_parse_error("missing/unsupported version (%r)" % (ver,))
  1366. elif ver != cls.json_version:
  1367. # mark older version as needing re-serializing
  1368. kwds['changed'] = True
  1369. if 'enckey' in kwds:
  1370. # handing encrypted key off to constructor, which handles the
  1371. # decryption. this lets it get ahold of (and store) the original
  1372. # encrypted key, so if to_json() is called again, the encrypted
  1373. # key can be re-used.
  1374. # XXX: wallet is known at this point, could decrypt key here.
  1375. assert 'key' not in kwds # shouldn't be present w/ enckey
  1376. kwds.update(key=kwds.pop("enckey"), format="encrypted")
  1377. elif 'key' not in kwds:
  1378. raise cls._dict_parse_error("missing 'enckey' / 'key'")
  1379. # XXX: could should set changed=True if active wallet is available,
  1380. # and source wasn't encrypted.
  1381. kwds.pop("last_counter", None) # extract legacy counter parameter
  1382. return kwds
  1383. @staticmethod
  1384. def _dict_parse_error(reason):
  1385. """dict parsing helper -- creates preformatted error message"""
  1386. return ValueError("Invalid totp data: %s" % (reason,))
  1387. def to_dict(self, encrypt=None):
  1388. """
  1389. Serialize configuration & internal state to a dict,
  1390. mainly useful for persisting client-specific state in a database.
  1391. :param encrypt:
  1392. Whether to output should be encrypted.
  1393. * ``None`` (the default) -- uses encrypted key if application
  1394. secrets are available, otherwise uses plaintext key.
  1395. * ``True`` -- uses encrypted key, or raises TypeError
  1396. if application secret wasn't provided to OTP constructor.
  1397. * ``False`` -- uses raw key.
  1398. :returns:
  1399. dictionary, containing basic (json serializable) datatypes.
  1400. """
  1401. # NOTE: 'type' may seem redundant, but using it so code can try to
  1402. # detect that this *is* a TOTP json string / dict.
  1403. state = dict(v=self.json_version, type="totp")
  1404. if self.alg != "sha1":
  1405. state['alg'] = self.alg
  1406. if self.digits != 6:
  1407. state['digits'] = self.digits
  1408. if self.period != 30:
  1409. state['period'] = self.period
  1410. # XXX: should we include label as part of json format?
  1411. if self.label:
  1412. state['label'] = self.label
  1413. issuer = self.issuer
  1414. if issuer and issuer != type(self).issuer:
  1415. # (omit issuer if it matches class default)
  1416. state['issuer'] = issuer
  1417. if encrypt is None:
  1418. wallet = self.wallet
  1419. encrypt = wallet and wallet.has_secrets
  1420. if encrypt:
  1421. state['enckey'] = self.encrypted_key
  1422. else:
  1423. state['key'] = self.base32_key
  1424. # NOTE: in the future, may add a "history" parameter
  1425. # containing a list of (time, skipped) pairs, encoding
  1426. # the last X successful verifications, to allow persisting
  1427. # & estimating client clock skew over time.
  1428. return state
  1429. #=============================================================================
  1430. # eoc
  1431. #=============================================================================
  1432. #=============================================================================
  1433. # TOTP helpers
  1434. #=============================================================================
  1435. class TotpToken(SequenceMixin):
  1436. """
  1437. Object returned by :meth:`TOTP.generate`.
  1438. It can be treated as a sequence of ``(token, expire_time)``,
  1439. or accessed via the following attributes:
  1440. .. autoattribute:: token
  1441. .. autoattribute:: expire_time
  1442. .. autoattribute:: counter
  1443. .. autoattribute:: remaining
  1444. .. autoattribute:: valid
  1445. """
  1446. #: TOTP object that generated this token
  1447. totp = None
  1448. #: Token as decimal-encoded ascii string.
  1449. token = None
  1450. #: HOTP counter value used to generate token (derived from time)
  1451. counter = None
  1452. def __init__(self, totp, token, counter):
  1453. """
  1454. .. warning::
  1455. the constructor signature is an internal detail, and is subject to change.
  1456. """
  1457. self.totp = totp
  1458. self.token = token
  1459. self.counter = counter
  1460. @memoized_property
  1461. def start_time(self):
  1462. """Timestamp marking beginning of period when token is valid"""
  1463. return self.totp._counter_to_time(self.counter)
  1464. @memoized_property
  1465. def expire_time(self):
  1466. """Timestamp marking end of period when token is valid"""
  1467. return self.totp._counter_to_time(self.counter + 1)
  1468. @property
  1469. def remaining(self):
  1470. """number of (float) seconds before token expires"""
  1471. return max(0, self.expire_time - self.totp.now())
  1472. @property
  1473. def valid(self):
  1474. """whether token is still valid"""
  1475. return bool(self.remaining)
  1476. def _as_tuple(self):
  1477. return self.token, self.expire_time
  1478. def __repr__(self):
  1479. expired = "" if self.remaining else " expired"
  1480. return "<TotpToken token='%s' expire_time=%d%s>" % \
  1481. (self.token, self.expire_time, expired)
  1482. class TotpMatch(SequenceMixin):
  1483. """
  1484. Object returned by :meth:`TOTP.match` and :meth:`TOTP.verify` on a successful match.
  1485. It can be treated as a sequence of ``(counter, time)``,
  1486. or accessed via the following attributes:
  1487. .. autoattribute:: counter
  1488. :annotation: = 0
  1489. .. autoattribute:: time
  1490. :annotation: = 0
  1491. .. autoattribute:: expected_counter
  1492. :annotation: = 0
  1493. .. autoattribute:: skipped
  1494. :annotation: = 0
  1495. .. autoattribute:: expire_time
  1496. :annotation: = 0
  1497. .. autoattribute:: cache_seconds
  1498. :annotation: = 60
  1499. .. autoattribute:: cache_time
  1500. :annotation: = 0
  1501. This object will always have a ``True`` boolean value.
  1502. """
  1503. #: TOTP object that generated this token
  1504. totp = None
  1505. #: TOTP counter value which matched token.
  1506. #: (Best practice is to subsequently ignore tokens matching this counter
  1507. #: or earlier)
  1508. counter = 0
  1509. #: Timestamp when verification was performed.
  1510. time = 0
  1511. #: Search window used by verify() (affects cache_time)
  1512. window = 30
  1513. def __init__(self, totp, counter, time, window=30):
  1514. """
  1515. .. warning::
  1516. the constructor signature is an internal detail, and is subject to change.
  1517. """
  1518. self.totp = totp
  1519. self.counter = counter
  1520. self.time = time
  1521. self.window = window
  1522. @memoized_property
  1523. def expected_counter(self):
  1524. """
  1525. Counter value expected for timestamp.
  1526. """
  1527. return self.totp._time_to_counter(self.time)
  1528. @memoized_property
  1529. def skipped(self):
  1530. """
  1531. How many steps were skipped between expected and actual matched counter
  1532. value (may be positive, zero, or negative).
  1533. """
  1534. return self.counter - self.expected_counter
  1535. # @memoized_property
  1536. # def start_time(self):
  1537. # """Timestamp marking start of period when token is valid"""
  1538. # return self.totp._counter_to_time(self.counter + 1)
  1539. @memoized_property
  1540. def expire_time(self):
  1541. """Timestamp marking end of period when token is valid"""
  1542. return self.totp._counter_to_time(self.counter + 1)
  1543. @memoized_property
  1544. def cache_seconds(self):
  1545. """
  1546. Number of seconds counter should be cached
  1547. before it's guaranteed to have passed outside of verification window.
  1548. """
  1549. # XXX: real value is 'cache_time - now()',
  1550. # but this is a cheaper upper bound.
  1551. return self.totp.period + self.window
  1552. @memoized_property
  1553. def cache_time(self):
  1554. """
  1555. Timestamp marking when counter has passed outside of verification window.
  1556. """
  1557. return self.expire_time + self.window
  1558. def _as_tuple(self):
  1559. return self.counter, self.time
  1560. def __repr__(self):
  1561. args = (self.counter, self.time, self.cache_seconds)
  1562. return "<TotpMatch counter=%d time=%d cache_seconds=%d>" % args
  1563. #=============================================================================
  1564. # convenience helpers
  1565. #=============================================================================
  1566. def generate_secret(entropy=256, charset=BASE64_CHARS[:-2]):
  1567. """
  1568. generate a random string suitable for use as an
  1569. :class:`AppWallet` application secret.
  1570. :param entropy:
  1571. number of bits of entropy (controls size/complexity of password).
  1572. """
  1573. assert entropy > 0
  1574. assert len(charset) > 1
  1575. count = int(math.ceil(entropy * math.log(2, len(charset))))
  1576. return getrandstr(rng, charset, count)
  1577. #=============================================================================
  1578. # eof
  1579. #=============================================================================