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.
 
 
 
 

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