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

125 statements  

« prev     ^ index     » next       coverage.py v7.0.5, created at 2023-01-17 06:13 +0000

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 warnings 

41import zlib 

42 

43from django.conf import settings 

44from django.utils.crypto import constant_time_compare, salted_hmac 

45from django.utils.deprecation import RemovedInDjango51Warning 

46from django.utils.encoding import force_bytes 

47from django.utils.module_loading import import_string 

48from django.utils.regex_helper import _lazy_re_compile 

49 

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

51BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 

52 

53 

54class BadSignature(Exception): 

55 """Signature does not match.""" 

56 

57 pass 

58 

59 

60class SignatureExpired(BadSignature): 

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

62 

63 pass 

64 

65 

66def b62_encode(s): 

67 if s == 0: 

68 return "0" 

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

70 s = abs(s) 

71 encoded = "" 

72 while s > 0: 

73 s, remainder = divmod(s, 62) 

74 encoded = BASE62_ALPHABET[remainder] + encoded 

75 return sign + encoded 

76 

77 

78def b62_decode(s): 

79 if s == "0": 

80 return 0 

81 sign = 1 

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

83 s = s[1:] 

84 sign = -1 

85 decoded = 0 

86 for digit in s: 

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

88 return sign * decoded 

89 

90 

91def b64_encode(s): 

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

93 

94 

95def b64_decode(s): 

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

97 return base64.urlsafe_b64decode(s + pad) 

98 

99 

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

101 return b64_encode( 

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

103 ).decode() 

104 

105 

106def _cookie_signer_key(key): 

107 # SECRET_KEYS items may be str or bytes. 

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

109 

110 

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

112 Signer = import_string(settings.SIGNING_BACKEND) 

113 return Signer( 

114 key=_cookie_signer_key(settings.SECRET_KEY), 

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

116 salt=salt, 

117 ) 

118 

119 

120class JSONSerializer: 

121 """ 

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

123 signing.loads. 

124 """ 

125 

126 def dumps(self, obj): 

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

128 

129 def loads(self, data): 

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

131 

132 

133def dumps( 

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

135): 

136 """ 

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

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

139 Signer algorithm. 

140 

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

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

143 in the signature, to protect against zip bombs. 

144 

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

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

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

148 application without good cause is a security risk. 

149 

150 The serializer is expected to return a bytestring. 

151 """ 

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

153 obj, serializer=serializer, compress=compress 

154 ) 

155 

156 

157def loads( 

158 s, 

159 key=None, 

160 salt="django.core.signing", 

161 serializer=JSONSerializer, 

162 max_age=None, 

163 fallback_keys=None, 

164): 

165 """ 

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

167 

168 The serializer is expected to accept a bytestring. 

169 """ 

170 return TimestampSigner( 

171 key=key, salt=salt, fallback_keys=fallback_keys 

172 ).unsign_object( 

173 s, 

174 serializer=serializer, 

175 max_age=max_age, 

176 ) 

177 

178 

179class Signer: 

180 # RemovedInDjango51Warning: When the deprecation ends, replace with: 

181 # def __init__( 

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

183 # ): 

184 def __init__( 

185 self, 

186 *args, 

187 key=None, 

188 sep=":", 

189 salt=None, 

190 algorithm=None, 

191 fallback_keys=None, 

192 ): 

193 self.key = key or settings.SECRET_KEY 

194 self.fallback_keys = ( 

195 fallback_keys 

196 if fallback_keys is not None 

197 else settings.SECRET_KEY_FALLBACKS 

198 ) 

199 self.sep = sep 

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

201 self.__class__.__module__, 

202 self.__class__.__name__, 

203 ) 

204 self.algorithm = algorithm or "sha256" 

205 # RemovedInDjango51Warning. 

206 if args: 

207 warnings.warn( 

208 f"Passing positional arguments to {self.__class__.__name__} is " 

209 f"deprecated.", 

210 RemovedInDjango51Warning, 

211 stacklevel=2, 

212 ) 

213 for arg, attr in zip( 

214 args, ["key", "sep", "salt", "algorithm", "fallback_keys"] 

215 ): 

216 if arg or attr == "sep": 

217 setattr(self, attr, arg) 

218 if _SEP_UNSAFE.match(self.sep): 

219 raise ValueError( 

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

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

222 ) 

223 

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

225 key = key or self.key 

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

227 

228 def sign(self, value): 

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

230 

231 def unsign(self, signed_value): 

232 if self.sep not in signed_value: 

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

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

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

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

237 return value 

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

239 

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

241 """ 

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

243 

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

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

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

247 

248 The serializer is expected to return a bytestring. 

249 """ 

250 data = serializer().dumps(obj) 

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

252 is_compressed = False 

253 

254 if compress: 

255 # Avoid zlib dependency unless compress is being used. 

256 compressed = zlib.compress(data) 

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

258 data = compressed 

259 is_compressed = True 

260 base64d = b64_encode(data).decode() 

261 if is_compressed: 

262 base64d = "." + base64d 

263 return self.sign(base64d) 

264 

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

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

267 # on bytes. 

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

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

270 if decompress: 

271 # It's compressed; uncompress it first. 

272 base64d = base64d[1:] 

273 data = b64_decode(base64d) 

274 if decompress: 

275 data = zlib.decompress(data) 

276 return serializer().loads(data) 

277 

278 

279class TimestampSigner(Signer): 

280 def timestamp(self): 

281 return b62_encode(int(time.time())) 

282 

283 def sign(self, value): 

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

285 return super().sign(value) 

286 

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

288 """ 

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

290 than max_age seconds ago. 

291 """ 

292 result = super().unsign(value) 

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

294 timestamp = b62_decode(timestamp) 

295 if max_age is not None: 

296 if isinstance(max_age, datetime.timedelta): 

297 max_age = max_age.total_seconds() 

298 # Check timestamp is not older than max_age 

299 age = time.time() - timestamp 

300 if age > max_age: 

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

302 return value