chiark / gitweb /
pwsafe: Trim an overly-long section boundary.
[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 --------------------------------------------------