chiark / gitweb /
pwsafe, catacomb/pwsafe.py: Push database creation into module.
[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 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 if commands[c][0](argv):
280   print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
281   exit(1)
282
283 ###----- That's all, folks --------------------------------------------------