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