[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [bridgedb/develop] Add a ScheduledInterval timeout to make GimpCaptcha stale.
commit 2d16b608449be112023cbeb3bccfe901502c5e44
Author: Isis Lovecruft <isis@xxxxxxxxxxxxxx>
Date: Wed May 14 21:54:39 2014 +0000
Add a ScheduledInterval timeout to make GimpCaptcha stale.
* FIXES #11215 which describes a problem where a successful challenge
string and CAPTCHA solution pair can be replayed to the server, once
per bridge hashring rotation period (every three hours) to get a new
set of bridges without solving a CAPTCHA.
* ADD a `GimpCaptcha.sched` attribute which holds a
`bridgedb.schedule.ScheduledInterval` for determining which time
period the CAPTCHA was created in, as well as for determining if
we're still in the same time period during CAPTCHA solution
verification.
* ADD a padded timestamp to the encrypted bloc, ENC_BLOB, inside the
format of the GimpCaptcha.challenge strings.
* ADD a captcha.CaptchaExpired exception which is raised when we're
checking a CAPTCHA solution that is no longer in the allowed time
interval.
---
lib/bridgedb/HTTPServer.py | 12 +++++--
lib/bridgedb/captcha.py | 79 ++++++++++++++++++++++++++++++++------------
2 files changed, 67 insertions(+), 24 deletions(-)
diff --git a/lib/bridgedb/HTTPServer.py b/lib/bridgedb/HTTPServer.py
index 8289be3..9d76e28 100644
--- a/lib/bridgedb/HTTPServer.py
+++ b/lib/bridgedb/HTTPServer.py
@@ -313,14 +313,20 @@ class GimpCaptchaProtectedResource(CaptchaProtectedResource):
:rtupe: bool
:returns: True, if the CAPTCHA solution was valid; False otherwise.
"""
+ valid = False
challenge, solution = self.extractClientSolution(request)
clientIP = self.getClientIP(request)
clientHMACKey = crypto.getHMAC(self.hmacKey, clientIP)
- valid = captcha.GimpCaptcha.check(challenge, solution,
- self.secretKey, clientHMACKey)
+
+ try:
+ valid = captcha.GimpCaptcha.check(challenge, solution,
+ self.secretKey, clientHMACKey)
+ except captcha.CaptchaExpired as error:
+ logging.warn(error)
+ valid = False
+
logging.debug("%sorrect captcha from %r: %r."
% ("C" if valid else "Inc", clientIP, solution))
-
return valid
def getCaptchaImage(self, request):
diff --git a/lib/bridgedb/captcha.py b/lib/bridgedb/captcha.py
index e10bc81..758c1f3 100644
--- a/lib/bridgedb/captcha.py
+++ b/lib/bridgedb/captcha.py
@@ -39,6 +39,7 @@
\_ GimpCaptcha - Class for obtaining a CAPTCHA from a local cache.
|- hmacKey - A client-specific key for HMAC generation.
|- cacheDir - The path to the local CAPTCHA cache directory.
+ |- sched - A class for timing out CAPTCHAs after an interval.
\_ get() - Get a CAPTCHA image from the cache and create a challenge.
..
@@ -58,6 +59,7 @@ from base64 import urlsafe_b64decode
import logging
import random
import os
+import time
import urllib2
from BeautifulSoup import BeautifulSoup
@@ -65,9 +67,13 @@ from BeautifulSoup import BeautifulSoup
from zope.interface import Interface, Attribute, implements
from bridgedb import crypto
+from bridgedb import schedule
from bridgedb.txrecaptcha import API_SSL_SERVER
+class CaptchaExpired(ValueError):
+ """Raised when a client's CAPTCHA is too stale."""
+
class CaptchaKeyError(Exception):
"""Raised if a CAPTCHA system's keys are invalid or missing."""
@@ -200,6 +206,8 @@ class GimpCaptcha(Captcha):
.. _gimp-captcha: https://github.com/isislovecruft/gimp-captcha
"""
+ sched = schedule.ScheduledInterval('minutes', 30)
+
def __init__(self, publicKey=None, secretKey=None, hmacKey=None,
cacheDir=None):
"""Create a ``GimpCaptcha`` which retrieves images from **cacheDir**.
@@ -242,37 +250,52 @@ class GimpCaptcha(Captcha):
:param str secretKey: A PKCS#1 OAEP-padded, private RSA key, used for
verifying the client's solution to the CAPTCHA.
:param bytes hmacKey: A private key for generating HMACs.
+ :raises CaptchaExpired: if the **solution** was for a stale CAPTCHA.
:rtype: bool
- :returns: True if the CAPTCHA solution was correct.
+ :returns: ``True`` if the CAPTCHA solution was correct and not
+ stale. ``False`` otherwise.
"""
- validHMAC = False
+ hmacIsValid = False
if not solution:
- return validHMAC
+ return hmacIsValid
logging.debug("Checking CAPTCHA solution %r against challenge %r"
% (solution, challenge))
try:
decoded = urlsafe_b64decode(challenge)
- hmac = decoded[:20]
- original = decoded[20:]
- verified = crypto.getHMAC(hmacKey, original)
- validHMAC = verified == hmac
+ hmacFromBlob = decoded[:20]
+ encBlob = decoded[20:]
+ hmacNew = crypto.getHMAC(hmacKey, encBlob)
+ hmacIsValid = hmacNew == hmacFromBlob
except Exception:
return False
finally:
- if validHMAC:
+ if hmacIsValid:
try:
- decrypted = secretKey.decrypt(original)
+ answerBlob = secretKey.decrypt(encBlob)
+
+ timestamp = answerBlob[:12].lstrip('0')
+ then = cls.sched.nextIntervalStarts(int(timestamp))
+ now = int(time.time())
+ answer = answerBlob[12:]
except Exception as error:
logging.warn(error.message)
else:
- if solution.lower() == decrypted.lower():
+ # If the beginning of the 'next' interval (the interval
+ # after the one when the CAPTCHA timestamp was created)
+ # has already passed, then the CAPTCHA is stale.
+ if now >= then:
+ exp = schedule.fromUnixSeconds(then).isoformat(sep=' ')
+ raise CaptchaExpired("Solution %r was for a CAPTCHA "
+ "which already expired at %s."
+ % (solution, exp))
+ if solution.lower() == answer.lower():
return True
return False
def createChallenge(self, answer):
- """Encrypt-then-HMAC the CAPTCHA **answer**.
+ """Encrypt-then-HMAC a timestamp plus the CAPTCHA **answer**.
A challenge string consists of a URL-safe, base64-encoded string which
contains an ``HMAC`` concatenated with an ``ENC_BLOB``, in the
@@ -295,34 +318,48 @@ class GimpCaptcha(Captcha):
| | applying :func:`~crypto.getHMAC` to the | |
| | ``ENC_BLOB``. | |
+-------------+--------------------------------------------+----------+
- | ENC_BLOB | An encrypted ``ANSWER``, created with | varies |
+ | ENC_BLOB | An encrypted ``ANSWER_BLOB``, created with | varies |
| | a PKCS#1 OAEP-padded RSA :ivar:`publicKey`.| |
+-------------+--------------------------------------------+----------+
+ | ANSWER_BLOB | Contains the concatenated ``TIMESTAMP`` | varies |
+ | | and ``ANSWER``. | |
+ +-------------+--------------------------------------------+----------+
+ | TIMESTAMP | A Unix Epoch timestamp, in seconds, | 12 bytes |
+ | | left-padded with "0"s. | |
+ +-------------+--------------------------------------------+----------+
| ANSWER | A string containing answer to this | 8 bytes |
| | CAPTCHA :ivar:`image`. | |
+-------------+--------------------------------------------+----------+
The steps taken to produce a ``CHALLENGE`` are then:
- 1. Encrypt the ``ANSWER`` to :ivar:`publicKey` to create
- the ``ENC_BLOB``.
+ 1. Create a ``TIMESTAMP``, and pad it on the left with ``0``s to 12
+ bytes in length.
+
+ 2. Next, take the **answer** to this CAPTCHA :ivar:`image: and
+ concatenate the padded ``TIMESTAMP`` and the ``ANSWER``, forming
+ an ``ANSWER_BLOB``.
+
+ 3. Encrypt the resulting ``ANSWER_BLOB`` to :ivar:`publicKey` to
+ create the ``ENC_BLOB``.
- 2. Use the client-specific :ivar:`hmacKey` to apply the
+ 4. Use the client-specific :ivar:`hmacKey` to apply the
:func:`~crypto.getHMAC` function to the ``ENC_BLOB``, obtaining
an ``HMAC``.
- 3. Create the final ``CHALLENGE`` string by concatenating the
+ 5. Create the final ``CHALLENGE`` string by concatenating the
``HMAC`` and ``ENC_BLOB``, then base64-encoding the result.
:param str answer: The answer to a CAPTCHA.
:rtype: str
:returns: A challenge string.
"""
- encrypted = self.publicKey.encrypt(answer)
- hmac = crypto.getHMAC(self.hmacKey, encrypted)
- challenge = hmac + encrypted
- encoded = urlsafe_b64encode(challenge)
- return encoded
+ timestamp = str(int(time.time())).zfill(12)
+ blob = timestamp + answer
+ encBlob = self.publicKey.encrypt(blob)
+ hmac = crypto.getHMAC(self.hmacKey, encBlob)
+ challenge = urlsafe_b64encode(hmac + encBlob)
+ return challenge
def get(self):
"""Get a random CAPTCHA from the cache directory.
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits