Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

auth.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. ###############################################################################
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) typedef int GmbH
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. ###############################################################################
  26. import os
  27. import base64
  28. import struct
  29. import time
  30. import binascii
  31. import hmac
  32. import hashlib
  33. import random
  34. from typing import Optional, Dict
  35. from autobahn.util import public
  36. from autobahn.util import xor as xor_array
  37. from autobahn.wamp.interfaces import IAuthenticator
  38. from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
  39. from cryptography.hazmat.primitives import hashes
  40. from cryptography.hazmat.backends import default_backend
  41. # if we don't have argon2/passlib (see "authentication" extra) then
  42. # you don't get AuthScram and variants
  43. try:
  44. from argon2.low_level import hash_secret
  45. from argon2 import Type
  46. from passlib.utils import saslprep
  47. HAS_ARGON = True
  48. except ImportError:
  49. HAS_ARGON = False
  50. __all__ = (
  51. 'AuthAnonymous',
  52. 'AuthScram',
  53. 'AuthCryptoSign',
  54. 'AuthWampCra',
  55. 'AuthTicket',
  56. 'create_authenticator',
  57. 'pbkdf2',
  58. 'generate_totp_secret',
  59. 'compute_totp',
  60. 'check_totp',
  61. 'qrcode_from_totp',
  62. 'derive_key',
  63. 'generate_wcs',
  64. 'compute_wcs',
  65. 'derive_scram_credential',
  66. )
  67. def create_authenticator(name, **kwargs):
  68. """
  69. Accepts various keys and values to configure an authenticator. The
  70. valid keys depend on the kind of authenticator but all can
  71. understand: `authextra`, `authid` and `authrole`
  72. :return: an instance implementing IAuthenticator with the given
  73. configuration.
  74. """
  75. try:
  76. klass = {
  77. AuthScram.name: AuthScram,
  78. AuthCryptoSign.name: AuthCryptoSign,
  79. AuthCryptoSignProxy.name: AuthCryptoSignProxy,
  80. AuthWampCra.name: AuthWampCra,
  81. AuthAnonymous.name: AuthAnonymous,
  82. AuthAnonymousProxy.name: AuthAnonymousProxy,
  83. AuthTicket.name: AuthTicket,
  84. }[name]
  85. except KeyError:
  86. raise ValueError(
  87. "Unknown authenticator '{}'".format(name)
  88. )
  89. # this may raise further ValueErrors if the kwargs are wrong
  90. authenticator = klass(**kwargs)
  91. return authenticator
  92. # experimental authentication API
  93. class AuthAnonymous(object):
  94. name = 'anonymous'
  95. def __init__(self, **kw):
  96. self._args = kw
  97. @property
  98. def authextra(self):
  99. return self._args.get('authextra', dict())
  100. def on_challenge(self, session, challenge):
  101. raise RuntimeError(
  102. "on_challenge called on anonymous authentication"
  103. )
  104. def on_welcome(self, msg, authextra):
  105. return None
  106. IAuthenticator.register(AuthAnonymous)
  107. class AuthAnonymousProxy(AuthAnonymous):
  108. name = 'anonymous-proxy'
  109. IAuthenticator.register(AuthAnonymousProxy)
  110. class AuthTicket(object):
  111. name = 'ticket'
  112. def __init__(self, **kw):
  113. self._args = kw
  114. try:
  115. self._ticket = self._args.pop('ticket')
  116. except KeyError:
  117. raise ValueError(
  118. "ticket authentication requires 'ticket=' kwarg"
  119. )
  120. @property
  121. def authextra(self):
  122. return self._args.get('authextra', dict())
  123. def on_challenge(self, session, challenge):
  124. assert challenge.method == "ticket"
  125. return self._ticket
  126. def on_welcome(self, msg, authextra):
  127. return None
  128. IAuthenticator.register(AuthTicket)
  129. class AuthCryptoSign(object):
  130. name = 'cryptosign'
  131. def __init__(self, **kw):
  132. # should put in checkconfig or similar
  133. for key in kw.keys():
  134. if key not in ['authextra', 'authid', 'authrole', 'privkey']:
  135. raise ValueError(
  136. "Unexpected key '{}' for {}".format(key, self.__class__.__name__)
  137. )
  138. for key in ['privkey']:
  139. if key not in kw:
  140. raise ValueError(
  141. "Must provide '{}' for cryptosign".format(key)
  142. )
  143. from autobahn.wamp.cryptosign import CryptosignKey
  144. self._privkey = CryptosignKey.from_bytes(
  145. binascii.a2b_hex(kw['privkey'])
  146. )
  147. if 'pubkey' in kw.get('authextra', dict()):
  148. pubkey = kw['authextra']['pubkey']
  149. if pubkey != self._privkey.public_key():
  150. raise ValueError(
  151. "Public key doesn't correspond to private key"
  152. )
  153. else:
  154. kw['authextra'] = kw.get('authextra', dict())
  155. kw['authextra']['pubkey'] = self._privkey.public_key()
  156. self._channel_binding = kw.get('authextra', dict()).get('channel_binding', None)
  157. self._args = kw
  158. @property
  159. def authextra(self):
  160. return self._args.get('authextra', dict())
  161. def on_challenge(self, session, challenge):
  162. channel_id = session._transport.transport_details.channel_id.get(self._channel_binding, None)
  163. return self._privkey.sign_challenge(challenge,
  164. channel_id=channel_id,
  165. channel_id_type=self._channel_binding)
  166. def on_welcome(self, msg, authextra):
  167. return None
  168. IAuthenticator.register(AuthCryptoSign)
  169. class AuthCryptoSignProxy(AuthCryptoSign):
  170. name = 'cryptosign-proxy'
  171. IAuthenticator.register(AuthCryptoSignProxy)
  172. def _hash_argon2id13_secret(password, salt, iterations, memory):
  173. """
  174. Internal helper. Returns the salted/hashed password using the
  175. argon2id-13 algorithm. The return value is base64-encoded.
  176. """
  177. rawhash = hash_secret(
  178. secret=password,
  179. salt=base64.b64decode(salt),
  180. time_cost=iterations,
  181. memory_cost=memory,
  182. parallelism=1, # hard-coded by WAMP-SCRAM spec
  183. hash_len=32,
  184. type=Type.ID,
  185. version=0x13, # note this is decimal "19" which appears in places
  186. )
  187. # spits out stuff like:
  188. # '$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
  189. _, tag, ver, options, salt_data, hash_data = rawhash.split(b'$')
  190. return hash_data
  191. def _hash_pbkdf2_secret(password, salt, iterations):
  192. """
  193. Internal helper for SCRAM authentication
  194. """
  195. return pbkdf2(password, salt, iterations, keylen=32)
  196. class AuthScram(object):
  197. """
  198. Implements "wamp-scram" authentication for components.
  199. NOTE: This is a prototype of a draft spec; see
  200. https://github.com/wamp-proto/wamp-proto/issues/135
  201. """
  202. name = 'scram'
  203. def __init__(self, **kw):
  204. if not HAS_ARGON:
  205. raise RuntimeError(
  206. "Cannot support WAMP-SCRAM without argon2_cffi and "
  207. "passlib libraries; install autobahn['scram']"
  208. )
  209. self._args = kw
  210. self._client_nonce = None
  211. @property
  212. def authextra(self):
  213. # is authextra() called exactly once per authentication?
  214. if self._client_nonce is None:
  215. self._client_nonce = base64.b64encode(os.urandom(16)).decode('ascii')
  216. return {
  217. "nonce": self._client_nonce,
  218. }
  219. def on_challenge(self, session, challenge):
  220. assert challenge.method == "scram"
  221. assert self._client_nonce is not None
  222. required_args = ['nonce', 'kdf', 'salt', 'iterations']
  223. optional_args = ['memory', 'channel_binding']
  224. for k in required_args:
  225. if k not in challenge.extra:
  226. raise RuntimeError(
  227. "WAMP-SCRAM challenge option '{}' is "
  228. " required but not specified".format(k)
  229. )
  230. for k in challenge.extra:
  231. if k not in optional_args + required_args:
  232. raise RuntimeError(
  233. "WAMP-SCRAM challenge has unknown attribute '{}'".format(k)
  234. )
  235. channel_binding = challenge.extra.get('channel_binding', '')
  236. server_nonce = challenge.extra['nonce'] # base64
  237. salt = challenge.extra['salt'] # base64
  238. iterations = int(challenge.extra['iterations'])
  239. memory = int(challenge.extra.get('memory', -1))
  240. password = self._args['password'].encode('utf8') # supplied by user
  241. authid = saslprep(self._args['authid'])
  242. algorithm = challenge.extra['kdf']
  243. client_nonce = self._client_nonce
  244. self._auth_message = (
  245. "{client_first_bare},{server_first},{client_final_no_proof}".format(
  246. client_first_bare="n={},r={}".format(authid, client_nonce),
  247. server_first="r={},s={},i={}".format(server_nonce, salt, iterations),
  248. client_final_no_proof="c={},r={}".format(channel_binding, server_nonce),
  249. )
  250. ).encode('ascii')
  251. if algorithm == 'argon2id-13':
  252. if memory == -1:
  253. raise ValueError(
  254. "WAMP-SCRAM 'argon2id-13' challenge requires 'memory' parameter"
  255. )
  256. self._salted_password = _hash_argon2id13_secret(password, salt, iterations, memory)
  257. elif algorithm == 'pbkdf2':
  258. self._salted_password = _hash_pbkdf2_secret(password, salt, iterations)
  259. else:
  260. raise RuntimeError(
  261. "WAMP-SCRAM specified unknown KDF '{}'".format(algorithm)
  262. )
  263. client_key = hmac.new(self._salted_password, b"Client Key", hashlib.sha256).digest()
  264. stored_key = hashlib.new('sha256', client_key).digest()
  265. client_signature = hmac.new(stored_key, self._auth_message, hashlib.sha256).digest()
  266. client_proof = xor_array(client_key, client_signature)
  267. return base64.b64encode(client_proof)
  268. def on_welcome(self, session, authextra):
  269. """
  270. When the server is satisfied, it sends a 'WELCOME' message.
  271. This hook allows us an opportunity to deny the session right
  272. before it gets set up -- we check the server-signature thus
  273. authorizing the server and if it fails we drop the connection.
  274. """
  275. alleged_server_sig = base64.b64decode(authextra['scram_server_signature'])
  276. server_key = hmac.new(self._salted_password, b"Server Key", hashlib.sha256).digest()
  277. server_signature = hmac.new(server_key, self._auth_message, hashlib.sha256).digest()
  278. if not hmac.compare_digest(server_signature, alleged_server_sig):
  279. session.log.error("Verification of server SCRAM signature failed")
  280. return "Verification of server SCRAM signature failed"
  281. session.log.info(
  282. "Verification of server SCRAM signature successful"
  283. )
  284. return None
  285. IAuthenticator.register(AuthScram)
  286. class AuthWampCra(object):
  287. name = 'wampcra'
  288. def __init__(self, **kw):
  289. # should put in checkconfig or similar
  290. for key in kw.keys():
  291. if key not in ['authextra', 'authid', 'authrole', 'secret']:
  292. raise ValueError(
  293. "Unexpected key '{}' for {}".format(key, self.__class__.__name__)
  294. )
  295. for key in ['secret', 'authid']:
  296. if key not in kw:
  297. raise ValueError(
  298. "Must provide '{}' for wampcra".format(key)
  299. )
  300. self._args = kw
  301. self._secret = kw.pop('secret')
  302. if not isinstance(self._secret, str):
  303. self._secret = self._secret.decode('utf8')
  304. @property
  305. def authextra(self):
  306. return self._args.get('authextra', dict())
  307. def on_challenge(self, session, challenge):
  308. key = self._secret.encode('utf8')
  309. if 'salt' in challenge.extra:
  310. key = derive_key(
  311. key,
  312. challenge.extra['salt'],
  313. challenge.extra['iterations'],
  314. challenge.extra['keylen']
  315. )
  316. signature = compute_wcs(
  317. key,
  318. challenge.extra['challenge'].encode('utf8')
  319. )
  320. return signature.decode('ascii')
  321. def on_welcome(self, msg, authextra):
  322. return None
  323. IAuthenticator.register(AuthWampCra)
  324. @public
  325. def generate_totp_secret(length=10):
  326. """
  327. Generates a new Base32 encoded, random secret.
  328. .. seealso:: http://en.wikipedia.org/wiki/Base32
  329. :param length: The length of the entropy used to generate the secret.
  330. :type length: int
  331. :returns: The generated secret in Base32 (letters ``A-Z`` and digits ``2-7``).
  332. The length of the generated secret is ``length * 8 / 5`` octets.
  333. :rtype: unicode
  334. """
  335. assert(type(length) == int)
  336. return base64.b32encode(os.urandom(length)).decode('ascii')
  337. @public
  338. def compute_totp(secret, offset=0):
  339. """
  340. Computes the current TOTP code.
  341. :param secret: Base32 encoded secret.
  342. :type secret: unicode
  343. :param offset: Time offset (in steps, use eg -1, 0, +1 for compliance with RFC6238)
  344. for which to compute TOTP.
  345. :type offset: int
  346. :returns: TOTP for current time (+/- offset).
  347. :rtype: unicode
  348. """
  349. assert(type(secret) == str)
  350. assert(type(offset) == int)
  351. try:
  352. key = base64.b32decode(secret)
  353. except TypeError:
  354. raise Exception('invalid secret')
  355. interval = offset + int(time.time()) // 30
  356. msg = struct.pack('>Q', interval)
  357. digest = hmac.new(key, msg, hashlib.sha1).digest()
  358. o = 15 & (digest[19])
  359. token = (struct.unpack('>I', digest[o:o + 4])[0] & 0x7fffffff) % 1000000
  360. return '{0:06d}'.format(token)
  361. @public
  362. def check_totp(secret, ticket):
  363. """
  364. Check a TOTP value received from a principal trying to authenticate against
  365. the expected value computed from the secret shared between the principal and
  366. the authenticating entity.
  367. The Internet can be slow, and clocks might not match exactly, so some
  368. leniency is allowed. RFC6238 recommends looking an extra time step in either
  369. direction, which essentially opens the window from 30 seconds to 90 seconds.
  370. :param secret: The secret shared between the principal (eg a client) that
  371. is authenticating, and the authenticating entity (eg a server).
  372. :type secret: unicode
  373. :param ticket: The TOTP value to be checked.
  374. :type ticket: unicode
  375. :returns: ``True`` if the TOTP value is correct, else ``False``.
  376. :rtype: bool
  377. """
  378. for offset in [0, 1, -1]:
  379. if ticket == compute_totp(secret, offset):
  380. return True
  381. return False
  382. @public
  383. def qrcode_from_totp(secret, label, issuer):
  384. if type(secret) != str:
  385. raise Exception('secret must be of type unicode, not {}'.format(type(secret)))
  386. if type(label) != str:
  387. raise Exception('label must be of type unicode, not {}'.format(type(label)))
  388. try:
  389. import qrcode
  390. import qrcode.image.svg
  391. except ImportError:
  392. raise Exception('qrcode not installed')
  393. return qrcode.make(
  394. 'otpauth://totp/{}?secret={}&issuer={}'.format(label, secret, issuer),
  395. box_size=3,
  396. image_factory=qrcode.image.svg.SvgImage).to_string()
  397. @public
  398. def pbkdf2(data, salt, iterations=1000, keylen=32, hashfunc=None):
  399. """
  400. Returns a binary digest for the PBKDF2 hash algorithm of ``data``
  401. with the given ``salt``. It iterates ``iterations`` time and produces a
  402. key of ``keylen`` bytes. By default SHA-256 is used as hash function,
  403. a different hashlib ``hashfunc`` can be provided.
  404. :param data: The data for which to compute the PBKDF2 derived key.
  405. :type data: bytes
  406. :param salt: The salt to use for deriving the key.
  407. :type salt: bytes
  408. :param iterations: The number of iterations to perform in PBKDF2.
  409. :type iterations: int
  410. :param keylen: The length of the cryptographic key to derive.
  411. :type keylen: int
  412. :param hashfunc: Name of the hash algorithm to use
  413. :type hashfunc: str
  414. :returns: The derived cryptographic key.
  415. :rtype: bytes
  416. """
  417. if not (type(data) == bytes) or \
  418. not (type(salt) == bytes) or \
  419. not (type(iterations) == int) or \
  420. not (type(keylen) == int):
  421. raise ValueError("Invalid argument types")
  422. # justification: WAMP-CRA uses SHA256 and users shouldn't have any
  423. # other reason to call this particular pbkdf2 function (arguably,
  424. # it should be private maybe?)
  425. if hashfunc is None:
  426. hashfunc = 'sha256'
  427. if hashfunc is callable:
  428. # used to take stuff from hashlib; translate?
  429. raise ValueError(
  430. "pbkdf2 now takes the name of a hash algorithm for 'hashfunc='"
  431. )
  432. backend = default_backend()
  433. # https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#pbkdf2
  434. kdf = PBKDF2HMAC(
  435. algorithm=getattr(hashes, hashfunc.upper())(),
  436. length=keylen,
  437. salt=salt,
  438. iterations=iterations,
  439. backend=backend,
  440. )
  441. return kdf.derive(data)
  442. @public
  443. def derive_key(secret, salt, iterations=1000, keylen=32):
  444. """
  445. Computes a derived cryptographic key from a password according to PBKDF2.
  446. .. seealso:: http://en.wikipedia.org/wiki/PBKDF2
  447. :param secret: The secret.
  448. :type secret: bytes or unicode
  449. :param salt: The salt to be used.
  450. :type salt: bytes or unicode
  451. :param iterations: Number of iterations of derivation algorithm to run.
  452. :type iterations: int
  453. :param keylen: Length of the key to derive in bytes.
  454. :type keylen: int
  455. :return: The derived key in Base64 encoding.
  456. :rtype: bytes
  457. """
  458. if not (type(secret) in [str, bytes]):
  459. raise ValueError("'secret' must be bytes")
  460. if not (type(salt) in [str, bytes]):
  461. raise ValueError("'salt' must be bytes")
  462. if not (type(iterations) == int):
  463. raise ValueError("'iterations' must be an integer")
  464. if not (type(keylen) == int):
  465. raise ValueError("'keylen' must be an integer")
  466. if type(secret) == str:
  467. secret = secret.encode('utf8')
  468. if type(salt) == str:
  469. salt = salt.encode('utf8')
  470. key = pbkdf2(secret, salt, iterations, keylen)
  471. return binascii.b2a_base64(key).strip()
  472. WCS_SECRET_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
  473. """
  474. The characters from which :func:`autobahn.wamp.auth.generate_wcs` generates secrets.
  475. """
  476. @public
  477. def generate_wcs(length=14):
  478. """
  479. Generates a new random secret for use with WAMP-CRA.
  480. The secret generated is a random character sequence drawn from
  481. - upper and lower case latin letters
  482. - digits
  483. -
  484. :param length: The length of the secret to generate.
  485. :type length: int
  486. :return: The generated secret. The length of the generated is ``length`` octets.
  487. :rtype: bytes
  488. """
  489. assert(type(length) == int)
  490. return "".join(random.choice(WCS_SECRET_CHARSET) for _ in range(length)).encode('ascii')
  491. @public
  492. def compute_wcs(key, challenge):
  493. """
  494. Compute an WAMP-CRA authentication signature from an authentication
  495. challenge and a (derived) key.
  496. :param key: The key derived (via PBKDF2) from the secret.
  497. :type key: bytes
  498. :param challenge: The authentication challenge to sign.
  499. :type challenge: bytes
  500. :return: The authentication signature.
  501. :rtype: bytes
  502. """
  503. assert(type(key) in [str, bytes])
  504. assert(type(challenge) in [str, bytes])
  505. if type(key) == str:
  506. key = key.encode('utf8')
  507. if type(challenge) == str:
  508. challenge = challenge.encode('utf8')
  509. sig = hmac.new(key, challenge, hashlib.sha256).digest()
  510. return binascii.b2a_base64(sig).strip()
  511. def derive_scram_credential(email: str, password: str, salt: Optional[bytes] = None) -> Dict:
  512. """
  513. Derive WAMP-SCRAM credentials from user email and password. The SCRAM parameters used
  514. are the following (these are also contained in the returned credentials):
  515. * kdf ``argon2id-13``
  516. * time cost ``4096``
  517. * memory cost ``512``
  518. * parallelism ``1``
  519. See `draft-irtf-cfrg-argon2 <https://datatracker.ietf.org/doc/draft-irtf-cfrg-argon2/>`__ and
  520. `argon2-cffi <https://argon2-cffi.readthedocs.io/en/stable/>`__.
  521. :param email: User email.
  522. :param password: User password.
  523. :param salt: Optional salt to use (must be 16 bytes long). If none is given, compute salt
  524. from email as ``salt = SHA256(email)[:16]``.
  525. :return: WAMP-SCRAM credentials. When serialized, the returned credentials can be copy-pasted
  526. into the ``config.json`` node configuration for a Crossbar.io node.
  527. """
  528. assert HAS_ARGON, 'missing dependency argon2'
  529. from argon2.low_level import hash_secret
  530. from argon2.low_level import Type
  531. # derive salt from email
  532. if not salt:
  533. m = hashlib.sha256()
  534. m.update(email.encode('utf8'))
  535. salt = m.digest()[:16]
  536. assert len(salt) == 16
  537. hash_data = hash_secret(
  538. secret=password.encode('utf8'),
  539. salt=salt,
  540. time_cost=4096,
  541. memory_cost=512,
  542. parallelism=1,
  543. hash_len=32,
  544. type=Type.ID,
  545. version=19,
  546. )
  547. _, tag, v, params, _, salted_password = hash_data.decode('ascii').split('$')
  548. assert tag == 'argon2id'
  549. assert v == 'v=19' # argon's version 1.3 is represented as 0x13, which is 19 decimal...
  550. params = {
  551. k: v
  552. for k, v in
  553. [x.split('=') for x in params.split(',')]
  554. }
  555. salted_password = salted_password.encode('ascii')
  556. client_key = hmac.new(salted_password, b"Client Key", hashlib.sha256).digest()
  557. stored_key = hashlib.new('sha256', client_key).digest()
  558. server_key = hmac.new(salted_password, b"Server Key", hashlib.sha256).digest()
  559. credential = {
  560. "kdf": "argon2id-13",
  561. "memory": int(params['m']),
  562. "iterations": int(params['t']),
  563. "salt": binascii.b2a_hex(salt).decode('ascii'),
  564. "stored-key": binascii.b2a_hex(stored_key).decode('ascii'),
  565. "server-key": binascii.b2a_hex(server_key).decode('ascii'),
  566. }
  567. return credential