chiark / gitweb /
pwsafe: Present the list of commands in alphabetical order.
[catacomb-python] / pwsafe
CommitLineData
24b3d57b 1#! /usr/bin/python
d1c45f5c
MW
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.
d7ab1bab 29
43c09851 30import gdbm as G
31from os import environ
d7ab1bab 32from sys import argv, exit, stdin, stdout, stderr
33from getopt import getopt, GetoptError
d7ab1bab 34from fnmatch import fnmatch
80dc9c94 35import re
2e6a3fda 36
d1c45f5c
MW
37import catacomb as C
38from catacomb.pwsafe import *
39
40###--------------------------------------------------------------------------
41### Utilities.
42
43## The program name.
2e6a3fda 44prog = re.sub(r'^.*[/\\]', '', argv[0])
d1c45f5c 45
2e6a3fda 46def moan(msg):
d1c45f5c 47 """Issue a warning message MSG."""
2e6a3fda 48 print >>stderr, '%s: %s' % (prog, msg)
d1c45f5c 49
2e6a3fda 50def die(msg):
d1c45f5c 51 """Report MSG as a fatal error, and exit."""
2e6a3fda 52 moan(msg)
53 exit(1)
d7ab1bab 54
d1c45f5c
MW
55def 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
61def 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
67def 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.
d7ab1bab 79
d7ab1bab 80def cmd_create(av):
d1c45f5c
MW
81
82 ## Default crypto-primitive selections.
d7ab1bab 83 cipher = 'blowfish-cbc'
84 hash = 'rmd160'
85 mac = None
d1c45f5c
MW
86
87 ## Parse the options.
d7ab1bab 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'
d1c45f5c 107
09b8041d
MW
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)
d7ab1bab 111
112def cmd_changepp(av):
113 if len(av) != 0:
114 return 1
115 pw = PW(file, 'w')
116 pw.changepp()
117
118def cmd_find(av):
119 if len(av) != 1:
120 return 1
121 pw = PW(file)
2e6a3fda 122 try:
123 print pw[av[0]]
124 except KeyError, exc:
125 die('Password `%s\' not found.' % exc.args[0])
d7ab1bab 126
127def 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')
43c09851 141 pw[av[0]] = chomp(pp)
d7ab1bab 142
143def 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
156def 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
168def cmd_topixie(av):
43c09851 169 if len(av) > 2:
d7ab1bab 170 return 1
171 pw = PW(file)
43c09851 172 pix = C.Pixie()
173 if len(av) == 0:
174 for tag in pw:
175 pix.set(tag, pw[tag])
d7ab1bab 176 else:
43c09851 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])
d7ab1bab 183
3aa33042 184def cmd_del(av):
185 if len(av) != 1:
186 return 1
187 pw = PW(file, 'w')
188 tag = av[0]
2e6a3fda 189 try:
190 del pw[tag]
191 except KeyError, exc:
192 die('Password `%s\' not found.' % exc.args[0])
3aa33042 193
d7ab1bab 194def 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
202commands = { 'create': [cmd_create,
d1c45f5c
MW
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.
d7ab1bab 215
216def version():
2e6a3fda 217 print '%s 1.0.0' % prog
d1c45f5c 218
d7ab1bab 219def usage(fp):
2e6a3fda 220 print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
d1c45f5c 221
d7ab1bab 222def help():
223 version()
224 print
b2687a0a 225 usage(stdout)
d7ab1bab 226 print '''
227Maintains passwords or other short secrets securely.
228
229Options:
230
231-h, --help Show this help text.
232-v, --version Show program version number.
233-u, --usage Show short usage message.
234
2e6a3fda 235-f, --file=FILE Where to find the password-safe file.
236
d7ab1bab 237Commands provided:
238'''
05a82542 239 for c in sorted(commands):
d7ab1bab 240 print '%s %s' % (c, commands[c][1])
241
d1c45f5c
MW
242## Choose a default database file.
243if 'PWSAFE' in environ:
244 file = environ['PWSAFE']
245else:
246 file = '%s/.pwsafe' % environ['HOME']
247
248## Parse the command-line options.
d7ab1bab 249try:
d1c45f5c
MW
250 opts, argv = getopt(argv[1:], 'hvuf:',
251 ['help', 'version', 'usage', 'file='])
d7ab1bab 252except GetoptError:
253 usage(stderr)
254 exit(1)
255for 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!'
269if len(argv) < 1:
270 usage(stderr)
271 exit(1)
272
d1c45f5c 273## Dispatch to a command handler.
d7ab1bab 274if argv[0] in commands:
275 c = argv[0]
276 argv = argv[1:]
277else:
278 c = 'find'
40b16f0c
MW
279try:
280 if commands[c][0](argv):
281 print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
282 exit(1)
283except DecryptError:
284 die("decryption failure")
d1c45f5c
MW
285
286###----- That's all, folks --------------------------------------------------