Format: text/x-python
⬇ Download File (7.9 KB)
   1  from __future__ import annotations
   2  
   3  import collections.abc as cabc
   4  import time
   5  import typing as t
   6  from datetime import datetime
   7  from datetime import timezone
   8  
   9  from .encoding import base64_decode
  10  from .encoding import base64_encode
  11  from .encoding import bytes_to_int
  12  from .encoding import int_to_bytes
  13  from .encoding import want_bytes
  14  from .exc import BadSignature
  15  from .exc import BadTimeSignature
  16  from .exc import SignatureExpired
  17  from .serializer import _TSerialized
  18  from .serializer import Serializer
  19  from .signer import Signer
  20  
  21  
  22  class TimestampSigner(Signer):
  23      """Works like the regular :class:`.Signer` but also records the time
  24      of the signing and can be used to expire signatures. The
  25      :meth:`unsign` method can raise :exc:`.SignatureExpired` if the
  26      unsigning failed because the signature is expired.
  27      """
  28  
  29      def get_timestamp(self) -> int:
  30          """Returns the current timestamp. The function must return an
  31          integer.
  32          """
  33          return int(time.time())
  34  
  35      def timestamp_to_datetime(self, ts: int) -> datetime:
  36          """Convert the timestamp from :meth:`get_timestamp` into an
  37          aware :class`datetime.datetime` in UTC.
  38  
  39          .. versionchanged:: 2.0
  40              The timestamp is returned as a timezone-aware ``datetime``
  41              in UTC rather than a naive ``datetime`` assumed to be UTC.
  42          """
  43          return datetime.fromtimestamp(ts, tz=timezone.utc)
  44  
  45      def sign(self, value: str | bytes) -> bytes:
  46          """Signs the given string and also attaches time information."""
  47          value = want_bytes(value)
  48          timestamp = base64_encode(int_to_bytes(self.get_timestamp()))
  49          sep = want_bytes(self.sep)
  50          value = value + sep + timestamp
  51          return value + sep + self.get_signature(value)
  52  
  53      # Ignore overlapping signatures check, return_timestamp is the only
  54      # parameter that affects the return type.
  55  
  56      @t.overload
  57      def unsign(  # type: ignore[overload-overlap]
  58          self,
  59          signed_value: str | bytes,
  60          max_age: int | None = None,
  61          return_timestamp: t.Literal[False] = False,
  62      ) -> bytes: ...
  63  
  64      @t.overload
  65      def unsign(
  66          self,
  67          signed_value: str | bytes,
  68          max_age: int | None = None,
  69          return_timestamp: t.Literal[True] = True,
  70      ) -> tuple[bytes, datetime]: ...
  71  
  72      def unsign(
  73          self,
  74          signed_value: str | bytes,
  75          max_age: int | None = None,
  76          return_timestamp: bool = False,
  77      ) -> tuple[bytes, datetime] | bytes:
  78          """Works like the regular :meth:`.Signer.unsign` but can also
  79          validate the time. See the base docstring of the class for
  80          the general behavior. If ``return_timestamp`` is ``True`` the
  81          timestamp of the signature will be returned as an aware
  82          :class:`datetime.datetime` object in UTC.
  83  
  84          .. versionchanged:: 2.0
  85              The timestamp is returned as a timezone-aware ``datetime``
  86              in UTC rather than a naive ``datetime`` assumed to be UTC.
  87          """
  88          try:
  89              result = super().unsign(signed_value)
  90              sig_error = None
  91          except BadSignature as e:
  92              sig_error = e
  93              result = e.payload or b""
  94  
  95          sep = want_bytes(self.sep)
  96  
  97          # If there is no timestamp in the result there is something
  98          # seriously wrong. In case there was a signature error, we raise
  99          # that one directly, otherwise we have a weird situation in
 100          # which we shouldn't have come except someone uses a time-based
 101          # serializer on non-timestamp data, so catch that.
 102          if sep not in result:
 103              if sig_error:
 104                  raise sig_error
 105  
 106              raise BadTimeSignature("timestamp missing", payload=result)
 107  
 108          value, ts_bytes = result.rsplit(sep, 1)
 109          ts_int: int | None = None
 110          ts_dt: datetime | None = None
 111  
 112          try:
 113              ts_int = bytes_to_int(base64_decode(ts_bytes))
 114          except Exception:
 115              pass
 116  
 117          # Signature is *not* okay. Raise a proper error now that we have
 118          # split the value and the timestamp.
 119          if sig_error is not None:
 120              if ts_int is not None:
 121                  try:
 122                      ts_dt = self.timestamp_to_datetime(ts_int)
 123                  except (ValueError, OSError, OverflowError) as exc:
 124                      # Windows raises OSError
 125                      # 32-bit raises OverflowError
 126                      raise BadTimeSignature(
 127                          "Malformed timestamp", payload=value
 128                      ) from exc
 129  
 130              raise BadTimeSignature(str(sig_error), payload=value, date_signed=ts_dt)
 131  
 132          # Signature was okay but the timestamp is actually not there or
 133          # malformed. Should not happen, but we handle it anyway.
 134          if ts_int is None:
 135              raise BadTimeSignature("Malformed timestamp", payload=value)
 136  
 137          # Check timestamp is not older than max_age
 138          if max_age is not None:
 139              age = self.get_timestamp() - ts_int
 140  
 141              if age > max_age:
 142                  raise SignatureExpired(
 143                      f"Signature age {age} > {max_age} seconds",
 144                      payload=value,
 145                      date_signed=self.timestamp_to_datetime(ts_int),
 146                  )
 147  
 148              if age < 0:
 149                  raise SignatureExpired(
 150                      f"Signature age {age} < 0 seconds",
 151                      payload=value,
 152                      date_signed=self.timestamp_to_datetime(ts_int),
 153                  )
 154  
 155          if return_timestamp:
 156              return value, self.timestamp_to_datetime(ts_int)
 157  
 158          return value
 159  
 160      def validate(self, signed_value: str | bytes, max_age: int | None = None) -> bool:
 161          """Only validates the given signed value. Returns ``True`` if
 162          the signature exists and is valid."""
 163          try:
 164              self.unsign(signed_value, max_age=max_age)
 165              return True
 166          except BadSignature:
 167              return False
 168  
 169  
 170  class TimedSerializer(Serializer[_TSerialized]):
 171      """Uses :class:`TimestampSigner` instead of the default
 172      :class:`.Signer`.
 173      """
 174  
 175      default_signer: type[TimestampSigner] = TimestampSigner
 176  
 177      def iter_unsigners(
 178          self, salt: str | bytes | None = None
 179      ) -> cabc.Iterator[TimestampSigner]:
 180          return t.cast("cabc.Iterator[TimestampSigner]", super().iter_unsigners(salt))
 181  
 182      # TODO: Signature is incompatible because parameters were added
 183      #  before salt.
 184  
 185      def loads(  # type: ignore[override]
 186          self,
 187          s: str | bytes,
 188          max_age: int | None = None,
 189          return_timestamp: bool = False,
 190          salt: str | bytes | None = None,
 191      ) -> t.Any:
 192          """Reverse of :meth:`dumps`, raises :exc:`.BadSignature` if the
 193          signature validation fails. If a ``max_age`` is provided it will
 194          ensure the signature is not older than that time in seconds. In
 195          case the signature is outdated, :exc:`.SignatureExpired` is
 196          raised. All arguments are forwarded to the signer's
 197          :meth:`~TimestampSigner.unsign` method.
 198          """
 199          s = want_bytes(s)
 200          last_exception = None
 201  
 202          for signer in self.iter_unsigners(salt):
 203              try:
 204                  base64d, timestamp = signer.unsign(
 205                      s, max_age=max_age, return_timestamp=True
 206                  )
 207                  payload = self.load_payload(base64d)
 208  
 209                  if return_timestamp:
 210                      return payload, timestamp
 211  
 212                  return payload
 213              except SignatureExpired:
 214                  # The signature was unsigned successfully but was
 215                  # expired. Do not try the next signer.
 216                  raise
 217              except BadSignature as err:
 218                  last_exception = err
 219  
 220          raise t.cast(BadSignature, last_exception)
 221  
 222      def loads_unsafe(  # type: ignore[override]
 223          self,
 224          s: str | bytes,
 225          max_age: int | None = None,
 226          salt: str | bytes | None = None,
 227      ) -> tuple[bool, t.Any]:
 228          return self._loads_unsafe_impl(s, salt, load_kwargs={"max_age": max_age})
 229