*.[ch]: Some preparatory reformatting for the Python 3 porting.
[catacomb-python] / pwsafe
1 #! /usr/bin/python
2 ### -*-python-*-
3 ###
4 ### Tool for maintaining a secure-ish password database
5 ###
6 ### (c) 2005 Straylight/Edgeware
7 ###
8
9 ###----- Licensing notice ---------------------------------------------------
10 ###
11 ### This file is part of the Python interface to Catacomb.
12 ###
13 ### Catacomb/Python is free software; you can redistribute it and/or modify
14 ### it under the terms of the GNU General Public License as published by
15 ### the Free Software Foundation; either version 2 of the License, or
16 ### (at your option) any later version.
17 ###
18 ### Catacomb/Python is distributed in the hope that it will be useful,
19 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ### GNU General Public License for more details.
22 ###
23 ### You should have received a copy of the GNU General Public License
24 ### along with Catacomb/Python; if not, write to the Free Software Foundation,
25 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26
27 ###--------------------------------------------------------------------------
28 ### Imported modules.
29
30 from __future__ import with_statement
31
32 from os import environ
33 from sys import argv, exit, stdin, stdout, stderr
34 from getopt import getopt, GetoptError
35 from fnmatch import fnmatch
36 import re
37
38 import catacomb as C
39 from catacomb.pwsafe import *
40
41 ###--------------------------------------------------------------------------
42 ### Python version portability.
43
44 def _text(bin): return bin
45 def _bin(text): return text
46
47 def _excval(): return SYS.exc_info()[1]
48
49 ###--------------------------------------------------------------------------
50 ### Utilities.
51
52 ## The program name.
53 prog = re.sub(r'^.*[/\\]', '', argv[0])
54
55 def moan(msg):
56 """Issue a warning message MSG."""
57 stderr.write('%s: %s\n' % (prog, msg))
58
59 def die(msg):
60 """Report MSG as a fatal error, and exit."""
61 moan(msg)
62 exit(1)
63
64 ###--------------------------------------------------------------------------
65 ### Subcommand implementations.
66
67 def cmd_create(av):
68
69 ## Default crypto-primitive selections.
70 cipher = 'rijndael-cbc'
71 hash = 'sha256'
72 mac = None
73
74 ## Parse the options.
75 try:
76 opts, args = getopt(av, 'c:d:h:m:',
77 ['cipher=', 'database=', 'mac=', 'hash='])
78 except GetoptError:
79 return 1
80 dbty = 'flat'
81 for o, a in opts:
82 if o in ('-c', '--cipher'): cipher = a
83 elif o in ('-m', '--mac'): mac = a
84 elif o in ('-h', '--hash'): hash = a
85 elif o in ('-d', '--database'): dbty = a
86 else: raise Exception("barf")
87 if len(args) > 2: return 1
88 if len(args) >= 1: tag = args[0]
89 else: tag = 'pwsafe'
90
91 ## Set up the database.
92 if mac is None: mac = hash + '-hmac'
93 try: dbcls = StorageBackend.byname(dbty)
94 except KeyError: die("Unknown database backend `%s'" % dbty)
95 PW.create(dbcls, file, tag,
96 C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac])
97
98 def cmd_changepp(av):
99 if len(av) != 0: return 1
100 with PW(file, writep = True) as pw: pw.changepp()
101
102 def cmd_find(av):
103 if len(av) != 1: return 1
104 with PW(file) as pw:
105 try: print(_text(pw[_bin(av[0])]))
106 except KeyError: die("Password `%s' not found" % _excval().args[0])
107
108 def cmd_store(av):
109 if len(av) < 1 or len(av) > 2: return 1
110 tag = av[0]
111 with PW(file, writep = True) as pw:
112 if len(av) < 2:
113 pp = C.getpass("Enter passphrase `%s': " % tag)
114 vpp = C.getpass("Confirm passphrase `%s': " % tag)
115 if pp != vpp: die("passphrases don't match")
116 elif av[1] == '-':
117 pp = _bin(stdin.readline().rstrip('\n'))
118 else:
119 pp = _bin(av[1])
120 pw[_bin(av[0])] = pp
121
122 def cmd_copy(av):
123 if len(av) < 1 or len(av) > 2: return 1
124 with PW(file) as pw_in:
125 with PW(av[0], writep = True) as pw_out:
126 if len(av) >= 3: pat = av[1]
127 else: pat = None
128 for k in pw_in:
129 ktext = _text(k)
130 if pat is None or fnmatch(ktext, pat): pw_out[k] = pw_in[k]
131
132 def cmd_list(av):
133 if len(av) > 1: return 1
134 with PW(file) as pw:
135 if len(av) >= 1: pat = av[0]
136 else: pat = None
137 for k in pw:
138 ktext = _text(k)
139 if pat is None or fnmatch(ktext, pat): print(ktext)
140
141 def cmd_topixie(av):
142 if len(av) > 2: return 1
143 with PW(file) as pw:
144 pix = C.Pixie()
145 if len(av) == 0:
146 for tag in pw: pix.set(tag, pw[_bin(tag)])
147 else:
148 tag = _bin(av[0])
149 if len(av) >= 2: pptag = av[1]
150 else: pptag = av[0]
151 pix.set(pptag, pw[tag])
152
153 def cmd_del(av):
154 if len(av) != 1: return 1
155 with PW(file, writep = True) as pw:
156 tag = _bin(av[0])
157 try: del pw[tag]
158 except KeyError: die("Password `%s' not found" % _excval().args[0])
159
160 def cmd_xfer(av):
161
162 ## Parse the command line.
163 try: opts, args = getopt(av, 'd:', ['database='])
164 except GetoptError: return 1
165 dbty = 'flat'
166 for o, a in opts:
167 if o in ('-d', '--database'): dbty = a
168 else: raise Exception("barf")
169 if len(args) != 1: return 1
170 try: dbcls = StorageBackend.byname(dbty)
171 except KeyError: die("Unknown database backend `%s'" % dbty)
172
173 ## Create the target database.
174 with StorageBackend.open(file) as db_in:
175 with dbcls.create(args[0]) as db_out:
176 for k, v in db_in.iter_meta(): db_out.put_meta(k, v)
177 for k, v in db_in.iter_passwds(): db_out.put_passwd(k, v)
178
179 commands = { 'create': [cmd_create,
180 '[-c CIPHER] [-d DBTYPE] [-h HASH] [-m MAC] [PP-TAG]'],
181 'find' : [cmd_find, 'LABEL'],
182 'store' : [cmd_store, 'LABEL [VALUE]'],
183 'list' : [cmd_list, '[GLOB-PATTERN]'],
184 'changepp' : [cmd_changepp, ''],
185 'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
186 'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'],
187 'delete' : [cmd_del, 'TAG'],
188 'xfer': [cmd_xfer, '[-d DBTYPE] DEST-FILE'] }
189
190 ###--------------------------------------------------------------------------
191 ### Command-line handling and dispatch.
192
193 def version():
194 print('%s 1.0.0' % prog)
195 print('Backend types: %s' %
196 ' '.join([c.NAME for c in StorageBackend.classes()]))
197
198 def usage(fp):
199 fp.write('Usage: %s COMMAND [ARGS...]\n' % prog)
200
201 def help():
202 version()
203 print('')
204 usage(stdout)
205 print('''
206 Maintains passwords or other short secrets securely.
207
208 Options:
209
210 -h, --help Show this help text.
211 -v, --version Show program version number.
212 -u, --usage Show short usage message.
213
214 -f, --file=FILE Where to find the password-safe file.
215
216 Commands provided:
217 ''')
218 for c in sorted(commands):
219 print('%s %s' % (c, commands[c][1]))
220
221 ## Choose a default database file.
222 if 'PWSAFE' in environ:
223 file = environ['PWSAFE']
224 else:
225 file = '%s/.pwsafe' % environ['HOME']
226
227 ## Parse the command-line options.
228 try:
229 opts, argv = getopt(argv[1:], 'hvuf:',
230 ['help', 'version', 'usage', 'file='])
231 except GetoptError:
232 usage(stderr)
233 exit(1)
234 for o, a in opts:
235 if o in ('-h', '--help'):
236 help()
237 exit(0)
238 elif o in ('-v', '--version'):
239 version()
240 exit(0)
241 elif o in ('-u', '--usage'):
242 usage(stdout)
243 exit(0)
244 elif o in ('-f', '--file'):
245 file = a
246 else:
247 raise Exception("barf")
248 if len(argv) < 1:
249 usage(stderr)
250 exit(1)
251
252 ## Dispatch to a command handler.
253 if argv[0] in commands:
254 c = argv[0]
255 argv = argv[1:]
256 else:
257 c = 'find'
258 try:
259 if commands[c][0](argv):
260 stderr.write('Usage: %s %s %s\n' % (prog, c, commands[c][1]))
261 exit(1)
262 except DecryptError:
263 die("decryption failure")
264
265 ###----- That's all, folks --------------------------------------------------