"""Secure Authenticator Cookies
Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved."""
import logging
import re
import time
from http.cookies import CookieError, SimpleCookie
from M2Crypto import Rand, m2, util
from typing import Pattern, Optional, Tuple, Union # noqa
_MIX_FORMAT = "exp=%f&data=%s&digest="
_MIX_RE = re.compile(r"exp=(\d+\.\d+)&data=(.+)&digest=(\S*)")
log = logging.getLogger(__name__)
[docs]
def mix(expiry: float, data: str, format: str = _MIX_FORMAT) -> str:
return format % (expiry, data)
[docs]
def unmix(
dough: Union[str, bytes], regex: Pattern = _MIX_RE
) -> Optional[Tuple[float, str]]:
mo = regex.match(dough)
if mo:
return float(mo.group(1)), mo.group(2)
else:
return None
[docs]
def unmix3(
dough: Union[str, bytes], regex: Pattern = _MIX_RE
) -> Optional[Tuple[float, str, str]]:
mo = regex.match(dough)
if mo:
return float(mo.group(1)), mo.group(2), mo.group(3)
else:
return None
_TOKEN: str = "_M2AUTH_"
[docs]
class AuthCookie:
def __init__(self, expiry: float, data: str, dough: str, mac: 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.
[docs]
def expiry(self) -> float:
"""Return the cookie's expiry time."""
return self._expiry
[docs]
def data(self) -> str:
"""Return the data portion of the cookie."""
return self._data
[docs]
def mac(self) -> str:
"""Return the cookie's MAC."""
return self._mac
[docs]
def output(self, header: str = "Set-Cookie:") -> str:
"""Return the cookie's output in "Set-Cookie" format."""
return self._cookie.output(header=header)
[docs]
def value(self) -> str:
"""Return the cookie's output minus the "Set-Cookie: " portion."""
return self._cookie[_TOKEN].value
[docs]
def isExpired(self) -> bool:
"""Return 1 if the cookie has expired, 0 otherwise."""
return isinstance(self._expiry, (float, int)) 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.
[docs]
def name(self) -> str:
return self._name
[docs]
class AuthCookieJar:
_keylen: int = 20
def __init__(self) -> None:
self._key = Rand.rand_bytes(self._keylen)
def _hmac(self, key: bytes, data: str) -> str:
return util.bin_to_hex(m2.hmac(key, data.encode(), m2.sha256()))
[docs]
def makeCookie(self, expiry: float, data: str) -> AuthCookie:
"""
Make a cookie
:param expiry: expiration time (float in seconds)
:param data: cookie content
:return: AuthCookie object
"""
if not isinstance(expiry, (int, 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))
[docs]
def isGoodCookie(self, cookie: AuthCookie) -> bool:
if cookie.isExpired():
return False
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())
)
[docs]
def isGoodCookieString(
self, cookie_str: Union[dict, str], _debug: bool = False
) -> bool:
c = SimpleCookie()
try:
c.load(cookie_str)
except CookieError:
log.warning("Illegal characters in cookie string (CVE-2026-0672)")
return False
if _TOKEN not in c:
log.debug("_TOKEN not in c (keys = %s)", dir(c))
return False
undough = unmix3(c[_TOKEN].value)
if undough is None:
log.debug("undough is None")
return False
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)