pax_global_header00006660000000000000000000000064121422202060014501gustar00rootroot0000000000000052 comment=1a36d85095f70598079ab5daec5b30a51bce97b2 assword-0.7/000077500000000000000000000000001214222020600130315ustar00rootroot00000000000000assword-0.7/.gitignore000066400000000000000000000000311214222020600150130ustar00rootroot00000000000000*~ *.pyc build assword.1 assword-0.7/COPYING000066400000000000000000000012141214222020600140620ustar00rootroot00000000000000Assword is free software. You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program, (in the COPYING-GPL-3 file in this directory). If not, see http://www.gnu.org/licenses/ assword-0.7/Makefile000066400000000000000000000011651214222020600144740ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- VERSION:=$(shell git describe --tags | sed -e s/_/~/ -e s/-/+/ -e s/-/~/) all: assword.1 assword.1: assword help2man ./assword -N -n 'Simple and secure password database and retrieval system' -o $@ .PHONY: all debian-snapshot clean clean: rm -f assword.1 debian-snapshot: rm -rf build/deb mkdir -p build/deb/debian git archive HEAD | tar -x -C build/deb/ git archive debian:debian | tar -x -C build/deb/debian/ cd build/deb; dch -b -v $(VERSION) -D UNRELEASED 'test build, not for upload' cd build/deb; echo '3.0 (native)' > debian/source/format cd build/deb; debuild -us -uc assword-0.7/README000066400000000000000000000036161214222020600137170ustar00rootroot00000000000000Assword ======= Simple and secure password management and retrieval system. Assword is a secure password manager that relies on your OpenPGP key for security and is designed to integrate in a minimal fashion into any X11 window manager. Passwords and context strings are stored in a single OpenPGP-encrypted and signed file (meaning entry contexts are not exposed to the filesystem). Along with a simple command-line interface, there is a streamlined GUI meant for X11 window manager integration. When invoked, the GUI produces a prompt to search stored contexts. New entries can also easily be created. Passwords are securely retrieved without displaying on the screen. Multiple retrieval methods are available, including auto-typing them directly into an X11 window (default), or inserting them into the X11 clipboard. Source ------ Clone the repo: $ git clone git://finestructure.net/assword $ cd assword Dependencies : * python (>= 2.6) * python-gpgme - Python wrapper for the GPGME library * python-gtk2 - Python bindings for the GTK+ widget set * python-pkg-resources - Package Discovery and Resource Access Recommends (for curses UI) : * python-xdo - Support for simulating X11 input (libxdo bindings) * xclip - Support for accessing X11 clipboard Debian ------ Debian/Ubuntu snapshot packages can be easily made from the git source. You can build the package from any branch but it requires an up-to-date local branch of origin/debian, e.g.: $ git branch debian origin/debian Then: $ sudo apt-get install build-essential devscripts pkg-config python-all-dev python-setuptools debhelper dpkg-dev fakeroot $ make debian-snapshot $ sudo apt-get install python-gpgme python-gtk2 python-pkg-resources python-xdo xclip $ sudo dpkg -i build/assword_0.*_amd64.deb Using Assword ============= See the included assword(1) man page or built-in help string for detailed usage. assword-0.7/TODO000066400000000000000000000047611214222020600135310ustar00rootroot00000000000000 * BUG: xsearch context drop-down truncates long contexts. This is problematic if you need to distinguish between multiple long contexts. * BUG: xsearch "create" button scales with window size. It should stay a fixed size, to allow the context entry to maximally fill the window width. * ENHANCEMENT: xdo has two input methods -- one based on XSendEvent, and one based on injecting keystrokes into the X session as a whole (the difference is whether a window ID parameter is passed to "type"). We use the latter right now in xsearch despite its possibility of leakage because some X11 applications ignore XSendEvent input. Provide some hinting mechanism derived from the window in question to conditionally enable XSendEvent instead. * ENHANCEMENT: add iterator to db class? * ENHANCEMENT: test suite! * ENHANCEMENT: be able to edit a context once it is created. for example, i often create a password when setting up an account on a remote web site. Some account signup workflows don't tell me if the account name i wanted was taken until after i've signed up. By then, i've created the password, and i want to adjust it manually. * ENHANCEMENT: can we use python clipboard bindings instead of "xclip -i"? * ENHANCEMENT: on xsearch if selected window is known browser, and it's possible to extract url, preseed context search with hostname of url. * ENHANCEMENT: xsearch create action should modify the database, move the user's focus back into the textentry (highlighting the whole entry text), disable the "Create" button, and *not* explicitly select and terminate. The user can then select in the usual way (pressing enter) or can continue without selecting. * ENHANCEMENT: review the label texts and make sure they're saying reasonable things in different parts of the workflow. * ENHANCEMENT: ctrl+del from xsearch when a matching context is present should allow deletion of the indicated password. This should probably prompt for confirmation. * ENHANCEMENT: ctrl+e from xsearch when a matching context is present should display another entry with the context's password in it; the user can then edit the password to adjust for stupid web site rules. the rest of the UI should be disabled (set_sensitive(False)) while this password editor is active. * ENHANCEMENT: consider how to deal with multiple DB backends, and post-save and pre-open hooks (e.g. to push to and fetch from a remote repository of these changes) assword-0.7/assword000077500000000000000000000212361214222020600144450ustar00rootroot00000000000000#!/usr/bin/env python import os import sys import getpass import json import gpgme import assword import subprocess import pkg_resources ############################################################ PROG = os.path.basename(sys.argv[0]) def version(): print PROG, pkg_resources.get_distribution('assword').version def usage(): print "Usage:", PROG, " [...]" print """ Commands: add [] Add a new entry. If context is '-' read from stdin. If not specified, user will be prompted for context. See ASSWORD_PASSWORD for information on passwords. dump [] Dump search results as json. If string not specified all entries are returned. Passwords will not be displayed unless ASSWORD_DUMP_PASSWORDS is set. gui [] GUI interface, good for X11 window manager integration. Upon invocation the user will be prompted to decrypt the database, after which a graphical search prompt will be presented. If an additional string is provided, it will be added as the initial search string. All matching results for the query will be presented to the user. When a result is selected, the password will be retrieved according to the method specified by ASSWORD_XPASTE. If no match is found, the user has the opportunity to generate and store a new password, which is then delivered via ASSWORD_XPASTE. remove Delete an entry from the database. version Report the version of this program. help This help. Environment: ASSWORD_DB Path to assword database file. Default: ~/.assword/db ASSWORD_KEYFILE File containing OpenPGP key ID of database encryption recipient. Default: ~/.assword/keyid ASSWORD_KEYID OpenPGP key ID of database encryption recipient. This overrides ASSWORD_KEYFILE if set. ASSWORD_PASSWORD For new entries, entropy of auto-generated password in bytes (actual generated password will be longer due to base64 encoding). If set to 'prompt' user will be prompted for for password. Default: %d ASSWORD_DUMP_PASSWORDS Include passwords in dump when set. ASSWORD_XPASTE Method for password retrieval. Options are: 'xdo', which attempts to type the password into the window that had focus on launch, or 'xclip' which inserts the password in the X clipboard. Default: xdo """%(assword.DEFAULT_NEW_PASSWORD_OCTETS) ############################################################ ASSWORD_DIR = os.path.join(os.path.expanduser('~'),'.assword') DBPATH = os.getenv('ASSWORD_DB', os.path.join(ASSWORD_DIR, 'db')) ############################################################ def xclip(text): p = subprocess.Popen(' '.join(["xclip", "-i"]), shell=True, stdin=subprocess.PIPE) p.communicate(text) ############################################################ # Return codes: # 1 command/load line error # 10 db error # 20 gpg/key error ############################################################ def open_db(keyid=None): try: db = assword.Database(DBPATH, keyid) except assword.DatabaseError as e: print >>sys.stderr, 'Assword database error: %s' % e.msg sys.exit(10) return db def get_keyid(): keyid = os.getenv('ASSWORD_KEYID') keyfile = os.getenv('ASSWORD_KEYFILE', os.path.join(ASSWORD_DIR, 'keyid')) if not keyid and os.path.exists(keyfile): with open(keyfile, 'r') as f: keyid = f.read().strip() save = False if not keyid: print >>sys.stderr, "OpenPGP key ID of encryption target not specified." print >>sys.stderr, "Please provide key ID in ASSWORD_KEYID environment variable," print >>sys.stderr, "or specify key ID now to save in ~/.assword/keyid file." keyid = raw_input('OpenPGP key ID: ') if keyid == '': keyid = None else: save = True if not keyid: sys.exit(20) try: gpg = gpgme.Context() gpg.get_key(keyid) except gpgme.GpgmeError: print >>sys.stderr, "Invalid key ID:", keyid sys.exit(20) if save: if not os.path.isdir(os.path.dirname(keyfile)): os.mkdir(os.path.dirname(keyfile)) with open(keyfile, 'w') as f: f.write(keyid) return keyid def password_prompt(): try: password0 = getpass.getpass('password: ') password1 = getpass.getpass('reenter password: ') if password0 != password1: print >>sys.stderr, "Error: Passwords do not match. Aborting." sys.exit(1) return password0 except KeyboardInterrupt: sys.exit(-1) # Add a password to the database. # First argument is potentially a context. def add(args): keyid = get_keyid() try: # get context as argument context = args[0] # or from stdin if context == '-': context = sys.stdin.read() # prompt for context if not specified except IndexError: context = raw_input('context: ') except KeyboardInterrupt: sys.exit(-1) db = open_db(keyid) # get password from prompt if requested if os.getenv('ASSWORD_PASSWORD') == 'prompt': password = password_prompt() # otherwise auto generate else: print >>sys.stderr, "Auto-generating password..." password = None db.add(context.strip(), password) # NOTE: This is what actually saves the new database! db.save() print >>sys.stderr, "New entry writen." def dump(args): query = ' '.join(args) if not os.path.exists(DBPATH): print >>sys.stderr, """Assword database does not exist. To add an entry to the database use 'assword add'. See 'assword help' for more information.""" sys.exit(10) db = open_db() results = db.search(query) output = {} for context in results: output[context] = {} output[context]['date'] = results[context]['date'] if os.getenv('ASSWORD_DUMP_PASSWORDS'): output[context]['password'] = results[context]['password'] print json.dumps(output, sort_keys=True, indent=2) # The X GUI def gui(args, method='xdo'): query = ' '.join(args) if method == 'xdo': try: import xdo except: print >>sys.stderr, "The xdo module is not found, so the 'xdo' paste method is not available." print >>sys.stderr, "Please install python-xdo." sys.exit(1) # initialize xdo x = xdo.xdo() # get the id of the currently focused window win = x.get_focused_window() elif method == 'xclip': pass else: print >>sys.stderr, "Unknown X paste method:", method sys.exit(1) # do it keyid = get_keyid() db = open_db(keyid) result = assword.Gui(db, query=query).returnValue() # type the password in the saved window if result: if method == 'xdo': x.focus_window(win) x.wait_for_window_focus(win) x.type(result['password']) elif method == 'xclip': xclip(result['password']) def remove(args): keyid = get_keyid() try: context = args[0] except IndexError: print >>sys.stderr, "Must specify index to remove." sys.exit(1) db = open_db(keyid) if context not in db.entries: print >>sys.stderr, "No entry with context '%s'." % (context) sys.exit(1) try: print >>sys.stderr, "Really remove entry '%s'?" % (context) response = raw_input("Type 'yes' to remove: ") except KeyboardInterrupt: sys.exit(-1) if response != 'yes': sys.exit(-1) db.remove(context) db.save() print >>sys.stderr, "Entry removed." ############################################################ # Basically main() if len(sys.argv) < 2: print >>sys.stderr, "Command not specified." usage() sys.exit(1) cmd = sys.argv[1] if cmd == 'add': add(sys.argv[2:]) elif cmd == 'dump': dump(sys.argv[2:]) elif cmd == 'gui': method = os.getenv('ASSWORD_XPASTE', 'xdo') gui(sys.argv[2:], method=method) elif cmd == 'remove': remove(sys.argv[2:]) elif cmd == 'version' or cmd == '--version': version() elif cmd == 'help' or cmd == '--help': usage() else: print >>sys.stderr, "Unknown command:", cmd print >>sys.stderr usage() sys.exit(1) assword-0.7/assword.py000066400000000000000000000211431214222020600150660ustar00rootroot00000000000000import os import io import gpgme import json import time import base64 import datetime import pygtk pygtk.require('2.0') import gtk import gobject ############################################################ DEFAULT_NEW_PASSWORD_OCTETS=18 def pwgen(bytes): s = os.urandom(bytes) return base64.b64encode(s) ############################################################ class DatabaseError(Exception): def __init__(self, msg): self.msg = msg def __str__(self): return repr(self.msg) class Database(): """An Assword database.""" def __init__(self, dbpath=None, keyid=None): """Database at dbpath will be decrypted and loaded into memory. If dbpath not specified, empty database will be initialized.""" self.dbpath = dbpath self.keyid = keyid # default database information self.type = 'assword' self.version = 1 self.entries = {} self.gpg = gpgme.Context() self.gpg.armor = True if self.dbpath and os.path.exists(self.dbpath): try: cleardata = self._decryptDB(self.dbpath) # FIXME: trap exception if json corrupt jsondata = json.loads(cleardata.getvalue()) except IOError as e: raise DatabaseError(e) except gpgme.GpgmeError as e: raise DatabaseError('Decryption error: %s' % (e[2])) # unpack the json data if 'type' not in jsondata or jsondata['type'] != self.type: raise DatabaseError('Database is not a proper assword database.') if 'version' not in jsondata or jsondata['version'] != self.version: raise DatabaseError('Incompatible database.') self.entries = jsondata['entries'] def _decryptDB(self, path): data = io.BytesIO() with io.BytesIO() as encdata: with open(path, 'rb') as f: encdata.write(f.read()) encdata.seek(0) sigs = self.gpg.decrypt_verify(encdata, data) # check signature if not sigs[0].validity >= gpgme.VALIDITY_FULL: raise DatabaseError(sigs, 'Signature on database was not fully valid.') data.seek(0) return data def _encryptDB(self, data, keyid): # The signer and the recipient are assumed to be the same. # FIXME: should these be separated? try: recipient = self.gpg.get_key(keyid or self.keyid) signer = self.gpg.get_key(keyid or self.keyid) except: raise DatabaseError('Could not retrieve GPG encryption key.') self.gpg.signers = [signer] encdata = io.BytesIO() data.seek(0) sigs = self.gpg.encrypt_sign([recipient], gpgme.ENCRYPT_ALWAYS_TRUST, data, encdata) encdata.seek(0) return encdata def add(self, context, password=None): """Add a new entry to the database. Database won't be saved to disk until save().""" if not password: bytes = int(os.getenv('ASSWORD_PASSWORD', DEFAULT_NEW_PASSWORD_OCTETS)) password = pwgen(bytes) e = {'password': password, 'date': datetime.datetime.now().isoformat()} self.entries[context] = e return e def remove(self, context): """Remove an entry from the database. Database won't be saved to disk until save().""" del self.entries[context] def save(self, keyid=None, path=None): """Save database to disk. Key ID must either be specified here or at database initialization. If path not specified, database will be saved at original dbpath location.""" # FIXME: should check that recipient is not different than who # the db was originally encrypted for if not keyid: keyid = self.keyid if not keyid: raise DatabaseError('Key ID for decryption not specified.') if not path: path = self.dbpath if not path: raise DatabaseError('Save path not specified.') jsondata = {'type': self.type, 'version': self.version, 'entries': self.entries} cleardata = io.BytesIO(json.dumps(jsondata, indent=2)) encdata = self._encryptDB(cleardata, keyid) newpath = path + '.new' bakpath = path + '.bak' with open(newpath, 'w') as f: f.write(encdata.getvalue()) if os.path.exists(path): os.rename(path, bakpath) os.rename(newpath, path) def search(self, query=None): """Search for query in contexts. If query is None, all entries will be returned.""" mset = {} for context, entry in self.entries.iteritems(): # simple substring match if query in context: mset[context] = entry return mset def __getitem__(self, context): '''Return database entry for exact context''' return self.entries[context] ############################################################ # Assumes that the func_data is set to the number of the text column in the # model. def match_func(completion, key, iter, column): model = completion.get_model() text = model[iter][column] if text.lower().find(key.lower()) > -1: return True return False class Gui: """Assword X-based query UI.""" def __init__(self, db, query=None): self.db = db self.query = None self.results = None self.selected = None self.window = None self.entry = None self.label = None if query: # If we have an intial query, directly do a search without # initializing any X objects. This will initialize the # database and potentially return entries. r = self.db.search(query) # If only a single entry is found, _search() will set the # result and attempt to close any X objects (of which # there are none). Since we don't need to initialize any # GUI, return the initialization immediately. # See .returnValue(). if len(r) == 1: self.selected = r[r.keys()[0]] return self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_border_width(10) self.entry = gtk.Entry() if query: self.entry.set_text(query) completion = gtk.EntryCompletion() self.entry.set_completion(completion) liststore = gtk.ListStore(gobject.TYPE_STRING) completion.set_model(liststore) completion.set_text_column(0) completion.set_match_func(match_func, 0) # 0 is column number for context in self.db.entries: liststore.append([context]) hbox = gtk.HBox() vbox = gtk.VBox() self.createbutton = gtk.Button("Create") self.label = gtk.Label("enter the context for the password you want:") self.window.add(vbox) vbox.add(self.label) vbox.add(hbox) hbox.add(self.entry) hbox.add(self.createbutton) self.entry.connect("activate", self.enter) self.entry.connect("changed", self.updatecreate) self.createbutton.connect("clicked", self.create) self.window.connect("destroy", self.destroy) self.window.connect("key-press-event", self.keypress) self.entry.show() self.label.show() vbox.show() hbox.show() self.createbutton.show() self.updatecreate(self.entry) self.window.show() def keypress(self, widget, event): if event.keyval == gtk.keysyms.Escape: gtk.main_quit() def updatecreate(self, widget, data=None): e = self.entry.get_text() self.createbutton.set_sensitive(e != '' and e not in self.db.entries) def enter(self, widget, data=None): e = self.entry.get_text() if e in self.db.entries: self.selected = self.db[e] if self.selected is None: self.label.set_text("weird -- no context found even though we thought there should be one") else: gtk.main_quit() else: self.label.set_text("no match") def create(self, widget, data=None): e = self.entry.get_text() self.selected = self.db.add(e) self.db.save() gtk.main_quit() def destroy(self, widget, data=None): gtk.main_quit() def returnValue(self): if self.selected is None: gtk.main() return self.selected assword-0.7/setup.py000077500000000000000000000007721214222020600145540ustar00rootroot00000000000000#!/usr/bin/env python from distutils.core import setup setup( name = 'assword', version = '0.7', description = 'Secure password management and retrieval system.', author = 'Jameson Rollins', author_email = 'jrollins@finestructure.net', url = 'http://finestructure.net/assword', py_modules = ['assword'], scripts = ['assword'], requires = [ 'gpgme', 'getpass', 'json', 'base64', 'gtk2', 'pkg_resources', ], )