Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/django/core/signing.py: 33%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

118 statements  

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/SHA-256 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-ze3TTx7gtSv42-modified") 

19... 

20BadSignature: Signature "ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified" does not match 

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 _cookie_signer_key(key): 

105 # SECRET_KEYS items may be str or bytes. 

106 return b"django.http.cookies" + force_bytes(key) 

107 

108 

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

110 Signer = import_string(settings.SIGNING_BACKEND) 

111 return Signer( 

112 key=_cookie_signer_key(settings.SECRET_KEY), 

113 fallback_keys=map(_cookie_signer_key, settings.SECRET_KEY_FALLBACKS), 

114 salt=salt, 

115 ) 

116 

117 

118class JSONSerializer: 

119 """ 

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

121 signing.loads. 

122 """ 

123 

124 def dumps(self, obj): 

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

126 

127 def loads(self, data): 

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

129 

130 

131def dumps( 

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

133): 

134 """ 

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

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

137 Signer algorithm. 

138 

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

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

141 in the signature, to protect against zip bombs. 

142 

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

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

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

146 application without good cause is a security risk. 

147 

148 The serializer is expected to return a bytestring. 

149 """ 

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

151 obj, serializer=serializer, compress=compress 

152 ) 

153 

154 

155def loads( 

156 s, 

157 key=None, 

158 salt="django.core.signing", 

159 serializer=JSONSerializer, 

160 max_age=None, 

161 fallback_keys=None, 

162): 

163 """ 

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

165 

166 The serializer is expected to accept a bytestring. 

167 """ 

168 return TimestampSigner( 

169 key=key, salt=salt, fallback_keys=fallback_keys 

170 ).unsign_object( 

171 s, 

172 serializer=serializer, 

173 max_age=max_age, 

174 ) 

175 

176 

177class Signer: 

178 def __init__( 

179 self, *, key=None, sep=":", salt=None, algorithm=None, fallback_keys=None 

180 ): 

181 self.key = key or settings.SECRET_KEY 

182 self.fallback_keys = ( 

183 fallback_keys 

184 if fallback_keys is not None 

185 else settings.SECRET_KEY_FALLBACKS 

186 ) 

187 self.sep = sep 

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

189 self.__class__.__module__, 

190 self.__class__.__name__, 

191 ) 

192 self.algorithm = algorithm or "sha256" 

193 if _SEP_UNSAFE.match(self.sep): 

194 raise ValueError( 

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

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

197 ) 

198 

199 def signature(self, value, key=None): 

200 key = key or self.key 

201 return base64_hmac(self.salt + "signer", value, key, algorithm=self.algorithm) 

202 

203 def sign(self, value): 

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

205 

206 def unsign(self, signed_value): 

207 if self.sep not in signed_value: 

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

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

210 for key in [self.key, *self.fallback_keys]: 

211 if constant_time_compare(sig, self.signature(value, key)): 

212 return value 

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

214 

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

216 """ 

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

218 

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

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

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

222 

223 The serializer is expected to return a bytestring. 

224 """ 

225 data = serializer().dumps(obj) 

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

227 is_compressed = False 

228 

229 if compress: 

230 # Avoid zlib dependency unless compress is being used. 

231 compressed = zlib.compress(data) 

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

233 data = compressed 

234 is_compressed = True 

235 base64d = b64_encode(data).decode() 

236 if is_compressed: 

237 base64d = "." + base64d 

238 return self.sign(base64d) 

239 

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

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

242 # on bytes. 

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

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

245 if decompress: 

246 # It's compressed; uncompress it first. 

247 base64d = base64d[1:] 

248 data = b64_decode(base64d) 

249 if decompress: 

250 data = zlib.decompress(data) 

251 return serializer().loads(data) 

252 

253 

254class TimestampSigner(Signer): 

255 def timestamp(self): 

256 return b62_encode(int(time.time())) 

257 

258 def sign(self, value): 

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

260 return super().sign(value) 

261 

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

263 """ 

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

265 than max_age seconds ago. 

266 """ 

267 result = super().unsign(value) 

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

269 timestamp = b62_decode(timestamp) 

270 if max_age is not None: 

271 if isinstance(max_age, datetime.timedelta): 

272 max_age = max_age.total_seconds() 

273 # Check timestamp is not older than max_age 

274 age = time.time() - timestamp 

275 if age > max_age: 

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

277 return value