PageRenderTime 47ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/gluon/utils.py

https://github.com/gokceneraslan/web2py
Python | 331 lines | 300 code | 17 blank | 14 comment | 13 complexity | 05ba6afade3db6e32e2b15dabccb3fcb MD5 | raw file
Possible License(s): BSD-2-Clause, MIT, BSD-3-Clause
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. This file is part of the web2py Web Framework
  5. Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
  6. License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
  7. This file specifically includes utilities for security.
  8. """
  9. import threading
  10. import struct
  11. #import hashlib
  12. #import hmac
  13. import uuid
  14. import random
  15. import time
  16. import os
  17. import re
  18. import sys
  19. import logging
  20. import socket
  21. import base64
  22. import zlib
  23. from types import ModuleType
  24. _struct_2_long_long = struct.Struct('=QQ')
  25. python_version = sys.version_info[0]
  26. if python_version == 2:
  27. import cPickle as pickle
  28. else:
  29. import pickle
  30. from hashlib import md5, sha1, sha224, sha256, sha384, sha512
  31. try:
  32. from Crypto.Cipher import AES
  33. except ImportError:
  34. import contrib.aes as AES
  35. import hmac
  36. try:
  37. try:
  38. from contrib.pbkdf2_ctypes import pbkdf2_hex
  39. except (ImportError, AttributeError):
  40. from contrib.pbkdf2 import pbkdf2_hex
  41. HAVE_PBKDF2 = True
  42. except ImportError:
  43. try:
  44. from .pbkdf2 import pbkdf2_hex
  45. HAVE_PBKDF2 = True
  46. except (ImportError, ValueError):
  47. HAVE_PBKDF2 = False
  48. logger = logging.getLogger("web2py")
  49. def AES_new(key, IV=None):
  50. """ Returns an AES cipher object and random IV if None specified """
  51. if IV is None:
  52. IV = fast_urandom16()
  53. return AES.new(key, AES.MODE_CBC, IV), IV
  54. def compare(a, b):
  55. """ compares two strings and not vulnerable to timing attacks """
  56. if len(a) != len(b):
  57. return False
  58. result = 0
  59. for x, y in zip(a, b):
  60. result |= ord(x) ^ ord(y)
  61. return result == 0
  62. def md5_hash(text):
  63. """ Generate a md5 hash with the given text """
  64. return md5(text).hexdigest()
  65. def simple_hash(text, key='', salt='', digest_alg='md5'):
  66. """
  67. Generates hash with the given text using the specified
  68. digest hashing algorithm
  69. """
  70. if not digest_alg:
  71. raise RuntimeError("simple_hash with digest_alg=None")
  72. elif not isinstance(digest_alg, str): # manual approach
  73. h = digest_alg(text + key + salt)
  74. elif digest_alg.startswith('pbkdf2'): # latest and coolest!
  75. iterations, keylen, alg = digest_alg[7:-1].split(',')
  76. return pbkdf2_hex(text, salt, int(iterations),
  77. int(keylen), get_digest(alg))
  78. elif key: # use hmac
  79. digest_alg = get_digest(digest_alg)
  80. h = hmac.new(key + salt, text, digest_alg)
  81. else: # compatible with third party systems
  82. h = get_digest(digest_alg)()
  83. h.update(text + salt)
  84. return h.hexdigest()
  85. def get_digest(value):
  86. """
  87. Returns a hashlib digest algorithm from a string
  88. """
  89. if not isinstance(value, str):
  90. return value
  91. value = value.lower()
  92. if value == "md5":
  93. return md5
  94. elif value == "sha1":
  95. return sha1
  96. elif value == "sha224":
  97. return sha224
  98. elif value == "sha256":
  99. return sha256
  100. elif value == "sha384":
  101. return sha384
  102. elif value == "sha512":
  103. return sha512
  104. else:
  105. raise ValueError("Invalid digest algorithm: %s" % value)
  106. DIGEST_ALG_BY_SIZE = {
  107. 128 / 4: 'md5',
  108. 160 / 4: 'sha1',
  109. 224 / 4: 'sha224',
  110. 256 / 4: 'sha256',
  111. 384 / 4: 'sha384',
  112. 512 / 4: 'sha512',
  113. }
  114. def pad(s, n=32, padchar=' '):
  115. return s + (32 - len(s) % 32) * padchar
  116. def secure_dumps(data, encryption_key, hash_key=None, compression_level=None):
  117. if not hash_key:
  118. hash_key = sha1(encryption_key).hexdigest()
  119. dump = pickle.dumps(data)
  120. if compression_level:
  121. dump = zlib.compress(dump, compression_level)
  122. key = pad(encryption_key[:32])
  123. cipher, IV = AES_new(key)
  124. encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump)))
  125. signature = hmac.new(hash_key, encrypted_data).hexdigest()
  126. return signature + ':' + encrypted_data
  127. def secure_loads(data, encryption_key, hash_key=None, compression_level=None):
  128. if not ':' in data:
  129. return None
  130. if not hash_key:
  131. hash_key = sha1(encryption_key).hexdigest()
  132. signature, encrypted_data = data.split(':', 1)
  133. actual_signature = hmac.new(hash_key, encrypted_data).hexdigest()
  134. if not compare(signature, actual_signature):
  135. return None
  136. key = pad(encryption_key[:32])
  137. encrypted_data = base64.urlsafe_b64decode(encrypted_data)
  138. IV, encrypted_data = encrypted_data[:16], encrypted_data[16:]
  139. cipher, _ = AES_new(key, IV=IV)
  140. try:
  141. data = cipher.decrypt(encrypted_data)
  142. data = data.rstrip(' ')
  143. if compression_level:
  144. data = zlib.decompress(data)
  145. return pickle.loads(data)
  146. except (TypeError, pickle.UnpicklingError):
  147. return None
  148. ### compute constant CTOKENS
  149. def initialize_urandom():
  150. """
  151. This function and the web2py_uuid follow from the following discussion:
  152. http://groups.google.com/group/web2py-developers/browse_thread/thread/7fd5789a7da3f09
  153. At startup web2py compute a unique ID that identifies the machine by adding
  154. uuid.getnode() + int(time.time() * 1e3)
  155. This is a 48-bit number. It converts the number into 16 8-bit tokens.
  156. It uses this value to initialize the entropy source ('/dev/urandom') and to seed random.
  157. If os.random() is not supported, it falls back to using random and issues a warning.
  158. """
  159. node_id = uuid.getnode()
  160. microseconds = int(time.time() * 1e6)
  161. ctokens = [((node_id + microseconds) >> ((i % 6) * 8)) %
  162. 256 for i in range(16)]
  163. random.seed(node_id + microseconds)
  164. try:
  165. os.urandom(1)
  166. have_urandom = True
  167. try:
  168. # try to add process-specific entropy
  169. frandom = open('/dev/urandom', 'wb')
  170. try:
  171. if python_version == 2:
  172. frandom.write(''.join(chr(t) for t in ctokens)) # python 2
  173. else:
  174. frandom.write(bytes([]).join(bytes([t]) for t in ctokens)) # python 3
  175. finally:
  176. frandom.close()
  177. except IOError:
  178. # works anyway
  179. pass
  180. except NotImplementedError:
  181. have_urandom = False
  182. logger.warning(
  183. """Cryptographically secure session management is not possible on your system because
  184. your system does not provide a cryptographically secure entropy source.
  185. This is not specific to web2py; consider deploying on a different operating system.""")
  186. if python_version == 2:
  187. packed = ''.join(chr(x) for x in ctokens) # python 2
  188. else:
  189. packed = bytes([]).join(bytes([x]) for x in ctokens) # python 3
  190. unpacked_ctokens = _struct_2_long_long.unpack(packed)
  191. return unpacked_ctokens, have_urandom
  192. UNPACKED_CTOKENS, HAVE_URANDOM = initialize_urandom()
  193. def fast_urandom16(urandom=[], locker=threading.RLock()):
  194. """
  195. this is 4x faster than calling os.urandom(16) and prevents
  196. the "too many files open" issue with concurrent access to os.urandom()
  197. """
  198. try:
  199. return urandom.pop()
  200. except IndexError:
  201. try:
  202. locker.acquire()
  203. ur = os.urandom(16 * 1024)
  204. urandom += [ur[i:i + 16] for i in xrange(16, 1024 * 16, 16)]
  205. return ur[0:16]
  206. finally:
  207. locker.release()
  208. def web2py_uuid(ctokens=UNPACKED_CTOKENS):
  209. """
  210. This function follows from the following discussion:
  211. http://groups.google.com/group/web2py-developers/browse_thread/thread/7fd5789a7da3f09
  212. It works like uuid.uuid4 except that tries to use os.urandom() if possible
  213. and it XORs the output with the tokens uniquely associated with this machine.
  214. """
  215. rand_longs = (random.getrandbits(64), random.getrandbits(64))
  216. if HAVE_URANDOM:
  217. urand_longs = _struct_2_long_long.unpack(fast_urandom16())
  218. byte_s = _struct_2_long_long.pack(rand_longs[0] ^ urand_longs[0] ^ ctokens[0],
  219. rand_longs[1] ^ urand_longs[1] ^ ctokens[1])
  220. else:
  221. byte_s = _struct_2_long_long.pack(rand_longs[0] ^ ctokens[0],
  222. rand_longs[1] ^ ctokens[1])
  223. return str(uuid.UUID(bytes=byte_s, version=4))
  224. REGEX_IPv4 = re.compile('(\d+)\.(\d+)\.(\d+)\.(\d+)')
  225. def is_valid_ip_address(address):
  226. """
  227. >>> is_valid_ip_address('127.0')
  228. False
  229. >>> is_valid_ip_address('127.0.0.1')
  230. True
  231. >>> is_valid_ip_address('2001:660::1')
  232. True
  233. """
  234. # deal with special cases
  235. if address.lower() in ('127.0.0.1', 'localhost', '::1', '::ffff:127.0.0.1'):
  236. return True
  237. elif address.lower() in ('unknown', ''):
  238. return False
  239. elif address.count('.') == 3: # assume IPv4
  240. if address.startswith('::ffff:'):
  241. address = address[7:]
  242. if hasattr(socket, 'inet_aton'): # try validate using the OS
  243. try:
  244. socket.inet_aton(address)
  245. return True
  246. except socket.error: # invalid address
  247. return False
  248. else: # try validate using Regex
  249. match = REGEX_IPv4.match(address)
  250. if match and all(0 <= int(match.group(i)) < 256 for i in (1, 2, 3, 4)):
  251. return True
  252. return False
  253. elif hasattr(socket, 'inet_pton'): # assume IPv6, try using the OS
  254. try:
  255. socket.inet_pton(socket.AF_INET6, address)
  256. return True
  257. except socket.error: # invalid address
  258. return False
  259. else: # do not know what to do? assume it is a valid address
  260. return True
  261. def is_loopback_ip_address(ip=None, addrinfo=None):
  262. """
  263. Determines whether the address appears to be a loopback address.
  264. This assumes that the IP is valid.
  265. """
  266. if addrinfo: # see socket.getaddrinfo() for layout of addrinfo tuple
  267. if addrinfo[0] == socket.AF_INET or addrinfo[0] == socket.AF_INET6:
  268. ip = addrinfo[4]
  269. if not isinstance(ip, basestring):
  270. return False
  271. # IPv4 or IPv6-embedded IPv4 or IPv4-compatible IPv6
  272. if ip.count('.') == 3:
  273. return ip.lower().startswith(('127', '::127', '0:0:0:0:0:0:127',
  274. '::ffff:127', '0:0:0:0:0:ffff:127'))
  275. return ip == '::1' or ip == '0:0:0:0:0:0:0:1' # IPv6 loopback
  276. def getipaddrinfo(host):
  277. """
  278. Filter out non-IP and bad IP addresses from getaddrinfo
  279. """
  280. try:
  281. return [addrinfo for addrinfo in socket.getaddrinfo(host, None)
  282. if (addrinfo[0] == socket.AF_INET or
  283. addrinfo[0] == socket.AF_INET6)
  284. and isinstance(addrinfo[4][0], basestring)]
  285. except socket.error:
  286. return []