Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

583 rindas
22 KiB

  1. """passlib.handlers.scram - hash for SCRAM credential storage"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. # core
  6. import logging; log = logging.getLogger(__name__)
  7. # site
  8. # pkg
  9. from passlib.utils import consteq, saslprep, to_native_str, splitcomma
  10. from passlib.utils.binary import ab64_decode, ab64_encode
  11. from passlib.utils.compat import bascii_to_str, iteritems, u, native_string_types
  12. from passlib.crypto.digest import pbkdf2_hmac, norm_hash_name
  13. import passlib.utils.handlers as uh
  14. # local
  15. __all__ = [
  16. "scram",
  17. ]
  18. #=============================================================================
  19. # scram credentials hash
  20. #=============================================================================
  21. class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
  22. """This class provides a format for storing SCRAM passwords, and follows
  23. the :ref:`password-hash-api`.
  24. It supports a variable-length salt, and a variable number of rounds.
  25. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  26. :type salt: bytes
  27. :param salt:
  28. Optional salt bytes.
  29. If specified, the length must be between 0-1024 bytes.
  30. If not specified, a 12 byte salt will be autogenerated
  31. (this is recommended).
  32. :type salt_size: int
  33. :param salt_size:
  34. Optional number of bytes to use when autogenerating new salts.
  35. Defaults to 12 bytes, but can be any value between 0 and 1024.
  36. :type rounds: int
  37. :param rounds:
  38. Optional number of rounds to use.
  39. Defaults to 100000, but must be within ``range(1,1<<32)``.
  40. :type algs: list of strings
  41. :param algs:
  42. Specify list of digest algorithms to use.
  43. By default each scram hash will contain digests for SHA-1,
  44. SHA-256, and SHA-512. This can be overridden by specify either be a
  45. list such as ``["sha-1", "sha-256"]``, or a comma-separated string
  46. such as ``"sha-1, sha-256"``. Names are case insensitive, and may
  47. use :mod:`!hashlib` or `IANA <http://www.iana.org/assignments/hash-function-text-names>`_
  48. hash names.
  49. :type relaxed: bool
  50. :param relaxed:
  51. By default, providing an invalid value for one of the other
  52. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  53. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  54. will be issued instead. Correctable errors include ``rounds``
  55. that are too small or too large, and ``salt`` strings that are too long.
  56. .. versionadded:: 1.6
  57. In addition to the standard :ref:`password-hash-api` methods,
  58. this class also provides the following methods for manipulating Passlib
  59. scram hashes in ways useful for pluging into a SCRAM protocol stack:
  60. .. automethod:: extract_digest_info
  61. .. automethod:: extract_digest_algs
  62. .. automethod:: derive_digest
  63. """
  64. #===================================================================
  65. # class attrs
  66. #===================================================================
  67. # NOTE: unlike most GenericHandler classes, the 'checksum' attr of
  68. # ScramHandler is actually a map from digest_name -> digest, so
  69. # many of the standard methods have been overridden.
  70. # NOTE: max_salt_size and max_rounds are arbitrarily chosen to provide
  71. # a sanity check; the underlying pbkdf2 specifies no bounds for either.
  72. #--GenericHandler--
  73. name = "scram"
  74. setting_kwds = ("salt", "salt_size", "rounds", "algs")
  75. ident = u("$scram$")
  76. #--HasSalt--
  77. default_salt_size = 12
  78. max_salt_size = 1024
  79. #--HasRounds--
  80. default_rounds = 100000
  81. min_rounds = 1
  82. max_rounds = 2**32-1
  83. rounds_cost = "linear"
  84. #--custom--
  85. # default algorithms when creating new hashes.
  86. default_algs = ["sha-1", "sha-256", "sha-512"]
  87. # list of algs verify prefers to use, in order.
  88. _verify_algs = ["sha-256", "sha-512", "sha-224", "sha-384", "sha-1"]
  89. #===================================================================
  90. # instance attrs
  91. #===================================================================
  92. # 'checksum' is different from most GenericHandler subclasses,
  93. # in that it contains a dict mapping from alg -> digest,
  94. # or None if no checksum present.
  95. # list of algorithms to create/compare digests for.
  96. algs = None
  97. #===================================================================
  98. # scram frontend helpers
  99. #===================================================================
  100. @classmethod
  101. def extract_digest_info(cls, hash, alg):
  102. """return (salt, rounds, digest) for specific hash algorithm.
  103. :type hash: str
  104. :arg hash:
  105. :class:`!scram` hash stored for desired user
  106. :type alg: str
  107. :arg alg:
  108. Name of digest algorithm (e.g. ``"sha-1"``) requested by client.
  109. This value is run through :func:`~passlib.crypto.digest.norm_hash_name`,
  110. so it is case-insensitive, and can be the raw SCRAM
  111. mechanism name (e.g. ``"SCRAM-SHA-1"``), the IANA name,
  112. or the hashlib name.
  113. :raises KeyError:
  114. If the hash does not contain an entry for the requested digest
  115. algorithm.
  116. :returns:
  117. A tuple containing ``(salt, rounds, digest)``,
  118. where *digest* matches the raw bytes returned by
  119. SCRAM's :func:`Hi` function for the stored password,
  120. the provided *salt*, and the iteration count (*rounds*).
  121. *salt* and *digest* are both raw (unencoded) bytes.
  122. """
  123. # XXX: this could be sped up by writing custom parsing routine
  124. # that just picks out relevant digest, and doesn't bother
  125. # with full structure validation each time it's called.
  126. alg = norm_hash_name(alg, 'iana')
  127. self = cls.from_string(hash)
  128. chkmap = self.checksum
  129. if not chkmap:
  130. raise ValueError("scram hash contains no digests")
  131. return self.salt, self.rounds, chkmap[alg]
  132. @classmethod
  133. def extract_digest_algs(cls, hash, format="iana"):
  134. """Return names of all algorithms stored in a given hash.
  135. :type hash: str
  136. :arg hash:
  137. The :class:`!scram` hash to parse
  138. :type format: str
  139. :param format:
  140. This changes the naming convention used by the
  141. returned algorithm names. By default the names
  142. are IANA-compatible; possible values are ``"iana"`` or ``"hashlib"``.
  143. :returns:
  144. Returns a list of digest algorithms; e.g. ``["sha-1"]``
  145. """
  146. # XXX: this could be sped up by writing custom parsing routine
  147. # that just picks out relevant names, and doesn't bother
  148. # with full structure validation each time it's called.
  149. algs = cls.from_string(hash).algs
  150. if format == "iana":
  151. return algs
  152. else:
  153. return [norm_hash_name(alg, format) for alg in algs]
  154. @classmethod
  155. def derive_digest(cls, password, salt, rounds, alg):
  156. """helper to create SaltedPassword digest for SCRAM.
  157. This performs the step in the SCRAM protocol described as::
  158. SaltedPassword := Hi(Normalize(password), salt, i)
  159. :type password: unicode or utf-8 bytes
  160. :arg password: password to run through digest
  161. :type salt: bytes
  162. :arg salt: raw salt data
  163. :type rounds: int
  164. :arg rounds: number of iterations.
  165. :type alg: str
  166. :arg alg: name of digest to use (e.g. ``"sha-1"``).
  167. :returns:
  168. raw bytes of ``SaltedPassword``
  169. """
  170. if isinstance(password, bytes):
  171. password = password.decode("utf-8")
  172. # NOTE: pbkdf2_hmac() will encode secret & salt using utf-8,
  173. # and handle normalizing alg name.
  174. return pbkdf2_hmac(alg, saslprep(password), salt, rounds)
  175. #===================================================================
  176. # serialization
  177. #===================================================================
  178. @classmethod
  179. def from_string(cls, hash):
  180. hash = to_native_str(hash, "ascii", "hash")
  181. if not hash.startswith("$scram$"):
  182. raise uh.exc.InvalidHashError(cls)
  183. parts = hash[7:].split("$")
  184. if len(parts) != 3:
  185. raise uh.exc.MalformedHashError(cls)
  186. rounds_str, salt_str, chk_str = parts
  187. # decode rounds
  188. rounds = int(rounds_str)
  189. if rounds_str != str(rounds): # forbid zero padding, etc.
  190. raise uh.exc.MalformedHashError(cls)
  191. # decode salt
  192. try:
  193. salt = ab64_decode(salt_str.encode("ascii"))
  194. except TypeError:
  195. raise uh.exc.MalformedHashError(cls)
  196. # decode algs/digest list
  197. if not chk_str:
  198. # scram hashes MUST have something here.
  199. raise uh.exc.MalformedHashError(cls)
  200. elif "=" in chk_str:
  201. # comma-separated list of 'alg=digest' pairs
  202. algs = None
  203. chkmap = {}
  204. for pair in chk_str.split(","):
  205. alg, digest = pair.split("=")
  206. try:
  207. chkmap[alg] = ab64_decode(digest.encode("ascii"))
  208. except TypeError:
  209. raise uh.exc.MalformedHashError(cls)
  210. else:
  211. # comma-separated list of alg names, no digests
  212. algs = chk_str
  213. chkmap = None
  214. # return new object
  215. return cls(
  216. rounds=rounds,
  217. salt=salt,
  218. checksum=chkmap,
  219. algs=algs,
  220. )
  221. def to_string(self):
  222. salt = bascii_to_str(ab64_encode(self.salt))
  223. chkmap = self.checksum
  224. chk_str = ",".join(
  225. "%s=%s" % (alg, bascii_to_str(ab64_encode(chkmap[alg])))
  226. for alg in self.algs
  227. )
  228. return '$scram$%d$%s$%s' % (self.rounds, salt, chk_str)
  229. #===================================================================
  230. # variant constructor
  231. #===================================================================
  232. @classmethod
  233. def using(cls, default_algs=None, algs=None, **kwds):
  234. # parse aliases
  235. if algs is not None:
  236. assert default_algs is None
  237. default_algs = algs
  238. # create subclass
  239. subcls = super(scram, cls).using(**kwds)
  240. # fill in algs
  241. if default_algs is not None:
  242. subcls.default_algs = cls._norm_algs(default_algs)
  243. return subcls
  244. #===================================================================
  245. # init
  246. #===================================================================
  247. def __init__(self, algs=None, **kwds):
  248. super(scram, self).__init__(**kwds)
  249. # init algs
  250. digest_map = self.checksum
  251. if algs is not None:
  252. if digest_map is not None:
  253. raise RuntimeError("checksum & algs kwds are mutually exclusive")
  254. algs = self._norm_algs(algs)
  255. elif digest_map is not None:
  256. # derive algs list from digest map (if present).
  257. algs = self._norm_algs(digest_map.keys())
  258. elif self.use_defaults:
  259. algs = list(self.default_algs)
  260. assert self._norm_algs(algs) == algs, "invalid default algs: %r" % (algs,)
  261. else:
  262. raise TypeError("no algs list specified")
  263. self.algs = algs
  264. def _norm_checksum(self, checksum, relaxed=False):
  265. if not isinstance(checksum, dict):
  266. raise uh.exc.ExpectedTypeError(checksum, "dict", "checksum")
  267. for alg, digest in iteritems(checksum):
  268. if alg != norm_hash_name(alg, 'iana'):
  269. raise ValueError("malformed algorithm name in scram hash: %r" %
  270. (alg,))
  271. if len(alg) > 9:
  272. raise ValueError("SCRAM limits algorithm names to "
  273. "9 characters: %r" % (alg,))
  274. if not isinstance(digest, bytes):
  275. raise uh.exc.ExpectedTypeError(digest, "raw bytes", "digests")
  276. # TODO: verify digest size (if digest is known)
  277. if 'sha-1' not in checksum:
  278. # NOTE: required because of SCRAM spec.
  279. raise ValueError("sha-1 must be in algorithm list of scram hash")
  280. return checksum
  281. @classmethod
  282. def _norm_algs(cls, algs):
  283. """normalize algs parameter"""
  284. if isinstance(algs, native_string_types):
  285. algs = splitcomma(algs)
  286. algs = sorted(norm_hash_name(alg, 'iana') for alg in algs)
  287. if any(len(alg)>9 for alg in algs):
  288. raise ValueError("SCRAM limits alg names to max of 9 characters")
  289. if 'sha-1' not in algs:
  290. # NOTE: required because of SCRAM spec (rfc 5802)
  291. raise ValueError("sha-1 must be in algorithm list of scram hash")
  292. return algs
  293. #===================================================================
  294. # migration
  295. #===================================================================
  296. def _calc_needs_update(self, **kwds):
  297. # marks hashes as deprecated if they don't include at least all default_algs.
  298. # XXX: should we deprecate if they aren't exactly the same,
  299. # to permit removing legacy hashes?
  300. if not set(self.algs).issuperset(self.default_algs):
  301. return True
  302. # hand off to base implementation
  303. return super(scram, self)._calc_needs_update(**kwds)
  304. #===================================================================
  305. # digest methods
  306. #===================================================================
  307. def _calc_checksum(self, secret, alg=None):
  308. rounds = self.rounds
  309. salt = self.salt
  310. hash = self.derive_digest
  311. if alg:
  312. # if requested, generate digest for specific alg
  313. return hash(secret, salt, rounds, alg)
  314. else:
  315. # by default, return dict containing digests for all algs
  316. return dict(
  317. (alg, hash(secret, salt, rounds, alg))
  318. for alg in self.algs
  319. )
  320. @classmethod
  321. def verify(cls, secret, hash, full=False):
  322. uh.validate_secret(secret)
  323. self = cls.from_string(hash)
  324. chkmap = self.checksum
  325. if not chkmap:
  326. raise ValueError("expected %s hash, got %s config string instead" %
  327. (cls.name, cls.name))
  328. # NOTE: to make the verify method efficient, we just calculate hash
  329. # of shortest digest by default. apps can pass in "full=True" to
  330. # check entire hash for consistency.
  331. if full:
  332. correct = failed = False
  333. for alg, digest in iteritems(chkmap):
  334. other = self._calc_checksum(secret, alg)
  335. # NOTE: could do this length check in norm_algs(),
  336. # but don't need to be that strict, and want to be able
  337. # to parse hashes containing algs not supported by platform.
  338. # it's fine if we fail here though.
  339. if len(digest) != len(other):
  340. raise ValueError("mis-sized %s digest in scram hash: %r != %r"
  341. % (alg, len(digest), len(other)))
  342. if consteq(other, digest):
  343. correct = True
  344. else:
  345. failed = True
  346. if correct and failed:
  347. raise ValueError("scram hash verified inconsistently, "
  348. "may be corrupted")
  349. else:
  350. return correct
  351. else:
  352. # XXX: should this just always use sha1 hash? would be faster.
  353. # otherwise only verify against one hash, pick one w/ best security.
  354. for alg in self._verify_algs:
  355. if alg in chkmap:
  356. other = self._calc_checksum(secret, alg)
  357. return consteq(other, chkmap[alg])
  358. # there should always be sha-1 at the very least,
  359. # or something went wrong inside _norm_algs()
  360. raise AssertionError("sha-1 digest not found!")
  361. #===================================================================
  362. #
  363. #===================================================================
  364. #=============================================================================
  365. # code used for testing scram against protocol examples during development.
  366. #=============================================================================
  367. ##def _test_reference_scram():
  368. ## "quick hack testing scram reference vectors"
  369. ## # NOTE: "n,," is GS2 header - see https://tools.ietf.org/html/rfc5801
  370. ## from passlib.utils.compat import print_
  371. ##
  372. ## engine = _scram_engine(
  373. ## alg="sha-1",
  374. ## salt='QSXCR+Q6sek8bf92'.decode("base64"),
  375. ## rounds=4096,
  376. ## password=u("pencil"),
  377. ## )
  378. ## print_(engine.digest.encode("base64").rstrip())
  379. ##
  380. ## msg = engine.format_auth_msg(
  381. ## username="user",
  382. ## client_nonce = "fyko+d2lbbFgONRv9qkxdawL",
  383. ## server_nonce = "3rfcNHYJY1ZVvWVs7j",
  384. ## header='c=biws',
  385. ## )
  386. ##
  387. ## cp = engine.get_encoded_client_proof(msg)
  388. ## assert cp == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=", cp
  389. ##
  390. ## ss = engine.get_encoded_server_sig(msg)
  391. ## assert ss == "rmF9pqV8S7suAoZWja4dJRkFsKQ=", ss
  392. ##
  393. ##class _scram_engine(object):
  394. ## """helper class for verifying scram hash behavior
  395. ## against SCRAM protocol examples. not officially part of Passlib.
  396. ##
  397. ## takes in alg, salt, rounds, and a digest or password.
  398. ##
  399. ## can calculate the various keys & messages of the scram protocol.
  400. ##
  401. ## """
  402. ## #=========================================================
  403. ## # init
  404. ## #=========================================================
  405. ##
  406. ## @classmethod
  407. ## def from_string(cls, hash, alg):
  408. ## "create record from scram hash, for given alg"
  409. ## return cls(alg, *scram.extract_digest_info(hash, alg))
  410. ##
  411. ## def __init__(self, alg, salt, rounds, digest=None, password=None):
  412. ## self.alg = norm_hash_name(alg)
  413. ## self.salt = salt
  414. ## self.rounds = rounds
  415. ## self.password = password
  416. ## if password:
  417. ## data = scram.derive_digest(password, salt, rounds, alg)
  418. ## if digest and data != digest:
  419. ## raise ValueError("password doesn't match digest")
  420. ## else:
  421. ## digest = data
  422. ## elif not digest:
  423. ## raise TypeError("must provide password or digest")
  424. ## self.digest = digest
  425. ##
  426. ## #=========================================================
  427. ## # frontend methods
  428. ## #=========================================================
  429. ## def get_hash(self, data):
  430. ## "return hash of raw data"
  431. ## return hashlib.new(iana_to_hashlib(self.alg), data).digest()
  432. ##
  433. ## def get_client_proof(self, msg):
  434. ## "return client proof of specified auth msg text"
  435. ## return xor_bytes(self.client_key, self.get_client_sig(msg))
  436. ##
  437. ## def get_encoded_client_proof(self, msg):
  438. ## return self.get_client_proof(msg).encode("base64").rstrip()
  439. ##
  440. ## def get_client_sig(self, msg):
  441. ## "return client signature of specified auth msg text"
  442. ## return self.get_hmac(self.stored_key, msg)
  443. ##
  444. ## def get_server_sig(self, msg):
  445. ## "return server signature of specified auth msg text"
  446. ## return self.get_hmac(self.server_key, msg)
  447. ##
  448. ## def get_encoded_server_sig(self, msg):
  449. ## return self.get_server_sig(msg).encode("base64").rstrip()
  450. ##
  451. ## def format_server_response(self, client_nonce, server_nonce):
  452. ## return 'r={client_nonce}{server_nonce},s={salt},i={rounds}'.format(
  453. ## client_nonce=client_nonce,
  454. ## server_nonce=server_nonce,
  455. ## rounds=self.rounds,
  456. ## salt=self.encoded_salt,
  457. ## )
  458. ##
  459. ## def format_auth_msg(self, username, client_nonce, server_nonce,
  460. ## header='c=biws'):
  461. ## return (
  462. ## 'n={username},r={client_nonce}'
  463. ## ','
  464. ## 'r={client_nonce}{server_nonce},s={salt},i={rounds}'
  465. ## ','
  466. ## '{header},r={client_nonce}{server_nonce}'
  467. ## ).format(
  468. ## username=username,
  469. ## client_nonce=client_nonce,
  470. ## server_nonce=server_nonce,
  471. ## salt=self.encoded_salt,
  472. ## rounds=self.rounds,
  473. ## header=header,
  474. ## )
  475. ##
  476. ## #=========================================================
  477. ## # helpers to calculate & cache constant data
  478. ## #=========================================================
  479. ## def _calc_get_hmac(self):
  480. ## return get_prf("hmac-" + iana_to_hashlib(self.alg))[0]
  481. ##
  482. ## def _calc_client_key(self):
  483. ## return self.get_hmac(self.digest, b("Client Key"))
  484. ##
  485. ## def _calc_stored_key(self):
  486. ## return self.get_hash(self.client_key)
  487. ##
  488. ## def _calc_server_key(self):
  489. ## return self.get_hmac(self.digest, b("Server Key"))
  490. ##
  491. ## def _calc_encoded_salt(self):
  492. ## return self.salt.encode("base64").rstrip()
  493. ##
  494. ## #=========================================================
  495. ## # hacks for calculated attributes
  496. ## #=========================================================
  497. ##
  498. ## def __getattr__(self, attr):
  499. ## if not attr.startswith("_"):
  500. ## f = getattr(self, "_calc_" + attr, None)
  501. ## if f:
  502. ## value = f()
  503. ## setattr(self, attr, value)
  504. ## return value
  505. ## raise AttributeError("attribute not found")
  506. ##
  507. ## def __dir__(self):
  508. ## cdir = dir(self.__class__)
  509. ## attrs = set(cdir)
  510. ## attrs.update(self.__dict__)
  511. ## attrs.update(attr[6:] for attr in cdir
  512. ## if attr.startswith("_calc_"))
  513. ## return sorted(attrs)
  514. ## #=========================================================
  515. ## # eoc
  516. ## #=========================================================
  517. #=============================================================================
  518. # eof
  519. #=============================================================================