chiark / gitweb /
catacomb/pwsafe.py: Make `PW' be a context manager, and use it.
[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 ### Utilities.
43
44 ## The program name.
45 prog = re.sub(r'^.*[/\\]', '', argv[0])
46
47 def moan(msg):
48   """Issue a warning message MSG."""
49   print >>stderr, '%s: %s' % (prog, msg)
50
51 def die(msg):
52   """Report MSG as a fatal error, and exit."""
53   moan(msg)
54   exit(1)
55
56 def chomp(pp):
57   """Return the string PP, without its trailing newline if it has one."""
58   if len(pp) > 0 and pp[-1] == '\n':
59     pp = pp[:-1]
60   return pp
61
62 ###--------------------------------------------------------------------------
63 ### Subcommand implementations.
64
65 def cmd_create(av):
66
67   ## Default crypto-primitive selections.
68   cipher = 'blowfish-cbc'
69   hash = 'rmd160'
70   mac = None
71
72   ## Parse the options.
73   try:
74     opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash='])
75   except GetoptError:
76     return 1
77   for o, a in opts:
78     if o in ('-c', '--cipher'): cipher = a
79     elif o in ('-m', '--mac'): mac = a
80     elif o in ('-h', '--hash'): hash = a
81     else: raise 'Barf!'
82   if len(args) > 2: return 1
83   if len(args) >= 1: tag = args[0]
84   else: tag = 'pwsafe'
85
86   ## Set up the database.
87   if mac is None: mac = hash + '-hmac'
88   PW.create(file, C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac], tag)
89
90 def cmd_changepp(av):
91   if len(av) != 0: return 1
92   with PW(file, writep = True) as pw: pw.changepp()
93
94 def cmd_find(av):
95   if len(av) != 1: return 1
96   with PW(file) as pw:
97     try: print pw[av[0]]
98     except KeyError, exc: die("Password `%s' not found" % exc.args[0])
99
100 def cmd_store(av):
101   if len(av) < 1 or len(av) > 2: return 1
102   tag = av[0]
103   with PW(file, writep = True) as pw:
104     if len(av) < 2:
105       pp = C.getpass("Enter passphrase `%s': " % tag)
106       vpp = C.getpass("Confirm passphrase `%s': " % tag)
107       if pp != vpp: die("passphrases don't match")
108     elif av[1] == '-':
109       pp = stdin.readline()
110     else:
111       pp = av[1]
112     pw[av[0]] = chomp(pp)
113
114 def cmd_copy(av):
115   if len(av) < 1 or len(av) > 2: return 1
116   with PW(file) as pw_in:
117     with PW(av[0], writep = True) as pw_out:
118       if len(av) >= 3: pat = av[1]
119       else: pat = None
120       for k in pw_in:
121         if pat is None or fnmatch(k, pat): pw_out[k] = pw_in[k]
122
123 def cmd_list(av):
124   if len(av) > 1: return 1
125   with PW(file) as pw:
126     if len(av) >= 1: pat = av[0]
127     else: pat = None
128     for k in pw:
129       if pat is None or fnmatch(k, pat): print k
130
131 def cmd_topixie(av):
132   if len(av) > 2: return 1
133   with PW(file) as pw:
134     pix = C.Pixie()
135     if len(av) == 0:
136       for tag in pw: pix.set(tag, pw[tag])
137     else:
138       tag = av[0]
139       if len(av) >= 2: pptag = av[1]
140       else: pptag = av[0]
141       pix.set(pptag, pw[tag])
142
143 def cmd_del(av):
144   if len(av) != 1: return 1
145   with PW(file, writep = True) as pw:
146     tag = av[0]
147     try: del pw[tag]
148     except KeyError, exc: die("Password `%s' not found" % exc.args[0])
149
150 commands = { 'create': [cmd_create,
151                         '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'],
152              'find' : [cmd_find, 'LABEL'],
153              'store' : [cmd_store, 'LABEL [VALUE]'],
154              'list' : [cmd_list, '[GLOB-PATTERN]'],
155              'changepp' : [cmd_changepp, ''],
156              'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
157              'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'],
158              'delete' : [cmd_del, 'TAG']}
159
160 ###--------------------------------------------------------------------------
161 ### Command-line handling and dispatch.
162
163 def version():
164   print '%s 1.0.0' % prog
165
166 def usage(fp):
167   print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
168
169 def help():
170   version()
171   print
172   usage(stdout)
173   print '''
174 Maintains passwords or other short secrets securely.
175
176 Options:
177
178 -h, --help              Show this help text.
179 -v, --version           Show program version number.
180 -u, --usage             Show short usage message.
181
182 -f, --file=FILE         Where to find the password-safe file.
183
184 Commands provided:
185 '''
186   for c in sorted(commands):
187     print '%s %s' % (c, commands[c][1])
188
189 ## Choose a default database file.
190 if 'PWSAFE' in environ:
191   file = environ['PWSAFE']
192 else:
193   file = '%s/.pwsafe' % environ['HOME']
194
195 ## Parse the command-line options.
196 try:
197   opts, argv = getopt(argv[1:], 'hvuf:',
198                       ['help', 'version', 'usage', 'file='])
199 except GetoptError:
200   usage(stderr)
201   exit(1)
202 for o, a in opts:
203   if o in ('-h', '--help'):
204     help()
205     exit(0)
206   elif o in ('-v', '--version'):
207     version()
208     exit(0)
209   elif o in ('-u', '--usage'):
210     usage(stdout)
211     exit(0)
212   elif o in ('-f', '--file'):
213     file = a
214   else:
215     raise 'Barf!'
216 if len(argv) < 1:
217   usage(stderr)
218   exit(1)
219
220 ## Dispatch to a command handler.
221 if argv[0] in commands:
222   c = argv[0]
223   argv = argv[1:]
224 else:
225   c = 'find'
226 try:
227   if commands[c][0](argv):
228     print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
229     exit(1)
230 except DecryptError:
231   die("decryption failure")
232
233 ###----- That's all, folks --------------------------------------------------