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