chiark / gitweb /
pwsafe, catacomb/pwsafe.py: Documentation and cleanup.
[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   ## Choose a passphrase, and generate master keys.
109   pp = C.ppread(tag, C.PMODE_VERIFY)
110   if not mac: mac = hash + '-hmac'
111   c = C.gcciphers[cipher]
112   h = C.gchashes[hash]
113   m = C.gcmacs[mac]
114   ppk = PW.PPK(pp, c, h, m)
115   ck = C.rand.block(c.keysz.default)
116   mk = C.rand.block(m.keysz.default)
117   k = Crypto(c, h, m, ck, mk)
118
119   ## Set up the database, storing the basic information we need.
120   db = G.open(file, 'n', 0600)
121   db['tag'] = tag
122   db['salt'] = ppk.salt
123   db['cipher'] = cipher
124   db['hash'] = hash
125   db['mac'] = mac
126   db['key'] = ppk.encrypt(wrapstr(ck) + wrapstr(mk))
127   db['magic'] = k.encrypt(C.rand.block(h.hashsz))
128
129 def cmd_changepp(av):
130   if len(av) != 0:
131     return 1
132   pw = PW(file, 'w')
133   pw.changepp()
134
135 def cmd_find(av):
136   if len(av) != 1:
137     return 1
138   pw = PW(file)
139   try:
140     print pw[av[0]]
141   except KeyError, exc:
142     die('Password `%s\' not found.' % exc.args[0])
143
144 def cmd_store(av):
145   if len(av) < 1 or len(av) > 2:
146     return 1
147   tag = av[0]
148   if len(av) < 2:
149     pp = C.getpass("Enter passphrase `%s': " % tag)
150     vpp = C.getpass("Confirm passphrase `%s': " % tag)
151     if pp != vpp:
152       raise ValueError, "passphrases don't match"
153   elif av[1] == '-':
154     pp = stdin.readline()
155   else:
156     pp = av[1]
157   pw = PW(file, 'w')
158   pw[av[0]] = chomp(pp)
159
160 def cmd_copy(av):
161   if len(av) < 1 or len(av) > 2:
162     return 1
163   pw_in = PW(file)
164   pw_out = PW(av[0], 'w')
165   if len(av) >= 3:
166     pat = av[1]
167   else:
168     pat = None
169   for k in pw_in:
170     if pat is None or fnmatch(k, pat):
171       pw_out[k] = pw_in[k]
172
173 def cmd_list(av):
174   if len(av) > 1:
175     return 1
176   pw = PW(file)
177   if len(av) >= 1:
178     pat = av[0]
179   else:
180     pat = None
181   for k in pw:
182     if pat is None or fnmatch(k, pat):
183       print k
184
185 def cmd_topixie(av):
186   if len(av) > 2:
187     return 1
188   pw = PW(file)
189   pix = C.Pixie()
190   if len(av) == 0:
191     for tag in pw:
192       pix.set(tag, pw[tag])
193   else:
194     tag = av[0]
195     if len(av) >= 2:
196       pptag = av[1]
197     else:
198       pptag = av[0]
199     pix.set(pptag, pw[tag])
200
201 def cmd_del(av):
202   if len(av) != 1:
203     return 1
204   pw = PW(file, 'w')
205   tag = av[0]
206   try:
207     del pw[tag]
208   except KeyError, exc:
209     die('Password `%s\' not found.' % exc.args[0])
210
211 def cmd_dump(av):
212   db = gdbm.open(file, 'r')
213   k = db.firstkey()
214   while True:
215     if k is None: break
216     print '%r: %r' % (present(k), present(db[k]))
217     k = db.nextkey(k)
218
219 commands = { 'create': [cmd_create,
220                         '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'],
221              'find' : [cmd_find, 'LABEL'],
222              'store' : [cmd_store, 'LABEL [VALUE]'],
223              'list' : [cmd_list, '[GLOB-PATTERN]'],
224              'changepp' : [cmd_changepp, ''],
225              'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
226              'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'],
227              'delete' : [cmd_del, 'TAG'],
228              'dump' : [cmd_dump, '']}
229
230 ###--------------------------------------------------------------------------
231 ### Command-line handling and dispatch.
232
233 def version():
234   print '%s 1.0.0' % prog
235
236 def usage(fp):
237   print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
238
239 def help():
240   version()
241   print
242   usage(stdout)
243   print '''
244 Maintains passwords or other short secrets securely.
245
246 Options:
247
248 -h, --help              Show this help text.
249 -v, --version           Show program version number.
250 -u, --usage             Show short usage message.
251
252 -f, --file=FILE         Where to find the password-safe file.
253
254 Commands provided:
255 '''
256   for c in commands:
257     print '%s %s' % (c, commands[c][1])
258
259 ## Choose a default database file.
260 if 'PWSAFE' in environ:
261   file = environ['PWSAFE']
262 else:
263   file = '%s/.pwsafe' % environ['HOME']
264
265 ## Parse the command-line options.
266 try:
267   opts, argv = getopt(argv[1:], 'hvuf:',
268                       ['help', 'version', 'usage', 'file='])
269 except GetoptError:
270   usage(stderr)
271   exit(1)
272 for o, a in opts:
273   if o in ('-h', '--help'):
274     help()
275     exit(0)
276   elif o in ('-v', '--version'):
277     version()
278     exit(0)
279   elif o in ('-u', '--usage'):
280     usage(stdout)
281     exit(0)
282   elif o in ('-f', '--file'):
283     file = a
284   else:
285     raise 'Barf!'
286 if len(argv) < 1:
287   usage(stderr)
288   exit(1)
289
290 ## Dispatch to a command handler.
291 if argv[0] in commands:
292   c = argv[0]
293   argv = argv[1:]
294 else:
295   c = 'find'
296 if commands[c][0](argv):
297   print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
298   exit(1)
299
300 ###----- That's all, folks --------------------------------------------------