argon2.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # -*- coding: utf-8 -*-
  2. """
  3. # Argon2 wrapper
  4. # Copyright (c) 2023-2024 Michael Büsch <m@bues.ch>
  5. # Licensed under the GNU/GPL version 2 or later.
  6. """
  7. from libpwman.exception import PWManError
  8. import gc
  9. import os
  10. __all__ = [
  11. "Argon2"
  12. ]
  13. class Argon2:
  14. """Abstraction layer for the Argon2 implementation.
  15. """
  16. __singleton = None
  17. DEBUG = False
  18. @classmethod
  19. def get(cls):
  20. """Get the Argon2 singleton.
  21. """
  22. if cls.__singleton is None:
  23. cls.__singleton = cls()
  24. return cls.__singleton
  25. def __init__(self):
  26. self.__argon2cffi = None
  27. self.__argon2pure = None
  28. argon2lib = os.getenv("PWMAN_ARGON2LIB", "").lower().strip()
  29. if argon2lib in ("", "argon2-cffi", "argon2cffi"):
  30. # Try to use argon2-cffi
  31. try:
  32. import argon2
  33. self.__argon2cffi = argon2
  34. return
  35. except ImportError as e:
  36. pass
  37. if argon2lib == "argon2pure":
  38. # Use argon2pure, but only if explicitly selected,
  39. # because it's really really slow.
  40. try:
  41. import argon2pure
  42. self.__argon2pure = argon2pure
  43. return
  44. except ImportError as e:
  45. pass
  46. msg = "Python module import error."
  47. if argon2lib == "":
  48. msg += "\n'argon2-cffi' is not installed."
  49. else:
  50. msg += "\n'PWMAN_ARGON2LIB=%s' is not supported or not installed." % argon2lib
  51. raise PWManError(msg)
  52. def argon2id_v1p3(self, passphrase, salt, timeCost, memCost, parallel, keyLen):
  53. """Run Argon2id v1.3.
  54. passphrase: The passphrase bytes.
  55. salt: The salt bytes.
  56. timeCost: The time cost, in number of iterations.
  57. memCost: The memory cost, in number of kiB.
  58. parallel: The number of parallel threads.
  59. keyLen: The number of bytes to return.
  60. """
  61. # Check parameters.
  62. if (not isinstance(passphrase, bytes) or
  63. len(passphrase) < 1 or
  64. len(passphrase) > ((1 << 32) - 1)):
  65. raise PWManError("Argon2id: Invalid passphrase.")
  66. if (not isinstance(salt, bytes) or
  67. len(salt) < 1 or
  68. len(salt) > ((1 << 32) - 1)):
  69. raise PWManError("Argon2id: Invalid salt.")
  70. if (not isinstance(timeCost, int) or
  71. timeCost < 1 or
  72. timeCost > ((1 << 32) - 1)):
  73. raise PWManError("Argon2id: Invalid time cost.")
  74. if (not isinstance(parallel, int) or
  75. parallel < 1 or
  76. parallel > ((1 << 24) - 1)):
  77. raise PWManError("Argon2id: Invalid parallelism.")
  78. if (not isinstance(memCost, int) or
  79. memCost < 8 * parallel or
  80. memCost > ((1 << 32) - 1)):
  81. raise PWManError("Argon2id: Invalid memory cost.")
  82. if (not isinstance(keyLen, int) or
  83. keyLen < 1 or
  84. keyLen > ((1 << 32) - 1)):
  85. raise PWManError("Argon2id: Invalid hash length.")
  86. # Memory is locked (for security reasons)
  87. # and we might not have much of it.
  88. # Try to free some unused memory to avoid OOM.
  89. gc.collect()
  90. if self.DEBUG:
  91. import time
  92. begin = time.time()
  93. key = None
  94. try:
  95. if self.__argon2cffi is not None:
  96. # Use argon2-cffi.
  97. low_level = self.__argon2cffi.low_level
  98. key = low_level.hash_secret_raw(
  99. secret=passphrase,
  100. salt=salt,
  101. time_cost=timeCost,
  102. memory_cost=memCost,
  103. parallelism=parallel,
  104. hash_len=keyLen,
  105. type=low_level.Type.ID,
  106. version=0x13,
  107. )
  108. elif self.__argon2pure is not None:
  109. # Use argon2pure.
  110. # Avoid subprocesses:
  111. # Do not use multiprocessing to keep all memory locked.
  112. # Subprocesses do not inherit mlockall().
  113. argon2pure = self.__argon2pure
  114. key = argon2pure.argon2(
  115. password=passphrase,
  116. salt=salt,
  117. time_cost=timeCost,
  118. memory_cost=memCost,
  119. parallelism=parallel,
  120. tag_length=keyLen,
  121. type_code=argon2pure.ARGON2ID,
  122. threads=1, # no threads
  123. use_threads=True, # no subprocesses
  124. version=0x13,
  125. )
  126. except Exception as e:
  127. raise PWManError("Argon2 error: %s: %s" % (type(e), str(e)))
  128. if key is None:
  129. raise PWManError("Argon2 not implemented.")
  130. if self.DEBUG:
  131. print("Argon2id took %.02f s." % (time.time() - begin))
  132. return key
  133. @classmethod
  134. def quickSelfTest(cls):
  135. """Run a quick algorithm self test.
  136. """
  137. inst = cls.get()
  138. h = inst.argon2id_v1p3(
  139. passphrase=b"namwp",
  140. salt=(b"pwman"*4),
  141. timeCost=4,
  142. memCost=16,
  143. parallel=2,
  144. keyLen=32,
  145. )
  146. if h != bytes.fromhex("6aa4b71bbf34cce1383577f2fcedecf1074fa7e1f5a664e00cf92f509fb54a35"):
  147. raise PWManError("Argon2id: Quick self test failed.")