chiark / gitweb /
Merge remote-tracking branch 'origin/HEAD'
[catacomb-python] / pwsafe
CommitLineData
24b3d57b 1#! /usr/bin/python
d1c45f5c
MW
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.
d7ab1bab 29
85f15f07
MW
30from __future__ import with_statement
31
43c09851 32from os import environ
d7ab1bab 33from sys import argv, exit, stdin, stdout, stderr
34from getopt import getopt, GetoptError
d7ab1bab 35from fnmatch import fnmatch
80dc9c94 36import re
2e6a3fda 37
d1c45f5c
MW
38import catacomb as C
39from catacomb.pwsafe import *
40
41###--------------------------------------------------------------------------
42### Utilities.
43
44## The program name.
2e6a3fda 45prog = re.sub(r'^.*[/\\]', '', argv[0])
d1c45f5c 46
2e6a3fda 47def moan(msg):
d1c45f5c 48 """Issue a warning message MSG."""
2e6a3fda 49 print >>stderr, '%s: %s' % (prog, msg)
d1c45f5c 50
2e6a3fda 51def die(msg):
d1c45f5c 52 """Report MSG as a fatal error, and exit."""
2e6a3fda 53 moan(msg)
54 exit(1)
d7ab1bab 55
d1c45f5c
MW
56###--------------------------------------------------------------------------
57### Subcommand implementations.
d7ab1bab 58
d7ab1bab 59def cmd_create(av):
d1c45f5c
MW
60
61 ## Default crypto-primitive selections.
d7ab1bab 62 cipher = 'blowfish-cbc'
63 hash = 'rmd160'
64 mac = None
d1c45f5c
MW
65
66 ## Parse the options.
d7ab1bab 67 try:
6baae405
MW
68 opts, args = getopt(av, 'c:d:h:m:',
69 ['cipher=', 'database=', 'mac=', 'hash='])
d7ab1bab 70 except GetoptError:
71 return 1
8501dc39 72 dbty = 'flat'
d7ab1bab 73 for o, a in opts:
9d0d0a7b
MW
74 if o in ('-c', '--cipher'): cipher = a
75 elif o in ('-m', '--mac'): mac = a
76 elif o in ('-h', '--hash'): hash = a
6baae405 77 elif o in ('-d', '--database'): dbty = a
9d0d0a7b
MW
78 else: raise 'Barf!'
79 if len(args) > 2: return 1
80 if len(args) >= 1: tag = args[0]
81 else: tag = 'pwsafe'
d1c45f5c 82
09b8041d
MW
83 ## Set up the database.
84 if mac is None: mac = hash + '-hmac'
6baae405
MW
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])
d7ab1bab 89
90def cmd_changepp(av):
9d0d0a7b 91 if len(av) != 0: return 1
5bf6e9f5 92 with PW(file, writep = True) as pw: pw.changepp()
d7ab1bab 93
94def cmd_find(av):
9d0d0a7b 95 if len(av) != 1: return 1
5bf6e9f5
MW
96 with PW(file) as pw:
97 try: print pw[av[0]]
98 except KeyError, exc: die("Password `%s' not found" % exc.args[0])
d7ab1bab 99
100def cmd_store(av):
5bf6e9f5 101 if len(av) < 1 or len(av) > 2: return 1
d7ab1bab 102 tag = av[0]
5bf6e9f5
MW
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] == '-':
63ba6dfa 109 pp = stdin.readline().rstrip('\n')
5bf6e9f5
MW
110 else:
111 pp = av[1]
63ba6dfa 112 pw[av[0]] = pp
d7ab1bab 113
114def cmd_copy(av):
9d0d0a7b 115 if len(av) < 1 or len(av) > 2: return 1
5bf6e9f5
MW
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]
d7ab1bab 122
123def cmd_list(av):
9d0d0a7b 124 if len(av) > 1: return 1
5bf6e9f5
MW
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
d7ab1bab 130
131def cmd_topixie(av):
9d0d0a7b 132 if len(av) > 2: return 1
5bf6e9f5
MW
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])
d7ab1bab 142
3aa33042 143def cmd_del(av):
9d0d0a7b 144 if len(av) != 1: return 1
5bf6e9f5
MW
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])
3aa33042 149
dab03511
MW
150def 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
d7ab1bab 169commands = { 'create': [cmd_create,
6baae405 170 '[-c CIPHER] [-d DBTYPE] [-h HASH] [-m MAC] [PP-TAG]'],
d1c45f5c
MW
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]]'],
dab03511
MW
177 'delete' : [cmd_del, 'TAG'],
178 'xfer': [cmd_xfer, '[-d DBTYPE] DEST-FILE'] }
d1c45f5c
MW
179
180###--------------------------------------------------------------------------
181### Command-line handling and dispatch.
d7ab1bab 182
183def version():
2e6a3fda 184 print '%s 1.0.0' % prog
6baae405
MW
185 print 'Backend types: %s' % \
186 ' '.join([c.NAME for c in StorageBackend.classes()])
d1c45f5c 187
d7ab1bab 188def usage(fp):
2e6a3fda 189 print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
d1c45f5c 190
d7ab1bab 191def help():
192 version()
193 print
b2687a0a 194 usage(stdout)
d7ab1bab 195 print '''
196Maintains passwords or other short secrets securely.
197
198Options:
199
200-h, --help Show this help text.
201-v, --version Show program version number.
202-u, --usage Show short usage message.
203
2e6a3fda 204-f, --file=FILE Where to find the password-safe file.
205
d7ab1bab 206Commands provided:
207'''
05a82542 208 for c in sorted(commands):
d7ab1bab 209 print '%s %s' % (c, commands[c][1])
210
d1c45f5c
MW
211## Choose a default database file.
212if 'PWSAFE' in environ:
213 file = environ['PWSAFE']
214else:
215 file = '%s/.pwsafe' % environ['HOME']
216
217## Parse the command-line options.
d7ab1bab 218try:
d1c45f5c
MW
219 opts, argv = getopt(argv[1:], 'hvuf:',
220 ['help', 'version', 'usage', 'file='])
d7ab1bab 221except GetoptError:
222 usage(stderr)
223 exit(1)
224for 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!'
238if len(argv) < 1:
239 usage(stderr)
240 exit(1)
241
d1c45f5c 242## Dispatch to a command handler.
d7ab1bab 243if argv[0] in commands:
244 c = argv[0]
245 argv = argv[1:]
246else:
247 c = 'find'
40b16f0c
MW
248try:
249 if commands[c][0](argv):
250 print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
251 exit(1)
252except DecryptError:
253 die("decryption failure")
d1c45f5c
MW
254
255###----- That's all, folks --------------------------------------------------