PKnG6xxpw/__init__.pyfrom __future__ import absolute_import, division, print_function from .store import Store, Entry __version__ = '0.8.1' PKmGn*AA pw/_yaml.pyfrom __future__ import absolute_import, division, print_function from .store import Entry import sys, warnings if sys.version_info[0] < 3: str = unicode EXTENSIONS = ['.yaml', '.yml'] def make_entry(key, dict): notes = ' | '.join(str(dict[key]) for key in ['L', 'N'] if key in dict) return Entry(key=key, user=str(dict.get('U', '')), password=str(dict.get('P', '')), notes=notes) def parse_entries(src): warnings.warn( "YAML support is deprecated and will be removed in the next version", DeprecationWarning) import yaml try: from yaml import CLoader as Loader except ImportError: from yaml import Loader as Loader root_node = yaml.load(src, Loader=Loader) return list(_collect_entries(root_node, '')) def _collect_entries(current_node, current_key): # list of accounts? if isinstance(current_node, list): for child_node in current_node: assert isinstance(child_node, dict), "expected list of accounts" yield make_entry(current_key, child_node) return # single acccount? if isinstance(current_node, dict) and 'P' in current_node: yield make_entry(current_key, current_node) return # single password? if not isinstance(current_node, dict): yield Entry(key=current_key, user='', password=str(current_node), notes='') return # otherwise: subtree! for key, child_node in current_node.items(): # ignore entries in parentheses if key.startswith('('): continue # recurse for entry in _collect_entries(child_node, current_key + '.' + key if current_key else key): yield entry PKmGW>>pw/__main__.py#!/usr/bin/env python from __future__ import absolute_import, division, print_function from functools import partial import os, os.path, random, signal, string, sys import click from . import __version__, Store, _gpg class Mode(object): COPY = 'Mode.COPY' ECHO = 'Mode.ECHO' RAW = 'Mode.RAW' def default_path(): return os.environ.get('PW_PATH') or click.get_app_dir('passwords.pw.asc') style_match = partial(click.style, fg='yellow', bold=True) style_error = style_password = partial(click.style, fg='red', bold=True) style_success = partial(click.style, fg='green', bold=True, reverse=True) def highlight_match(pattern, str): return style_match(pattern).join(str.split(pattern)) if pattern else str RANDOM_PASSWORD_DEFAULT_LENGTH = 32 RANDOM_PASSWORD_ALPHABET = string.ascii_letters + string.digits @click.command() @click.argument('key_pattern', metavar='[USER@][KEY]', default='') @click.argument('user_pattern', metavar='[USER]', default='') @click.option( '--copy', '-C', 'mode', flag_value=Mode.COPY, default=True, help= 'Display account information, but copy password to clipboard (default mode).' ) @click.option( '--echo', '-E', 'mode', flag_value=Mode.ECHO, help= 'Display account information as well as password in plaintext (alternative mode).' ) @click.option('--raw', '-R', 'mode', flag_value=Mode.RAW, help='Only display password in plaintext (alternative mode).') @click.option('--strict', '-S', 'strict_flag', is_flag=True, help='Fail unless precisely a single result has been found.') @click.option('--user', '-U', 'user_flag', is_flag=True, help="Copy or display username instead of password.") @click.option('--file', '-f', metavar='PATH', default=default_path(), help='Path to password file.') @click.option('--edit', 'edit_subcommand', is_flag=True, help='Launch editor to edit password database and exit.') @click.option('--gen', 'gen_subcommand', is_flag=True, help='Generate a random password and exit.') @click.version_option(version=__version__, message='pw version %(version)s\npython ' + sys.version) @click.pass_context def pw(ctx, key_pattern, user_pattern, mode, strict_flag, user_flag, file, edit_subcommand, gen_subcommand): """Search for USER and KEY in GPG-encrypted password file.""" # install silent Ctrl-C handler def handle_sigint(*_): click.echo() ctx.exit(1) signal.signal(signal.SIGINT, handle_sigint) # invoke a subcommand? if gen_subcommand: length = int(key_pattern) if key_pattern else None generate_password(mode, length) return elif edit_subcommand: launch_editor(ctx, file) return # verify that database file is present if not os.path.exists(file): click.echo("error: password store not found at '%s'" % file, err=True) ctx.exit(1) # load database store = Store.load(file) # if no user query provided, split key query according to right-most "@" sign (since usernames are typically email addresses) if not user_pattern: user_pattern, _, key_pattern = key_pattern.rpartition('@') # search database results = store.search(key_pattern, user_pattern) results = list(results) # if strict flag is enabled, check that precisely a single record was found if strict_flag and len(results) != 1: click.echo( 'error: multiple or no records found (but using --strict flag)', err=True) ctx.exit(2) # raw mode? if mode == Mode.RAW: for entry in results: click.echo(entry.user if user_flag else entry.password) return # print results for idx, entry in enumerate(results): # start with key and user line = highlight_match(key_pattern, entry.key) if entry.user: line += ': ' + highlight_match(user_pattern, entry.user) # add password or copy&paste sucess message if mode == Mode.ECHO and not user_flag: line += ' | ' + style_password(entry.password) elif mode == Mode.COPY and idx == 0: try: import pyperclip pyperclip.copy(entry.user if user_flag else entry.password) result = style_success('*** %s COPIED TO CLIPBOARD ***' % ("USERNAME" if user_flag else "PASSWORD")) except ImportError: result = style_error( '*** PYTHON PACKAGE "PYPERCLIP" NOT FOUND ***') line += ' | ' + result # add notes if entry.notes: if idx == 0: line += '\n' line += "\n".join(" " + line for line in entry.notes.splitlines()) else: lines = entry.notes.splitlines() line += ' | ' + lines[0] if len(lines) > 1: line += " (...)" click.echo(line) def launch_editor(ctx, file): """launch editor with decrypted password database""" # do not use EDITOR environment variable (rather force user to make a concious choice) editor = os.environ.get('PW_EDITOR') if not editor: click.echo('error: no editor set in PW_EDITOR environment variables') ctx.exit(1) # verify that database file is present if not os.path.exists(file): click.echo("error: password store not found at '%s'" % file, err=True) ctx.exit(1) # load source (decrypting if necessary) is_encrypted = _gpg.is_encrypted(file) if is_encrypted: original = _gpg.decrypt(file) else: original = open(file, 'rb').read() # if encrypted, determine recipient if is_encrypted: recipient = os.environ.get('PW_GPG_RECIPIENT') if not recipient: click.echo( 'error: no recipient set in PW_GPG_RECIPIENT environment variables') ctx.exit(1) # launch the editor ext = _gpg.unencrypted_ext(file) modified = click.edit( original.decode('utf-8'), editor=editor, require_save=True, extension=ext) if modified is None: click.echo("not modified") return modified = modified.encode('utf-8') # not encrypted? simply overwrite file if not is_encrypted: with open(file, 'wb') as fp: fp.write(modified) return # otherwise, the process is somewhat more complicated _gpg.encrypt(recipient=recipient, dest_path=file, content=modified) def generate_password(mode, length): """generate a random password""" # generate random password r = random.SystemRandom() length = length or RANDOM_PASSWORD_DEFAULT_LENGTH password = ''.join(r.choice(RANDOM_PASSWORD_ALPHABET) for _ in range(length)) # copy or echo generated password if mode == Mode.ECHO: click.echo(style_password(password)) elif mode == Mode.COPY: try: import pyperclip pyperclip.copy(password) result = style_success('*** PASSWORD COPIED TO CLIPBOARD ***') except ImportError: result = style_error( '*** PYTHON PACKAGE "PYPERCLIP" NOT FOUND ***') click.echo(result) elif mode == Mode.RAW: click.echo(password) if __name__ == '__main__': pw(prog_name='pw') PKmG>B pw/store.pyfrom __future__ import absolute_import, division, print_function import os.path, sys from collections import namedtuple from io import StringIO if sys.version_info[0] < 3: from ushlex import shlex else: from shlex import shlex from . import _gpg Entry = namedtuple('Entry', ['key', 'user', 'password', 'notes']) def _normalized_key(key): return key.replace(' ', '_').lower() class Store: """Password store.""" def __init__(self, path, entries): # normalize keys self.entries = [e._replace(key=_normalized_key(e.key)) for e in entries] self.path = path def search(self, key_pattern, user_pattern): """Search database for given key and user pattern.""" # normalize key key_pattern = _normalized_key(key_pattern) # search results = [] for entry in self.entries: if key_pattern in entry.key and user_pattern in entry.user: results.append(entry) # sort results according to key (stability of sorted() ensures that the order of accounts for any given key remains untouched) return sorted(results, key=lambda e: e.key) @staticmethod def load(path): """Load password store from file.""" # load source (decrypting if necessary) if _gpg.is_encrypted(path): src_bytes = _gpg.decrypt(path) else: src_bytes = open(path, 'rb').read() src = src_bytes.decode('utf-8') # parse database source ext = _gpg.unencrypted_ext(path) if ext in ['.yml', '.yaml']: from . import _yaml entries = _yaml.parse_entries(src) else: entries = _parse_entries(src) return Store(path, entries) class SyntaxError(Exception): def __init__(self, lineno, line, reason): super(SyntaxError, self).__init__('line %s: %s (%r)' % (lineno + 1, reason, line)) _EXPECT_ENTRY = 'expecting entry' _EXPECT_ENTRY_OR_NOTES = 'expecting entry or notes' def _parse_entries(src): entries = [] state = _EXPECT_ENTRY for lineno, line in enumerate(src.splitlines()): # empty lines are skipped (but also terminate the notes section) sline = line.strip() if not sline or line.startswith('#'): state = _EXPECT_ENTRY continue # non-empty line with leading spaces is interpreted as a notes line if line[0] in [' ', '\t']: if state != _EXPECT_ENTRY_OR_NOTES: raise SyntaxError(lineno, line, state) # add line of notes notes = entries[-1].notes if notes: notes += "\n" notes += sline entries[-1] = entries[-1]._replace(notes=notes) continue # otherwise, parse as an entry sio = StringIO(line) lexer = shlex(sio, posix=True) lexer.whitespace_split = True try: key = lexer.get_token() except ValueError as e: raise SyntaxError(lineno, line, str(e)) key = key.rstrip(':') assert key try: user = lexer.get_token() except ValueError as e: raise SyntaxError(lineno, line, str(e)) try: password = lexer.get_token() except ValueError as e: raise SyntaxError(lineno, line, str(e)) if not user and not password: raise SyntaxError(lineno, line, state) if not password: password = user user = notes = u'' else: password = password notes = sio.read().strip() entries.append(Entry(key, user, password, notes)) state = _EXPECT_ENTRY_OR_NOTES return entries PKmGX pw/_gpg.pyfrom __future__ import absolute_import, division, print_function import os.path import subprocess _HAS_ARMOR = {'.gpg': False, '.asc': True} _EXTENSIONS = _HAS_ARMOR.keys() _OVERRIDE_HOMEDIR = None # useful for unit tests def is_encrypted(path): _, ext = os.path.splitext(path) return ext in _EXTENSIONS def has_armor(path): _, ext = os.path.splitext(path) if ext not in _EXTENSIONS: raise ValueError('File extension not recognized as encrypted (%r).' % ext) return _HAS_ARMOR[ext] def unencrypted_ext(path): root, ext = os.path.splitext(path) if ext in _EXTENSIONS: _, ext = os.path.splitext(root) return ext def _base_args(): args = ['gpg2', '--use-agent', '--quiet', '--batch', '--yes'] if _OVERRIDE_HOMEDIR: args += ['--homedir', _OVERRIDE_HOMEDIR] return args def decrypt(path): args = ['--decrypt', path] return subprocess.check_output(_base_args() + args) def encrypt(recipient, dest_path, content): args = ["--encrypt"] if has_armor(dest_path): args += ["--armor"] args += ["--recipient", recipient, "--output", dest_path] popen = subprocess.Popen(_base_args() + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = popen.communicate(content) assert popen.returncode == 0, stderr PKnG "pw-0.8.1.dist-info/DESCRIPTION.rstpw |Build Status| |Latest Version| |Supported Python Versions| ============================================================== ``pw`` is a Python tool to search in a GPG-encrypted password database. :: Usage: pw [OPTIONS] [USER@][KEY] [USER] Search for USER and KEY in GPG-encrypted password file. Options: -C, --copy Display account information, but copy password to clipboard (default mode). -E, --echo Display account information as well as password in plaintext (alternative mode). -R, --raw Only display password in plaintext (alternative mode). -S, --strict Fail unless precisely a single result has been found. -U, --user Copy or display username instead of password. -f, --file PATH Path to password file. --edit Launch editor to edit password database and exit. --gen Generate a random password and exit. --version Show the version and exit. --help Show this message and exit. Installation ------------ To install ``pw``, simply run: .. code:: bash $ pip install pw .. |Build Status| image:: https://travis-ci.org/catch22/pw.svg?branch=master :target: https://travis-ci.org/catch22/pw .. |Latest Version| image:: https://img.shields.io/pypi/v/pw.svg :target: https://pypi.python.org/pypi/pw/ .. |Supported Python Versions| image:: https://img.shields.io/pypi/pyversions/pw.svg :target: https://pypi.python.org/pypi/pw/ PKnGC^''#pw-0.8.1.dist-info/entry_points.txt[console_scripts] pw = pw.__main__:pw PKnG yX pw-0.8.1.dist-info/metadata.json{"classifiers": ["Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Development Status :: 4 - Beta", "Environment :: Console", "License :: OSI Approved :: MIT License"], "extensions": {"python.commands": {"wrap_console": {"pw": "pw.__main__:pw"}}, "python.details": {"contacts": [{"email": "michael.walter@gmail.com", "name": "Michael Walter", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/catch22/pw"}}, "python.exports": {"console_scripts": {"pw": "pw.__main__:pw"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "license": "MIT", "metadata_version": "2.0", "name": "pw", "run_requires": [{"requires": ["click (>=5.1)", "colorama", "pyperclip (>=1.5.11)", "ushlex"]}], "summary": "Search in GPG-encrypted password file.", "test_requires": [{"requires": ["PyYAML", "pytest"]}], "version": "0.8.1"}PKnGm+ pw-0.8.1.dist-info/top_level.txtpw PKnGndnnpw-0.8.1.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PKnGVpw-0.8.1.dist-info/METADATAMetadata-Version: 2.0 Name: pw Version: 0.8.1 Summary: Search in GPG-encrypted password file. Home-page: https://github.com/catch22/pw Author: Michael Walter Author-email: michael.walter@gmail.com License: MIT Platform: UNKNOWN Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: License :: OSI Approved :: MIT License Requires-Dist: click (>=5.1) Requires-Dist: colorama Requires-Dist: pyperclip (>=1.5.11) Requires-Dist: ushlex pw |Build Status| |Latest Version| |Supported Python Versions| ============================================================== ``pw`` is a Python tool to search in a GPG-encrypted password database. :: Usage: pw [OPTIONS] [USER@][KEY] [USER] Search for USER and KEY in GPG-encrypted password file. Options: -C, --copy Display account information, but copy password to clipboard (default mode). -E, --echo Display account information as well as password in plaintext (alternative mode). -R, --raw Only display password in plaintext (alternative mode). -S, --strict Fail unless precisely a single result has been found. -U, --user Copy or display username instead of password. -f, --file PATH Path to password file. --edit Launch editor to edit password database and exit. --gen Generate a random password and exit. --version Show the version and exit. --help Show this message and exit. Installation ------------ To install ``pw``, simply run: .. code:: bash $ pip install pw .. |Build Status| image:: https://travis-ci.org/catch22/pw.svg?branch=master :target: https://travis-ci.org/catch22/pw .. |Latest Version| image:: https://img.shields.io/pypi/v/pw.svg :target: https://pypi.python.org/pypi/pw/ .. |Supported Python Versions| image:: https://img.shields.io/pypi/pyversions/pw.svg :target: https://pypi.python.org/pypi/pw/ PKnGat pw-0.8.1.dist-info/RECORDpw/__init__.py,sha256=Jdw0ENKedoCmlGUf00-NDIu7OKSAF9vQP15APCAOzvA,120 pw/__main__.py,sha256=QiDzq4SVJ5GZbpHGwhIeFO4z3SRlJLkxLGjlgJBLJDQ,7810 pw/_gpg.py,sha256=Wzbe_3yvPFcYDs7Pa3y6vbNbb88Ms39o76IgRSym0LA,1463 pw/_yaml.py,sha256=Z79zbCcJmPh_A1k7gGHgSnU5CpHHbZU4PUNAdwTOiHg,1857 pw/store.py,sha256=qz_gkAOdp_W8XqN_n5CpVBV91A_oA3M4D1xDxRc7yyI,3834 pw-0.8.1.dist-info/DESCRIPTION.rst,sha256=psg6gKKzLMOC8009F4az0A_dxxU_WeFcFQpHI3zMFLI,1494 pw-0.8.1.dist-info/METADATA,sha256=qU38YOW1FVN8-xED2BBSQLR4eLP828FJMbApGHe3uF4,2209 pw-0.8.1.dist-info/RECORD,, pw-0.8.1.dist-info/WHEEL,sha256=GrqQvamwgBV4nLoJe0vhYRSWzWsx7xjlt74FT0SWYfE,110 pw-0.8.1.dist-info/entry_points.txt,sha256=r7GYznYHT5LNTOna98icUv6ndrO2IhNe0SR27LMxlnc,39 pw-0.8.1.dist-info/metadata.json,sha256=co_id4h9QlLZGFFw_Vi4KgLkHRZrEl6HajxQc3eKaE4,1029 pw-0.8.1.dist-info/top_level.txt,sha256=PwujzYfKVLSE6-guDBI97d3bN8JkHTkckUh99q8YMuI,3 PKnG6xxpw/__init__.pyPKmGn*AA pw/_yaml.pyPKmGW>>pw/__main__.pyPKmG>B &pw/store.pyPKmGX 5pw/_gpg.pyPKnG ";pw-0.8.1.dist-info/DESCRIPTION.rstPKnGC^''#Apw-0.8.1.dist-info/entry_points.txtPKnG yX Ppw-0.8.1.dist-info/RECORDPK 5S