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