chiark / gitweb /
debian/: Use `dh_python2' for packaging.
[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 --------------------------------------------------