CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

175 lines
5.4 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. # This file is a modified version of the file available at:
  17. # https://github.com/pyca/cryptography/blob/master/src/cryptography/fernet.py
  18. # This original file, part of the cryptography Python3 package
  19. # (https://cryptography.io/en/latest/) was dual licensed under the terms of the
  20. # Apache License, Version 2.0, and the BSD License. See
  21. # https://github.com/pyca/cryptography/blob/master/LICENSE for complete
  22. # details.
  23. """
  24. Fernet-style encryption forked from the
  25. [`cryptography`](https://cryptography.io/en/latest/) package. Implements
  26. Fernet encryption, but without the bade64-encoding step (produces raw binary
  27. data).
  28. """
  29. import binascii
  30. import os
  31. import struct
  32. import time
  33. import six
  34. from cryptography.exceptions import InvalidSignature
  35. from cryptography.hazmat.backends import default_backend
  36. from cryptography.hazmat.primitives import hashes, padding
  37. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  38. from cryptography.hazmat.primitives.hmac import HMAC
  39. _nodoc = {
  40. "InvalidSignature",
  41. "default_backend",
  42. "hashes",
  43. "padding",
  44. "Cipher",
  45. "algorithms",
  46. "modes",
  47. "HMAC",
  48. "InvalidToken",
  49. }
  50. class InvalidToken(Exception):
  51. pass
  52. _MAX_CLOCK_SKEW = 60
  53. class RawFernet(object):
  54. """
  55. Class (forked from the `Fernet` class in the `cryptography` package) that
  56. implements a raw binary form of Fernet encryption.
  57. """
  58. def __init__(self, key, backend=None):
  59. if backend is None:
  60. backend = default_backend()
  61. if len(key) != 32:
  62. raise ValueError("Fernet key must be 32 bytes.")
  63. self._signing_key = key[:16]
  64. self._encryption_key = key[16:]
  65. self._backend = backend
  66. @classmethod
  67. def generate_key(cls):
  68. return os.urandom(32)
  69. def encrypt(self, data):
  70. current_time = int(time.time())
  71. iv = os.urandom(16)
  72. return self._encrypt_from_parts(data, current_time, iv)
  73. def _encrypt_from_parts(self, data, current_time, iv):
  74. if not isinstance(data, bytes):
  75. raise TypeError("data must be bytes.")
  76. padder = padding.PKCS7(algorithms.AES.block_size).padder()
  77. padded_data = padder.update(data) + padder.finalize()
  78. encryptor = Cipher(
  79. algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend
  80. ).encryptor()
  81. ciphertext = encryptor.update(padded_data) + encryptor.finalize()
  82. basic_parts = b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext
  83. h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend)
  84. h.update(basic_parts)
  85. hmac = h.finalize()
  86. return basic_parts + hmac
  87. def decrypt(self, token, ttl=None):
  88. timestamp, data = RawFernet._get_unverified_token_data(token)
  89. return self._decrypt_data(data, timestamp, ttl)
  90. def extract_timestamp(self, token):
  91. timestamp, data = RawFernet._get_unverified_token_data(token)
  92. # Verify the token was not tampered with.
  93. self._verify_signature(data)
  94. return timestamp
  95. @staticmethod
  96. def _get_unverified_token_data(token):
  97. if not isinstance(token, bytes):
  98. raise TypeError("token must be bytes.")
  99. try:
  100. data = token
  101. except (TypeError, binascii.Error):
  102. raise InvalidToken
  103. if not data or six.indexbytes(data, 0) != 0x80:
  104. raise InvalidToken
  105. try:
  106. timestamp, = struct.unpack(">Q", data[1:9])
  107. except struct.error:
  108. raise InvalidToken
  109. return timestamp, data
  110. def _verify_signature(self, data):
  111. h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend)
  112. h.update(data[:-32])
  113. try:
  114. h.verify(data[-32:])
  115. except InvalidSignature:
  116. raise InvalidToken
  117. def _decrypt_data(self, data, timestamp, ttl):
  118. current_time = int(time.time())
  119. if ttl is not None:
  120. if timestamp + ttl < current_time:
  121. raise InvalidToken
  122. if current_time + _MAX_CLOCK_SKEW < timestamp:
  123. raise InvalidToken
  124. self._verify_signature(data)
  125. iv = data[9:25]
  126. ciphertext = data[25:-32]
  127. decryptor = Cipher(
  128. algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend
  129. ).decryptor()
  130. plaintext_padded = decryptor.update(ciphertext)
  131. try:
  132. plaintext_padded += decryptor.finalize()
  133. except ValueError:
  134. raise InvalidToken
  135. unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
  136. unpadded = unpadder.update(plaintext_padded)
  137. try:
  138. unpadded += unpadder.finalize()
  139. except ValueError:
  140. raise InvalidToken
  141. return unpadded