des_crypt.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. """passlib.handlers.des_crypt - traditional unix (DES) crypt and variants"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. # core
  6. import re
  7. import logging; log = logging.getLogger(__name__)
  8. from warnings import warn
  9. # site
  10. # pkg
  11. from passlib.utils import safe_crypt, test_crypt, to_unicode
  12. from passlib.utils.binary import h64, h64big
  13. from passlib.utils.compat import byte_elem_value, u, uascii_to_str, unicode, suppress_cause
  14. from passlib.crypto.des import des_encrypt_int_block
  15. import passlib.utils.handlers as uh
  16. # local
  17. __all__ = [
  18. "des_crypt",
  19. "bsdi_crypt",
  20. "bigcrypt",
  21. "crypt16",
  22. ]
  23. #=============================================================================
  24. # pure-python backend for des_crypt family
  25. #=============================================================================
  26. _BNULL = b'\x00'
  27. def _crypt_secret_to_key(secret):
  28. """convert secret to 64-bit DES key.
  29. this only uses the first 8 bytes of the secret,
  30. and discards the high 8th bit of each byte at that.
  31. a null parity bit is inserted after every 7th bit of the output.
  32. """
  33. # NOTE: this would set the parity bits correctly,
  34. # but des_encrypt_int_block() would just ignore them...
  35. ##return sum(expand_7bit(byte_elem_value(c) & 0x7f) << (56-i*8)
  36. ## for i, c in enumerate(secret[:8]))
  37. return sum((byte_elem_value(c) & 0x7f) << (57-i*8)
  38. for i, c in enumerate(secret[:8]))
  39. def _raw_des_crypt(secret, salt):
  40. """pure-python backed for des_crypt"""
  41. assert len(salt) == 2
  42. # NOTE: some OSes will accept non-HASH64 characters in the salt,
  43. # but what value they assign these characters varies wildy,
  44. # so just rejecting them outright.
  45. # the same goes for single-character salts...
  46. # some OSes duplicate the char, some insert a '.' char,
  47. # and openbsd does (something) which creates an invalid hash.
  48. salt_value = h64.decode_int12(salt)
  49. # gotta do something - no official policy since this predates unicode
  50. if isinstance(secret, unicode):
  51. secret = secret.encode("utf-8")
  52. assert isinstance(secret, bytes)
  53. # forbidding NULL char because underlying crypt() rejects them too.
  54. if _BNULL in secret:
  55. raise uh.exc.NullPasswordError(des_crypt)
  56. # convert first 8 bytes of secret string into an integer
  57. key_value = _crypt_secret_to_key(secret)
  58. # run data through des using input of 0
  59. result = des_encrypt_int_block(key_value, 0, salt_value, 25)
  60. # run h64 encode on result
  61. return h64big.encode_int64(result)
  62. def _bsdi_secret_to_key(secret):
  63. """convert secret to DES key used by bsdi_crypt"""
  64. key_value = _crypt_secret_to_key(secret)
  65. idx = 8
  66. end = len(secret)
  67. while idx < end:
  68. next = idx + 8
  69. tmp_value = _crypt_secret_to_key(secret[idx:next])
  70. key_value = des_encrypt_int_block(key_value, key_value) ^ tmp_value
  71. idx = next
  72. return key_value
  73. def _raw_bsdi_crypt(secret, rounds, salt):
  74. """pure-python backend for bsdi_crypt"""
  75. # decode salt
  76. salt_value = h64.decode_int24(salt)
  77. # gotta do something - no official policy since this predates unicode
  78. if isinstance(secret, unicode):
  79. secret = secret.encode("utf-8")
  80. assert isinstance(secret, bytes)
  81. # forbidding NULL char because underlying crypt() rejects them too.
  82. if _BNULL in secret:
  83. raise uh.exc.NullPasswordError(bsdi_crypt)
  84. # convert secret string into an integer
  85. key_value = _bsdi_secret_to_key(secret)
  86. # run data through des using input of 0
  87. result = des_encrypt_int_block(key_value, 0, salt_value, rounds)
  88. # run h64 encode on result
  89. return h64big.encode_int64(result)
  90. #=============================================================================
  91. # handlers
  92. #=============================================================================
  93. class des_crypt(uh.TruncateMixin, uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
  94. """This class implements the des-crypt password hash, and follows the :ref:`password-hash-api`.
  95. It supports a fixed-length salt.
  96. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  97. :type salt: str
  98. :param salt:
  99. Optional salt string.
  100. If not specified, one will be autogenerated (this is recommended).
  101. If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
  102. :param bool truncate_error:
  103. By default, des_crypt will silently truncate passwords larger than 8 bytes.
  104. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
  105. to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
  106. .. versionadded:: 1.7
  107. :type relaxed: bool
  108. :param relaxed:
  109. By default, providing an invalid value for one of the other
  110. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  111. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  112. will be issued instead. Correctable errors include
  113. ``salt`` strings that are too long.
  114. .. versionadded:: 1.6
  115. """
  116. #===================================================================
  117. # class attrs
  118. #===================================================================
  119. #--------------------
  120. # PasswordHash
  121. #--------------------
  122. name = "des_crypt"
  123. setting_kwds = ("salt", "truncate_error")
  124. #--------------------
  125. # GenericHandler
  126. #--------------------
  127. checksum_chars = uh.HASH64_CHARS
  128. checksum_size = 11
  129. #--------------------
  130. # HasSalt
  131. #--------------------
  132. min_salt_size = max_salt_size = 2
  133. salt_chars = uh.HASH64_CHARS
  134. #--------------------
  135. # TruncateMixin
  136. #--------------------
  137. truncate_size = 8
  138. #===================================================================
  139. # formatting
  140. #===================================================================
  141. # FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
  142. _hash_regex = re.compile(u(r"""
  143. ^
  144. (?P<salt>[./a-z0-9]{2})
  145. (?P<chk>[./a-z0-9]{11})?
  146. $"""), re.X|re.I)
  147. @classmethod
  148. def from_string(cls, hash):
  149. hash = to_unicode(hash, "ascii", "hash")
  150. salt, chk = hash[:2], hash[2:]
  151. return cls(salt=salt, checksum=chk or None)
  152. def to_string(self):
  153. hash = u("%s%s") % (self.salt, self.checksum)
  154. return uascii_to_str(hash)
  155. #===================================================================
  156. # digest calculation
  157. #===================================================================
  158. def _calc_checksum(self, secret):
  159. # check for truncation (during .hash() calls only)
  160. if self.use_defaults:
  161. self._check_truncate_policy(secret)
  162. return self._calc_checksum_backend(secret)
  163. #===================================================================
  164. # backend
  165. #===================================================================
  166. backends = ("os_crypt", "builtin")
  167. #---------------------------------------------------------------
  168. # os_crypt backend
  169. #---------------------------------------------------------------
  170. @classmethod
  171. def _load_backend_os_crypt(cls):
  172. if test_crypt("test", 'abgOeLfPimXQo'):
  173. cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
  174. return True
  175. else:
  176. return False
  177. def _calc_checksum_os_crypt(self, secret):
  178. # NOTE: we let safe_crypt() encode unicode secret -> utf8;
  179. # no official policy since des-crypt predates unicode
  180. hash = safe_crypt(secret, self.salt)
  181. if hash:
  182. assert hash.startswith(self.salt) and len(hash) == 13
  183. return hash[2:]
  184. else:
  185. # py3's crypt.crypt() can't handle non-utf8 bytes.
  186. # fallback to builtin alg, which is always available.
  187. return self._calc_checksum_builtin(secret)
  188. #---------------------------------------------------------------
  189. # builtin backend
  190. #---------------------------------------------------------------
  191. @classmethod
  192. def _load_backend_builtin(cls):
  193. cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
  194. return True
  195. def _calc_checksum_builtin(self, secret):
  196. return _raw_des_crypt(secret, self.salt.encode("ascii")).decode("ascii")
  197. #===================================================================
  198. # eoc
  199. #===================================================================
  200. class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
  201. """This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`.
  202. It supports a fixed-length salt, and a variable number of rounds.
  203. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  204. :type salt: str
  205. :param salt:
  206. Optional salt string.
  207. If not specified, one will be autogenerated (this is recommended).
  208. If specified, it must be 4 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
  209. :type rounds: int
  210. :param rounds:
  211. Optional number of rounds to use.
  212. Defaults to 5001, must be between 1 and 16777215, inclusive.
  213. :type relaxed: bool
  214. :param relaxed:
  215. By default, providing an invalid value for one of the other
  216. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  217. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  218. will be issued instead. Correctable errors include ``rounds``
  219. that are too small or too large, and ``salt`` strings that are too long.
  220. .. versionadded:: 1.6
  221. .. versionchanged:: 1.6
  222. :meth:`hash` will now issue a warning if an even number of rounds is used
  223. (see :ref:`bsdi-crypt-security-issues` regarding weak DES keys).
  224. """
  225. #===================================================================
  226. # class attrs
  227. #===================================================================
  228. #--GenericHandler--
  229. name = "bsdi_crypt"
  230. setting_kwds = ("salt", "rounds")
  231. checksum_size = 11
  232. checksum_chars = uh.HASH64_CHARS
  233. #--HasSalt--
  234. min_salt_size = max_salt_size = 4
  235. salt_chars = uh.HASH64_CHARS
  236. #--HasRounds--
  237. default_rounds = 5001
  238. min_rounds = 1
  239. max_rounds = 16777215 # (1<<24)-1
  240. rounds_cost = "linear"
  241. # NOTE: OpenBSD login.conf reports 7250 as minimum allowed rounds,
  242. # but that seems to be an OS policy, not a algorithm limitation.
  243. #===================================================================
  244. # parsing
  245. #===================================================================
  246. _hash_regex = re.compile(u(r"""
  247. ^
  248. _
  249. (?P<rounds>[./a-z0-9]{4})
  250. (?P<salt>[./a-z0-9]{4})
  251. (?P<chk>[./a-z0-9]{11})?
  252. $"""), re.X|re.I)
  253. @classmethod
  254. def from_string(cls, hash):
  255. hash = to_unicode(hash, "ascii", "hash")
  256. m = cls._hash_regex.match(hash)
  257. if not m:
  258. raise uh.exc.InvalidHashError(cls)
  259. rounds, salt, chk = m.group("rounds", "salt", "chk")
  260. return cls(
  261. rounds=h64.decode_int24(rounds.encode("ascii")),
  262. salt=salt,
  263. checksum=chk,
  264. )
  265. def to_string(self):
  266. hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"),
  267. self.salt, self.checksum)
  268. return uascii_to_str(hash)
  269. #===================================================================
  270. # validation
  271. #===================================================================
  272. # NOTE: keeping this flag for admin/choose_rounds.py script.
  273. # want to eventually expose rounds logic to that script in better way.
  274. _avoid_even_rounds = True
  275. @classmethod
  276. def using(cls, **kwds):
  277. subcls = super(bsdi_crypt, cls).using(**kwds)
  278. if not subcls.default_rounds & 1:
  279. # issue warning if caller set an even 'rounds' value.
  280. warn("bsdi_crypt rounds should be odd, as even rounds may reveal weak DES keys",
  281. uh.exc.PasslibSecurityWarning)
  282. return subcls
  283. @classmethod
  284. def _generate_rounds(cls):
  285. rounds = super(bsdi_crypt, cls)._generate_rounds()
  286. # ensure autogenerated rounds are always odd
  287. # NOTE: doing this even for default_rounds so needs_update() doesn't get
  288. # caught in a loop.
  289. # FIXME: this technically might generate a rounds value 1 larger
  290. # than the requested upper bound - but better to err on side of safety.
  291. return rounds|1
  292. #===================================================================
  293. # migration
  294. #===================================================================
  295. def _calc_needs_update(self, **kwds):
  296. # mark bsdi_crypt hashes as deprecated if they have even rounds.
  297. if not self.rounds & 1:
  298. return True
  299. # hand off to base implementation
  300. return super(bsdi_crypt, self)._calc_needs_update(**kwds)
  301. #===================================================================
  302. # backends
  303. #===================================================================
  304. backends = ("os_crypt", "builtin")
  305. #---------------------------------------------------------------
  306. # os_crypt backend
  307. #---------------------------------------------------------------
  308. @classmethod
  309. def _load_backend_os_crypt(cls):
  310. if test_crypt("test", '_/...lLDAxARksGCHin.'):
  311. cls._set_calc_checksum_backend(cls._calc_checksum_os_crypt)
  312. return True
  313. else:
  314. return False
  315. def _calc_checksum_os_crypt(self, secret):
  316. config = self.to_string()
  317. hash = safe_crypt(secret, config)
  318. if hash:
  319. assert hash.startswith(config[:9]) and len(hash) == 20
  320. return hash[-11:]
  321. else:
  322. # py3's crypt.crypt() can't handle non-utf8 bytes.
  323. # fallback to builtin alg, which is always available.
  324. return self._calc_checksum_builtin(secret)
  325. #---------------------------------------------------------------
  326. # builtin backend
  327. #---------------------------------------------------------------
  328. @classmethod
  329. def _load_backend_builtin(cls):
  330. cls._set_calc_checksum_backend(cls._calc_checksum_builtin)
  331. return True
  332. def _calc_checksum_builtin(self, secret):
  333. return _raw_bsdi_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii")
  334. #===================================================================
  335. # eoc
  336. #===================================================================
  337. class bigcrypt(uh.HasSalt, uh.GenericHandler):
  338. """This class implements the BigCrypt password hash, and follows the :ref:`password-hash-api`.
  339. It supports a fixed-length salt.
  340. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  341. :type salt: str
  342. :param salt:
  343. Optional salt string.
  344. If not specified, one will be autogenerated (this is recommended).
  345. If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
  346. :type relaxed: bool
  347. :param relaxed:
  348. By default, providing an invalid value for one of the other
  349. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  350. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  351. will be issued instead. Correctable errors include
  352. ``salt`` strings that are too long.
  353. .. versionadded:: 1.6
  354. """
  355. #===================================================================
  356. # class attrs
  357. #===================================================================
  358. #--GenericHandler--
  359. name = "bigcrypt"
  360. setting_kwds = ("salt",)
  361. checksum_chars = uh.HASH64_CHARS
  362. # NOTE: checksum chars must be multiple of 11
  363. #--HasSalt--
  364. min_salt_size = max_salt_size = 2
  365. salt_chars = uh.HASH64_CHARS
  366. #===================================================================
  367. # internal helpers
  368. #===================================================================
  369. _hash_regex = re.compile(u(r"""
  370. ^
  371. (?P<salt>[./a-z0-9]{2})
  372. (?P<chk>([./a-z0-9]{11})+)?
  373. $"""), re.X|re.I)
  374. @classmethod
  375. def from_string(cls, hash):
  376. hash = to_unicode(hash, "ascii", "hash")
  377. m = cls._hash_regex.match(hash)
  378. if not m:
  379. raise uh.exc.InvalidHashError(cls)
  380. salt, chk = m.group("salt", "chk")
  381. return cls(salt=salt, checksum=chk)
  382. def to_string(self):
  383. hash = u("%s%s") % (self.salt, self.checksum)
  384. return uascii_to_str(hash)
  385. def _norm_checksum(self, checksum, relaxed=False):
  386. checksum = super(bigcrypt, self)._norm_checksum(checksum, relaxed=relaxed)
  387. if len(checksum) % 11:
  388. raise uh.exc.InvalidHashError(self)
  389. return checksum
  390. #===================================================================
  391. # backend
  392. #===================================================================
  393. def _calc_checksum(self, secret):
  394. if isinstance(secret, unicode):
  395. secret = secret.encode("utf-8")
  396. chk = _raw_des_crypt(secret, self.salt.encode("ascii"))
  397. idx = 8
  398. end = len(secret)
  399. while idx < end:
  400. next = idx + 8
  401. chk += _raw_des_crypt(secret[idx:next], chk[-11:-9])
  402. idx = next
  403. return chk.decode("ascii")
  404. #===================================================================
  405. # eoc
  406. #===================================================================
  407. class crypt16(uh.TruncateMixin, uh.HasSalt, uh.GenericHandler):
  408. """This class implements the crypt16 password hash, and follows the :ref:`password-hash-api`.
  409. It supports a fixed-length salt.
  410. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  411. :type salt: str
  412. :param salt:
  413. Optional salt string.
  414. If not specified, one will be autogenerated (this is recommended).
  415. If specified, it must be 2 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
  416. :param bool truncate_error:
  417. By default, crypt16 will silently truncate passwords larger than 16 bytes.
  418. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
  419. to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
  420. .. versionadded:: 1.7
  421. :type relaxed: bool
  422. :param relaxed:
  423. By default, providing an invalid value for one of the other
  424. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  425. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  426. will be issued instead. Correctable errors include
  427. ``salt`` strings that are too long.
  428. .. versionadded:: 1.6
  429. """
  430. #===================================================================
  431. # class attrs
  432. #===================================================================
  433. #--------------------
  434. # PasswordHash
  435. #--------------------
  436. name = "crypt16"
  437. setting_kwds = ("salt", "truncate_error")
  438. #--------------------
  439. # GenericHandler
  440. #--------------------
  441. checksum_size = 22
  442. checksum_chars = uh.HASH64_CHARS
  443. #--------------------
  444. # HasSalt
  445. #--------------------
  446. min_salt_size = max_salt_size = 2
  447. salt_chars = uh.HASH64_CHARS
  448. #--------------------
  449. # TruncateMixin
  450. #--------------------
  451. truncate_size = 16
  452. #===================================================================
  453. # internal helpers
  454. #===================================================================
  455. _hash_regex = re.compile(u(r"""
  456. ^
  457. (?P<salt>[./a-z0-9]{2})
  458. (?P<chk>[./a-z0-9]{22})?
  459. $"""), re.X|re.I)
  460. @classmethod
  461. def from_string(cls, hash):
  462. hash = to_unicode(hash, "ascii", "hash")
  463. m = cls._hash_regex.match(hash)
  464. if not m:
  465. raise uh.exc.InvalidHashError(cls)
  466. salt, chk = m.group("salt", "chk")
  467. return cls(salt=salt, checksum=chk)
  468. def to_string(self):
  469. hash = u("%s%s") % (self.salt, self.checksum)
  470. return uascii_to_str(hash)
  471. #===================================================================
  472. # backend
  473. #===================================================================
  474. def _calc_checksum(self, secret):
  475. if isinstance(secret, unicode):
  476. secret = secret.encode("utf-8")
  477. # check for truncation (during .hash() calls only)
  478. if self.use_defaults:
  479. self._check_truncate_policy(secret)
  480. # parse salt value
  481. try:
  482. salt_value = h64.decode_int12(self.salt.encode("ascii"))
  483. except ValueError: # pragma: no cover - caught by class
  484. raise suppress_cause(ValueError("invalid chars in salt"))
  485. # convert first 8 byts of secret string into an integer,
  486. key1 = _crypt_secret_to_key(secret)
  487. # run data through des using input of 0
  488. result1 = des_encrypt_int_block(key1, 0, salt_value, 20)
  489. # convert next 8 bytes of secret string into integer (key=0 if secret < 8 chars)
  490. key2 = _crypt_secret_to_key(secret[8:16])
  491. # run data through des using input of 0
  492. result2 = des_encrypt_int_block(key2, 0, salt_value, 5)
  493. # done
  494. chk = h64big.encode_int64(result1) + h64big.encode_int64(result2)
  495. return chk.decode("ascii")
  496. #===================================================================
  497. # eoc
  498. #===================================================================
  499. #=============================================================================
  500. # eof
  501. #=============================================================================