Skip to content

Commit

Permalink
Merge pull request #150 from coffeemakr/fallback_to_password_auth
Browse files Browse the repository at this point in the history
Fallback to password authentication
  • Loading branch information
meejah committed Dec 27, 2015
2 parents 96cdeac + 4e7f8e2 commit 3ba6a07
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 42 deletions.
65 changes: 64 additions & 1 deletion test/test_torcontrolprotocol.py
Expand Up @@ -335,14 +335,77 @@ def test_authenticate_safecookie(self):
)
self.assertTrue('AUTHENTICATE ' in self.transport.value())

def test_authenticate_cookie_without_reading(self):
server_nonce = str(bytearray([0] * 32))
server_hash = str(bytearray([0] * 32))
try:
self.protocol._safecookie_authchallenge(
'250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' %
(base64.b16encode(server_hash), base64.b16encode(server_nonce))
)
self.assertTrue(False)
except RuntimeError, e:
self.assertTrue('not read' in str(e))

def test_authenticate_unexisting_cookie_file(self):
unexisting_file = __file__ + "-unexisting"
try:
self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % unexisting_file)
self.assertTrue(False)
except RuntimeError:
pass

def test_authenticate_unexisting_safecookie_file(self):
unexisting_file = __file__ + "-unexisting"
try:
self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % unexisting_file)
self.assertTrue(False)
except RuntimeError:
pass

def test_authenticate_dont_send_cookiefile(self):
try:
self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE
VERSION Tor="0.2.2.35"
OK''')
self.assertTrue(False)
except RuntimeError:
pass

def test_authenticate_password_when_cookie_unavailable(self):
unexisting_file = __file__ + "-unexisting"
self.protocol.password_function = lambda: 'foo'
self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % unexisting_file)
self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))


def test_authenticate_password_when_safecookie_unavailable(self):
unexisting_file = __file__ + "-unexisting"
self.protocol.password_function = lambda: 'foo'
self.protocol._do_authenticate('''PROTOCOLINFO 1
AUTH METHODS=SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="%s"
VERSION Tor="0.2.2.35"
OK''' % unexisting_file)
self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))

def test_authenticate_safecookie_wrong_hash(self):
cookiedata = str(bytearray([0] * 32))
server_nonce = str(bytearray([0] * 32))
server_hash = str(bytearray([0] * 32))

# pretend we already did PROTOCOLINFO and read the cookie
# file
self.protocol.cookie_data = cookiedata
self.protocol._cookie_data = cookiedata
self.protocol.client_nonce = server_nonce # all 0's anyway
try:
self.protocol._safecookie_authchallenge(
Expand Down
2 changes: 1 addition & 1 deletion test/test_torinfo.py
Expand Up @@ -73,7 +73,7 @@ def test_with_arg(self):

# answer all the requests generated by TorControlProtocol
# boostrapping etc.
self.send('250-AUTH METHODS=PASSWORD')
self.send('250-AUTH METHODS=HASHEDPASSWORD')
self.send('250 OK')

# response to AUTHENTICATE
Expand Down
100 changes: 60 additions & 40 deletions txtorcon/torcontrolprotocol.py
Expand Up @@ -214,6 +214,9 @@ def __init__(self, password_function=None):
authentication to Tor (default is to use COOKIE, however). May
return Deferred."""

self._cookie_data = None
"""Data read from cookie file used to authenticate."""

self.version = None
"""Version of Tor we've connected to."""

Expand Down Expand Up @@ -616,15 +619,16 @@ def _safecookie_authchallenge(self, reply):
"""
Callback on AUTHCHALLENGE SAFECOOKIE
"""

if self._cookie_data is None:
raise RuntimeError("Cookie data not read.")
kw = parse_keywords(reply.replace(' ', '\n'))

server_hash = base64.b16decode(kw['SERVERHASH'])
server_nonce = base64.b16decode(kw['SERVERNONCE'])
# FIXME put string in global. or something.
expected_server_hash = hmac_sha256(
"Tor safe cookie authentication server-to-controller hash",
self.cookie_data + self.client_nonce + server_nonce
self._cookie_data + self.client_nonce + server_nonce
)

if not compare_via_hash(expected_server_hash, server_hash):
Expand All @@ -636,18 +640,32 @@ def _safecookie_authchallenge(self, reply):

client_hash = hmac_sha256(
"Tor safe cookie authentication controller-to-server hash",
self.cookie_data + self.client_nonce + server_nonce
self._cookie_data + self.client_nonce + server_nonce
)
client_hash_hex = base64.b16encode(client_hash)
return self.queue_command('AUTHENTICATE %s' % client_hash_hex)

def _read_cookie(self, cookiefile):
"""
Open and read a cookie file
:param cookie: Path to the cookie file
"""
self._cookie_data = None
self._cookie_data = open(cookiefile, 'rb').read()
if len(self._cookie_data) != 32:
raise RuntimeError(
"Expected authentication cookie to be 32 bytes, got %d" %
len(self._cookie_data)
)

def _do_authenticate(self, protoinfo):
"""
Callback on PROTOCOLINFO to actually authenticate once we know
what's supported.
"""

methods = None
cookie_auth = False
for line in protoinfo.split('\n'):
if line[:5] == 'AUTH ':
kw = parse_keywords(line[5:].replace(' ', '\n'))
Expand All @@ -657,43 +675,45 @@ def _do_authenticate(self, protoinfo):
"Didn't find AUTH line in PROTOCOLINFO response."
)

if 'SAFECOOKIE' in methods:
cookie = re.search('COOKIEFILE="(.*)"', protoinfo).group(1)
self.cookie_data = open(cookie, 'r').read()
if len(self.cookie_data) != 32:
raise RuntimeError(
"Expected authentication cookie to be 32 bytes, got %d" %
len(self.cookie_data)
)
txtorlog.msg("Using SAFECOOKIE authentication", cookie,
len(self.cookie_data), "bytes")
self.client_nonce = os.urandom(32)

cmd = 'AUTHCHALLENGE SAFECOOKIE ' + \
base64.b16encode(self.client_nonce)
d = self.queue_command(cmd)
d.addCallback(self._safecookie_authchallenge)
d.addCallback(self._bootstrap)
d.addErrback(self._auth_failed)
return

elif 'COOKIE' in methods:
cookie = re.search('COOKIEFILE="(.*)"', protoinfo).group(1)
with open(cookie, 'r') as cookiefile:
data = cookiefile.read()
if len(data) != 32:
raise RuntimeError(
"Expected authentication cookie to be 32 "
"bytes, got %d instead." % len(data)
)
txtorlog.msg("Using COOKIE authentication",
cookie, len(data), "bytes")
d = self.authenticate(data)
d.addCallback(self._bootstrap)
d.addErrback(self._auth_failed)
return

if self.password_function:
if 'SAFECOOKIE' in methods or 'COOKIE' in methods:
cookiefile_match = re.search(r'COOKIEFILE="((?:[^"\\]|\\.)*)"', protoinfo)
if cookiefile_match:
cookiefile = cookiefile_match.group(1)
cookiefile = cookiefile.replace('\\\\', '\\')
cookiefile = cookiefile.replace('\\"', '"')
try:
self._read_cookie(cookiefile)
except IOError as why:
txtorlog.msg("Reading COOKIEFILE failed: " + str(why))
cookie_auth = False
else:
cookie_auth = True
else:
txtorlog.msg("Didn't get COOKIEFILE")

if cookie_auth:
if 'SAFECOOKIE' in methods:
txtorlog.msg("Using SAFECOOKIE authentication", cookiefile,
len(self._cookie_data), "bytes")
self.client_nonce = os.urandom(32)

cmd = 'AUTHCHALLENGE SAFECOOKIE ' + \
base64.b16encode(self.client_nonce)
d = self.queue_command(cmd)
d.addCallback(self._safecookie_authchallenge)
d.addCallback(self._bootstrap)
d.addErrback(self._auth_failed)
return

elif 'COOKIE' in methods:
txtorlog.msg("Using COOKIE authentication",
cookiefile, len(self._cookie_data), "bytes")
d = self.authenticate(self._cookie_data)
d.addCallback(self._bootstrap)
d.addErrback(self._auth_failed)
return

if self.password_function and 'HASHEDPASSWORD' in methods:
d = defer.maybeDeferred(self.password_function)
d.addCallback(self._do_password_authentication)
d.addErrback(self._auth_failed)
Expand Down

0 comments on commit 3ba6a07

Please sign in to comment.