__main__.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. # -*- coding: utf-8 -*-
  2. """
  3. # Simple password manager
  4. # Copyright (c) 2011-2024 Michael Büsch <m@bues.ch>
  5. # Licensed under the GNU/GPL version 2 or later.
  6. """
  7. import argparse
  8. import importlib
  9. import libpwman
  10. import pathlib
  11. import sys
  12. import traceback
  13. __all__ = [
  14. "main",
  15. ]
  16. def getPassphrase(dbPath, verbose=True, infoFile=sys.stdout):
  17. dbExists = dbPath.exists()
  18. if verbose:
  19. if dbExists:
  20. print("Opening database '%s'..." % dbPath,
  21. file=infoFile)
  22. else:
  23. print("Creating NEW database '%s'..." % dbPath,
  24. file=infoFile)
  25. promptSuffix = ""
  26. else:
  27. promptSuffix = " (%s)" % dbPath
  28. passphrase = libpwman.util.readPassphrase("Master passphrase%s" % promptSuffix,
  29. verify=not dbExists)
  30. return passphrase
  31. def run_infodump(dbPath):
  32. try:
  33. fc = libpwman.fileobj.FileObjCollection.parseFile(dbPath)
  34. print("pwman database: %s" % dbPath)
  35. head = fc.get(b"HEAD")
  36. if head != libpwman.cryptsql.CryptSQL.CSQL_HEADER:
  37. head = str(head)
  38. if len(head) > 16:
  39. head = head[:16] + "..."
  40. raise libpwman.PWManError("Invalid HEAD: %s" % head)
  41. for obj in fc.objects:
  42. name = bytes(obj.getName())
  43. data = bytes(obj.getData())
  44. trunc = False
  45. if name == b"PAYLOAD" and len(data) > 8:
  46. data = data[:8]
  47. trunc = True
  48. try:
  49. name = name.decode("UTF-8")
  50. except UnicodeError as e:
  51. raise libpwman.PWManError(
  52. "Failed to decode file header name.")
  53. try:
  54. data = data.decode("UTF-8")
  55. except UnicodeError as e:
  56. data = data.hex()
  57. if trunc:
  58. data += "..."
  59. pad = " " * (12 - len(name))
  60. print(" %s%s: %s" % (name, pad, data))
  61. except libpwman.fileobj.FileObjError as e:
  62. raise libpwman.PWManError(str(e))
  63. return 0
  64. def run_diff(dbPath, oldDbPath, diffFormat):
  65. for p in (dbPath, oldDbPath):
  66. if not p.exists():
  67. print("Database '%s' does not exist." % p,
  68. file=sys.stderr)
  69. return 1
  70. # Open the new database
  71. dbPassphrase = getPassphrase(dbPath, verbose=True,
  72. infoFile=sys.stderr)
  73. if dbPassphrase is None:
  74. return 1
  75. db = libpwman.database.PWManDatabase(filename=dbPath,
  76. passphrase=dbPassphrase,
  77. readOnly=True)
  78. try:
  79. # Try to open the old database with the passphrase
  80. # of the new database.
  81. oldDb = libpwman.database.PWManDatabase(filename=oldDbPath,
  82. passphrase=dbPassphrase,
  83. readOnly=True)
  84. except libpwman.PWManError:
  85. # The attempt failed. Ask the user for the proper passphrase.
  86. dbPassphrase = getPassphrase(oldDbPath, verbose=True,
  87. infoFile=sys.stderr)
  88. if dbPassphrase is None:
  89. return 1
  90. oldDb = libpwman.database.PWManDatabase(filename=oldDbPath,
  91. passphrase=dbPassphrase,
  92. readOnly=True)
  93. diff = libpwman.dbdiff.PWManDatabaseDiff(db=db, oldDb=oldDb)
  94. if diffFormat == "unified":
  95. print(diff.getUnifiedDiff())
  96. elif diffFormat == "context":
  97. print(diff.getContextDiff())
  98. elif diffFormat == "ndiff":
  99. print(diff.getNdiffDiff())
  100. elif diffFormat == "html":
  101. print(diff.getHtmlDiff())
  102. else:
  103. assert 0, "Invalid diffFormat"
  104. return 1
  105. return 0
  106. def run_script(dbPath, pyModName):
  107. try:
  108. if pyModName.lower().endswith(".py"):
  109. pyModName = pyModName[:-3]
  110. pyMod = importlib.import_module(pyModName)
  111. except ImportError as e:
  112. print("Failed to import --call-pymod "
  113. "Python module '%s':\n%s" % (
  114. pyModName, str(e)),
  115. file=sys.stderr)
  116. return 1
  117. run = getattr(pyMod, "run", None)
  118. if not callable(run):
  119. print("%s.run is not a callable." % (
  120. pyModName),
  121. file=sys.stderr)
  122. return 1
  123. passphrase = getPassphrase(dbPath, verbose=False)
  124. if passphrase is None:
  125. return 1
  126. db = libpwman.database.PWManDatabase(filename=dbPath,
  127. passphrase=passphrase,
  128. readOnly=False)
  129. try:
  130. run(db)
  131. except Exception as e:
  132. print("%s.run(database) raised an exception:\n\n%s" % (
  133. pyModName, traceback.format_exc()),
  134. file=sys.stderr)
  135. return 1
  136. db.flunkDirty()
  137. return 0
  138. def run_ui(dbPath, timeout, commands):
  139. passphrase = getPassphrase(dbPath, verbose=not commands)
  140. if passphrase is None:
  141. return 1
  142. try:
  143. p = libpwman.PWMan(filename=dbPath,
  144. passphrase=passphrase,
  145. timeout=timeout)
  146. if commands:
  147. for command in commands:
  148. p.runOneCommand(command)
  149. else:
  150. p.interactive()
  151. p.flunkDirty()
  152. except libpwman.PWManTimeout as e:
  153. libpwman.util.clearScreen()
  154. print("pwman session timeout after %d seconds of inactivity." % (
  155. e.seconds), file=sys.stderr)
  156. p.flunkDirty()
  157. print("exiting...", file=sys.stderr)
  158. return 1
  159. return 0
  160. def runQuickSelfTests():
  161. from libpwman.argon2 import Argon2
  162. Argon2.get().quickSelfTest()
  163. from libpwman.aes import AES
  164. AES.get().quickSelfTest()
  165. def main():
  166. p = argparse.ArgumentParser(
  167. description="Commandline password manager - "
  168. "pwman version %s" % libpwman.__version__)
  169. p.add_argument("-v", "--version", action="store_true",
  170. help="show the pwman version and exit")
  171. grp = p.add_mutually_exclusive_group()
  172. grp.add_argument("-p", "--call-pymod", type=str, metavar="PYTHONSCRIPT.py",
  173. help="Calls the Python function run(database) from "
  174. "Python module PYTHONSCRIPT. An open PWManDatabase "
  175. "object is passed to run().")
  176. grp.add_argument("-D", "--diff", type=pathlib.Path, default=None, metavar="OLD_DB_PATH",
  177. help="Diff the database (see DB_PATH) to the "
  178. "older version specified as OLD_DB_PATH.")
  179. grp.add_argument("-c", "--command", action="append",
  180. help="Run this command instead of starting in interactive mode. "
  181. "-c|--command may be used multiple times.")
  182. grp.add_argument("-I", "--info", action="store_true",
  183. help="Dump basic information about the database (without decrypting it).")
  184. p.add_argument("-F", "--diff-format", type=lambda x: str(x).lower().strip(),
  185. default="unified",
  186. choices=("unified", "context", "ndiff", "html"),
  187. help="Select the diff format for the -D|--diff argument.")
  188. p.add_argument("database", nargs="?", metavar="DB_PATH",
  189. type=pathlib.Path, default=libpwman.database.getDefaultDatabase(),
  190. help="Use DB_PATH as database file. If not given, %s is used." % (
  191. libpwman.database.getDefaultDatabase()))
  192. p.add_argument("--no-mlock", action="store_true",
  193. help="Do not lock memory and allow swapping to disk. "
  194. "Do not use this option, if you don't know what this means, "
  195. "because this option has security implications.")
  196. if libpwman.util.osIsPosix:
  197. p.add_argument("-t", "--timeout", type=int, default=600, metavar="SECONDS",
  198. help="Sets the session timeout in seconds. Default is 10 minutes.")
  199. args = p.parse_args()
  200. if args.version:
  201. print("pwman version %s" % libpwman.__version__)
  202. return 0
  203. exitcode = 1
  204. try:
  205. interactiveMode = (not args.command and
  206. not args.diff and
  207. not args.call_pymod and
  208. not args.info)
  209. # Lock memory to RAM.
  210. if not args.no_mlock and not args.info:
  211. err = libpwman.mlock.MLockWrapper.get().mlockall()
  212. baseMsg1 = "Failed to lock the pwman program memory to RAM to avoid "\
  213. "swapping secrets to disk.\nThe system call returned:"
  214. baseMsg2 = "The contents of the decrypted password database "\
  215. "or the master password could possibly be written "\
  216. "to an unencrypted swap-file or swap-partition on disk."
  217. baseMsg3 = "If you have an unencrypted swap space and if this is a problem, "\
  218. "please abort NOW."
  219. if err and interactiveMode:
  220. print("\nWARNING: %s '%s'\n%s\n%s\n" % (
  221. baseMsg1,
  222. err,
  223. baseMsg2,
  224. baseMsg3),
  225. file=sys.stderr)
  226. if err and not interactiveMode:
  227. raise libpwman.PWManError("Failed to lock memory: %s\n"
  228. "%s" % (
  229. err, baseMsg))
  230. if not args.info:
  231. runQuickSelfTests()
  232. if args.info:
  233. assert not interactiveMode
  234. exitcode = run_infodump(dbPath=args.database)
  235. elif args.diff:
  236. assert not interactiveMode
  237. exitcode = run_diff(dbPath=args.database,
  238. oldDbPath=args.diff,
  239. diffFormat=args.diff_format)
  240. elif args.call_pymod:
  241. assert not interactiveMode
  242. exitcode = run_script(dbPath=args.database,
  243. pyModName=args.call_pymod)
  244. else:
  245. assert interactiveMode != bool(args.command)
  246. exitcode = run_ui(dbPath=args.database,
  247. timeout=args.timeout if libpwman.util.osIsPosix else None,
  248. commands=args.command)
  249. except libpwman.database.CSQLError as e:
  250. print("SQL error: " + str(e), file=sys.stderr)
  251. return 1
  252. except libpwman.PWManError as e:
  253. print("Error: " + str(e), file=sys.stderr)
  254. return 1
  255. return exitcode
  256. if __name__ == "__main__":
  257. sys.exit(main())