mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Change to using SHA-256 for the nonce
Also make nonce generation/validation code re-useable for other auth schemes.
This commit is contained in:
parent
25eb0e6100
commit
f6e4eaf375
@ -7,7 +7,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import binascii, os, random, struct, base64, httplib
|
import binascii, os, random, struct, base64, httplib
|
||||||
from hashlib import md5, sha1
|
from hashlib import md5, sha1, sha256
|
||||||
|
from itertools import permutations
|
||||||
|
|
||||||
from calibre.srv.errors import HTTPAuthRequired, HTTPSimpleResponse, InvalidCredentials
|
from calibre.srv.errors import HTTPAuthRequired, HTTPSimpleResponse, InvalidCredentials
|
||||||
from calibre.srv.http_request import parse_uri
|
from calibre.srv.http_request import parse_uri
|
||||||
@ -27,9 +28,39 @@ def md5_hex(s):
|
|||||||
def sha1_hex(s):
|
def sha1_hex(s):
|
||||||
return sha1(as_bytestring(s)).hexdigest().decode('ascii')
|
return sha1(as_bytestring(s)).hexdigest().decode('ascii')
|
||||||
|
|
||||||
|
def sha256_hex(s):
|
||||||
|
return sha256(as_bytestring(s)).hexdigest().decode('ascii')
|
||||||
|
|
||||||
def base64_decode(s):
|
def base64_decode(s):
|
||||||
return base64.standard_b64decode(as_bytestring(s)).decode('utf-8')
|
return base64.standard_b64decode(as_bytestring(s)).decode('utf-8')
|
||||||
|
|
||||||
|
def synthesize_nonce(key_order, realm, secret, timestamp=None):
|
||||||
|
'''
|
||||||
|
Create a nonce. Can be used for either digest or cookie based auth.
|
||||||
|
The nonce is of the form timestamp:hash with has being a hash of the
|
||||||
|
timestamp, server secret and realm. This allows the timestamp to be
|
||||||
|
validated and stale nonce's to be rejected.
|
||||||
|
'''
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = binascii.hexlify(struct.pack(b'!d', float(monotonic())))
|
||||||
|
h = sha256_hex(key_order.format(timestamp, realm, secret))
|
||||||
|
nonce = ':'.join((timestamp, h))
|
||||||
|
return nonce
|
||||||
|
|
||||||
|
def validate_nonce(key_order, nonce, realm, secret):
|
||||||
|
timestamp, hashpart = nonce.partition(':')[::2]
|
||||||
|
s_nonce = synthesize_nonce(key_order, realm, secret, timestamp)
|
||||||
|
return s_nonce == nonce
|
||||||
|
|
||||||
|
def is_nonce_stale(nonce, max_age_seconds=MAX_AGE_SECONDS):
|
||||||
|
try:
|
||||||
|
timestamp = struct.unpack(b'!d', binascii.unhexlify(as_bytestring(nonce.partition(':')[0])))[0]
|
||||||
|
return timestamp + max_age_seconds < monotonic()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DigestAuth(object): # {{{
|
class DigestAuth(object): # {{{
|
||||||
|
|
||||||
valid_algorithms = {'MD5', 'MD5-SESS'}
|
valid_algorithms = {'MD5', 'MD5-SESS'}
|
||||||
@ -64,31 +95,6 @@ class DigestAuth(object): # {{{
|
|||||||
if self.cnonce or self.nonce_count:
|
if self.cnonce or self.nonce_count:
|
||||||
raise HTTPSimpleResponse(httplib.BAD_REQUEST, 'qop missing')
|
raise HTTPSimpleResponse(httplib.BAD_REQUEST, 'qop missing')
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def synthesize_nonce(realm, secret, timestamp=None):
|
|
||||||
'''Create a nonce for HTTP Digest AUTH.
|
|
||||||
The nonce is of the form timestamp:hash with has being a hash of the
|
|
||||||
timestamp, server secret and realm. This allows the timestamp to be
|
|
||||||
validated and stale nonce's to be rejected.'''
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = binascii.hexlify(struct.pack(b'!d', float(monotonic())))
|
|
||||||
h = sha1_hex(':'.join((timestamp, realm, secret)))
|
|
||||||
nonce = ':'.join((timestamp, h))
|
|
||||||
return nonce
|
|
||||||
|
|
||||||
def validate_nonce(self, realm, secret):
|
|
||||||
timestamp, hashpart = self.nonce.partition(':')[::2]
|
|
||||||
s_nonce = DigestAuth.synthesize_nonce(realm, secret, timestamp)
|
|
||||||
return s_nonce == self.nonce
|
|
||||||
|
|
||||||
def is_nonce_stale(self, max_age_seconds=MAX_AGE_SECONDS):
|
|
||||||
try:
|
|
||||||
timestamp = struct.unpack(b'!d', binascii.unhexlify(as_bytestring(self.nonce.partition(':')[0])))[0]
|
|
||||||
return timestamp + max_age_seconds < monotonic()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return True
|
|
||||||
|
|
||||||
def H(self, val):
|
def H(self, val):
|
||||||
return md5_hex(val)
|
return md5_hex(val)
|
||||||
|
|
||||||
@ -149,12 +155,38 @@ class DigestAuth(object): # {{{
|
|||||||
|
|
||||||
class AuthController(object):
|
class AuthController(object):
|
||||||
|
|
||||||
|
'''
|
||||||
|
Implement Basic/Digest authentication for the content server. Android browsers
|
||||||
|
cannot handle HTTP AUTH when downloading files, as the download is handed
|
||||||
|
off to a separate process. So we use a cookie based authentication scheme
|
||||||
|
for some endpoints (/get) to allow downloads to work on android. Apparently,
|
||||||
|
cookies are passed to the download process. The cookie expires after
|
||||||
|
MAX_AGE_SECONDS.
|
||||||
|
|
||||||
|
The android browser appears to send a GET request to the server and only if
|
||||||
|
that request succeeds is the download handed off to the download process.
|
||||||
|
We could reduce MAX_AGE_SECONDS, but we leave it high as the download
|
||||||
|
process might have downloads queued and therefore not start the download
|
||||||
|
immediately.
|
||||||
|
|
||||||
|
Note that this makes the server vulnerable to session-hijacking (i.e. some
|
||||||
|
one can sniff the traffic and create their own requests to /get with the
|
||||||
|
appropriate cookie, for an hour). The fix is to use https, but since this
|
||||||
|
is usually run as a private server, that cannot be done. If you care about
|
||||||
|
this vulnerability, run the server behind a reverse proxy that uses HTTPS.
|
||||||
|
|
||||||
|
Also, note that digest auth is itself vulnerable to partial session
|
||||||
|
hijacking, since we have to ignore repeated nc values, because Firefox does
|
||||||
|
not implement the digest auth spec properly (it sends out of order nc
|
||||||
|
values).
|
||||||
|
'''
|
||||||
|
|
||||||
def __init__(self, user_credentials=None, prefer_basic_auth=False, realm='calibre', max_age_seconds=MAX_AGE_SECONDS, log=None):
|
def __init__(self, user_credentials=None, prefer_basic_auth=False, realm='calibre', max_age_seconds=MAX_AGE_SECONDS, log=None):
|
||||||
self.user_credentials, self.prefer_basic_auth = user_credentials, prefer_basic_auth
|
self.user_credentials, self.prefer_basic_auth = user_credentials, prefer_basic_auth
|
||||||
self.log = log
|
self.log = log
|
||||||
self.secret = binascii.hexlify(os.urandom(random.randint(20, 30))).decode('ascii')
|
self.secret = binascii.hexlify(os.urandom(random.randint(20, 30))).decode('ascii')
|
||||||
self.max_age_seconds = max_age_seconds
|
self.max_age_seconds = max_age_seconds
|
||||||
self.key_order = random.choice(('{0}:{1}', '{1}:{0}'))
|
self.key_order = '{%d}:{%d}:{%d}' % random.choice(tuple(permutations((0,1,2))))
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
if '"' in realm:
|
if '"' in realm:
|
||||||
raise ValueError('Double-quotes are not allowed in the authentication realm')
|
raise ValueError('Double-quotes are not allowed in the authentication realm')
|
||||||
@ -184,10 +216,10 @@ class AuthController(object):
|
|||||||
scheme = scheme.lower()
|
scheme = scheme.lower()
|
||||||
if scheme == 'digest':
|
if scheme == 'digest':
|
||||||
da = DigestAuth(rest.strip())
|
da = DigestAuth(rest.strip())
|
||||||
if da.validate_nonce(self.realm, self.secret):
|
if validate_nonce(self.key_order, da.nonce, self.realm, self.secret):
|
||||||
pw = self.user_credentials.get(da.username)
|
pw = self.user_credentials.get(da.username)
|
||||||
if pw and da.validate_request(pw, data, self.log):
|
if pw and da.validate_request(pw, data, self.log):
|
||||||
nonce_is_stale = da.is_nonce_stale(self.max_age_seconds)
|
nonce_is_stale = is_nonce_stale(da.nonce, self.max_age_seconds)
|
||||||
if not nonce_is_stale:
|
if not nonce_is_stale:
|
||||||
data.username = da.username
|
data.username = da.username
|
||||||
return
|
return
|
||||||
@ -212,7 +244,7 @@ class AuthController(object):
|
|||||||
raise HTTPAuthRequired('Basic realm="%s"' % self.realm, log=log_msg)
|
raise HTTPAuthRequired('Basic realm="%s"' % self.realm, log=log_msg)
|
||||||
|
|
||||||
s = 'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"' % (
|
s = 'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"' % (
|
||||||
self.realm, DigestAuth.synthesize_nonce(self.realm, self.secret))
|
self.realm, synthesize_nonce(self.key_order, self.realm, self.secret))
|
||||||
if nonce_is_stale:
|
if nonce_is_stale:
|
||||||
s += ', stale="true"'
|
s += ', stale="true"'
|
||||||
raise HTTPAuthRequired(s, log=log_msg)
|
raise HTTPAuthRequired(s, log=log_msg)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user