pwsafe: Present the list of commands in alphabetical order.
[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 import gdbm as G
31 from os import environ
32 from sys import argv, exit, stdin, stdout, stderr
33 from getopt import getopt, GetoptError
34 from fnmatch import fnmatch
35 import re
36
37 import catacomb as C
38 from catacomb.pwsafe import *
39
40 ###--------------------------------------------------------------------------
41 ### Utilities.
42
43 ## The program name.
44 prog = re.sub(r'^.*[/\\]', '', argv[0])
45
46 def moan(msg):
47 """Issue a warning message MSG."""
48 print >>stderr, '%s: %s' % (prog, msg)
49
50 def die(msg):
51 """Report MSG as a fatal error, and exit."""
52 moan(msg)
53 exit(1)
54
55 def chomp(pp):
56 """Return the string PP, without its trailing newline if it has one."""
57 if len(pp) > 0 and pp[-1] == '\n':
58 pp = pp[:-1]
59 return pp
60
61 def asciip(s):
62 """Answer whether all of the characters of S are plain ASCII."""
63 for ch in s:
64 if ch < ' ' or ch > '~': return False
65 return True
66
67 def present(s):
68 """
69 Return a presentation form of the string S.
70
71 If S is plain ASCII, then return S unchanged; otherwise return it as one of
72 Catacomb's ByteString objects.
73 """
74 if asciip(s): return s
75 return C.ByteString(s)
76
77 ###--------------------------------------------------------------------------
78 ### Subcommand implementations.
79
80 def cmd_create(av):
81
82 ## Default crypto-primitive selections.
83 cipher = 'blowfish-cbc'
84 hash = 'rmd160'
85 mac = None
86
87 ## Parse the options.
88 try:
89 opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash='])
90 except GetoptError:
91 return 1
92 for o, a in opts:
93 if o in ('-c', '--cipher'):
94 cipher = a
95 elif o in ('-m', '--mac'):
96 mac = a
97 elif o in ('-h', '--hash'):
98 hash = a
99 else:
100 raise 'Barf!'
101 if len(args) > 2:
102 return 1
103 if len(args) >= 1:
104 tag = args[0]
105 else:
106 tag = 'pwsafe'
107
108 ## Set up the database.
109 if mac is None: mac = hash + '-hmac'
110 PW.create(file, C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac], tag)
111
112 def cmd_changepp(av):
113 if len(av) != 0:
114 return 1
115 pw = PW(file, 'w')
116 pw.changepp()
117
118 def cmd_find(av):
119 if len(av) != 1:
120 return 1
121 pw = PW(file)
122 try:
123 print pw[av[0]]
124 except KeyError, exc:
125 die('Password `%s\' not found.' % exc.args[0])
126
127 def cmd_store(av):
128 if len(av) < 1 or len(av) > 2:
129 return 1
130 tag = av[0]
131 if len(av) < 2:
132 pp = C.getpass("Enter passphrase `%s': " % tag)
133 vpp = C.getpass("Confirm passphrase `%s': " % tag)
134 if pp != vpp:
135 raise ValueError, "passphrases don't match"
136 elif av[1] == '-':
137 pp = stdin.readline()
138 else:
139 pp = av[1]
140 pw = PW(file, 'w')
141 pw[av[0]] = chomp(pp)
142
143 def cmd_copy(av):
144 if len(av) < 1 or len(av) > 2:
145 return 1
146 pw_in = PW(file)
147 pw_out = PW(av[0], 'w')
148 if len(av) >= 3:
149 pat = av[1]
150 else:
151 pat = None
152 for k in pw_in:
153 if pat is None or fnmatch(k, pat):
154 pw_out[k] = pw_in[k]
155
156 def cmd_list(av):
157 if len(av) > 1:
158 return 1
159 pw = PW(file)
160 if len(av) >= 1:
161 pat = av[0]
162 else:
163 pat = None
164 for k in pw:
165 if pat is None or fnmatch(k, pat):
166 print k
167
168 def cmd_topixie(av):
169 if len(av) > 2:
170 return 1
171 pw = PW(file)
172 pix = C.Pixie()
173 if len(av) == 0:
174 for tag in pw:
175 pix.set(tag, pw[tag])
176 else:
177 tag = av[0]
178 if len(av) >= 2:
179 pptag = av[1]
180 else:
181 pptag = av[0]
182 pix.set(pptag, pw[tag])
183
184 def cmd_del(av):
185 if len(av) != 1:
186 return 1
187 pw = PW(file, 'w')
188 tag = av[0]
189 try:
190 del pw[tag]
191 except KeyError, exc:
192 die('Password `%s\' not found.' % exc.args[0])
193
194 def cmd_dump(av):
195 db = gdbm.open(file, 'r')
196 k = db.firstkey()
197 while True:
198 if k is None: break
199 print '%r: %r' % (present(k), present(db[k]))
200 k = db.nextkey(k)
201
202 commands = { 'create': [cmd_create,
203 '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'],
204 'find' : [cmd_find, 'LABEL'],
205 'store' : [cmd_store, 'LABEL [VALUE]'],
206 'list' : [cmd_list, '[GLOB-PATTERN]'],
207 'changepp' : [cmd_changepp, ''],
208 'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
209 'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'],
210 'delete' : [cmd_del, 'TAG'],
211 'dump' : [cmd_dump, '']}
212
213 ###--------------------------------------------------------------------------
214 ### Command-line handling and dispatch.
215
216 def version():
217 print '%s 1.0.0' % prog
218
219 def usage(fp):
220 print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
221
222 def help():
223 version()
224 print
225 usage(stdout)
226 print '''
227 Maintains passwords or other short secrets securely.
228
229 Options:
230
231 -h, --help Show this help text.
232 -v, --version Show program version number.
233 -u, --usage Show short usage message.
234
235 -f, --file=FILE Where to find the password-safe file.
236
237 Commands provided:
238 '''
239 for c in sorted(commands):
240 print '%s %s' % (c, commands[c][1])
241
242 ## Choose a default database file.
243 if 'PWSAFE' in environ:
244 file = environ['PWSAFE']
245 else:
246 file = '%s/.pwsafe' % environ['HOME']
247
248 ## Parse the command-line options.
249 try:
250 opts, argv = getopt(argv[1:], 'hvuf:',
251 ['help', 'version', 'usage', 'file='])
252 except GetoptError:
253 usage(stderr)
254 exit(1)
255 for o, a in opts:
256 if o in ('-h', '--help'):
257 help()
258 exit(0)
259 elif o in ('-v', '--version'):
260 version()
261 exit(0)
262 elif o in ('-u', '--usage'):
263 usage(stdout)
264 exit(0)
265 elif o in ('-f', '--file'):
266 file = a
267 else:
268 raise 'Barf!'
269 if len(argv) < 1:
270 usage(stderr)
271 exit(1)
272
273 ## Dispatch to a command handler.
274 if argv[0] in commands:
275 c = argv[0]
276 argv = argv[1:]
277 else:
278 c = 'find'
279 try:
280 if commands[c][0](argv):
281 print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
282 exit(1)
283 except DecryptError:
284 die("decryption failure")
285
286 ###----- That's all, folks --------------------------------------------------