summaryrefslogtreecommitdiff
path: root/src/M2Crypto/AuthCookie.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/M2Crypto/AuthCookie.py')
-rw-r--r--src/M2Crypto/AuthCookie.py168
1 files changed, 168 insertions, 0 deletions
diff --git a/src/M2Crypto/AuthCookie.py b/src/M2Crypto/AuthCookie.py
new file mode 100644
index 0000000..ff6df2b
--- /dev/null
+++ b/src/M2Crypto/AuthCookie.py
@@ -0,0 +1,168 @@
+from __future__ import absolute_import
+
+"""Secure Authenticator Cookies
+
+Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved."""
+
+import logging
+import re
+import time
+
+from M2Crypto import Rand, m2, six, util
+from M2Crypto.six.moves.http_cookies import SimpleCookie
+
+from typing import re as type_re, AnyStr, Optional, Union # noqa
+
+_MIX_FORMAT = 'exp=%f&data=%s&digest='
+_MIX_RE = re.compile(r'exp=(\d+\.\d+)&data=(.+)&digest=(\S*)')
+
+log = logging.getLogger(__name__)
+
+
+def mix(expiry, data, format=_MIX_FORMAT):
+ # type: (float, AnyStr, str) -> AnyStr
+ return format % (expiry, data)
+
+
+def unmix(dough, regex=_MIX_RE):
+ # type: (AnyStr, type_re) -> object
+ mo = regex.match(dough)
+ if mo:
+ return float(mo.group(1)), mo.group(2)
+ else:
+ return None
+
+
+def unmix3(dough, regex=_MIX_RE):
+ # type: (AnyStr, type_re) -> Optional[tuple[float, AnyStr, AnyStr]]
+ mo = regex.match(dough)
+ if mo:
+ return float(mo.group(1)), mo.group(2), mo.group(3)
+ else:
+ return None
+
+
+_TOKEN = '_M2AUTH_' # type: str
+
+
+class AuthCookieJar(object):
+
+ _keylen = 20 # type: int
+
+ def __init__(self):
+ # type: () -> None
+ self._key = Rand.rand_bytes(self._keylen)
+
+ def _hmac(self, key, data):
+ # type: (bytes, str) -> str
+ return util.bin_to_hex(m2.hmac(key, six.ensure_binary(data), m2.sha1()))
+
+ def makeCookie(self, expiry, data):
+ # type: (float, str) -> AuthCookie
+ """
+ Make a cookie
+
+ :param expiry: expiration time (float in seconds)
+ :param data: cookie content
+ :return: AuthCookie object
+ """
+ if not isinstance(expiry, (six.integer_types, float)):
+ raise ValueError('Expiration time must be number, not "%s' % expiry)
+ dough = mix(expiry, data)
+ return AuthCookie(expiry, data, dough, self._hmac(self._key, dough))
+
+ def isGoodCookie(self, cookie):
+ # type: (AuthCookie) -> Union[bool, int]
+ assert isinstance(cookie, AuthCookie)
+ if cookie.isExpired():
+ return 0
+ c = self.makeCookie(cookie._expiry, cookie._data)
+ return (c._expiry == cookie._expiry) \
+ and (c._data == cookie._data) \
+ and (c._mac == cookie._mac) \
+ and (c.output() == cookie.output())
+
+ def isGoodCookieString(self, cookie_str, _debug=False):
+ # type: (Union[dict, bytes], bool) -> Union[bool, int]
+ c = SimpleCookie()
+ c.load(cookie_str)
+ if _TOKEN not in c:
+ log.debug('_TOKEN not in c (keys = %s)', dir(c))
+ return 0
+ undough = unmix3(c[_TOKEN].value)
+ if undough is None:
+ log.debug('undough is None')
+ return 0
+ exp, data, mac = undough
+ c2 = self.makeCookie(exp, data)
+ if _debug and (c2._mac == mac):
+ log.error('cookie_str = %s', cookie_str)
+ log.error('c2.isExpired = %s', c2.isExpired())
+ log.error('mac = %s', mac)
+ log.error('c2._mac = %s', c2._mac)
+ log.error('c2._mac == mac: %s', str(c2._mac == mac))
+ return (not c2.isExpired()) and (c2._mac == mac)
+
+
+class AuthCookie(object):
+
+ def __init__(self, expiry, data, dough, mac):
+ # type: (float, str, str, str) -> None
+ """
+ Create new authentication cookie
+
+ :param expiry: expiration time (in seconds)
+ :param data: cookie payload (as a string)
+ :param dough: expiry & data concatenated to URL compliant
+ string
+ :param mac: SHA1-based HMAC of dough and random key
+ """
+ self._expiry = expiry
+ self._data = data
+ self._mac = mac
+ self._cookie = SimpleCookie()
+ self._cookie[_TOKEN] = '%s%s' % (dough, mac)
+ self._name = '%s%s' % (dough, mac) # WebKit only.
+
+ def expiry(self):
+ # type: () -> float
+ """Return the cookie's expiry time."""
+ return self._expiry
+
+ def data(self):
+ # type: () -> str
+ """Return the data portion of the cookie."""
+ return self._data
+
+ def mac(self):
+ # type: () -> str
+ """Return the cookie's MAC."""
+ return self._mac
+
+ def output(self, header="Set-Cookie:"):
+ # type: (Optional[str]) -> str
+ """Return the cookie's output in "Set-Cookie" format."""
+ return self._cookie.output(header=header)
+
+ def value(self):
+ # type: () -> str
+ """Return the cookie's output minus the "Set-Cookie: " portion.
+ """
+ return self._cookie[_TOKEN].value
+
+ def isExpired(self):
+ # type: () -> bool
+ """Return 1 if the cookie has expired, 0 otherwise."""
+ return isinstance(self._expiry, (float, six.integer_types)) and \
+ (time.time() > self._expiry)
+
+ # Following two methods are for WebKit only.
+ # I may wish to push them to WKAuthCookie, but they are part
+ # of the API now. Oh well.
+ def name(self):
+ # type: () -> str
+ return self._name
+
+ def headerValue(self):
+ # type: () -> str
+ return self.value()