chiark / gitweb /
catacomb/pwsafe.py: Split out the GDBM-specifics from StorageBackend.
[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:
68 opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash='])
69 except GetoptError:
70 return 1
71 for o, a in opts:
9d0d0a7b
MW
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'
d1c45f5c 79
09b8041d
MW
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)
d7ab1bab 83
84def cmd_changepp(av):
9d0d0a7b 85 if len(av) != 0: return 1
5bf6e9f5 86 with PW(file, writep = True) as pw: pw.changepp()
d7ab1bab 87
88def cmd_find(av):
9d0d0a7b 89 if len(av) != 1: return 1
5bf6e9f5
MW
90 with PW(file) as pw:
91 try: print pw[av[0]]
92 except KeyError, exc: die("Password `%s' not found" % exc.args[0])
d7ab1bab 93
94def cmd_store(av):
5bf6e9f5 95 if len(av) < 1 or len(av) > 2: return 1
d7ab1bab 96 tag = av[0]
5bf6e9f5
MW
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] == '-':
63ba6dfa 103 pp = stdin.readline().rstrip('\n')
5bf6e9f5
MW
104 else:
105 pp = av[1]
63ba6dfa 106 pw[av[0]] = pp
d7ab1bab 107
108def cmd_copy(av):
9d0d0a7b 109 if len(av) < 1 or len(av) > 2: return 1
5bf6e9f5
MW
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]
d7ab1bab 116
117def cmd_list(av):
9d0d0a7b 118 if len(av) > 1: return 1
5bf6e9f5
MW
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
d7ab1bab 124
125def cmd_topixie(av):
9d0d0a7b 126 if len(av) > 2: return 1
5bf6e9f5
MW
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])
d7ab1bab 136
3aa33042 137def cmd_del(av):
9d0d0a7b 138 if len(av) != 1: return 1
5bf6e9f5
MW
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])
3aa33042 143
d7ab1bab 144commands = { 'create': [cmd_create,
d1c45f5c
MW
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]]'],
85f15f07 152 'delete' : [cmd_del, 'TAG']}
d1c45f5c
MW
153
154###--------------------------------------------------------------------------
155### Command-line handling and dispatch.
d7ab1bab 156
157def version():
2e6a3fda 158 print '%s 1.0.0' % prog
d1c45f5c 159
d7ab1bab 160def usage(fp):
2e6a3fda 161 print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
d1c45f5c 162
d7ab1bab 163def help():
164 version()
165 print
b2687a0a 166 usage(stdout)
d7ab1bab 167 print '''
168Maintains passwords or other short secrets securely.
169
170Options:
171
172-h, --help Show this help text.
173-v, --version Show program version number.
174-u, --usage Show short usage message.
175
2e6a3fda 176-f, --file=FILE Where to find the password-safe file.
177
d7ab1bab 178Commands provided:
179'''
05a82542 180 for c in sorted(commands):
d7ab1bab 181 print '%s %s' % (c, commands[c][1])
182
d1c45f5c
MW
183## Choose a default database file.
184if 'PWSAFE' in environ:
185 file = environ['PWSAFE']
186else:
187 file = '%s/.pwsafe' % environ['HOME']
188
189## Parse the command-line options.
d7ab1bab 190try:
d1c45f5c
MW
191 opts, argv = getopt(argv[1:], 'hvuf:',
192 ['help', 'version', 'usage', 'file='])
d7ab1bab 193except GetoptError:
194 usage(stderr)
195 exit(1)
196for 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!'
210if len(argv) < 1:
211 usage(stderr)
212 exit(1)
213
d1c45f5c 214## Dispatch to a command handler.
d7ab1bab 215if argv[0] in commands:
216 c = argv[0]
217 argv = argv[1:]
218else:
219 c = 'find'
40b16f0c
MW
220try:
221 if commands[c][0](argv):
222 print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
223 exit(1)
224except DecryptError:
225 die("decryption failure")
d1c45f5c
MW
226
227###----- That's all, folks --------------------------------------------------