aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2015-06-03 16:41:58 -0500
committerPaul Kehrer <paul.l.kehrer@gmail.com>2015-06-03 16:41:58 -0500
commitd3532d4dc0f7a09efbf98890eba07a45e500f66a (patch)
treef4d817cd3a8261f168b5bbe93d28b21a9af6cad8
parent4d025ab7b4596a2dc12abe96f092ef5b772361da (diff)
parent840a99b253e11554c166ccd7de22b553db627ee3 (diff)
downloadcryptography-d3532d4dc0f7a09efbf98890eba07a45e500f66a.tar.gz
cryptography-d3532d4dc0f7a09efbf98890eba07a45e500f66a.tar.bz2
cryptography-d3532d4dc0f7a09efbf98890eba07a45e500f66a.zip
Merge pull request #1990 from tonyseek/key-uri
Add "get_provisioning_uri" utility for HOTP/TOTP.
-rw-r--r--docs/hazmat/primitives/twofactor.rst41
-rw-r--r--src/cryptography/hazmat/primitives/twofactor/hotp.py6
-rw-r--r--src/cryptography/hazmat/primitives/twofactor/totp.py6
-rw-r--r--src/cryptography/hazmat/primitives/twofactor/utils.py30
-rw-r--r--tests/hazmat/primitives/twofactor/test_hotp.py13
-rw-r--r--tests/hazmat/primitives/twofactor/test_totp.py13
6 files changed, 109 insertions, 0 deletions
diff --git a/docs/hazmat/primitives/twofactor.rst b/docs/hazmat/primitives/twofactor.rst
index dd3e0250..f49d02f9 100644
--- a/docs/hazmat/primitives/twofactor.rst
+++ b/docs/hazmat/primitives/twofactor.rst
@@ -74,6 +74,15 @@ codes (HMAC).
:raises cryptography.hazmat.primitives.twofactor.InvalidToken: This
is raised when the supplied HOTP does not match the expected HOTP.
+ .. method:: get_provisioning_uri(account_name, counter, issuer)
+
+ :param str account_name: The display name of account, such as
+ ``'Alice Smith'`` or ``'alice@example.com'``.
+ :param issuer: The optional display name of issuer.
+ :type issuer: `string` or `None`
+ :param int counter: The current value of counter.
+ :return str: An URI string.
+
Throttling
~~~~~~~~~~
@@ -171,3 +180,35 @@ similar to the following code.
:param int time: The time value to validate against.
:raises cryptography.hazmat.primitives.twofactor.InvalidToken: This
is raised when the supplied TOTP does not match the expected TOTP.
+
+ .. method:: get_provisioning_uri(account_name, issuer)
+
+ :param str account_name: The display name of account, such as
+ ``'Alice Smith'`` or ``'alice@example.com'``.
+ :param issuer: The optional display name of issuer.
+ :type issuer: `string` or `None`
+ :return str: An URI string.
+
+Provisioning URI
+~~~~~~~~~~~~~~~~
+
+The provisioning URI of HOTP and TOTP is not actual the part of RFC 4226 and
+RFC 6238, but a `spec of Google Authenticator`_. It is widely supported by web
+sites and mobile applications which are using Two-Factor authentication.
+
+For generating a provisioning URI, you could use the ``get_provisioning_uri``
+method of HOTP/TOTP instances.
+
+.. code-block:: python
+
+ counter = 5
+ account_name = 'alice@example.com'
+ issuer_name = 'Example Inc'
+
+ hotp_uri = hotp.get_provisioning_uri(account_name, counter, issuer_name)
+ totp_uri = totp.get_provisioning_uri(account_name, issuer_name)
+
+A common usage is encoding the provisioning URI into QR code and guiding users
+to scan it with Two-Factor authentication applications in their mobile devices.
+
+.. _`spec of Google Authenticator`: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
diff --git a/src/cryptography/hazmat/primitives/twofactor/hotp.py b/src/cryptography/hazmat/primitives/twofactor/hotp.py
index ba228b40..8c0cec14 100644
--- a/src/cryptography/hazmat/primitives/twofactor/hotp.py
+++ b/src/cryptography/hazmat/primitives/twofactor/hotp.py
@@ -15,6 +15,7 @@ from cryptography.hazmat.backends.interfaces import HMACBackend
from cryptography.hazmat.primitives import constant_time, hmac
from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512
from cryptography.hazmat.primitives.twofactor import InvalidToken
+from cryptography.hazmat.primitives.twofactor.utils import _generate_uri
class HOTP(object):
@@ -59,3 +60,8 @@ class HOTP(object):
offset = six.indexbytes(hmac_value, len(hmac_value) - 1) & 0b1111
p = hmac_value[offset:offset + 4]
return struct.unpack(">I", p)[0] & 0x7fffffff
+
+ def get_provisioning_uri(self, account_name, counter, issuer):
+ return _generate_uri(self, 'hotp', account_name, issuer, [
+ ('counter', int(counter)),
+ ])
diff --git a/src/cryptography/hazmat/primitives/twofactor/totp.py b/src/cryptography/hazmat/primitives/twofactor/totp.py
index 03df9292..98493b6d 100644
--- a/src/cryptography/hazmat/primitives/twofactor/totp.py
+++ b/src/cryptography/hazmat/primitives/twofactor/totp.py
@@ -11,6 +11,7 @@ from cryptography.hazmat.backends.interfaces import HMACBackend
from cryptography.hazmat.primitives import constant_time
from cryptography.hazmat.primitives.twofactor import InvalidToken
from cryptography.hazmat.primitives.twofactor.hotp import HOTP
+from cryptography.hazmat.primitives.twofactor.utils import _generate_uri
class TOTP(object):
@@ -31,3 +32,8 @@ class TOTP(object):
def verify(self, totp, time):
if not constant_time.bytes_eq(self.generate(time), totp):
raise InvalidToken("Supplied TOTP value does not match.")
+
+ def get_provisioning_uri(self, account_name, issuer):
+ return _generate_uri(self._hotp, 'totp', account_name, issuer, [
+ ('period', int(self._time_step)),
+ ])
diff --git a/src/cryptography/hazmat/primitives/twofactor/utils.py b/src/cryptography/hazmat/primitives/twofactor/utils.py
new file mode 100644
index 00000000..91d2e148
--- /dev/null
+++ b/src/cryptography/hazmat/primitives/twofactor/utils.py
@@ -0,0 +1,30 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import base64
+
+from six.moves.urllib.parse import quote, urlencode
+
+
+def _generate_uri(hotp, type_name, account_name, issuer, extra_parameters):
+ parameters = [
+ ('digits', hotp._length),
+ ('secret', base64.b32encode(hotp._key)),
+ ('algorithm', hotp._algorithm.name.upper()),
+ ]
+
+ if issuer is not None:
+ parameters.append(('issuer', issuer))
+
+ parameters.extend(extra_parameters)
+
+ uriparts = {
+ 'type': type_name,
+ 'label': ('%s:%s' % (quote(issuer), quote(account_name)) if issuer
+ else quote(account_name)),
+ 'parameters': urlencode(parameters),
+ }
+ return 'otpauth://{type}/{label}?{parameters}'.format(**uriparts)
diff --git a/tests/hazmat/primitives/twofactor/test_hotp.py b/tests/hazmat/primitives/twofactor/test_hotp.py
index a5d1c284..ab5f93c5 100644
--- a/tests/hazmat/primitives/twofactor/test_hotp.py
+++ b/tests/hazmat/primitives/twofactor/test_hotp.py
@@ -92,6 +92,19 @@ class TestHOTP(object):
with pytest.raises(TypeError):
HOTP(secret, b"foo", SHA1(), backend)
+ def test_get_provisioning_uri(self, backend):
+ secret = b"12345678901234567890"
+ hotp = HOTP(secret, 6, SHA1(), backend)
+
+ assert hotp.get_provisioning_uri("Alice Smith", 1, None) == (
+ "otpauth://hotp/Alice%20Smith?digits=6&secret=GEZDGNBV"
+ "GY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&counter=1")
+
+ assert hotp.get_provisioning_uri("Alice Smith", 1, 'Foo') == (
+ "otpauth://hotp/Foo:Alice%20Smith?digits=6&secret=GEZD"
+ "GNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&issuer=Foo"
+ "&counter=1")
+
def test_invalid_backend():
secret = b"12345678901234567890"
diff --git a/tests/hazmat/primitives/twofactor/test_totp.py b/tests/hazmat/primitives/twofactor/test_totp.py
index 6039983e..95829713 100644
--- a/tests/hazmat/primitives/twofactor/test_totp.py
+++ b/tests/hazmat/primitives/twofactor/test_totp.py
@@ -126,6 +126,19 @@ class TestTOTP(object):
assert totp.generate(time) == b"94287082"
+ def test_get_provisioning_uri(self, backend):
+ secret = b"12345678901234567890"
+ totp = TOTP(secret, 6, hashes.SHA1(), 30, backend=backend)
+
+ assert totp.get_provisioning_uri("Alice Smith", None) == (
+ "otpauth://totp/Alice%20Smith?digits=6&secret=GEZDGNBVG"
+ "Y3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&period=30")
+
+ assert totp.get_provisioning_uri("Alice Smith", 'World') == (
+ "otpauth://totp/World:Alice%20Smith?digits=6&secret=GEZ"
+ "DGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&issuer=World"
+ "&period=30")
+
def test_invalid_backend():
secret = b"12345678901234567890"