Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/signing.py: 37%

114 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2Functions for creating and restoring url-safe signed JSON objects. 

3 

4The format used looks like this: 

5 

6>>> signing.dumps("hello") 

7'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk' 

8 

9There are two components here, separated by a ':'. The first component is a 

10URLsafe base64 encoded JSON of the object passed to dumps(). The second 

11component is a base64 encoded hmac/SHA1 hash of "$first_component:$secret" 

12 

13signing.loads(s) checks the signature and returns the deserialized object. 

14If the signature fails, a BadSignature exception is raised. 

15 

16>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk") 

17'hello' 

18>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified") 

19... 

20BadSignature: Signature failed: ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified 

21 

22You can optionally compress the JSON prior to base64 encoding it to save 

23space, using the compress=True argument. This checks if compression actually 

24helps and only applies compression if the result is a shorter string: 

25 

26>>> signing.dumps(list(range(1, 20)), compress=True) 

27'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ' 

28 

29The fact that the string is compressed is signalled by the prefixed '.' at the 

30start of the base64 JSON. 

31 

32There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'. 

33These functions make use of all of them. 

34""" 

35 

36import base64 

37import datetime 

38import json 

39import time 

40import zlib 

41 

42from django.conf import settings 

43from django.utils.crypto import constant_time_compare, salted_hmac 

44from django.utils.encoding import force_bytes 

45from django.utils.module_loading import import_string 

46from django.utils.regex_helper import _lazy_re_compile 

47 

48_SEP_UNSAFE = _lazy_re_compile(r"^[A-z0-9-_=]*$") 

49BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 

50 

51 

52class BadSignature(Exception): 

53 """Signature does not match.""" 

54 

55 pass 

56 

57 

58class SignatureExpired(BadSignature): 

59 """Signature timestamp is older than required max_age.""" 

60 

61 pass 

62 

63 

64def b62_encode(s): 

65 if s == 0: 

66 return "0" 

67 sign = "-" if s < 0 else "" 

68 s = abs(s) 

69 encoded = "" 

70 while s > 0: 

71 s, remainder = divmod(s, 62) 

72 encoded = BASE62_ALPHABET[remainder] + encoded 

73 return sign + encoded 

74 

75 

76def b62_decode(s): 

77 if s == "0": 

78 return 0 

79 sign = 1 

80 if s[0] == "-": 

81 s = s[1:] 

82 sign = -1 

83 decoded = 0 

84 for digit in s: 

85 decoded = decoded * 62 + BASE62_ALPHABET.index(digit) 

86 return sign * decoded 

87 

88 

89def b64_encode(s): 

90 return base64.urlsafe_b64encode(s).strip(b"=") 

91 

92 

93def b64_decode(s): 

94 pad = b"=" * (-len(s) % 4) 

95 return base64.urlsafe_b64decode(s + pad) 

96 

97 

98def base64_hmac(salt, value, key, algorithm="sha1"): 

99 return b64_encode( 

100 salted_hmac(salt, value, key, algorithm=algorithm).digest() 

101 ).decode() 

102 

103 

104def get_cookie_signer(salt="django.core.signing.get_cookie_signer"): 

105 Signer = import_string(settings.SIGNING_BACKEND) 

106 key = force_bytes(settings.SECRET_KEY) # SECRET_KEY may be str or bytes. 

107 return Signer(b"django.http.cookies" + key, salt=salt) 

108 

109 

110class JSONSerializer: 

111 """ 

112 Simple wrapper around json to be used in signing.dumps and 

113 signing.loads. 

114 """ 

115 

116 def dumps(self, obj): 

117 return json.dumps(obj, separators=(",", ":")).encode("latin-1") 

118 

119 def loads(self, data): 

120 return json.loads(data.decode("latin-1")) 

121 

122 

123def dumps( 

124 obj, key=None, salt="django.core.signing", serializer=JSONSerializer, compress=False 

125): 

126 """ 

127 Return URL-safe, hmac signed base64 compressed JSON string. If key is 

128 None, use settings.SECRET_KEY instead. The hmac algorithm is the default 

129 Signer algorithm. 

130 

131 If compress is True (not the default), check if compressing using zlib can 

132 save some space. Prepend a '.' to signify compression. This is included 

133 in the signature, to protect against zip bombs. 

134 

135 Salt can be used to namespace the hash, so that a signed string is 

136 only valid for a given namespace. Leaving this at the default 

137 value or re-using a salt value across different parts of your 

138 application without good cause is a security risk. 

139 

140 The serializer is expected to return a bytestring. 

141 """ 

142 return TimestampSigner(key, salt=salt).sign_object( 

143 obj, serializer=serializer, compress=compress 

144 ) 

145 

146 

147def loads( 

148 s, key=None, salt="django.core.signing", serializer=JSONSerializer, max_age=None 

149): 

150 """ 

151 Reverse of dumps(), raise BadSignature if signature fails. 

152 

153 The serializer is expected to accept a bytestring. 

154 """ 

155 return TimestampSigner(key, salt=salt).unsign_object( 

156 s, serializer=serializer, max_age=max_age 

157 ) 

158 

159 

160class Signer: 

161 def __init__(self, key=None, sep=":", salt=None, algorithm=None): 

162 self.key = key or settings.SECRET_KEY 

163 self.sep = sep 

164 if _SEP_UNSAFE.match(self.sep): 164 ↛ 165line 164 didn't jump to line 165, because the condition on line 164 was never true

165 raise ValueError( 

166 "Unsafe Signer separator: %r (cannot be empty or consist of " 

167 "only A-z0-9-_=)" % sep, 

168 ) 

169 self.salt = salt or "%s.%s" % ( 

170 self.__class__.__module__, 

171 self.__class__.__name__, 

172 ) 

173 self.algorithm = algorithm or "sha256" 

174 

175 def signature(self, value): 

176 return base64_hmac( 

177 self.salt + "signer", value, self.key, algorithm=self.algorithm 

178 ) 

179 

180 def sign(self, value): 

181 return "%s%s%s" % (value, self.sep, self.signature(value)) 

182 

183 def unsign(self, signed_value): 

184 if self.sep not in signed_value: 

185 raise BadSignature('No "%s" found in value' % self.sep) 

186 value, sig = signed_value.rsplit(self.sep, 1) 

187 if constant_time_compare(sig, self.signature(value)): 

188 return value 

189 raise BadSignature('Signature "%s" does not match' % sig) 

190 

191 def sign_object(self, obj, serializer=JSONSerializer, compress=False): 

192 """ 

193 Return URL-safe, hmac signed base64 compressed JSON string. 

194 

195 If compress is True (not the default), check if compressing using zlib 

196 can save some space. Prepend a '.' to signify compression. This is 

197 included in the signature, to protect against zip bombs. 

198 

199 The serializer is expected to return a bytestring. 

200 """ 

201 data = serializer().dumps(obj) 

202 # Flag for if it's been compressed or not. 

203 is_compressed = False 

204 

205 if compress: 

206 # Avoid zlib dependency unless compress is being used. 

207 compressed = zlib.compress(data) 

208 if len(compressed) < (len(data) - 1): 

209 data = compressed 

210 is_compressed = True 

211 base64d = b64_encode(data).decode() 

212 if is_compressed: 

213 base64d = "." + base64d 

214 return self.sign(base64d) 

215 

216 def unsign_object(self, signed_obj, serializer=JSONSerializer, **kwargs): 

217 # Signer.unsign() returns str but base64 and zlib compression operate 

218 # on bytes. 

219 base64d = self.unsign(signed_obj, **kwargs).encode() 

220 decompress = base64d[:1] == b"." 

221 if decompress: 

222 # It's compressed; uncompress it first. 

223 base64d = base64d[1:] 

224 data = b64_decode(base64d) 

225 if decompress: 

226 data = zlib.decompress(data) 

227 return serializer().loads(data) 

228 

229 

230class TimestampSigner(Signer): 

231 def timestamp(self): 

232 return b62_encode(int(time.time())) 

233 

234 def sign(self, value): 

235 value = "%s%s%s" % (value, self.sep, self.timestamp()) 

236 return super().sign(value) 

237 

238 def unsign(self, value, max_age=None): 

239 """ 

240 Retrieve original value and check it wasn't signed more 

241 than max_age seconds ago. 

242 """ 

243 result = super().unsign(value) 

244 value, timestamp = result.rsplit(self.sep, 1) 

245 timestamp = b62_decode(timestamp) 

246 if max_age is not None: 

247 if isinstance(max_age, datetime.timedelta): 

248 max_age = max_age.total_seconds() 

249 # Check timestamp is not older than max_age 

250 age = time.time() - timestamp 

251 if age > max_age: 

252 raise SignatureExpired("Signature age %s > %s seconds" % (age, max_age)) 

253 return value