aboutsummaryrefslogtreecommitdiffstats
path: root/libpwman/__main__.py
blob: 4107d4e2a8c031405b090476309c41cda7ff1578 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# -*- coding: utf-8 -*-
"""
# Simple password manager
# Copyright (c) 2011-2024 Michael Büsch <m@bues.ch>
# Licensed under the GNU/GPL version 2 or later.
"""

import argparse
import importlib
import libpwman
import pathlib
import sys
import traceback

__all__ = [
	"main",
]

def getPassphrase(dbPath, verbose=True, infoFile=sys.stdout):
	dbExists = dbPath.exists()
	if verbose:
		if dbExists:
			print("Opening database '%s'..." % dbPath,
			      file=infoFile)
		else:
			print("Creating NEW database '%s'..." % dbPath,
			      file=infoFile)
		promptSuffix = ""
	else:
		promptSuffix = " (%s)" % dbPath
	passphrase = libpwman.util.readPassphrase("Master passphrase%s" % promptSuffix,
						  verify=not dbExists)
	return passphrase

def run_infodump(dbPath):
	try:
		fc = libpwman.fileobj.FileObjCollection.parseFile(dbPath)
		print("pwman database: %s" % dbPath)
		head = fc.get(b"HEAD")
		if head != libpwman.cryptsql.CryptSQL.CSQL_HEADER:
			head = str(head)
			if len(head) > 16:
				head = head[:16] + "..."
			raise libpwman.PWManError("Invalid HEAD: %s" % head)
		for obj in fc.objects:
			name = bytes(obj.getName())
			data = bytes(obj.getData())
			trunc = False
			if name == b"PAYLOAD" and len(data) > 8:
				data = data[:8]
				trunc = True
			try:
				name = name.decode("UTF-8")
			except UnicodeError as e:
				raise libpwman.PWManError(
					"Failed to decode file header name.")
			try:
				data = data.decode("UTF-8")
			except UnicodeError as e:
				data = data.hex()
			if trunc:
				data += "..."
			pad = " " * (12 - len(name))
			print("  %s%s: %s" % (name, pad, data))
	except libpwman.fileobj.FileObjError as e:
		raise libpwman.PWManError(str(e))
	return 0

def run_diff(dbPath, oldDbPath, diffFormat):
	for p in (dbPath, oldDbPath):
		if not p.exists():
			print("Database '%s' does not exist." % p,
			      file=sys.stderr)
			return 1

	# Open the new database
	dbPassphrase = getPassphrase(dbPath, verbose=True,
				     infoFile=sys.stderr)
	if dbPassphrase is None:
		return 1
	db = libpwman.database.PWManDatabase(filename=dbPath,
					     passphrase=dbPassphrase,
					     readOnly=True)

	try:
		# Try to open the old database with the passphrase
		# of the new database.
		oldDb = libpwman.database.PWManDatabase(filename=oldDbPath,
							passphrase=dbPassphrase,
							readOnly=True)
	except libpwman.PWManError:
		# The attempt failed. Ask the user for the proper passphrase.
		dbPassphrase = getPassphrase(oldDbPath, verbose=True,
					     infoFile=sys.stderr)
		if dbPassphrase is None:
			return 1
		oldDb = libpwman.database.PWManDatabase(filename=oldDbPath,
							passphrase=dbPassphrase,
							readOnly=True)

	diff = libpwman.dbdiff.PWManDatabaseDiff(db=db, oldDb=oldDb)
	if diffFormat == "unified":
		print(diff.getUnifiedDiff())
	elif diffFormat == "context":
		print(diff.getContextDiff())
	elif diffFormat == "ndiff":
		print(diff.getNdiffDiff())
	elif diffFormat == "html":
		print(diff.getHtmlDiff())
	else:
		assert 0, "Invalid diffFormat"
		return 1
	return 0

def run_script(dbPath, pyModName):
	try:
		if pyModName.lower().endswith(".py"):
			pyModName = pyModName[:-3]
		pyMod = importlib.import_module(pyModName)
	except ImportError as e:
		print("Failed to import --call-pymod "
		      "Python module '%s':\n%s" % (
		      pyModName, str(e)),
		      file=sys.stderr)
		return 1
	run = getattr(pyMod, "run", None)
	if not callable(run):
		print("%s.run is not a callable." % (
		      pyModName),
		      file=sys.stderr)
		return 1

	passphrase = getPassphrase(dbPath, verbose=False)
	if passphrase is None:
		return 1
	db = libpwman.database.PWManDatabase(filename=dbPath,
					     passphrase=passphrase,
					     readOnly=False)
	try:
		run(db)
	except Exception as e:
		print("%s.run(database) raised an exception:\n\n%s" % (
		      pyModName, traceback.format_exc()),
		      file=sys.stderr)
		return 1
	db.flunkDirty()
	return 0

def run_ui(dbPath, timeout, commands):
	passphrase = getPassphrase(dbPath, verbose=not commands)
	if passphrase is None:
		return 1
	try:
		p = libpwman.PWMan(filename=dbPath,
				   passphrase=passphrase,
				   timeout=timeout)
		if commands:
			for command in commands:
				p.runOneCommand(command)
		else:
			p.interactive()
		p.flunkDirty()
	except libpwman.PWManTimeout as e:
		libpwman.util.clearScreen()
		print("pwman session timeout after %d seconds of inactivity." % (
		      e.seconds), file=sys.stderr)
		p.flunkDirty()
		print("exiting...", file=sys.stderr)
		return 1
	return 0

def runQuickSelfTests():
	from libpwman.argon2 import Argon2
	Argon2.get().quickSelfTest()

	from libpwman.aes import AES
	AES.get().quickSelfTest()

def main():
	p = argparse.ArgumentParser(
		description="Commandline password manager - "
			    "pwman version %s" % libpwman.__version__)
	p.add_argument("-v", "--version", action="store_true",
		       help="show the pwman version and exit")
	grp = p.add_mutually_exclusive_group()
	grp.add_argument("-p", "--call-pymod", type=str, metavar="PYTHONSCRIPT.py",
			 help="Calls the Python function run(database) from "
			      "Python module PYTHONSCRIPT. An open PWManDatabase "
			      "object is passed to run().")
	grp.add_argument("-D", "--diff", type=pathlib.Path, default=None, metavar="OLD_DB_PATH",
			 help="Diff the database (see DB_PATH) to the "
			      "older version specified as OLD_DB_PATH.")
	grp.add_argument("-c", "--command", action="append",
			 help="Run this command instead of starting in interactive mode. "
			      "-c|--command may be used multiple times.")
	grp.add_argument("-I", "--info", action="store_true",
			 help="Dump basic information about the database (without decrypting it).")
	p.add_argument("-F", "--diff-format", type=lambda x: str(x).lower().strip(),
		       default="unified",
		       choices=("unified", "context", "ndiff", "html"),
		       help="Select the diff format for the -D|--diff argument.")
	p.add_argument("database", nargs="?", metavar="DB_PATH",
		       type=pathlib.Path, default=libpwman.database.getDefaultDatabase(),
		       help="Use DB_PATH as database file. If not given, %s is used." % (
			    libpwman.database.getDefaultDatabase()))
	p.add_argument("--no-mlock", action="store_true",
		       help="Do not lock memory and allow swapping to disk. "
			    "Do not use this option, if you don't know what this means, "
			    "because this option has security implications.")
	if libpwman.util.osIsPosix:
		p.add_argument("-t", "--timeout", type=int, default=600, metavar="SECONDS",
			       help="Sets the session timeout in seconds. Default is 10 minutes.")
	args = p.parse_args()

	if args.version:
		print("pwman version %s" % libpwman.__version__)
		return 0

	exitcode = 1
	try:
		interactiveMode = (not args.command and
				   not args.diff and
				   not args.call_pymod and
				   not args.info)

		# Lock memory to RAM.
		if not args.no_mlock and not args.info:
			err = libpwman.mlock.MLockWrapper.get().mlockall()
			baseMsg1 = "Failed to lock the pwman program memory to RAM to avoid "\
				   "swapping secrets to disk.\nThe system call returned:"
			baseMsg2 = "The contents of the decrypted password database "\
				   "or the master password could possibly be written "\
				   "to an unencrypted swap-file or swap-partition on disk."
			baseMsg3 = "If you have an unencrypted swap space and if this is a problem, "\
				   "please abort NOW."
			if err and interactiveMode:
				print("\nWARNING: %s '%s'\n%s\n%s\n" % (
				      baseMsg1,
				      err,
				      baseMsg2,
				      baseMsg3),
				      file=sys.stderr)
			if err and not interactiveMode:
				raise libpwman.PWManError("Failed to lock memory: %s\n"
							  "%s" % (
							  err, baseMsg))

		if not args.info:
			runQuickSelfTests()

		if args.info:
			assert not interactiveMode
			exitcode = run_infodump(dbPath=args.database)
		elif args.diff:
			assert not interactiveMode
			exitcode = run_diff(dbPath=args.database,
					    oldDbPath=args.diff,
					    diffFormat=args.diff_format)
		elif args.call_pymod:
			assert not interactiveMode
			exitcode = run_script(dbPath=args.database,
					      pyModName=args.call_pymod)
		else:
			assert interactiveMode != bool(args.command)
			exitcode = run_ui(dbPath=args.database,
					  timeout=args.timeout if libpwman.util.osIsPosix else None,
					  commands=args.command)
	except libpwman.database.CSQLError as e:
		print("SQL error: " + str(e), file=sys.stderr)
		return 1
	except libpwman.PWManError as e:
		print("Error: " + str(e), file=sys.stderr)
		return 1
	return exitcode

if __name__ == "__main__":
	sys.exit(main())
bues.ch cgit interface