chiark / gitweb /
pwsafe: Report password mismatch as an error, not an exception.
[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   pw = PW(file, writep = True)
93   pw.changepp()
94
95 def cmd_find(av):
96   if len(av) != 1: return 1
97   pw = PW(file)
98   try: print pw[av[0]]
99   except KeyError, exc: die("Password `%s' not found." % exc.args[0])
100
101 def cmd_store(av):
102   if len(av) < 1 or len(av) > 2:
103     return 1
104   tag = av[0]
105   if len(av) < 2:
106     pp = C.getpass("Enter passphrase `%s': " % tag)
107     vpp = C.getpass("Confirm passphrase `%s': " % tag)
108     if pp != vpp: die("passphrases don't match")
109   elif av[1] == '-':
110     pp = stdin.readline()
111   else:
112     pp = av[1]
113   pw = PW(file, writep = True)
114   pw[av[0]] = chomp(pp)
115
116 def cmd_copy(av):
117   if len(av) < 1 or len(av) > 2: return 1
118   pw_in = PW(file)
119   pw_out = PW(av[0], 'w')
120   if len(av) >= 3: pat = av[1]
121   else: pat = None
122   for k in pw_in:
123     if pat is None or fnmatch(k, pat): pw_out[k] = pw_in[k]
124
125 def cmd_list(av):
126   if len(av) > 1: return 1
127   pw = PW(file)
128   if len(av) >= 1: pat = av[0]
129   else: pat = None
130   for k in pw:
131     if pat is None or fnmatch(k, pat): print k
132
133 def cmd_topixie(av):
134   if len(av) > 2: return 1
135   pw = PW(file)
136   pix = C.Pixie()
137   if len(av) == 0:
138     for tag in pw: pix.set(tag, pw[tag])
139   else:
140     tag = av[0]
141     if len(av) >= 2: pptag = av[1]
142     else: pptag = av[0]
143     pix.set(pptag, pw[tag])
144
145 def cmd_del(av):
146   if len(av) != 1: return 1
147   pw = PW(file, writep = True)
148   tag = av[0]
149   try: del pw[tag]
150   except KeyError, exc: die("Password `%s' not found." % exc.args[0])
151
152 commands = { 'create': [cmd_create,
153                         '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'],
154              'find' : [cmd_find, 'LABEL'],
155              'store' : [cmd_store, 'LABEL [VALUE]'],
156              'list' : [cmd_list, '[GLOB-PATTERN]'],
157              'changepp' : [cmd_changepp, ''],
158              'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
159              'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'],
160              'delete' : [cmd_del, 'TAG']}
161
162 ###--------------------------------------------------------------------------
163 ### Command-line handling and dispatch.
164
165 def version():
166   print '%s 1.0.0' % prog
167
168 def usage(fp):
169   print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
170
171 def help():
172   version()
173   print
174   usage(stdout)
175   print '''
176 Maintains passwords or other short secrets securely.
177
178 Options:
179
180 -h, --help              Show this help text.
181 -v, --version           Show program version number.
182 -u, --usage             Show short usage message.
183
184 -f, --file=FILE         Where to find the password-safe file.
185
186 Commands provided:
187 '''
188   for c in sorted(commands):
189     print '%s %s' % (c, commands[c][1])
190
191 ## Choose a default database file.
192 if 'PWSAFE' in environ:
193   file = environ['PWSAFE']
194 else:
195   file = '%s/.pwsafe' % environ['HOME']
196
197 ## Parse the command-line options.
198 try:
199   opts, argv = getopt(argv[1:], 'hvuf:',
200                       ['help', 'version', 'usage', 'file='])
201 except GetoptError:
202   usage(stderr)
203   exit(1)
204 for o, a in opts:
205   if o in ('-h', '--help'):
206     help()
207     exit(0)
208   elif o in ('-v', '--version'):
209     version()
210     exit(0)
211   elif o in ('-u', '--usage'):
212     usage(stdout)
213     exit(0)
214   elif o in ('-f', '--file'):
215     file = a
216   else:
217     raise 'Barf!'
218 if len(argv) < 1:
219   usage(stderr)
220   exit(1)
221
222 ## Dispatch to a command handler.
223 if argv[0] in commands:
224   c = argv[0]
225   argv = argv[1:]
226 else:
227   c = 'find'
228 try:
229   if commands[c][0](argv):
230     print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
231     exit(1)
232 except DecryptError:
233   die("decryption failure")
234
235 ###----- That's all, folks --------------------------------------------------