├── test ├── __init__.py ├── basetest │ ├── __init__.py │ └── basetest.py ├── .test_config ├── test_noop.py ├── test_modify.py ├── test_passworddb.py ├── test_update.py ├── passwords │ ├── testpassword │ ├── gentest │ └── test ├── test_escrow.py ├── test_list_recipients.py ├── test_list.py ├── test_generate.py ├── test_create.py ├── test_info.py ├── test_distribute.py ├── test_password.py ├── test_id.py ├── test_rename.py ├── test_crypto.py ├── test_show.py └── pki │ └── generatepki.sh ├── bin └── pkpass ├── libpkpass ├── commands │ ├── __init__.py │ ├── card.py │ ├── cli.py │ ├── recover.py │ ├── modify.py │ ├── create.py │ ├── list.py │ ├── delete.py │ ├── export.py │ ├── clip.py │ ├── pkinterface.py │ ├── info.py │ ├── generate.py │ ├── listrecipients.py │ ├── update.py │ ├── rename.py │ ├── distribute.py │ ├── fileimport.py │ ├── verifyinstall.py │ ├── populate.py │ ├── show.py │ └── arguments.py ├── connectors │ ├── __init__.py │ └── connectorinterface.py ├── models │ ├── __init__.py │ ├── cert.py │ └── recipient.py ├── __init__.py ├── escrow.py ├── errors.py ├── passworddb.py └── identities.py ├── .gitattributes ├── MANIFEST.in ├── .coveragerc ├── .gitlab-ci.yml ├── githooks ├── README.md └── pre-commit ├── list_pip_requirements.sh ├── docs ├── source │ ├── Windows.rst │ ├── Software Dependencies.rst │ ├── _templates │ │ └── footer.html │ ├── General Usage.rst │ ├── Development And Testing.rst │ ├── Alternate Backend Support.rst │ ├── Setup.rst │ ├── index.rst │ ├── conf.py │ └── Configuration.rst ├── Makefile └── generate_commands.sh ├── requirements.txt ├── sonar-project.properties ├── CONTRIBUTING.md ├── LICENSE.md ├── .github └── workflows │ ├── unittests.yml │ ├── readthedocs.yml │ ├── pythonpublish.yml │ └── codeql-analysis.yml ├── setup.sh ├── setup.cfg ├── pkpass.py ├── .gitignore ├── README.md └── example_pkpassrc /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/pkpass: -------------------------------------------------------------------------------- 1 | ../pkpass.py -------------------------------------------------------------------------------- /test/basetest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libpkpass/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libpkpass/connectors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | libpkpass/_version.py export-subst 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include example_pkpassrc 2 | include versioneer.py 3 | include libpkpass/_version.py 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */.local/* 4 | /usr/* 5 | */__init__.py 6 | */test/* 7 | -------------------------------------------------------------------------------- /libpkpass/models/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stages: 3 | - test 4 | 5 | # run sonar scan 6 | Sonar Scan: 7 | stage: test 8 | image: $SONAR_SCAN_IMAGE 9 | script: 10 | - /usr/bin/entrypoint.sh 11 | -------------------------------------------------------------------------------- /githooks/README.md: -------------------------------------------------------------------------------- 1 | # This directory is helpful to prevent dumb commits that someone kept doing 2 | 3 | to enable run this command in the base directory 4 | 5 | `git config core.hooksPath githooks` 6 | -------------------------------------------------------------------------------- /list_pip_requirements.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pip list --not-required -l --format freeze 2>/dev/null | grep -ve '^wheel==' \ 3 | | grep -ve '^pip==' | grep -ve '^pkg-resources==' | grep -ve '^pylint==' 4 | -------------------------------------------------------------------------------- /test/.test_config: -------------------------------------------------------------------------------- 1 | certpath: test/pki/intermediate/certs 2 | keypath: test/pki/intermediate/private 3 | cabundle: test/pki/intermediate/certs/ca-bundle 4 | pwstore: test/passwords 5 | 6 | rules_map: 7 | default: "[^\\s]{20}" 8 | -------------------------------------------------------------------------------- /libpkpass/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ._version import get_versions 3 | 4 | __version__ = get_versions()["version"] 5 | del get_versions 6 | 7 | LOGGER = logging.getLogger(__name__) 8 | logging.basicConfig( 9 | level="DEBUG", 10 | format="%(message)s", 11 | ) 12 | -------------------------------------------------------------------------------- /docs/source/Windows.rst: -------------------------------------------------------------------------------- 1 | Windows Consideration 2 | ===================== 3 | There has not been much (if any) testing around the windows ecosystem. Coding has been attempted to comply with portability standards; 4 | but compatibility is not guaranteed. If you need it, feel free to submit a PR 😀 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML==6.0.2 2 | SQLAlchemy==1.4.30 3 | colored==1.4.4 4 | cryptography==36.0.1 5 | exrex==0.11.0 6 | mock==4.0.3 7 | pem==21.2.0 8 | pylibyaml==0.1.0 9 | pyperclip==1.8.2 10 | pyseltongue==1.0.1 11 | python-dateutil==2.8.2 12 | ruamel.yaml.clib==0.2.8 13 | ruamel.yaml==0.17.20 14 | setuptools==65.5.1 15 | tqdm==4.62.3 16 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=pkpass 2 | sonar.projectName=pkpass 3 | sonar.projectVersion=2.2.0 - the project version 4 | These 3 properties are required 5 | sonar.sources=libpkpass, test, pkpass.py 6 | sonar.language=py - the programming language (Java, Python, etc.) 7 | sonar.python.pylint_config=.pylintrc 8 | # sonar.python.coverage.reportPaths="*coverage.xml" 9 | -------------------------------------------------------------------------------- /docs/source/Software Dependencies.rst: -------------------------------------------------------------------------------- 1 | Software Dependencies 2 | ===================== 3 | Pkpass has few dependencies. Fernet is a crypto library used to allow automatic symmetric encrypting. Fernet can be installed using pip: 4 | ``pip install cryptography`` 5 | 6 | Other dependencies can be found in requirements.txt 7 | 8 | *Note:* All dependencies will be installed if the setup script is run. 9 | -------------------------------------------------------------------------------- /test/test_noop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Boilerplate test Module""" 3 | import unittest 4 | 5 | 6 | class TestBasicFunction(unittest.TestCase): 7 | """Boilerplate test class""" 8 | 9 | # def setUp(self): 10 | # self.func = BasicFunction() 11 | 12 | def test_1(self): 13 | """Boilerplate test case""" 14 | self.assertTrue(True) 15 | 16 | 17 | if __name__ == "__main__": 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git diff --cached --name-only | if grep --quiet ".py"; then 3 | echo "Running Unittests" 4 | python -m unittest discover -b 5 | fi 6 | git diff --cached --name-only | if grep --quiet "pkpass.py"; then 7 | char=$(cat pkpass.py | grep 'Exception as err:' | cut -c1) 8 | if [[ $char == "#" ]]; then 9 | echo "Generic exception appears to be commented out" 10 | exit 1 11 | fi 12 | fi 13 | git reset HEAD test/passwords 14 | git checkout -- test/passwords 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/generate_commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | commands="$(python ../pkpass.py -h | grep '{.*}' | head -1 | tr -d '[:space:]{}')" 4 | IFS="," 5 | 6 | cat << EOF > /tmp/test 7 | Commands 8 | ======== 9 | The Commands can be listed out by passing the help flag to pkpass as seen below 10 | 11 | EOF 12 | 13 | { 14 | echo ".. code-block:: bash 15 | 16 | " 17 | python ../pkpass.py -h | awk '{ print " "$0 }' 18 | } >> /tmp/test 19 | 20 | for com in $commands; do 21 | { 22 | echo " 23 | ${com^}" 24 | awk "BEGIN{for(c=0;c<${#com};c++) printf \"-\"}" 25 | echo " 26 | Blurb 27 | 28 | .. code-block:: bash 29 | " 30 | python ../pkpass.py "$com" -h | awk '{ print " "$0 }' 31 | } >> /tmp/test 32 | done 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Pull Request Process 2 | 1. ensure pylint is installed to verify you are coding to our standards as shown in the pylintrc file 3 | 2. run the unittests before commit 4 | 3. create new unittests as necessary 5 | 4. update the README if applicable 6 | 5. update the setup script if applicable 7 | 8 | # Setting up a development environment 9 | 1. After checking out this repository, create a python3 environment (in ./venv) with `python3 -m venv venv` 10 | 2. source the environment with `source venv/bin/activate` 11 | 3. Install all pkpass prerequisites into that virtual environment by running `pip install -r requirements.txt` 12 | 4. You should be able to run the latest checked out pkpass with `python pkpass.py` 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | PKPass: Public Key Based Password Managment 2 | Copyright (C) 2017 UT-Battelle, LLC 3 | All Rights Reserved 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /libpkpass/models/cert.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import String, Boolean, DateTime 2 | from sqlalchemy.schema import Column 3 | from libpkpass.models import Base 4 | 5 | 6 | class Cert(Base): 7 | ######################################### 8 | """Table of Certificates""" 9 | ######################################### 10 | 11 | __tablename__ = "cert" 12 | cert_bytes = Column(String()) 13 | verified = Column(Boolean) 14 | fingerprint = Column(String(), primary_key=True) 15 | subject = Column(String()) 16 | issuer = Column(String()) 17 | enddate = Column(DateTime()) 18 | issuerhash = Column(String()) 19 | subjecthash = Column(String()) 20 | 21 | def __repr__(self): 22 | return f"" 23 | 24 | def __iter__(self): 25 | for col in self.__table__.columns: 26 | yield col.name, getattr(self, col.name) 27 | -------------------------------------------------------------------------------- /.github/workflows/unittests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Unit Tests 3 | 4 | on: [push, workflow_dispatch] 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ubuntu-latest 10 | strategy: 11 | max-parallel: 4 12 | matrix: 13 | python-version: [3.8, 3.9,3.12] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | sudo apt-get install opensc 24 | python -m pip install --upgrade pip 25 | pip install -r requirements.txt 26 | cd test/pki 27 | ./generatepki.sh 28 | cd ../../ 29 | ls test/pki/ca 30 | ls test/pki/intermediate 31 | - name: Test with pytest 32 | run: python -m unittest discover -b 33 | -------------------------------------------------------------------------------- /test/test_modify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the modify module""" 3 | import builtins 4 | import unittest 5 | import mock 6 | from libpkpass.commands.cli import Cli 7 | from libpkpass.errors import DecryptionError 8 | from .basetest.basetest import patch_args 9 | 10 | 11 | class ModifyTests(unittest.TestCase): 12 | """This class tests the modify class""" 13 | 14 | def test_modify_success(self): 15 | """Test modifying the metadata of a password""" 16 | ret = True 17 | try: 18 | with patch_args( 19 | subparser_name="modify", 20 | identity="r3", 21 | nopassphrase="true", 22 | pwname="gentest", 23 | ): 24 | with mock.patch.object(builtins, "input", lambda _: "y"): 25 | "".join(Cli().run()) 26 | except DecryptionError: 27 | ret = False 28 | self.assertTrue(ret) 29 | -------------------------------------------------------------------------------- /test/test_passworddb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module tests the passworddb module""" 3 | import unittest 4 | from libpkpass.passworddb import PasswordDB 5 | 6 | 7 | class TestBasicFunction(unittest.TestCase): 8 | """This class tests the passworddb class""" 9 | 10 | def setUp(self): 11 | self.file1 = "test/passwords/testpassword" 12 | self.file2 = "test/scratch/testpassword" 13 | 14 | def test_read_write(self): 15 | """Test read write to the password db""" 16 | passworddb = PasswordDB() 17 | passwordentry = passworddb.load_password_data(self.file1) 18 | passworddb.pwdb[self.file2] = passworddb.pwdb[self.file1] 19 | passworddb.save_password_data(self.file2, overwrite=True) 20 | 21 | with open(self.file1, "r") as file1: 22 | with open(self.file2, "r") as file2: 23 | self.assertTrue(file1.read() == file2.read()) 24 | 25 | 26 | if __name__ == "__main__": 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /.github/workflows/readthedocs.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Read The Docs 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | # This workflow contains a single job called "build" 14 | build: 15 | # The type of runner that the job will run on 16 | runs-on: ubuntu-latest 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - uses: actions/checkout@v2 22 | 23 | # Runs a single command using the runners shell 24 | - name: Run a one-line script 25 | run: | 26 | curl -X POST -H "Authorization: Token ${{ secrets.READTHEDOCSTOKEN }}" -H "Content-Length: 0" https://readthedocs.org/api/v3/projects/pkpass/versions/latest/builds/ 27 | -------------------------------------------------------------------------------- /test/test_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the update module""" 3 | import getpass 4 | import builtins 5 | import unittest 6 | import mock 7 | from libpkpass.commands.cli import Cli 8 | from libpkpass.errors import DecryptionError 9 | from .basetest.basetest import patch_args 10 | 11 | 12 | class UpdateTests(unittest.TestCase): 13 | """This class tests the update class""" 14 | 15 | def test_update_success(self): 16 | """Test a successful update of a password""" 17 | ret = True 18 | try: 19 | with patch_args( 20 | subparser_name="update", 21 | identity="r3", 22 | nopassphrase="true", 23 | pwname="gentest", 24 | no_cache=True, 25 | ): 26 | with mock.patch.object(builtins, "input", lambda _: "y"): 27 | with mock.patch.object(getpass, "getpass", lambda _: "y"): 28 | "".join(Cli().run()) 29 | except DecryptionError: 30 | ret = False 31 | self.assertTrue(ret) 32 | -------------------------------------------------------------------------------- /libpkpass/models/recipient.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import String 2 | from sqlalchemy.orm import relationship 3 | from sqlalchemy.schema import Column, ForeignKey, Table 4 | from libpkpass.models import Base 5 | 6 | recipient_cert = Table( 7 | "recipient_cert", 8 | Base.metadata, 9 | Column("recipient_name", String(), ForeignKey("recipient.name")), 10 | Column("cert_fingerprint", String(), ForeignKey("cert.fingerprint")), 11 | ) 12 | 13 | 14 | class Recipient(Base): 15 | ######################################### 16 | """table of health status of services""" 17 | ######################################### 18 | 19 | __tablename__ = "recipient" 20 | name = Column(String(), primary_key=True) 21 | key = Column(String(), nullable=True) 22 | certs = relationship( 23 | "Cert", secondary=recipient_cert, backref="recipients", lazy="joined" 24 | ) 25 | 26 | def __repr__(self): 27 | return f"" 28 | 29 | def __iter__(self): 30 | for col in self.__table__.columns: 31 | yield col.name, getattr(self, col.name) 32 | -------------------------------------------------------------------------------- /docs/source/_templates/footer.html: -------------------------------------------------------------------------------- 1 |
2 | {% if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %} 3 | 11 | {% endif %} 12 | 13 |
14 | 15 | {%- if show_sphinx %} 16 | {% trans %}Built with Sphinx using a theme provided by Read the Docs{% endtrans %}. 17 | {%- endif %} 18 | 19 | {%- block extrafooter %} {% endblock %} 20 | 21 |
22 | -------------------------------------------------------------------------------- /libpkpass/commands/card.py: -------------------------------------------------------------------------------- 1 | """This Module allows for the listing of users cards""" 2 | from libpkpass.commands.command import Command 3 | from libpkpass.crypto import print_card_info 4 | 5 | 6 | class Card(Command): 7 | #################################################################### 8 | """This class implements the cli card command""" 9 | #################################################################### 10 | 11 | name = "card" 12 | description = "List the available cards and which card you have selected" 13 | selected_args = Command.selected_args 14 | 15 | def _run_command_execution(self): 16 | #################################################################### 17 | """Run function for class.""" 18 | #################################################################### 19 | return print_card_info( 20 | self.args["card_slot"], 21 | self.iddb.id, 22 | 2, 23 | self.args["color"], 24 | self.args["theme_map"], 25 | self.args["SCBackend"], 26 | ) 27 | 28 | def _validate_args(self): 29 | pass 30 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function print_seperator(){ 4 | echo "**************************************************" 5 | echo "**************************************************" 6 | echo "**************************************************" 7 | echo "**************************************************" 8 | echo "$1" 9 | echo "**************************************************" 10 | echo "**************************************************" 11 | echo "**************************************************" 12 | echo "**************************************************" 13 | } 14 | if [[ -d "./venv" ]]; then 15 | print_seperator "Removing previous venv exists" 16 | rm -rf ./venv 17 | fi 18 | 19 | print_seperator "Creating python3 virtualenv" 20 | python3 -m venv venv 21 | py3="./venv/bin/python3" 22 | if [[ -f $py3 ]]; then 23 | print_seperator "Running Python Installation" 24 | $py3 setup.py install 25 | print_seperator "Starting RC File Generation" 26 | $py3 setup.py rcfile 27 | print_seperator "Running RC File verification" 28 | $py3 setup.py verify 29 | else 30 | echo "Python3 venv package not available" 31 | fi 32 | -------------------------------------------------------------------------------- /test/passwords/testpassword: -------------------------------------------------------------------------------- 1 | metadata: 2 | authorizer: uua 3 | creator: uua 4 | description: This is the first password for account 'password 01' 5 | name: Password1 6 | schemaVersion: v1 7 | signature: null 8 | recipients: 9 | uua: 10 | derived_key: J_5vEt4ZYNYJ_jBHRd_Z9e9TgdvVOKvNDnh89X6C2jJz8eAYc1d5YT9G43WW_ySya8KQvyNKYps2EdKuNCDnO83hjlfNQ_pRdNOy0sb34PU1TeQWIitsWtD7cnweaUjN9_RpTR0EEfnNiZF2hm0FXJJwrluoWwqnUPV7bSBZBYXDZ85Ox7ItB5ZIUcDGpn3jmp8sBx2VUfReaIa1hrL78Q298XporJBTC7OsNwCQRN_uwBbTWMTVVYLio517acs793ABEsUcXmxneLUwc9WvVRz6vMuOG3jsKHHbsGk60691LjG-aM_6X30jo4UITGv-J5fabABeTnku6ClS4jXELQ== 11 | distributor: uua 12 | distributor_hash: e237d0e6 13 | encrypted_secret: gAAAAABaj4SDXNTr5TBg0ZDbctMPKRNCnfOzQQVcChhAAzUP98rvADncKHMVyt8wsTMYq3uk2hXjypyN1mQOX2zknEqVN14Kaw== 14 | encryption_algorithm: rsautl 15 | recipient_hash: e237d0e6 16 | signature: B7YEx5VQNyWLxdOhuYoifK3wZu_2GyKu0PWY-YMMvxGxF3rN7Edf2P70yFWtV0MZhmTPmswcvYF9raKDJIUtKI1soA97DkGQt9rSwwjHWu31Mo0WluPPdp7w9bvGL1_9NnEr4gbY-lM1Bqasp7fKeEUybisk6Fw74JkhKzG--kkMxk-6oIkXCoyJqW4qzvFcbPkIPdM7Y-svJxxtF-HA9nJw3LNT9_40URnHNlR4Hb7bBLppcgzCSjuyeBPgCQMzM9XJA4Pm3zp_g2vj2menJEJUfa-QLK1MJPzpBw55IpkgPWIKCdvcDR8JuSMp25uLYRPCIVPa-sIGvpygQ9lMqQ== 17 | timestamp: 1519355011.228398 18 | -------------------------------------------------------------------------------- /test/basetest/basetest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module provides base testing capabilites""" 3 | import sys 4 | import argparse 5 | from contextlib import contextmanager 6 | from io import StringIO 7 | import mock 8 | 9 | CONFIG = "./test/.test_config" 10 | BADPIN = "Error decrypting password named 'test'. Perhaps a bad pin/passphrase?" 11 | ERROR_MSGS = { 12 | "pwname": "'pwname' is a required argument", 13 | "rep": "Error: Your user 'bleh' is not in the recipient database", 14 | } 15 | 16 | 17 | @contextmanager 18 | def captured_output(): 19 | """capture stdout and stderr for use of parsing pkpass output""" 20 | new_out, new_err = StringIO(), StringIO() 21 | old_out, old_err = sys.stdout, sys.stderr 22 | try: 23 | sys.stdout, sys.stderr = new_out, new_err 24 | yield sys.stdout, sys.stderr 25 | finally: 26 | sys.stdout, sys.stderr = old_out, old_err 27 | 28 | 29 | def patch_args(**kwargs): 30 | """Patch argparse arguments intended for use with `with` statement 31 | uses default config path and no color output""" 32 | return mock.patch( 33 | "argparse.ArgumentParser.parse_args", 34 | return_value=argparse.Namespace(config=CONFIG, color=False, **kwargs), 35 | ) 36 | -------------------------------------------------------------------------------- /libpkpass/connectors/connectorinterface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This is a base class interface for cert connectors""" 3 | from abc import ABCMeta 4 | 5 | 6 | class ConnectorInterface: 7 | #################################################################### 8 | """Simple interface for a Connector""" 9 | #################################################################### 10 | __metaclass__ = ABCMeta 11 | 12 | def __init__(self): 13 | pass 14 | 15 | def __getitem__(self, key): 16 | return getattr(self, key) 17 | 18 | def list_certificates(self): 19 | #################################################################### 20 | """This should return a dict with the key being username and the value being 21 | a list of certicates""" 22 | #################################################################### 23 | raise NotImplementedError 24 | 25 | def get_db(self): 26 | #################################################################### 27 | """This should return a sqlite db that matches the models from 28 | libpkpass.models 29 | """ 30 | #################################################################### 31 | raise NotImplementedError 32 | -------------------------------------------------------------------------------- /test/test_escrow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the recover module""" 3 | import unittest 4 | from libpkpass.escrow import pk_split_secret, pk_recover_secret 5 | 6 | PASSWORD = "boomtown" 7 | 8 | 9 | class EscrowTests(unittest.TestCase): 10 | """This class tests the recover class""" 11 | 12 | shares = pk_split_secret(PASSWORD, ["r1", "r2", "r3"], 2) 13 | 14 | def test_spliting_with_no_min(self): 15 | """Test SSSS without a minimum requirement""" 16 | shares = pk_split_secret(PASSWORD, ["r1", "r2", "r3"], None) 17 | passwd = pk_recover_secret(shares) 18 | self.assertEqual(passwd, PASSWORD) 19 | 20 | def test_split_secret_all_shares(self): 21 | """test recovery with all shares functionality""" 22 | passwd = pk_recover_secret(self.shares) 23 | self.assertEqual(passwd, PASSWORD) 24 | 25 | def test_split_secret_min_shares(self): 26 | """test recovery with min shares functionality""" 27 | passwd = pk_recover_secret(self.shares[0:2]) 28 | self.assertEqual(passwd, PASSWORD) 29 | 30 | def test_split_secret_failure(self): 31 | """test recovery failure with not enough shares""" 32 | passwd = pk_recover_secret(self.shares[0:1]) 33 | self.assertNotEqual(passwd, PASSWORD) 34 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | # https://docs.pypi.org/trusted-publishers/using-a-publisher/ 12 | # https://github.com/marketplace/actions/pypi-publish 13 | # IMPORTANT: this permission is mandatory for trusted publishing 14 | id-token: write 15 | environment: 16 | name: pypi 17 | url: https://pypi.org/p/pkpass-olcf 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Set up Python 21 | uses: actions/setup-python@v1 22 | with: 23 | python-version: '3.x' 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install setuptools wheel twine 28 | - name: Build 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | - name: Publish package distributions to PyPI 32 | uses: pypa/gh-action-pypi-publish@release/v1 33 | #- name: Build and publish 34 | #env: 35 | # TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 36 | # TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 37 | # run: | 38 | # python setup.py sdist bdist_wheel 39 | # twine upload dist/* --verbose 40 | -------------------------------------------------------------------------------- /libpkpass/escrow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module handles the escrow functions i.e. creating shares and recovery""" 3 | from pyseltongue import PlaintextToHexSecretSharer as ptohss 4 | from .errors import EscrowError 5 | 6 | ############################################################################## 7 | def pk_split_secret(plaintext_string, escrow_list, minimum=None): 8 | """split a secret into multiple shares for encryption""" 9 | ############################################################################## 10 | escrow_len = len(escrow_list) 11 | if not minimum and escrow_len % 2 != 0: 12 | minimum = int((escrow_len + 1) / 2) 13 | elif not minimum: 14 | minimum = int((escrow_len / 2) + 1) 15 | if minimum < 2: 16 | raise EscrowError("minimum escrow", 2, minimum) 17 | if escrow_len < 3: 18 | raise EscrowError("escrow users list", 3, escrow_len) 19 | return ptohss.split_secret(str(plaintext_string), minimum, escrow_len) 20 | 21 | ############################################################################## 22 | 23 | 24 | def pk_recover_secret(shares): 25 | """Take Decrypted strings from crypto and recover a secret""" 26 | ############################################################################## 27 | return ptohss.recover_secret(shares) 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pkpass-olcf 3 | author = Noah Ginsburg 4 | author_email = ginsburgnm@gmail.com 5 | url = https://github.com/olcf/pkpass 6 | description = Public Key based Password Management 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license = GPLV3 10 | classifiers = 11 | Environment :: Console 12 | Intended Audience :: Developers 13 | Intended Audience :: Information Technology 14 | Intended Audience :: System Administrators 15 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 16 | Operating System :: OS Independent 17 | Programming Language :: Python :: 3.5 18 | Programming Language :: Python :: 3.6 19 | Programming Language :: Python :: 3.7 20 | Programming Language :: Python :: 3.8 21 | Topic :: Security :: Cryptography 22 | 23 | [options] 24 | include_package_data = True 25 | allow-all-external = yes 26 | trusted-host = 27 | gitlab.* 28 | bitbucket.org 29 | github.com 30 | packages = find: 31 | scripts = 32 | bin/pkpass 33 | 34 | [options.packages.find] 35 | exclude = 36 | tests 37 | 38 | [versioneer] 39 | VCS = git 40 | style = pep440 41 | versionfile_source = libpkpass/_version.py 42 | versionfile_build = libpkpass/_version.py 43 | tag_prefix = 44 | parentdir_prefix = libpkpass 45 | -------------------------------------------------------------------------------- /libpkpass/commands/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module implements a CLI""" 3 | from libpkpass.util import show_version 4 | from libpkpass.commands.pkinterface import PkInterface 5 | from libpkpass.commands.interpreter import Interpreter 6 | 7 | 8 | class Cli(PkInterface): 9 | #################################################################### 10 | """Class for parsing command line. Observes subclasses of Command to Register 11 | those commands in the actions list.""" 12 | #################################################################### 13 | 14 | def __init__(self): 15 | #################################################################### 16 | """Intialization function for class. Register all subcommands""" 17 | #################################################################### 18 | PkInterface.__init__(self) 19 | 20 | Interpreter(self) 21 | self.parser.add_argument( 22 | "--version", action="store_true", help="Show the version of PkPass and exit" 23 | ) 24 | 25 | self.parser.set_default_subparser(self.parser, name="interpreter") 26 | self.parsedargs = self.parser.parse_args() 27 | 28 | def run(self): 29 | if "version" in self.parsedargs and self.parsedargs.version: 30 | return show_version() 31 | return self.actions[self.parsedargs.subparser_name].run(self.parsedargs) 32 | -------------------------------------------------------------------------------- /libpkpass/commands/recover.py: -------------------------------------------------------------------------------- 1 | """This module handles the CLI for password recovery""" 2 | from sys import stdin 3 | from libpkpass.escrow import pk_recover_secret 4 | from libpkpass.commands.command import Command 5 | 6 | 7 | class Recover(Command): 8 | #################################################################### 9 | """This class implements the CLI functionality of recovery for passwords""" 10 | #################################################################### 11 | 12 | name = "recover" 13 | description = "Recover a password that has been distributed using escrow functions" 14 | selected_args = Command.selected_args + [ 15 | "pwstore", 16 | "keypath", 17 | "nosign", 18 | "escrow_users", 19 | "min_escrow", 20 | "stdin", 21 | ] 22 | 23 | def _run_command_execution(self): 24 | #################################################################### 25 | """Run function for class.""" 26 | #################################################################### 27 | yield "If the password returned is not correct, you may need more shares" 28 | if self.args["stdin"]: 29 | shares = "".join(stdin.readlines()).strip() 30 | else: 31 | shares = input("Enter comma separated list of shares: ") 32 | yield pk_recover_secret(map(str.strip, shares.split(","))) 33 | 34 | def _validate_args(self): 35 | pass 36 | 37 | def _validate_combinatorial_args(self): 38 | pass 39 | -------------------------------------------------------------------------------- /libpkpass/commands/modify.py: -------------------------------------------------------------------------------- 1 | """This Module allows for editing metadata of passwords""" 2 | from os import path 3 | from libpkpass.commands.command import Command 4 | from libpkpass.password import PasswordEntry 5 | from libpkpass.errors import CliArgumentError 6 | 7 | 8 | class Modify(Command): 9 | #################################################################### 10 | """This class implements the cli list""" 11 | #################################################################### 12 | name = "modify" 13 | description = "Modify the metadata of a password" 14 | selected_args = Command.selected_args + ["pwname", "pwstore"] 15 | 16 | def _run_command_execution(self): 17 | #################################################################### 18 | """Run function for class.""" 19 | #################################################################### 20 | full_path = path.join(self.args["pwstore"], self.args["pwname"]) 21 | password = PasswordEntry() 22 | password.read_password_data(full_path) 23 | editable = ["authorizer", "description"] 24 | for key, value in password["metadata"].items(): 25 | if key in editable: 26 | yield f"""{self.color_print(f"Current value for '{key}':", "first_level")} {value}""" 27 | password["metadata"][key] = input(f"New Value for {key}: ") 28 | 29 | password.write_password_data(full_path) 30 | 31 | def _validate_args(self): 32 | for argument in ["pwname", "pwstore"]: 33 | if argument not in self.args or self.args[argument] is None: 34 | raise CliArgumentError(f"'{argument}' is a required argument") 35 | -------------------------------------------------------------------------------- /pkpass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module handles the CLI and any error that comes from it""" 3 | from sys import version_info, argv 4 | 5 | if version_info[0] < 3: 6 | raise Exception("Python version 3 is required 3.6 and higher is actively tested") 7 | 8 | # pylint: disable=wrong-import-position 9 | # Reasoning for the disablement is because we want people to not try to run with python2 10 | from argparse import ArgumentParser 11 | from traceback import format_exception_only 12 | from libpkpass.errors import PKPassError 13 | from libpkpass.commands.cli import Cli 14 | 15 | if __name__ == "__main__": 16 | PARSER = ArgumentParser(add_help=False) 17 | # This debug flag exists in both this parser and the main parser 18 | # of Cli 19 | PARSER.add_argument("--debug", action="store_true") 20 | HIGH_LEVEL_ARGS, ARGS = PARSER.parse_known_args() 21 | # allow '--debug' to be placed at end of command and not interrupt the subparsers 22 | if "--debug" in argv: 23 | argv.remove("--debug") 24 | try: 25 | for mesg in Cli().run(): 26 | if mesg: 27 | print(mesg) 28 | except PKPassError as error: 29 | print(f"\n\n{str(type(error).__name__)}: {error.msg}") 30 | except KeyboardInterrupt: 31 | print("\nExiting") 32 | # This is so that users don't see tracebacks, an error will still print out 33 | # so that we can investigate 34 | # Comment this out for debugging 35 | except Exception as err: # pylint: disable=broad-except 36 | if HIGH_LEVEL_ARGS.debug: 37 | raise err 38 | print( 39 | f"Generic exception caught: \n\t{format_exception_only(type(err), err)[0]}" 40 | ) 41 | -------------------------------------------------------------------------------- /test/test_list_recipients.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the listrecipients module""" 3 | import logging 4 | import unittest 5 | import pylibyaml # pylint: disable=unused-import 6 | import yaml 7 | from libpkpass.commands.cli import Cli 8 | from .basetest.basetest import patch_args 9 | 10 | 11 | class ListrecipientsTests(unittest.TestCase): 12 | """This class tests the listrecipients class""" 13 | 14 | def setUp(self): 15 | logging.disable(logging.CRITICAL) 16 | self.out_dict = { 17 | "r3": { 18 | "certs": { 19 | "verified": True, 20 | } 21 | } 22 | } 23 | 24 | def test_listrecipients(self): 25 | """Test a listing of our recipients""" 26 | self.maxDiff = None 27 | with patch_args( 28 | subparser_name="listrecipients", 29 | identity="r1", 30 | nopassphrase="true", 31 | filter="r3", 32 | ): 33 | out = "\n".join(Cli().run()).replace("\t", " ") 34 | output = yaml.safe_load(out) 35 | # we remove these things because the actual values depend on creation 36 | # moreover, some of the outputs on different operating systems appear 37 | # to utilize different delimiters. 38 | del output["r3"]["certs"]["enddate"] 39 | del output["r3"]["certs"]["subjecthash"] 40 | del output["r3"]["certs"]["issuerhash"] 41 | del output["r3"]["certs"]["fingerprint"] 42 | del output["r3"]["certs"]["subject"] 43 | del output["r3"]["certs"]["issuer"] 44 | self.assertDictEqual(output, self.out_dict) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /docs/source/General Usage.rst: -------------------------------------------------------------------------------- 1 | General Usage 2 | ============= 3 | 4 | Run ./pkpass.py with the '-h' flag for a list of options as well as syntax. Some common usage examples follow: 5 | - Create a new security team root password in the password store: 6 | 7 | .. code-block:: bash 8 | 9 | ./pkpass.py create security-team/rootpw 10 | 11 | - Distribute the security team root password to other team members 'foo' and 'bar': 12 | 13 | .. code-block:: bash 14 | 15 | ./pkpass.py distribute security-team/rootpw -u foo,bar 16 | 17 | - Distribute the security team passwords to the group secadmins 18 | 19 | .. code-block:: bash 20 | 21 | ./pkpass.py distribute 'security-team/*' -g secadmins 22 | 23 | - List the names of all passwords that have been distributed to you: 24 | 25 | .. code-block:: bash 26 | 27 | ./pkpass.py list 28 | 29 | - List the names of all escrow passwords that have been distributed to you: 30 | 31 | .. code-block:: bash 32 | 33 | ./pkpass.py list -r 34 | 35 | - Show the infrastructure team root password: 36 | 37 | .. code-block:: bash 38 | 39 | ./pkpass.py show infra-team/rootpw 40 | 41 | - Show all the passwords that you know: 42 | 43 | .. code-block:: bash 44 | 45 | ./pkpass.py show -a 46 | 47 | - Show all the passwords that you know whose filename has rpm (case-insensitive): 48 | 49 | .. code-block:: bash 50 | 51 | ./pkpass.py show -a rpm 52 | 53 | - List the names of all passwords that have been distributed to user identity 'foo': 54 | 55 | .. code-block:: bash 56 | 57 | ./pkpass.py list -i foo 58 | 59 | - Show the users that pkpass detects certificates for in the certificate repository: 60 | 61 | .. code-block:: bash 62 | 63 | ./pkpass.py listrecipients 64 | -------------------------------------------------------------------------------- /test/test_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the list module""" 3 | import unittest 4 | import pylibyaml # pylint: disable=unused-import 5 | import yaml 6 | from libpkpass.commands.cli import Cli 7 | from libpkpass.errors import CliArgumentError 8 | from .basetest.basetest import patch_args, ERROR_MSGS 9 | 10 | 11 | class ListTests(unittest.TestCase): 12 | """This class tests the list class""" 13 | 14 | def setUp(self): 15 | self.password_list_0 = {"Passwords for 'r2'": None} 16 | self.password_list_1 = { 17 | "Passwords for 'r1'": None, 18 | "test/passwords/test": {"Distributor": "r1", "Name": "test"}, 19 | } 20 | 21 | def test_recipient_not_in_database(self): 22 | """test bad recipient functionality""" 23 | with self.assertRaises(CliArgumentError) as context: 24 | with patch_args( 25 | subparser_name="list", identity="bleh", nopassphrase="true" 26 | ): 27 | "".join(Cli().run()) 28 | self.assertEqual(context.exception.msg, ERROR_MSGS["rep"]) 29 | 30 | def test_list_none(self): 31 | """test list functionality for no passwords""" 32 | with patch_args(subparser_name="list", identity="r2", nopassphrase="true"): 33 | out = "\n".join(Cli().run()) 34 | output = yaml.safe_load(out) 35 | self.assertDictEqual(output, self.password_list_0) 36 | 37 | def test_list_one(self): 38 | """test list functionality for one password""" 39 | with patch_args(subparser_name="list", identity="r1", nopassphrase="true"): 40 | out = "\n".join(Cli().run()) 41 | output = yaml.safe_load(out) 42 | self.assertDictEqual(output, self.password_list_1) 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /docs/source/Development And Testing.rst: -------------------------------------------------------------------------------- 1 | Development and Testing 2 | ======================= 3 | 4 | Testing Scripts 5 | --------------- 6 | Currently there exists a shell script ``./test/pki/generatepki.sh`` that will generate certificates for a developer to use for unittests 7 | After running this script, you can run tox or the ``python -m unittest discover`` note that ``python -m unittest discover`` does not test multiple versions of 8 | python like tox does 9 | 10 | Plugin Behavior - Connectors 11 | ---------------------------- 12 | We currently support dropping arbitary connection plugins into ``./libpkpass/connectors`` the connectors should return 13 | certificates, example usage here is if your organization stores certs in a custom web application, or in ldap or 14 | the like, you can create a connector to interface with that and feed pkpass certs in this manner 15 | 16 | Connectors will be ignored due to the gitignore, I recommend creating a separate repo for that purpose. To use 17 | a connector pkpass needs a ``connect`` argument 18 | 19 | .. code-block:: bash 20 | 21 | connect: 22 | base_directory: /path/to/local/certs # or /tmp 23 | ConnectorName: 24 | arbitary_argument1: aa1_value 25 | aa2: aa2_value 26 | 27 | This connect argument is a dictionary, the upper level key is the class that python will attempt to import. 28 | This class name should also be in a module that is its name in all lowercase. 29 | 30 | Example: the class ConnectorName would be in module connectorname 31 | 32 | The value of "ConnectorName" in our example above will all be passed to init as a dictionary. 33 | this means that "arbitrary_argument1" and "aa2" will both be available for the connector class 34 | As you can see the connect argument is a json file, and as such; you may pass multiple connectors in at the same time. 35 | -------------------------------------------------------------------------------- /test/test_generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the generate module""" 3 | import getpass 4 | import builtins 5 | import unittest 6 | import mock 7 | from libpkpass.commands.cli import Cli 8 | from libpkpass.errors import CliArgumentError, DecryptionError 9 | from .basetest.basetest import patch_args, ERROR_MSGS 10 | 11 | 12 | class GenerateTests(unittest.TestCase): 13 | """This class tests the generate class""" 14 | 15 | def test_generate_success(self): 16 | """Test a successful generation of a password""" 17 | ret = True 18 | try: 19 | with patch_args( 20 | subparser_name="generate", 21 | identity="r3", 22 | nopassphrase="true", 23 | pwname="gentest", 24 | ): 25 | with mock.patch.object(builtins, "input", lambda _: "y"): 26 | with mock.patch.object(getpass, "getpass", lambda _: "y"): 27 | "".join(Cli().run()) 28 | except DecryptionError: 29 | ret = False 30 | self.assertTrue(ret) 31 | 32 | def test_generate_no_pass(self): 33 | """test what happens with pwname is not supplied""" 34 | with self.assertRaises(CliArgumentError) as context: 35 | with patch_args( 36 | subparser_name="generate", 37 | identity="r3", 38 | nopassphrase="true", 39 | pwname=None, 40 | ): 41 | with mock.patch.object(builtins, "input", lambda _: "y"): 42 | with mock.patch.object(getpass, "getpass", lambda _: "y"): 43 | "".join(Cli().run()) 44 | self.assertEqual(context.exception.msg, ERROR_MSGS["pwname"]) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /docs/source/Alternate Backend Support.rst: -------------------------------------------------------------------------------- 1 | Alternate Backend Support 2 | ============================ 3 | 4 | A backend of yubico-piv-tool with libp11 is now supported for users as an alternative to opensc. 5 | 6 | The default backend is opensc, but users have the ability to use yubico-piv-tool and libp11 instead. 7 | 8 | The alternate backend is intended to be used primarily by users on MacOs, where packages can be most easily installed with brew. 9 | 10 | This can be enabled by adding the following line to the .pkpassrc: 11 | 12 | ``SCBackend: yubi`` 13 | 14 | To support this alternate backend, the path to the PCKS11 module is an argument that can be modified by setting PKCS11_module_path in the .pkpassrc. 15 | 16 | If not specified, PKCS11_module_path will be set to this default value: 17 | 18 | ``PKCS11_module_path="/usr/local/lib/libykcs11.dylib"`` 19 | 20 | This default value is intended for opensc usage. For alternate backend it should be modified. 21 | 22 | To modify PKCS11_module_path to support yubico-piv-tool with libp11, add this line to the .pkpassrc: 23 | 24 | ``PKCS11_module_path="/path/to/libp11/libpkcs11.dylib"`` 25 | 26 | The alternate backend is also supported by the verify install command. 27 | 28 | To confirm that the alternate backend is configured correctly run: 29 | 30 | ``pkpass verifyinstall`` 31 | 32 | If the alternate backend support is enabled and a brew installation is found, the verifyinstall function will attempt to check if the dependencies have been installed through brew or through other means. If packages are installed through brew, checks will be done to confirm the presence of required .dylib files. If a .dylib file is not found a warning will be displayed. A warning may be displayed if there is no .dylib file in the default PKCS11_module_path, but if PKCS11_module_path is NOT set to the default path then this can be ignored. 33 | -------------------------------------------------------------------------------- /test/test_create.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the create module""" 3 | import getpass 4 | import builtins 5 | import unittest 6 | import mock 7 | from libpkpass.commands.cli import Cli 8 | from libpkpass.errors import CliArgumentError, DecryptionError 9 | from .basetest.basetest import patch_args, ERROR_MSGS 10 | 11 | 12 | class CreateTests(unittest.TestCase): 13 | """This class tests the create class""" 14 | 15 | def test_create_success(self): 16 | """Test a success creation of a password""" 17 | ret = True 18 | try: 19 | with patch_args( 20 | subparser_name="create", 21 | identity="r1", 22 | nopassphrase="true", 23 | pwname="test", 24 | escrow_users="r2,r3,r4", 25 | min_escrow=2, 26 | ): 27 | with mock.patch.object(builtins, "input", lambda _: "y"): 28 | with mock.patch.object(getpass, "getpass", lambda _: "y"): 29 | "".join(Cli().run()) 30 | except DecryptionError: 31 | ret = False 32 | self.assertTrue(ret) 33 | 34 | def test_create_no_pass(self): 35 | """Test what happens with pwname is not supplied""" 36 | with self.assertRaises(CliArgumentError) as context: 37 | with patch_args( 38 | subparser_name="create", 39 | identity="r1", 40 | nopassphrase="true", 41 | pwname=None, 42 | ): 43 | with mock.patch.object(builtins, "input", lambda _: "y"): 44 | with mock.patch.object(getpass, "getpass", lambda _: "y"): 45 | "".join(Cli().run()) 46 | self.assertEqual(context.exception.msg, ERROR_MSGS["pwname"]) 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /test/test_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the info module""" 3 | import unittest 4 | import pylibyaml # pylint: disable=unused-import 5 | import yaml 6 | from libpkpass.commands.cli import Cli 7 | from libpkpass.errors import CliArgumentError 8 | from .basetest.basetest import patch_args, ERROR_MSGS 9 | 10 | 11 | class InfoTests(unittest.TestCase): 12 | """This class tests the info class""" 13 | 14 | def setUp(self): 15 | self.check_dict = { 16 | "Metadata": { 17 | "Authorizer": "y", 18 | "Creator": "r1", 19 | "Description": "y", 20 | "Name": "test", 21 | "Schemaversion": "v2", 22 | "Signature": "None", 23 | }, 24 | "Escrow Group": { 25 | "Creator": "r1", 26 | "Share Holders": "r2, r3, r4", 27 | "Total Group Share Holders": 3, 28 | "Minimum_escrow": 2, 29 | }, 30 | "Recipients": "r1", 31 | "Total Recipients": 1, 32 | } 33 | 34 | def test_info(self): 35 | """Test what info shows on a password""" 36 | with patch_args( 37 | subparser_name="info", identity="r1", nopassphrase="true", pwname="test" 38 | ): 39 | out = "\n".join(Cli().run()) 40 | output = yaml.safe_load(out) 41 | del output["Earliest distribute timestamp"] 42 | del output["Escrow Group"]["Group creation time"] 43 | self.assertDictEqual(output, self.check_dict) 44 | 45 | def test_info_no_pass(self): 46 | """Test what happens when pwname is not supplied""" 47 | with patch_args( 48 | subparser_name="info", identity="r1", nopassphrase="true", pwname=None 49 | ): 50 | with self.assertRaises(CliArgumentError) as context: 51 | "".join(Cli().run()) 52 | self.assertEqual(context.exception.msg, ERROR_MSGS["pwname"]) 53 | 54 | 55 | if __name__ == "__main__": 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /test/test_distribute.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the distribute module""" 3 | import builtins 4 | import unittest 5 | import mock 6 | from libpkpass.commands.cli import Cli 7 | from libpkpass.errors import CliArgumentError, DecryptionError 8 | from .basetest.basetest import patch_args, ERROR_MSGS 9 | 10 | 11 | class DistributeTests(unittest.TestCase): 12 | """This class tests the distribute class""" 13 | 14 | def test_recipient_not_in_database(self): 15 | """Test what happens with a recipient is not within the appropriate directory""" 16 | with self.assertRaises(CliArgumentError) as context: 17 | with patch_args( 18 | subparser_name="distribute", 19 | identity="bleh", 20 | nopassphrase="true", 21 | pwname="test", 22 | ): 23 | "".join(Cli().run()) 24 | self.assertEqual(context.exception.msg, ERROR_MSGS["rep"]) 25 | 26 | def test_distribute_cli_error(self): 27 | """Test what happens with pwname is not supplied""" 28 | with self.assertRaises(CliArgumentError) as context: 29 | with patch_args( 30 | subparser_name="distribute", 31 | identity="r1", 32 | nopassphrase="true", 33 | pwname=None, 34 | ): 35 | "".join(Cli().run()) 36 | self.assertEqual(context.exception.msg, ERROR_MSGS["pwname"]) 37 | 38 | def test_distribute_success(self): 39 | """Test a successful distribute""" 40 | ret = True 41 | try: 42 | with patch_args( 43 | subparser_name="distribute", 44 | identity="r1", 45 | nopassphrase="true", 46 | pwname="test", 47 | escrow_users="r2,r3,r4", 48 | min_escrow=2, 49 | ): 50 | with mock.patch.object(builtins, "input", lambda _: "y"): 51 | "".join(Cli().run()) 52 | except DecryptionError: 53 | ret = False 54 | self.assertTrue(ret) 55 | 56 | 57 | if __name__ == "__main__": 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /docs/source/Setup.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | Pip install is available via: 4 | | ``pip install pkpass-olcf`` 5 | 6 | Brew install is available via: 7 | | ``brew install olcf/tap/pkpass`` 8 | 9 | You may clone the pkpass.py tool like this: 10 | | ``git clone https://github.com/olcf/pkpass.git`` 11 | 12 | If you are using additional PIV/X509 certificate repositories or password repositories, you will need to create local directories for them, or create repositories in a git server that you have access to. Note that while the passwords are safely encrypted and can be distributed without fear of 13 | compromise, there may be other information such as system names, account names, and personnel information that you do not want to be publicly available. 14 | 15 | 16 | RC file 17 | ------- 18 | Pkpass has an RC file that can store default values for you so you don't have to write an essay everytime you want to look at or create passwords. 19 | 20 | An example file is below 21 | 22 | .. code-block:: bash 23 | 24 | certpath: /Users/username/passdb/certs/ 25 | keypath: /Users/username/passdb/keys/ 26 | cabundle: /Users/username/passdb/cabundles/ca.bundle 27 | pwstore: /Users/username/passdb/passwords/ 28 | 29 | In this case, 'passdb' is the name of the directory in the user's home area that contains x509 certificates, keys (if necessary) and the ca bundle. 30 | 31 | The RC file can store any command line argument that is not a true/false value. See Configuration for more details 32 | 33 | 34 | CA Bundle 35 | --------- 36 | You can create a ca bundle by combining all CA Certificates that you trust into one file and moving the file to the cabundle path. Usually the site admins create this CA Bundle for users as part of their certificate management practices. 37 | Example 38 | 39 | .. code-block:: bash 40 | 41 | cd "${directory_with_ca_certs}" 42 | cat * > ca.bundle 43 | cp ca.bundle "${cabundle_path_in_rc_file}" 44 | 45 | Additionally, note that most options you can pass on the command line may be passed in through the .pkpassrc file as well. 46 | true/false options however (such as --noverify or --nocache), cannot at this time be passed into the command like 47 | -------------------------------------------------------------------------------- /libpkpass/commands/create.py: -------------------------------------------------------------------------------- 1 | """This module allows for the creation of passwords""" 2 | import getpass 3 | from sys import stdin 4 | from libpkpass.commands.command import Command 5 | from libpkpass.errors import CliArgumentError, PasswordMismatchError, BlankPasswordError 6 | 7 | 8 | class Create(Command): 9 | #################################################################### 10 | """This class implements the CLI functionality of creation of passwords""" 11 | #################################################################### 12 | name = "create" 13 | description = "Create a new password entry and encrypt it for yourself" 14 | selected_args = Command.selected_args + [ 15 | "pwname", 16 | "pwstore", 17 | "overwrite", 18 | "stdin", 19 | "keypath", 20 | "nopassphrase", 21 | "nosign", 22 | "card_slot", 23 | "escrow_users", 24 | "min_escrow", 25 | "noescrow", 26 | "description", 27 | "authorizer", 28 | ] 29 | 30 | def _run_command_execution(self): 31 | #################################################################### 32 | """Run function for class.""" 33 | #################################################################### 34 | 35 | if not self.args["stdin"]: 36 | password1 = getpass.getpass("Enter password to create: ") 37 | if password1.strip() == "": 38 | raise BlankPasswordError 39 | password2 = getpass.getpass("Enter password to create again: ") 40 | if password1 != password2: 41 | raise PasswordMismatchError 42 | else: 43 | password1 = stdin.read() 44 | 45 | if "description" not in self.args or not self.args["description"]: 46 | self.args["description"] = input("Description: ") 47 | 48 | if "authorizer" not in self.args or not self.args["authorizer"]: 49 | self.args["authorizer"] = input("Authorizer: ") 50 | 51 | self.create_or_update_pass( 52 | password1, self.args["description"], self.args["authorizer"] 53 | ) 54 | # necessary for print statement 55 | yield "" 56 | 57 | def _validate_args(self): 58 | for argument in ["pwname", "keypath"]: 59 | if argument not in self.args or self.args[argument] is None: 60 | raise CliArgumentError(f"'{argument}' is a required argument") 61 | -------------------------------------------------------------------------------- /test/test_password.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module tests the password entry module""" 3 | import unittest 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import sessionmaker 6 | from libpkpass.password import PasswordEntry 7 | from libpkpass.identities import IdentityDB 8 | 9 | 10 | class TestBasicFunction(unittest.TestCase): 11 | """This Class tests the password entry class""" 12 | 13 | def setUp(self): 14 | self.certdir = "test/pki/intermediate/certs" 15 | self.keydir = "test/pki/intermediate/private" 16 | self.cabundle = "test/pki/intermediate/certs/ca-bundle" 17 | self.file1 = "test/passwords/testpassword" 18 | self.file2 = "test/scratch/testpassword" 19 | self.secret = "Secret" 20 | self.textblob = "Testing TextField" 21 | self.sender = "r1" 22 | 23 | self.iddb = IdentityDB() 24 | self.iddb.id = self.sender 25 | self.iddb.recipient_list = ["r2", "r3"] 26 | db_path = "test/pki/intermediate/certs/rd.db" 27 | self.iddb.args = { 28 | "db": { 29 | "uri": f"sqlite+pysqlite:///{db_path}", 30 | "engine": create_engine(f"sqlite+pysqlite:///{db_path}"), 31 | }, 32 | } 33 | self.iddb.session = sessionmaker(bind=self.iddb.args["db"]["engine"])() 34 | self.iddb.load_certs_from_directory(self.certdir, self.cabundle) 35 | self.iddb.load_keys_from_directory(self.keydir) 36 | 37 | def test_create_encrypt_decrypt(self): 38 | """create a password entry""" 39 | passwordentry = PasswordEntry( 40 | name="testcreate", description=self.textblob, creator="r1", authorizer="r1" 41 | ) 42 | 43 | passwordentry.add_recipients( 44 | secret=self.secret, 45 | distributor="r1", 46 | recipients=self.iddb.recipient_list, 47 | session=self.iddb.session, 48 | ) 49 | 50 | def test_read_write(self): 51 | """Read and write password entry data""" 52 | passwordentry = PasswordEntry() 53 | passwordentry.read_password_data(self.file1) 54 | passwordentry.write_password_data(self.file2, overwrite=True) 55 | 56 | with open(self.file1, "r", encoding="ASCII") as file1: 57 | with open(self.file2, "r", encoding="ASCII") as file2: 58 | self.assertTrue(file1.read() == file2.read()) 59 | 60 | 61 | if __name__ == "__main__": 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /test/test_id.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module tests iddb module""" 3 | import unittest 4 | import os.path 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import sessionmaker 7 | from libpkpass.identities import IdentityDB 8 | from libpkpass.models.recipient import Recipient 9 | 10 | 11 | class TestBasicFunction(unittest.TestCase): 12 | """This class tests the iddb class""" 13 | 14 | def setUp(self): 15 | self.certdir = "test/pki/intermediate/certs" 16 | self.keydir = "test/pki/intermediate/private" 17 | self.cabundle = "test/pki/intermediate/certs/ca-bundle" 18 | db_path = "test/pki/intermediate/certs/rd.db" 19 | self.iddb = IdentityDB() 20 | self.iddb.args = { 21 | "db": { 22 | "uri": f"sqlite+pysqlite:///{db_path}", 23 | "engine": create_engine(f"sqlite+pysqlite:///{db_path}"), 24 | } 25 | } 26 | self.iddb.session = sessionmaker(bind=self.iddb.args["db"]["engine"])() 27 | 28 | def test_certificate_loading(self): 29 | """Load a cert into our test""" 30 | self.iddb.load_certs_from_directory(self.certdir, self.cabundle) 31 | for identity in ("r1", "r2", "r3"): 32 | assert identity in [ 33 | x[0] for x in self.iddb.session.query(Recipient.name).all() 34 | ] 35 | 36 | def test_key_loading(self): 37 | """Load a key into our test""" 38 | self.iddb.load_keys_from_directory(self.keydir) 39 | for identity in ("r1", "r2", "r3"): 40 | assert identity in [ 41 | x[0] for x in self.iddb.session.query(Recipient.name).all() 42 | ] 43 | test_id = ( 44 | self.iddb.session.query(Recipient) 45 | .filter(Recipient.name == identity) 46 | .first() 47 | ) 48 | assert os.path.isfile(test_id.key) 49 | 50 | def test_keycert_loading(self): 51 | """Load a keycert into our test""" 52 | self.iddb.load_certs_from_directory(self.certdir, self.cabundle) 53 | self.iddb.load_keys_from_directory(self.keydir) 54 | for identity in ("r1", "r2", "r3"): 55 | assert identity in [ 56 | x[0] for x in self.iddb.session.query(Recipient.name).all() 57 | ] 58 | test_id = ( 59 | self.iddb.session.query(Recipient) 60 | .filter(Recipient.name == identity) 61 | .first() 62 | ) 63 | assert os.path.isfile(test_id.key) 64 | 65 | 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pkpass specific 2 | .pkpassrc 3 | test/pki/ca 4 | test/pki/intermediate 5 | test/scratch/* 6 | !libpkpass/connectors/connectorinterface.py 7 | libpkpass/connectors/* 8 | plugin-requirements.txt 9 | 10 | # sonarqube 11 | .scannerwork/ 12 | .sonar/ 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | pip-wheel-metadata/ 37 | share/python-wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | *.py,cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | # docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # celery beat schedule file 108 | celerybeat-schedule 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '34 18 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /test/test_rename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the rename module""" 3 | import builtins 4 | import unittest 5 | import mock 6 | from libpkpass.commands.cli import Cli 7 | from libpkpass.errors import DecryptionError, CliArgumentError 8 | from .basetest.basetest import patch_args, ERROR_MSGS 9 | 10 | 11 | class RenameTests(unittest.TestCase): 12 | """This class tests the rename class""" 13 | 14 | def test_recipient_not_in_database(self): 15 | """Test what happens when a recipient is not in the appropriate directory""" 16 | with self.assertRaises(CliArgumentError) as context: 17 | with patch_args( 18 | subparser_name="rename", 19 | identity="bleh", 20 | nopassphrase="true", 21 | all=None, 22 | pwname="test", 23 | rename="retest", 24 | ): 25 | "".join(Cli().run()) 26 | self.assertEqual(context.exception.msg, ERROR_MSGS["rep"]) 27 | 28 | def test_rename_success(self): 29 | """Test a successful rename (then rename it back)""" 30 | ret = True 31 | try: 32 | with patch_args( 33 | subparser_name="rename", 34 | identity="r1", 35 | nopassphrase="true", 36 | all=True, 37 | pwname="test", 38 | rename="retest", 39 | overwrite="true", 40 | ): 41 | with mock.patch.object(builtins, "input", lambda _: "y"): 42 | "".join(Cli().run()) 43 | with patch_args( 44 | subparser_name="rename", 45 | identity="r1", 46 | nopassphrase="true", 47 | all=True, 48 | pwname="retest", 49 | rename="test", 50 | overwrite="true", 51 | ): 52 | with mock.patch.object(builtins, "input", lambda _: "y"): 53 | "".join(Cli().run()) 54 | except DecryptionError: 55 | ret = False 56 | self.assertTrue(ret) 57 | 58 | def test_rename_cli_error(self): 59 | """Test what happens when pwname is not supplied""" 60 | with self.assertRaises(CliArgumentError) as context: 61 | with patch_args( 62 | subparser_name="rename", 63 | identity="r1", 64 | nopassphrase="true", 65 | all=True, 66 | rename="test", 67 | overwrite="true", 68 | ): 69 | "".join(Cli().run()) 70 | self.assertEqual(context.exception.msg, ERROR_MSGS["pwname"]) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /libpkpass/commands/list.py: -------------------------------------------------------------------------------- 1 | """This Module allows for the listing of users passwords""" 2 | from libpkpass.util import dictionary_filter 3 | from libpkpass.commands.command import Command 4 | from libpkpass.errors import CliArgumentError 5 | 6 | 7 | class List(Command): 8 | #################################################################### 9 | """This class implements the cli list""" 10 | #################################################################### 11 | name = "list" 12 | description = "List passwords you have access to" 13 | selected_args = Command.selected_args + ["pwstore", "stdin", "recovery", "filter"] 14 | 15 | def _run_command_execution(self): 16 | #################################################################### 17 | """Run function for class.""" 18 | #################################################################### 19 | result = {} 20 | for pwname, passwordentry in self.passworddb.pwdb.items(): 21 | if self.args["recovery"] and passwordentry.escrow: 22 | recipients = passwordentry.escrow["recipients"] 23 | for key, value in recipients.items(): 24 | if key == self.args["identity"]: 25 | result[pwname] = { 26 | "name": passwordentry.metadata["name"], 27 | "stake_holders": list(recipients.keys()), 28 | "distributor": value["distributor"], 29 | "minimum_shares": passwordentry.escrow["metadata"][ 30 | "minimum_escrow" 31 | ], 32 | } 33 | elif ( 34 | not self.args["recovery"] 35 | and self.args["identity"] in passwordentry.recipients.keys() 36 | ): 37 | result[pwname] = { 38 | "name": passwordentry.metadata["name"], 39 | "distributor": passwordentry.recipients[self.args["identity"]][ 40 | "distributor" 41 | ], 42 | } 43 | 44 | if "filter" in self.args and self.args["filter"]: 45 | result = dictionary_filter(self.args["filter"], result) 46 | 47 | yield f"Passwords for '{self.args['identity']}':" 48 | for key, value in sorted(result.items()): 49 | yield f"{self.color_print(key + ':', 'first_level')}\n {self.color_print('Distributor: ', 'second_level') + value['distributor']}\n {self.color_print('Name: ', 'second_level') + value['name']}" 50 | 51 | def _validate_args(self): 52 | for argument in ["keypath"]: 53 | if argument not in self.args or self.args[argument] is None: 54 | raise CliArgumentError(f"'{argument}' is a required argument") 55 | -------------------------------------------------------------------------------- /test/passwords/gentest: -------------------------------------------------------------------------------- 1 | metadata: 2 | authorizer: y 3 | creator: r3 4 | description: y 5 | name: gentest 6 | schemaVersion: v2 7 | signature: null 8 | recipients: 9 | r3: 10 | distributor: r3 11 | distributor_hash: d939a67c 12 | encrypted_secrets: 13 | FD:F0:6F:5E:8E:D0:41:04:1A:30:FE:B4:9F:5C:F1:82:66:38:99:A9: 14 | derived_key: !!binary | 15 | Q0xhdkVXOUkxbF9CY3FrcE1pQTY1WEFsVWhVOXpyMjZEUVBkY2h6RkdCZEdOejdBRkRVcEd0MV90 16 | YWNlV09qSGI1Tk53NDRMQ0tINmxDVDFmRjlDMlpEeEY5Q01hQmh3OGtlMGpaNFlQQTkwak1fVDVD 17 | VnVNd3VLQkJIdUJCUk5lUHdnN09Sb2wtdHJ0bFlxVEsta05RR2ZPUEdndlUzRnZJV0VVaHJhVzZn 18 | ZkVHdWVUZFNDYjNEcU1vLS1LY1lPblZwZzNfRnpGUUg2RXlfMkxoX0FmZU9vNGFodm5ZVUdQcUxB 19 | TnVaYXY1UDR1YVRDVFpJVUxaUkFJbUpmSW5pY1JwdndSNldrc3QyV3dLNHhiYmJoaW1pU0xEYW5U 20 | Y0FTM09HTWVZRlVHZjZNQ2NUVlFlVEFHWjBoc1QzbjZWaU9tcFUzQzd6cldVYTlyM1NTQnppNzlo 21 | Tm5QVlVWZFVHbkRPcUlRcWc1aXBTcGdObVRxdlZYa0JSeVFHRDBvV04tWXNIb0pfd2ZYOTRzUTFM 22 | N19aYllXNUlNQkVNUWlNLXg3a21odXJPSlhuUkpjZE5MUGJQOW0zMkJKWDZDa0hWMkFSVnVHUVNO 23 | ZWNwT2JCUWpyU0UxaFBQTVFxb2NmWVBqVW1vZnhIbjdGM2ROdXdoVnd5andnS0dtckFfSldFLS1M 24 | eV96bkk2RU1FX0pJajR3RTBHWC1fS1FZYWZzOHZEZnFvT3hkdEpwODg3SUFLb3ExN2hXeXZlVkFn 25 | WWpvbThjTkZtM1hRaTU4RDV1emk4YWZsUFBTVk5UNTluYTZXemZZOVRFMGdZUWNrSGIxN2U1VG9h 26 | NWl2QmJ4S21DcFVJVjNSR0VGcEJEVnp1T3BCbTRHcDZ4RWk2X3BfRl85TjZSNWg1WjhtMFVDZHM9 27 | encrypted_secret: !!binary | 28 | Z0FBQUFBQmg1eEtnaVdubVdqbFdUT1Z0X3Fwd1BPLTUzZ1V5M0h5MWhMU3VuYmNFUlNjcjljbEV3 29 | bjljMVBMTTJjem5HakM1Qk9ETlZDVDlNejFwZUhBNGFoREFic2Rmb2tvT2VZM1pCNkhOZTlBbVo4 30 | OGNWZzQ9 31 | recipient_hash: b779ae6e 32 | encryption_algorithm: rsautl 33 | signature: !!binary | 34 | WjhTMVczZkZoNUlENDJfdjhtejBpUG1mOGk3YzZ3ZHhlVzREYmc5RWx4N081NUM3UnNYbndXdXNG 35 | SHg5M0E3WGc3RWNLZm5Td3dQUU5CalNDallMYnR5V3Byd0g3VkR1V19sa085b01COXNGZkREcmxV 36 | cFVUeTlkT2lmVHNsREM4TlNDYXZNRDJ5SlVlc3lMUFRkNmZueEwxODF2T21pWUdCdUh0Q3JqaWZK 37 | NmFEWVJTSUplb1BwMGY4cmF1alpxV241UU5oX0lVb3NtaHNSWkRKZ2tfSDFlaEtRQTFTcm9VaGJF 38 | MHhuMDJSRU15bjYyd2E4S2ZtVzYwVUFwcWwwd1gxTmRLWF9XWEM3X1MwSEhTeWpSbEFYeFJXNWpL 39 | N3ZoVjdncHdDNmUzNlE4YjkxdDhzT1lWZFkxamZpVWNXYlRLV2RoaVB6d1VJU0dEQnhUcVI3TENh 40 | cjhTQmtTTzNibFBrcV9iSzVXc0ZiNVU1SVNaLUhrOWFlTEdfVlhvN1k1TGhGZFZuVjM5MmR4NG1G 41 | U0lmcTIwMmRyWURyX0FvQksxSEpxaFc4VXJIQndmSlRQLTVpNEhNNV9vMV9PeUhnOXF6ODVydnNY 42 | LVR3THZ4akM5dVBFZERLN0Z2Q1hVVWhpSnRkZURjYVlkc1BYSk9GdHQwOGhxRERseWE0V3RaZ1My 43 | OFBLbTZkMnNVZ1kxSUs2ZEYzcGk3T21HY0NQNFpVQWNsTER4VDdkYmxSNFMtOEQwYTFRY0pzYmRQ 44 | RWV1aVNTQTFyby04RkM4TjFnMF9zYUE3RUZseUNXOExvalkweG0yMXI2Rlo1YVVVLWIxcmZuTlRY 45 | SWlFcWliMk01UUtwd2ozUHUzc0VGTF81c0NQRG1LWjlIcU93WFlyVWxZLXRZNFB2WmxRZExRLWM9 46 | timestamp: '1642533537.10' 47 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to PkPass's documentation! 2 | ================================== 3 | 4 | Overview 5 | -------- 6 | This is a basic password store and password manager for maintaining arbitrary secrets. 7 | 8 | The password management solution provides: 9 | 10 | - Encryption at Rest 11 | - Password distribution/organization based on definable hierarchies 12 | - Password creation timestamps 13 | - Password history and change logs 14 | - Distributed backup capabilities 15 | - PIV/Smartcard Credential encryption/decryption 16 | - Import and export functionality 17 | 18 | Passwords that are created are distributed to recipients by public key encryption. The x509 certificate of the intended recipient is used to create an encrypted copy of the distributed password that is then saved in a password-specific git repository. Multiple encrypted copies of the secret are created, one for each user. End users then check out the git repo and are able to read passwords using their PIV/Smartcard credential to decrypt. 19 | 20 | 21 | x509 Certificate Repository 22 | --------------------------- 23 | PKPass needs a trusted x509 certificate repository, which typically is managed using git. Certificates in this repository should all be 24 | signed by Certificate Authorities that can be found in the CABundle file that PKPass is configured to look at. Since this repository should be 25 | considered 'trusted', it is typically managed by a smaller trusted set of site administrators. PKPass validates all encryption certificates as they are used to make sure they are signed by a trusted Certificate Authority (CA). 26 | 27 | 28 | You may also use a local x509 certificate repository that you sync with others using RSYNC, NFS, shared volumes, etc. You can configure the directory that pkpass will use for the certificate repository either on the command line, or through the .pkpassrc file. 29 | 30 | The CABundle file to use can also be configured in the .pkpassrc file or on the command line. 31 | 32 | Additionally, certificates should be named .cert. For example, the certificate for user 'jason' should be named 'jason.cert' inside this x509 directory. 33 | 34 | 35 | Password Repository 36 | ------------------- 37 | PKPass also needs a directory to serve as a 'password database'. Like the x509 certificate repository, it is also typically managed with git to provide change control, history, and tracking of changes. Local directories can also be used and shared via rsync, NFS, shared volumes, etc if preferred. 38 | 39 | To change the default password repository, you may specify another directory on the command line or in the .pkpassrc file. 40 | 41 | 42 | .. toctree:: 43 | :maxdepth: 2 44 | 45 | Setup 46 | Commands 47 | General Usage 48 | Configuration 49 | Development And Testing 50 | Software Dependencies 51 | Python2 52 | Windows 53 | 54 | -------------------------------------------------------------------------------- /libpkpass/commands/delete.py: -------------------------------------------------------------------------------- 1 | """This module allows for the deletion of passwords""" 2 | from os import path, remove 3 | from sys import exit as sysexit 4 | from libpkpass.commands.command import Command 5 | from libpkpass.errors import CliArgumentError, NotThePasswordOwnerError, PasswordIOError 6 | 7 | 8 | class Delete(Command): 9 | #################################################################### 10 | """This class implements the CLI functionality of deletion of passwords""" 11 | #################################################################### 12 | name = "delete" 13 | description = "Delete a password in the repository" 14 | selected_args = Command.selected_args + [ 15 | "pwname", 16 | "pwstore", 17 | "overwrite", 18 | "stdin", 19 | "keypath", 20 | "card_slot", 21 | ] 22 | 23 | def _run_command_execution(self): 24 | #################################################################### 25 | """Run function for class.""" 26 | #################################################################### 27 | safe, owner = self.safety_check() 28 | if safe or self.args["overwrite"]: 29 | self._confirmation() 30 | else: 31 | raise NotThePasswordOwnerError( 32 | self.args["identity"], owner, self.args["pwname"] 33 | ) 34 | # necessary for print statement 35 | yield "" 36 | 37 | def delete_pass(self): 38 | ################################################################## 39 | """This deletes a password that the user has created, useful for testing""" 40 | ################################################################## 41 | filepath = path.join(self.args["pwstore"], self.args["pwname"]) 42 | try: 43 | remove(filepath) 44 | except OSError as err: 45 | raise PasswordIOError( 46 | f"Password '{self.args['pwname']}' not found" 47 | ) from err 48 | 49 | def _confirmation(self): 50 | #################################################################### 51 | """Verify password to modify""" 52 | #################################################################### 53 | yes = {"yes", "y", "ye", ""} 54 | deny = {"no", "n"} 55 | confirmation = input( 56 | f"{self.args['pwname']}: \nDelete this password?(Defaults yes): " 57 | ) 58 | if confirmation.lower() in yes: 59 | self.delete_pass() 60 | elif confirmation.lower() in deny: 61 | sysexit() 62 | else: 63 | print("please respond with yes or no") 64 | self._confirmation() 65 | 66 | def _validate_args(self): 67 | for argument in ["pwname", "keypath"]: 68 | if argument not in self.args or self.args[argument] is None: 69 | raise CliArgumentError(f"'{argument}' is a required argument") 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PKPass: Public Key Based Password Manager 2 | ============= 3 | 4 | ![Unit Tests](https://github.com/olcf/pkpass/workflows/Unit%20Tests/badge.svg) [![Documentation Status](https://readthedocs.org/projects/pkpass/badge/?version=latest)](https://pkpass.readthedocs.io/en/latest/?badge=latest) [![CodeQL](https://github.com/olcf/pkpass/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/olcf/pkpass/actions/workflows/codeql-analysis.yml) 5 | 6 | RTD 7 | === 8 | https://pkpass.readthedocs.io/en/latest/ 9 | 10 | Requires >= Python 3.6 11 | 12 | Overview 13 | -------- 14 | This is a basic password store and password manager for maintaining arbitrary secrets. 15 | 16 | The password management solution provides: 17 | 18 | - Encryption at Rest 19 | - Password distribution/organization based on definable hierarchies 20 | - Password creation timestamps 21 | - Password history and change logs 22 | - Distributed backup capabilities 23 | - PIV/Smartcard Credential encryption/decryption 24 | - Import and export functionality 25 | 26 | Passwords that are created are distributed to recipients by public key encryption. The x509 certificate of the intended recipient is used to create an encrypted copy of the distributed password that is then saved in a password-specific git repository. Multiple encrypted copies of the secret are created, one for each user. End users then check out the git repo and are able to read passwords using their PIV/Smartcard credential to decrypt. 27 | 28 | Install 29 | ------- 30 | 31 | ### Everything: 32 | `pip install pkpass-olcf` 33 | 34 | ### MacOs: 35 | `brew install olcf/tap/pkpass` 36 | 37 | 38 | x509 Certificate Repository 39 | ------- 40 | PKPass needs a trusted x509 certificate repository, which typically is managed using git. Certificates in this repository should all be 41 | signed by Certificate Authorities that can be found in the CABundle file that PKPass is configured to look at. Since this repository should be 42 | considered 'trusted', it is typically managed by a smaller trusted set of site administrators. PKPass validates all encryption certificates as they are used to make sure they are signed by a trusted Certificate Authority (CA). 43 | 44 | 45 | You may also use a local x509 certificate repository that you sync with others using RSYNC, NFS, shared volumes, etc. You can configure the directory that pkpass will use for the certificate repository either on the command line, or through the .pkpassrc file. 46 | 47 | The CABundle file to use can also be configured in the .pkpassrc file or on the command line. 48 | 49 | Additionally, certificates should be named .cert. For example, the certificate for user 'jason' should be named 'jason.cert' inside this x509 directory. 50 | 51 | 52 | Password Repository 53 | -------- 54 | PKPass also needs a directory to serve as a 'password database'. Like the x509 certificate repository, it is also typically managed with git to provide change control, history, and tracking of changes. Local directories can also be used and shared via rsync, NFS, shared volumes, etc if preferred. 55 | 56 | To change the default password repository, you may specify another directory on the command line or in the .pkpassrc file. 57 | -------------------------------------------------------------------------------- /libpkpass/commands/export.py: -------------------------------------------------------------------------------- 1 | """This Module allows for the export of passwords for the purpose of importing to a new card""" 2 | import getpass 3 | from tqdm import tqdm 4 | from libpkpass.commands.command import Command 5 | from libpkpass.errors import CliArgumentError, PasswordMismatchError 6 | 7 | 8 | class Export(Command): 9 | #################################################################### 10 | """This Class implements the cli functionality of export""" 11 | #################################################################### 12 | name = "export" 13 | description = "Export passwords that you have access to and encrypt with aes" 14 | selected_args = Command.selected_args + [ 15 | "pwfile", 16 | "stdin", 17 | "nopassphrase", 18 | "card_slot", 19 | "nocrypto", 20 | ] 21 | 22 | def _run_command_execution(self): 23 | #################################################################### 24 | """Run function for class.""" 25 | #################################################################### 26 | crypt_pass = False 27 | if not self.args["nocrypto"]: 28 | crypt_pass = getpass.getpass("Please enter a password for the encryption: ") 29 | verify_pass = getpass.getpass("Please enter again for verification: ") 30 | if crypt_pass != verify_pass: 31 | raise PasswordMismatchError() 32 | 33 | self._iterate_pdb(self.passworddb, crypt_pass) 34 | # necessary for print statement 35 | yield "" 36 | 37 | def _iterate_pdb(self, passworddb, crypt_pass=False): 38 | #################################################################### 39 | """Iterate through the passwords that we can decrypt""" 40 | #################################################################### 41 | uid = self.iddb.id["name"] 42 | all_passwords = { 43 | k: v for (k, v) in passworddb.pwdb.items() if uid in v.recipients.keys() 44 | } 45 | for _, password in tqdm(all_passwords.items()): 46 | plaintext_pw = password.decrypt_entry( 47 | identity=self.iddb.id, 48 | passphrase=self.passphrase, 49 | card_slot=self.args["card_slot"], 50 | SCBackend=self.args["SCBackend"], 51 | PKCS11_module_path=self.args["PKCS11_module_path"], 52 | ) 53 | password.recipients[uid]["encrypted_secret"] = plaintext_pw.encode("UTF-8") 54 | password.write_password_data( 55 | self.args["pwfile"], 56 | overwrite=False, 57 | encrypted_export=not self.args["nocrypto"], 58 | password=crypt_pass, 59 | export=True, 60 | ) 61 | 62 | def _validate_args(self): 63 | #################################################################### 64 | """Ensure arguments are appropriate for this command""" 65 | #################################################################### 66 | for argument in ["pwfile", "keypath"]: 67 | if argument not in self.args or self.args[argument] is None: 68 | raise CliArgumentError(f"'{argument}' is a required argument") 69 | 70 | def _validate_combinatorial_args(self): 71 | pass 72 | -------------------------------------------------------------------------------- /libpkpass/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module is the base module for custom errors to allow for reduced tracebacks and 3 | more actionable items on the user's side""" 4 | 5 | 6 | class PKPassError(Exception): 7 | """Base class for pkpass exceptions.""" 8 | 9 | def __init__(self, arg): 10 | self.msg = arg 11 | 12 | 13 | class BlankPasswordError(PKPassError): 14 | def __init__(self): 15 | self.msg = "User Provided password is blank or only spaces" 16 | 17 | 18 | class CliArgumentError(PKPassError): 19 | pass 20 | 21 | 22 | class ConfigParseError(PKPassError): 23 | pass 24 | 25 | 26 | class DecryptionError(PKPassError): 27 | pass 28 | 29 | 30 | class EncryptionError(PKPassError): 31 | pass 32 | 33 | 34 | class BadBackendError(PKPassError): 35 | def __init__(self, value): 36 | self.msg = f"Invalid value for SCBackend: {value}" 37 | 38 | 39 | class EscrowError(PKPassError): 40 | def __init__(self, field, constant, value): 41 | self.msg = f"{field}, must be greater than {constant}, current value: {value}" 42 | 43 | 44 | class FileOpenError(PKPassError): 45 | def __init__(self, value, reason): 46 | self.msg = f"File {value} found in config, could not be opened due to {reason}" 47 | 48 | 49 | class GroupDefinitionError(PKPassError): 50 | def __init__(self, value): 51 | self.msg = f"Group {value} is not defined in the config" 52 | 53 | 54 | class JsonArgumentError(PKPassError): 55 | def __init__(self, value, reason): 56 | self.msg = f"Parse error for '{value}' because '{reason}'" 57 | 58 | 59 | class LegacyImportFormatError(PKPassError): 60 | def __init__(self): 61 | self.msg = "Passwords in import file not in key:value notation" 62 | 63 | 64 | class NotARecipientError(PKPassError): 65 | pass 66 | 67 | 68 | class NotThePasswordOwnerError(PKPassError): 69 | def __init__(self, identity, owner, pwname): 70 | self.msg = f"User '{identity}' is not the owner of password '{pwname}', not overwriting;\n\t\ 71 | please use another password name or ask the owner ({owner}) to distribute to you" 72 | 73 | 74 | class NullRecipientError(PKPassError): 75 | def __init__(self): 76 | self.msg = ( 77 | "There is a blank Recipient in the list, please check for trailing commas" 78 | ) 79 | 80 | 81 | class PasswordIOError(PKPassError): 82 | pass 83 | 84 | 85 | class PasswordMismatchError(PKPassError): 86 | def __init__(self): 87 | self.msg = "Passwords do not match" 88 | 89 | 90 | class PasswordValidationError(PKPassError): 91 | def __init__(self, field, value): 92 | self.msg = f"Error validating password field: {field} with value {value}" 93 | 94 | 95 | class RulesMapError(PKPassError): 96 | def __init__(self, reason): 97 | self.msg = reason 98 | 99 | 100 | class RSAKeyError(PKPassError): 101 | pass 102 | 103 | 104 | class SignatureCreationError(PKPassError): 105 | pass 106 | 107 | 108 | class TrustChainVerificationError(PKPassError): 109 | pass 110 | 111 | 112 | class X509CertificateError(PKPassError): 113 | pass 114 | 115 | 116 | class YamlFormatError(PKPassError): 117 | def __init__(self, value, reason): 118 | self.msg = f"Error {value} due to: {reason}" 119 | -------------------------------------------------------------------------------- /libpkpass/commands/clip.py: -------------------------------------------------------------------------------- 1 | """This Module allows for copying a password directly to the clipboard""" 2 | from time import sleep 3 | from os import path 4 | from pyperclip import copy, paste 5 | from libpkpass import LOGGER 6 | from libpkpass.commands.command import Command 7 | from libpkpass.password import PasswordEntry 8 | from libpkpass.errors import CliArgumentError, PasswordIOError 9 | from libpkpass.models.recipient import Recipient 10 | 11 | 12 | class Clip(Command): 13 | #################################################################### 14 | """This class allows for the copying of a password to the clipboard""" 15 | 16 | #################################################################### 17 | name = "clip" 18 | description = "Copy a password to clipboard" 19 | selected_args = Command.selected_args + [ 20 | "pwname", 21 | "pwstore", 22 | "stdin", 23 | "time", 24 | "keypath", 25 | "nopassphrase", 26 | "noverify", 27 | "card_slot", 28 | ] 29 | 30 | def _run_command_execution(self): 31 | #################################################################### 32 | """Run function for class.""" 33 | #################################################################### 34 | password = PasswordEntry() 35 | password.read_password_data( 36 | path.join(self.args["pwstore"], self.args["pwname"]) 37 | ) 38 | distributor = password.recipients[self.iddb.id["name"]]["distributor"] 39 | plaintext_pw = password.decrypt_entry( 40 | identity=self.iddb.id, 41 | passphrase=self.passphrase, 42 | card_slot=self.args["card_slot"], 43 | SCBackend=self.args["SCBackend"], 44 | PKCS11_module_path=self.args["PKCS11_module_path"], 45 | ) 46 | if not self.args["noverify"]: 47 | result = password.verify_entry( 48 | self.iddb.id["name"], 49 | self.iddb, 50 | distributor, 51 | self.iddb.session.query(Recipient) 52 | .filter(Recipient.name == distributor) 53 | .first() 54 | .certs, 55 | ) 56 | if not result["sigOK"]: 57 | LOGGER.warning( 58 | "Could not verify that %s correctly signed your password entry.", 59 | result["distributor"], 60 | ) 61 | if not result["certOK"]: 62 | LOGGER.warning( 63 | "Could not verify the certificate authenticity of user '%s'.", 64 | result["distributor"], 65 | ) 66 | oldclip = paste() 67 | try: 68 | copy(plaintext_pw) 69 | yield f"Password copied into paste buffer for {self.args['time']} seconds" 70 | sleep(self.args["time"]) 71 | finally: 72 | copy(oldclip) 73 | 74 | def _validate_args(self): 75 | for argument in ["keypath"]: 76 | if argument not in self.args or self.args[argument] is None: 77 | raise CliArgumentError(f"'{argument}' is a required argument") 78 | 79 | def _pre_check(self): 80 | if path.exists(path.join(self.args["pwstore"], self.args["pwname"])): 81 | return True 82 | raise PasswordIOError( 83 | f"{path.join(self.args['pwstore'], self.args['pwname'])} does not exist" 84 | ) 85 | -------------------------------------------------------------------------------- /libpkpass/commands/pkinterface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This is a super class to handle commonalities between the cli and interpreter""" 3 | from argparse import ArgumentParser 4 | from os import path 5 | from libpkpass.util import set_default_subparser 6 | from libpkpass.commands import ( 7 | card, 8 | clip, 9 | create, 10 | delete, 11 | distribute, 12 | export, 13 | generate, 14 | info, 15 | listrecipients, 16 | modify, 17 | recover, 18 | rename, 19 | show, 20 | populate, 21 | update, 22 | verifyinstall, 23 | ) 24 | import libpkpass.commands.fileimport as pkimport 25 | import libpkpass.commands.list as pklist 26 | 27 | 28 | class PkInterface: 29 | ############################################################################## 30 | """Class for parsing command line. Observes subclasses of Command to Register 31 | those commands in the actions list.""" 32 | ############################################################################## 33 | 34 | def __init__(self): 35 | #################################################################### 36 | """Intialization function for class. Register all subcommands""" 37 | #################################################################### 38 | # Hash of registered subparser actions, mapping string to actual subparser 39 | self.actions = {} 40 | home = path.expanduser("~") 41 | defaultrc = None 42 | for file in [".pkpassrc", ".pkpassrc.yml", ".pkpassrc.yaml"]: 43 | if path.isfile(path.join(home, file)): 44 | defaultrc = path.join(home, file) 45 | if not defaultrc: 46 | defaultrc = path.join(home, ".pkpassrc") 47 | self.parser = ArgumentParser(description="Public Key Password Manager") 48 | self.parser.set_default_subparser = set_default_subparser 49 | self.parser.add_argument( 50 | "--config", 51 | type=str, 52 | help="Path to a PKPass configuration file. Defaults to '~/.pkpassrc{,.yml,.yaml}'", 53 | default=defaultrc, 54 | ) 55 | self.parser.add_argument( 56 | "--debug", action="store_true", help="Errors are more verbose" 57 | ) 58 | self.subparsers = self.parser.add_subparsers( 59 | help="sub-commands", dest="subparser_name" 60 | ) 61 | 62 | card.Card(self) 63 | clip.Clip(self) 64 | create.Create(self) 65 | delete.Delete(self) 66 | distribute.Distribute(self) 67 | export.Export(self) 68 | generate.Generate(self) 69 | pkimport.Import(self) 70 | info.Info(self) 71 | pklist.List(self) 72 | listrecipients.Listrecipients(self) 73 | modify.Modify(self) 74 | recover.Recover(self) 75 | rename.Rename(self) 76 | show.Show(self) 77 | populate.Populate(self) 78 | update.Update(self) 79 | verifyinstall.VerifyInstall(self) 80 | 81 | def register(self, command_obj, command_name, command_description): 82 | #################################################################### 83 | """Register command objects and names using an observer pattern""" 84 | #################################################################### 85 | self.actions[command_name] = command_obj 86 | parser = self.subparsers.add_parser(command_name, help=command_description) 87 | command_obj.register(parser) 88 | -------------------------------------------------------------------------------- /libpkpass/commands/info.py: -------------------------------------------------------------------------------- 1 | """This module allows for inspecting metadata passwords""" 2 | from os import path, linesep 3 | from datetime import datetime 4 | from libpkpass.password import PasswordEntry 5 | from libpkpass.commands.command import Command 6 | from libpkpass.errors import CliArgumentError 7 | 8 | 9 | class Info(Command): 10 | #################################################################### 11 | """This class implements the display of metadata to users""" 12 | #################################################################### 13 | name = "info" 14 | description = "Displays metadata about a password" 15 | selected_args = Command.selected_args + ["pwname", "pwstore"] 16 | 17 | def _run_command_execution(self): 18 | #################################################################### 19 | """Run function for class.""" 20 | #################################################################### 21 | password = PasswordEntry() 22 | password.read_password_data( 23 | path.join(self.args["pwstore"], self.args["pwname"]) 24 | ) 25 | 26 | # Metadata 27 | yield self.color_print("Metadata:", "first_level") 28 | for key, value in password.metadata.items(): 29 | yield f" {self.color_print(key.capitalize(), 'second_level')}: {value}" 30 | 31 | # Escrow 32 | if password.escrow: 33 | yield self.color_print("\nEscrow Group:", "first_level") 34 | for key, value in password.escrow["metadata"].items(): 35 | yield f" {self.color_print(key.capitalize() + ':', 'second_level')} {str(value)}" 36 | 37 | yield f" {self.color_print('Share Holders:', 'second_level')} {', '.join(list(password.escrow['recipients'].keys()))}" 38 | yield f" {self.color_print('Total Group Share Holders:', 'second_level')} {len(list(password.escrow['recipients'].keys()))}" 39 | 40 | timestamp_list = [ 41 | x["timestamp"] 42 | for x in list(password.escrow["recipients"].values()) 43 | if "timestamp" in x 44 | ] 45 | if timestamp_list: 46 | timestamp = int(round(float(min(timestamp_list)))) 47 | timestamp = datetime.fromtimestamp(timestamp).strftime( 48 | "%Y-%m-%d %H:%M:%S" 49 | ) 50 | yield f" {self.color_print('Group creation time:', 'second_level')} {timestamp}" 51 | 52 | # Recipients 53 | yield f"{self.color_print(linesep + 'Recipients:', 'first_level')} {', '.join(list(password.recipients.keys()))}" 54 | yield f"{self.color_print('Total Recipients:', 'first_level')} {len(list(password.recipients.keys()))}" 55 | rec_timestamp = int( 56 | round( 57 | float( 58 | min( 59 | [ 60 | x["timestamp"] 61 | for x in list(password.recipients.values()) 62 | if "timestamp" in x 63 | ] 64 | ) 65 | ) 66 | ) 67 | ) 68 | rec_timestamp = datetime.fromtimestamp(rec_timestamp).strftime( 69 | "%Y-%m-%d %H:%M:%S" 70 | ) 71 | yield f"{self.color_print('Earliest distribute timestamp:', 'first_level')} {rec_timestamp}" 72 | 73 | def _validate_args(self): 74 | for argument in ["pwname", "keypath"]: 75 | if argument not in self.args or self.args[argument] is None: 76 | raise CliArgumentError(f"'{argument}' is a required argument") 77 | -------------------------------------------------------------------------------- /libpkpass/commands/generate.py: -------------------------------------------------------------------------------- 1 | """This module allows for the automated generation of passwords""" 2 | from re import search 3 | from re import error as re_error 4 | from exrex import getone 5 | from libpkpass.commands.command import Command 6 | from libpkpass.errors import CliArgumentError, NotThePasswordOwnerError, RulesMapError 7 | from libpkpass.util import parse_json_arguments 8 | 9 | 10 | class Generate(Command): 11 | #################################################################### 12 | """This class implements the CLI functionality of automatic generation of passwords""" 13 | #################################################################### 14 | name = "generate" 15 | description = "Generate a new password entry and encrypt it for yourself" 16 | selected_args = Command.selected_args + [ 17 | "pwname", 18 | "pwstore", 19 | "overwrite", 20 | "stdin", 21 | "keypath", 22 | "nopassphrase", 23 | "nosign", 24 | "card_slot", 25 | "escrow_users", 26 | "min_escrow", 27 | "noescrow", 28 | "rules", 29 | "rules_map", 30 | "description", 31 | "authorizer", 32 | ] 33 | 34 | def _run_command_execution(self): 35 | #################################################################### 36 | """Run function for class.""" 37 | #################################################################### 38 | safe, owner = self.safety_check() 39 | 40 | if safe or self.args["overwrite"]: 41 | password = self._generate_pass() 42 | 43 | if "description" not in self.args or not self.args["description"]: 44 | self.args["description"] = input("Description: ") 45 | 46 | if "authorizer" not in self.args or not self.args["authorizer"]: 47 | self.args["authorizer"] = input("Authorizer: ") 48 | 49 | self.create_or_update_pass( 50 | password, self.args["description"], self.args["authorizer"] 51 | ) 52 | else: 53 | raise NotThePasswordOwnerError( 54 | self.args["identity"], owner, self.args["pwname"] 55 | ) 56 | # necessary for print statement 57 | yield "" 58 | 59 | def _generate_pass(self): 60 | #################################################################### 61 | """Generate a password based on a rules list that has been provided""" 62 | ####################################################################### 63 | try: 64 | regex_rule = self.args["rules_map"][self.args["rules"]] 65 | password = getone(regex_rule) 66 | # The following regex search verifies it finds a match from exrex 67 | return search(regex_rule, password).group(0) 68 | except TypeError as err: 69 | raise RulesMapError( 70 | "Poorly formatted Rules Map, please check it is in json format" 71 | ) from err 72 | except re_error as err: 73 | raise RulesMapError("Poorly formatted regex, or unsupported") from err 74 | 75 | def _validate_args(self): 76 | self.args["rules_map"] = parse_json_arguments(self.args, "rules_map") 77 | for argument in ["pwname", "keypath", "rules_map"]: 78 | if argument not in self.args or self.args[argument] is None: 79 | raise CliArgumentError(f"'{argument}' is a required argument") 80 | if ( 81 | self.args["rules"] not in self.args["rules_map"] 82 | or self.args["rules_map"][self.args["rules"]] == "" 83 | ): 84 | raise RulesMapError(f"No Rule set defined as {self.args['rules']}") 85 | -------------------------------------------------------------------------------- /libpkpass/commands/listrecipients.py: -------------------------------------------------------------------------------- 1 | """This Module allows for the listing of recipients""" 2 | from libpkpass import LOGGER 3 | from libpkpass.commands.command import Command 4 | from libpkpass.errors import CliArgumentError 5 | from libpkpass.models.cert import Cert 6 | from libpkpass.models.recipient import Recipient 7 | 8 | 9 | class Listrecipients(Command): 10 | #################################################################### 11 | """This class implements the cli functionality to list recipients""" 12 | #################################################################### 13 | name = "listrecipients" 14 | description = "List the recipients that pkpass knows about" 15 | selected_args = Command.selected_args + ["stdin", "filter"] 16 | 17 | def _run_command_execution(self): 18 | #################################################################### 19 | """Run function for class.""" 20 | #################################################################### 21 | LOGGER.info("Certificate store: %s", self.args["certpath"]) 22 | LOGGER.info("Key store: %s", self.args["keypath"]) 23 | LOGGER.info("CA Bundle file: %s", self.args["cabundle"]) 24 | LOGGER.info("Looking for Key Extension: %s", self.iddb.extensions["key"]) 25 | LOGGER.info( 26 | "Looking for Certificate Extension: %s", 27 | self.iddb.extensions["certificate"], 28 | ) 29 | LOGGER.info( 30 | "Loaded %s identities", len(self.iddb.session.query(Recipient).all()) 31 | ) 32 | 33 | if "filter" in self.args and self.args["filter"]: 34 | identities = self.iddb.session.query(Recipient).filter( 35 | Recipient.name.like(self.args["filter"].replace("*", "%")) 36 | ) 37 | else: 38 | identities = self.iddb.session.query(Recipient).all() 39 | for identity in identities: 40 | yield self._print_identity( 41 | identity, 42 | self.iddb.session.query(Cert) 43 | .filter(Cert.recipients.contains(identity)) 44 | .all(), 45 | ) 46 | 47 | def _print_identity(self, identity, certs): 48 | #################################################################### 49 | """Print off identity""" 50 | #################################################################### 51 | identity_list = [] 52 | identity_list.append(f"{self.color_print(identity.name, 'first_level')}:") 53 | identity_list.append(f"\t{self.color_print('certs', 'second_level')}:") 54 | for cert in certs: 55 | for info in [ 56 | "verified", 57 | "subject", 58 | "subjecthash", 59 | "issuer", 60 | "issuerhash", 61 | "fingerprint", 62 | "enddate", 63 | ]: 64 | identity_list.append( 65 | f"\t\t{self.color_print(info + ':', 'third_level')} {dict(cert)[info]}" 66 | ) 67 | identity_list.append("\n") 68 | return "\n".join(identity_list) 69 | 70 | def _validate_args(self): 71 | #################################################################### 72 | """Ensure arguments are appropriate for this command""" 73 | #################################################################### 74 | for argument in ["keypath"]: 75 | if argument not in self.args or self.args[argument] is None: 76 | raise CliArgumentError(f"'{argument}' is a required argument") 77 | 78 | def _validate_identities(self, _=None): 79 | #################################################################### 80 | """Ensure identities are appropriate for this command""" 81 | #################################################################### 82 | return 83 | -------------------------------------------------------------------------------- /test/test_crypto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This Module tests crypto functionality""" 3 | import unittest 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import sessionmaker 6 | from libpkpass.crypto import ( 7 | pk_encrypt_string, 8 | pk_decrypt_string, 9 | pk_sign_string, 10 | pk_verify_signature, 11 | get_cert_fingerprint, 12 | ) 13 | from libpkpass.identities import IdentityDB 14 | from libpkpass.models.recipient import Recipient 15 | from libpkpass.models.cert import Cert 16 | 17 | 18 | class TestBasicFunction(unittest.TestCase): 19 | """This class tests the crypto class""" 20 | 21 | def setUp(self): 22 | self.plaintext = "PLAINTEXT" 23 | 24 | self.certdir = "test/pki/intermediate/certs" 25 | self.keydir = "test/pki/intermediate/private" 26 | self.cabundle = "test/pki/intermediate/certs/ca-bundle" 27 | 28 | self.iddb = IdentityDB() 29 | db_path = "test/pki/intermediate/certs/rd.db" 30 | self.iddb.args = { 31 | "db": { 32 | "uri": f"sqlite+pysqlite:///{db_path}", 33 | "engine": create_engine(f"sqlite+pysqlite:///{db_path}"), 34 | }, 35 | } 36 | self.iddb.session = sessionmaker(bind=self.iddb.args["db"]["engine"])() 37 | self.iddb.load_certs_from_directory(self.certdir, self.cabundle) 38 | self.iddb.load_keys_from_directory(self.keydir) 39 | 40 | # Encrypt strings for all test identities and make sure they are different 41 | def test_encrypt_string(self): 42 | """Test encrypting a string""" 43 | results = [] 44 | for identity in self.iddb.session.query(Recipient).all(): 45 | cert = ( 46 | self.iddb.session.query(Cert) 47 | .filter(Cert.recipients.contains(identity)) 48 | .first() 49 | ) 50 | results.append(pk_encrypt_string(self.plaintext, cert.cert_bytes)) 51 | self.assertTrue(results[0] != results[1]) 52 | 53 | # Encrypt/Decrypt strings for all test identities and make sure we get back what we put in 54 | for identity in self.iddb.session.query(Recipient).all(): 55 | cert = ( 56 | self.iddb.session.query(Cert) 57 | .filter(Cert.recipients.contains(identity)) 58 | .first() 59 | ) 60 | (ciphertext, derived_key) = pk_encrypt_string( 61 | self.plaintext, cert.cert_bytes 62 | ) 63 | plaintext = pk_decrypt_string(ciphertext, derived_key, dict(identity), None) 64 | self.assertEqual(self.plaintext, plaintext) 65 | 66 | def test_verify_string(self): 67 | """verify string is correct""" 68 | results = [] 69 | for identity in self.iddb.session.query(Recipient).all(): 70 | results.append(pk_sign_string(self.plaintext, dict(identity), None)) 71 | self.assertTrue(results[0] != results[1]) 72 | 73 | for identity in self.iddb.session.query(Recipient).all(): 74 | signature = pk_sign_string(self.plaintext, dict(identity), None) 75 | cert = ( 76 | self.iddb.session.query(Cert) 77 | .filter(Cert.recipients.contains(identity)) 78 | .first() 79 | ) 80 | self.assertTrue(pk_verify_signature(self.plaintext, signature, [cert])) 81 | 82 | def test_cert_fingerprint(self): 83 | """Verify fingerprint is correct""" 84 | for identity in self.iddb.session.query(Recipient).all(): 85 | cert = ( 86 | self.iddb.session.query(Cert) 87 | .filter(Cert.recipients.contains(identity)) 88 | .first() 89 | ) 90 | fingerprint = get_cert_fingerprint(cert.cert_bytes) 91 | self.assertTrue(len(fingerprint.split(":")) == 20) 92 | 93 | 94 | if __name__ == "__main__": 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /test/test_show.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This module tests the show module""" 3 | import sys 4 | import io 5 | import unittest 6 | from unittest.mock import patch 7 | from libpkpass.commands.cli import Cli 8 | from libpkpass.errors import DecryptionError, CliArgumentError 9 | 10 | # from libpkpass.escrow import pk_recover_secret 11 | from .basetest.basetest import ERROR_MSGS, patch_args 12 | 13 | 14 | class ShowErrors(unittest.TestCase): 15 | """This class tests the show class""" 16 | 17 | def test_decryption(self): 18 | """Test successful decryption""" 19 | ret = True 20 | try: 21 | with patch_args( 22 | subparser_name="show", 23 | identity="r1", 24 | nopassphrase="true", 25 | all=None, 26 | pwname="test", 27 | ): 28 | "".join(Cli().run()) 29 | except DecryptionError: 30 | ret = False 31 | self.assertTrue(ret) 32 | 33 | def test_recipient_not_in_database(self): 34 | """test what happens when the recipient is not in the appropriate directory""" 35 | with self.assertRaises(CliArgumentError) as context: 36 | with patch_args( 37 | subparser_name="show", 38 | identity="bleh", 39 | nopassphrase="true", 40 | all=None, 41 | pwname="test", 42 | ): 43 | Cli().run() 44 | self.assertEqual(context.exception.msg, ERROR_MSGS["rep"]) 45 | 46 | def test_showall_decryption(self): 47 | """Test showing all passwords""" 48 | ret = True 49 | try: 50 | with patch_args( 51 | subparser_name="show", 52 | identity="r1", 53 | nopassphrase="true", 54 | all=True, 55 | pwname="*test*", 56 | ): 57 | Cli().run() 58 | except DecryptionError: 59 | ret = False 60 | self.assertTrue(ret) 61 | 62 | def test_show_nopass_error(self): 63 | """Test what happens when neither pwname or the all flag are supplied""" 64 | ret = False 65 | try: 66 | with patch_args( 67 | subparser_name="show", identity="r1", nopassphrase="true", all=None 68 | ): 69 | "".join(Cli().run()) 70 | except KeyError as error: 71 | if str(error) == "'pwname'": 72 | ret = str(error) 73 | self.assertEqual(ret, "'pwname'") 74 | 75 | def test_show_recovery(self): 76 | try: 77 | shares = [] 78 | with patch_args( 79 | pwname="test", 80 | subparser_name="show", 81 | identity="r2", 82 | nopassphrase=True, 83 | all=None, 84 | recovery="true", 85 | ): 86 | shares.append("".join(Cli().run()).split("test: ")[1]) 87 | print(shares) 88 | with patch_args( 89 | pwname="test", 90 | subparser_name="show", 91 | identity="r3", 92 | nopassphrase=True, 93 | all=None, 94 | recovery="true", 95 | ): 96 | shares.append("".join(Cli().run()).split("test: ")[1]) 97 | with patch_args( 98 | pwname="test", 99 | subparser_name="recover", 100 | identity="r3", 101 | nopassphrase=True, 102 | ): 103 | with unittest.mock.patch( 104 | "builtins.input", return_value=",".join(shares) 105 | ): 106 | passwd = "|".join(Cli().run()) 107 | except DecryptionError as err: 108 | raise err 109 | self.assertEqual(passwd.split("|")[1], "y") 110 | 111 | 112 | if __name__ == "__main__": 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /test/pki/generatepki.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | rm -rf ca intermediate 4 | 5 | for ca in 'ca' 'intermediate'; do 6 | mkdir -p $ca/certs $ca/crl $ca/newcerts $ca/private $ca/csr 7 | chmod 700 $ca/private 8 | touch $ca/index.txt 9 | touch $ca/index.txt.attr 10 | echo '1000' > $ca/serial 11 | done 12 | 13 | cat << EOF > ca/openssl.cnf 14 | [ca] 15 | default_ca = default 16 | 17 | [default] 18 | dir = ca 19 | certs = \$dir/certs 20 | new_certs_dir = \$dir/newcerts 21 | database = \$dir/index.txt 22 | serial = \$dir/serial 23 | RANDFILE = \$dir/private/.rand 24 | certificate = \$dir/certs/ca.cert 25 | private_key = \$dir/private/ca.key 26 | default_days = 605 27 | default_crl_days = 30 28 | default_md = sha256 29 | preserve = no 30 | policy = default_policy 31 | 32 | 33 | [default_policy] 34 | countryName = optional 35 | stateOrProvinceName = optional 36 | localityName = optional 37 | organizationName = optional 38 | organizationalUnitName = optional 39 | commonName = supplied 40 | emailAddress = optional 41 | 42 | 43 | [ req ] 44 | prompt = no 45 | default_bits = 4096 46 | distinguished_name = req_distinguished_name 47 | string_mask = utf8only 48 | default_md = sha256 49 | req_extensions = v3_ca 50 | 51 | [ req_distinguished_name ] 52 | C = UT 53 | ST = unittesting 54 | L = unittesting 55 | O = unittesting 56 | CN = unittesting 57 | 58 | [ v3_ca ] 59 | basicConstraints = critical, CA:true 60 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 61 | 62 | EOF 63 | intermediate(){ 64 | cat << EOF > intermediate/openssl.cnf 65 | [ca] 66 | default_ca = default 67 | 68 | [default] 69 | dir = ca 70 | certs = \$dir/certs 71 | new_certs_dir = \$dir/newcerts 72 | database = \$dir/index.txt 73 | serial = \$dir/serial 74 | RANDFILE = \$dir/private/.rand 75 | certificate = \$dir/certs/ca.cert 76 | private_key = \$dir/private/ca.key 77 | default_days = 605 78 | default_crl_days = 30 79 | default_md = sha256 80 | preserve = no 81 | policy = default_policy 82 | 83 | 84 | [default_policy] 85 | countryName = optional 86 | stateOrProvinceName = optional 87 | localityName = optional 88 | organizationName = optional 89 | organizationalUnitName = optional 90 | commonName = supplied 91 | emailAddress = optional 92 | 93 | 94 | [ req ] 95 | prompt = no 96 | default_bits = 4096 97 | distinguished_name = req_distinguished_name 98 | string_mask = utf8only 99 | default_md = sha256 100 | req_extensions = v3_ca 101 | 102 | [ req_distinguished_name ] 103 | C = UT 104 | ST = unittesting 105 | L = unittesting 106 | O = unittesting 107 | CN = $carecipient 108 | 109 | [ v3_ca ] 110 | basicConstraints = critical, CA:true 111 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 112 | 113 | EOF 114 | 115 | } 116 | export carecipient=unitt 117 | intermediate 118 | 119 | # Create Root CA 120 | openssl genrsa -out ca/private/ca.key 4096 121 | openssl req -config ca/openssl.cnf -key ca/private/ca.key -new -x509 -days 600 -out ca/certs/ca.cert 122 | chmod 400 ca/private/ca.key 123 | 124 | # Create Intermediate CA 125 | openssl genrsa -out intermediate/private/ca.key 4096 126 | chmod 400 ca/private/ca.key 127 | 128 | openssl req -config intermediate/openssl.cnf -key intermediate/private/ca.key -new -x509 -days 600 -out intermediate/certs/ca.cert 129 | cat ca/certs/ca.cert intermediate/certs/ca.cert > intermediate/certs/ca-bundle 130 | chmod 444 intermediate/certs/ca-bundle 131 | 132 | for recipient in 'r1' 'r2' 'r3' 'r4' 'r5'; do 133 | export carecipient="$recipient" 134 | intermediate 135 | openssl genrsa -out intermediate/private/${recipient}.key 4096 136 | chmod 400 intermediate/private/${recipient}.key 137 | 138 | openssl req -config intermediate/openssl.cnf -key intermediate/private/${recipient}.key -new -sha256 -out intermediate/csr/${recipient}.csr 139 | 140 | openssl ca -config ca/openssl.cnf -days 600 -in intermediate/csr/${recipient}.csr -out intermediate/certs/${recipient}.cert -batch 141 | chmod 444 intermediate/certs/${recipient}.cert 142 | 143 | done 144 | -------------------------------------------------------------------------------- /example_pkpassrc: -------------------------------------------------------------------------------- 1 | # I was not positive how exactly to group these arguments, the name groups make sense 2 | # to me at the time of writing this. 3 | 4 | ################################################################################### 5 | # Data Stores # 6 | ################################################################################### 7 | # cabundle is used to validate certificates 8 | cabundle: ~/path/to/cabundle 9 | 10 | # certpath and keypath together make up the recipients database 11 | certpath: ~/path/to/certificates 12 | keypath: ~/path/to/keys 13 | 14 | # pwstore is which the passwords will be saved 15 | pwstore: ~/passdb/passwords/base 16 | 17 | ################################################################################### 18 | # System Resources # 19 | ################################################################################### 20 | # card_slot is the pkcs library card to be used, if not specified it will be 0 21 | # card_slot: 0 22 | 23 | # identity is the active user of pkpass, defaults to system username 24 | identity: ${put_your_cert_name_here} 25 | 26 | # time is use in the clipboard functionality to decide how long to keep a password in the 27 | # clipboard 28 | time: 30 29 | 30 | ################################################################################### 31 | # Password Modifications # 32 | ################################################################################### 33 | # Escrow is a feature that allows someone to shard shares of passwords to a group of individuals 34 | # none of these shards by themselves will reveal the password, however, enough parts of the shards 35 | # together allow for recovery of said password. 36 | 37 | # escrow_users are the group of users that you shard passwords to 38 | # min_escrow is the minimum number of escrow_users needed to recover the password 39 | # escrow_users: bob, alice, jim 40 | # min_escrow: 2 41 | 42 | # Pkpass supports the ability to generate passwords based on a user-defined regular expression 43 | # rules_map is used in pkpass's generation functionality, if you ask for a generated password 44 | # it will by default look at 'default'; if you define in your rc file a value for 'rules' 45 | # the new default will be that key 46 | 47 | # rules_map: 48 | # default: "[^\\s]{20}" 49 | # different_pattern: "([a-z]|[A-Z]|[0-9]){15}" 50 | 51 | # rules: different_pattern 52 | 53 | ################################################################################### 54 | # Custom PkPass functionality # 55 | ################################################################################### 56 | # You can custom define groups in this file as well, is this example mitm 57 | # consists of users bob, alice, eve, chuck and you could distribute a password 58 | # to this group with `pkpass.py distribute -g mitm` 59 | # mitm: bob, alice, eve, chuck 60 | 61 | # pkpass supports allowing plugins to connect out to external services for certificates 62 | # connect defines an arbitrary dictionary that will be used for a given connector 63 | # The root name will need to match the installed python module The names of the keys 64 | # under 'ExampleConnector' should match what the connector library is expecting 65 | # there are not global defined keys to use. Consult the library you are importing for what the keys should be 66 | # base directory tells it where to store the certs locally 67 | 68 | # connect: 69 | # base_directory: /path/to/local/certs 70 | # ExampleConnector: 71 | # url_arg: "https://example.com" 72 | # id_arg: "567" 73 | # key_arg: "super_secret_key" 74 | # arbitrary_list_arg: 75 | # - "26994" 76 | # or an empty map 77 | # NoReqConnector: {} 78 | 79 | ################################################################################### 80 | # Theming # 81 | ################################################################################### 82 | # pkpass supports colorful output you can turn this off by setting the following to 83 | # false instead 84 | color: true 85 | # pkpass also supports the user changing the colors that will appear by providing a 86 | # color map using 256 colors 87 | 88 | # theme_map: 89 | # you can pass in the numerical representation 90 | # info: "1" 91 | # if you pass a wrong value, it will go with the default color 92 | # first_level: "257" 93 | # you can pass a hex value 94 | # second_level: "#0000FF" 95 | # or you could pass the color name 96 | # warning: "yellow" 97 | -------------------------------------------------------------------------------- /libpkpass/commands/update.py: -------------------------------------------------------------------------------- 1 | """This module allows for the updating of passwords""" 2 | import getpass 3 | from os import path 4 | from libpkpass import LOGGER 5 | from libpkpass.util import sort 6 | from libpkpass.password import PasswordEntry 7 | from libpkpass.commands.command import Command 8 | from libpkpass.errors import ( 9 | CliArgumentError, 10 | PasswordMismatchError, 11 | NotThePasswordOwnerError, 12 | PasswordIOError, 13 | BlankPasswordError, 14 | ) 15 | from libpkpass.models.recipient import Recipient 16 | 17 | 18 | class Update(Command): 19 | #################################################################### 20 | """This class implements the CLI functionality of updating existing passwords""" 21 | #################################################################### 22 | name = "update" 23 | description = "Change a password value and redistribute to recipients" 24 | selected_args = Command.selected_args + [ 25 | "pwname", 26 | "pwstore", 27 | "overwrite", 28 | "stdin", 29 | "keypath", 30 | "nopassphrase", 31 | "nosign", 32 | "card_slot", 33 | "escrow_users", 34 | "min_escrow", 35 | "noescrow", 36 | ] 37 | 38 | def _run_command_execution(self): 39 | #################################################################### 40 | """Run function for class.""" 41 | #################################################################### 42 | password = PasswordEntry() 43 | password.read_password_data( 44 | path.join(self.args["pwstore"], self.args["pwname"]) 45 | ) 46 | safe, owner = self.safety_check() 47 | if safe or self.args["overwrite"]: 48 | self.recipient_list = list(password["recipients"].keys()) 49 | self.recipient_list.append(str(self.args["identity"])) 50 | self.recipient_list = list(set(self.recipient_list)) 51 | yield from self._confirm_recipients() 52 | self._validate_identities(self.recipient_list) 53 | 54 | password1 = getpass.getpass("Enter updated password: ") 55 | if password1.strip() == "": 56 | raise BlankPasswordError 57 | password2 = getpass.getpass("Enter updated password again: ") 58 | if password1 != password2: 59 | raise PasswordMismatchError 60 | 61 | # This is due to a poor naming convention; we don't want to go through the 62 | # create or update, because we are not updating a single record in the yaml 63 | # we are burning it to the ground and redoing the entire thing 64 | self.create_pass( 65 | password1, 66 | password["metadata"]["description"], 67 | password["metadata"]["authorizer"], 68 | self.recipient_list, 69 | ) 70 | else: 71 | raise NotThePasswordOwnerError( 72 | self.args["identity"], owner, self.args["pwname"] 73 | ) 74 | 75 | def _confirm_recipients(self): 76 | not_in_db = [] 77 | in_db = [x.name for x in self.iddb.session.query(Recipient).all()] 78 | for recipient in self.recipient_list: 79 | if recipient not in in_db: 80 | not_in_db.append(recipient) 81 | if not_in_db: 82 | LOGGER.warning( 83 | "The following recipients are not in the db, removing %s", 84 | ", ".join(not_in_db), 85 | ) 86 | self.recipient_list = [x for x in self.recipient_list if x not in not_in_db] 87 | yield "The following users will receive the password: " 88 | yield ", ".join(sort(self.recipient_list)) 89 | correct = input("Are these correct? (y/N) ") 90 | if not correct or correct.lower()[0] == "n": 91 | self.recipient_list = input("Please enter a comma delimited list: ") 92 | self.recipient_list = list( 93 | {x.strip() for x in self.recipient_list.split(",")} 94 | ) 95 | yield from self._confirm_recipients() 96 | 97 | def _validate_args(self): 98 | for argument in ["pwname", "keypath"]: 99 | if argument not in self.args or self.args[argument] is None: 100 | raise CliArgumentError(f"'{argument}' is a required argument") 101 | 102 | def _pre_check(self): 103 | if path.exists(path.join(self.args["pwstore"], self.args["pwname"])): 104 | return True 105 | raise PasswordIOError( 106 | f"{path.join(self.args['pwstore'], self.args['pwname'])} does not exist" 107 | ) 108 | -------------------------------------------------------------------------------- /libpkpass/commands/rename.py: -------------------------------------------------------------------------------- 1 | """This module allows for the renaming of passwords""" 2 | from os import path, rename 3 | from sys import exit as sys_exit 4 | from libpkpass.commands.command import Command 5 | from libpkpass.password import PasswordEntry 6 | from libpkpass.errors import CliArgumentError, NotThePasswordOwnerError, PasswordIOError 7 | 8 | 9 | class Rename(Command): 10 | #################################################################### 11 | """This class implements the CLI functionality of renaming of passwords""" 12 | #################################################################### 13 | 14 | name = "rename" 15 | description = "Rename a password in the repository" 16 | selected_args = Command.selected_args + [ 17 | "pwname", 18 | "pwstore", 19 | "overwrite", 20 | "stdin", 21 | "nopassphrase", 22 | "keypath", 23 | "card_slot", 24 | "rename", 25 | ] 26 | 27 | def _run_command_execution(self): 28 | #################################################################### 29 | """Run function for class.""" 30 | #################################################################### 31 | safe, owner = self.safety_check() 32 | if safe and owner: 33 | orig_pass = self.args["pwname"] 34 | self.args["pwname"] = self.args["rename"] 35 | resafe, reowner = self.safety_check() 36 | self.args["pwname"] = orig_pass 37 | if resafe or self.args["overwrite"]: 38 | password = PasswordEntry() 39 | password.read_password_data( 40 | path.join(self.args["pwstore"], self.args["pwname"]) 41 | ) 42 | plaintext_pw = password.decrypt_entry( 43 | identity=self.iddb.id, 44 | passphrase=self.passphrase, 45 | card_slot=self.args["card_slot"], 46 | SCBackend=self.args["SCBackend"], 47 | PKCS11_module_path=self.args["PKCS11_module_path"], 48 | ) 49 | self._confirmation(plaintext_pw) 50 | else: 51 | raise NotThePasswordOwnerError( 52 | self.args["identity"], reowner, self.args["rename"] 53 | ) 54 | else: 55 | raise NotThePasswordOwnerError( 56 | self.args["identity"], owner, self.args["pwname"] 57 | ) 58 | # necessary for print statement 59 | yield "" 60 | 61 | def rename_pass(self): 62 | ################################################################## 63 | """This renames a password that the user has created""" 64 | ################################################################## 65 | oldpath = path.join(self.args["pwstore"], self.args["pwname"]) 66 | newpath = path.join(self.args["pwstore"], self.args["rename"]) 67 | try: 68 | rename(oldpath, newpath) 69 | password = PasswordEntry() 70 | password.read_password_data(newpath) 71 | password["metadata"]["name"] = self.args["rename"] 72 | password.write_password_data(newpath) 73 | 74 | except OSError as err: 75 | raise PasswordIOError( 76 | f"Password '{self.args['pwname']}' not found" 77 | ) from err 78 | 79 | def _confirmation(self, plaintext_pw): 80 | #################################################################### 81 | """Run confirmation for rename""" 82 | #################################################################### 83 | yes = {"yes", "y", "ye", ""} 84 | deny = {"no", "n"} 85 | confirmation = input( 86 | f"{self.args['pwname']}: {plaintext_pw}\nRename this password?(Defaults yes): " 87 | ) 88 | if confirmation.lower() in yes: 89 | self.rename_pass() 90 | elif confirmation.lower() in deny: 91 | sys_exit() 92 | else: 93 | print("please respond with yes or no") 94 | self._confirmation(plaintext_pw) 95 | 96 | def _validate_args(self): 97 | #################################################################### 98 | """Validate necessary arguments""" 99 | #################################################################### 100 | for argument in ["pwname", "keypath", "rename"]: 101 | if argument not in self.args or self.args[argument] is None: 102 | raise CliArgumentError(f"'{argument}' is a required argument") 103 | 104 | def _pre_check(self): 105 | if path.exists(path.join(self.args["pwstore"], self.args["pwname"])): 106 | return True 107 | raise PasswordIOError( 108 | f"{path.join(self.args['pwstore'], self.args['pwname'])} does not exist" 109 | ) 110 | -------------------------------------------------------------------------------- /libpkpass/commands/distribute.py: -------------------------------------------------------------------------------- 1 | """This Modules allows for distributing created passwords to other users""" 2 | from os import path 3 | from tqdm import tqdm 4 | from libpkpass import LOGGER 5 | from libpkpass.util import dictionary_filter, sort 6 | from libpkpass.commands.command import Command 7 | from libpkpass.password import PasswordEntry 8 | from libpkpass.errors import CliArgumentError 9 | from libpkpass.models.recipient import Recipient 10 | 11 | 12 | class Distribute(Command): 13 | #################################################################### 14 | """This Class implements the CLI functionality for ditribution""" 15 | #################################################################### 16 | name = "distribute" 17 | description = "Distribute existing password entry/ies to another entity [matching uses python fnmatch]" 18 | selected_args = Command.selected_args + [ 19 | "pwname", 20 | "pwstore", 21 | "users", 22 | "groups", 23 | "stdin", 24 | "min_escrow", 25 | "escrow_users", 26 | "keypath", 27 | "nopassphrase", 28 | "nosign", 29 | "card_slot", 30 | "noescrow", 31 | ] 32 | 33 | def __init__(self, *args, **kwargs): 34 | Command.__init__(self, *args, **kwargs) 35 | self.filtered_pdb = {} 36 | 37 | def _run_command_execution(self): 38 | #################################################################### 39 | """Run function for class.""" 40 | #################################################################### 41 | yield from self._confirm_pdb() 42 | self.recipient_list.append(str(self.args["identity"])) 43 | self.recipient_list = list(set(self.recipient_list)) 44 | yield from self._confirm_recipients() 45 | for dist_pass, _ in tqdm(self.filtered_pdb.items()): 46 | password = PasswordEntry() 47 | password.read_password_data(dist_pass) 48 | if self.args["identity"] in password.recipients.keys(): 49 | plaintext_pw = password.decrypt_entry( 50 | self.iddb.id, 51 | passphrase=self.passphrase, 52 | card_slot=self.args["card_slot"], 53 | SCBackend=self.args["SCBackend"], 54 | PKCS11_module_path=self.args["PKCS11_module_path"], 55 | ) 56 | password.add_recipients( 57 | secret=plaintext_pw, 58 | distributor=self.args["identity"], 59 | recipients=self.recipient_list, 60 | session=self.iddb.session, 61 | passphrase=self.passphrase, 62 | card_slot=self.args["card_slot"], 63 | escrow_users=self.args["escrow_users"], 64 | minimum=self.args["min_escrow"], 65 | SCBackend=self.args["SCBackend"], 66 | PKCS11_module_path=self.args["PKCS11_module_path"], 67 | ) 68 | 69 | password.write_password_data(dist_pass) 70 | 71 | def _validate_args(self): 72 | for argument in ["pwname", "keypath"]: 73 | if argument not in self.args or self.args[argument] is None: 74 | raise CliArgumentError(f"'{argument}' is a required argument") 75 | 76 | def _confirm_pdb(self): 77 | self.filtered_pdb = dictionary_filter( 78 | path.join(self.args["pwstore"], self.args["pwname"]), 79 | self.passworddb.pwdb, 80 | [self.args["identity"], "recipients"], 81 | ) 82 | yield "The following password files have matched:" 83 | yield "\n".join(self.filtered_pdb.keys()) 84 | correct = input("Are these correct? (y/N) ") 85 | if not correct or correct.lower()[0] == "n": 86 | self.args["pwname"] = input("Please try a new filter: ") 87 | yield from self._confirm_pdb() 88 | 89 | def _confirm_recipients(self): 90 | not_in_db = [] 91 | in_db = [x.name for x in self.iddb.session.query(Recipient).all()] 92 | for recipient in self.recipient_list: 93 | if recipient not in in_db: 94 | not_in_db.append(recipient) 95 | if not_in_db: 96 | LOGGER.warning( 97 | "The following recipients are not in the db, removing %s", 98 | ", ".join(not_in_db), 99 | ) 100 | self.recipient_list = [x for x in self.recipient_list if x not in not_in_db] 101 | yield "The following user(s) will be added: " 102 | yield ", ".join(sort(self.recipient_list)) 103 | correct = input("Are these correct? (y/N) ") 104 | if not correct or correct.lower()[0] == "n": 105 | self.recipient_list = input("Please enter a comma delimited list: ") 106 | self.recipient_list = list( 107 | {x.strip() for x in self.recipient_list.split(",")} 108 | ) 109 | yield from self._confirm_recipients() 110 | -------------------------------------------------------------------------------- /libpkpass/commands/fileimport.py: -------------------------------------------------------------------------------- 1 | """This Module allows for the import of passwords""" 2 | import getpass 3 | import os.path 4 | from yaml import safe_load, scanner 5 | from tqdm import tqdm 6 | from libpkpass import LOGGER 7 | from libpkpass.crypto import sk_decrypt_string 8 | from libpkpass.commands.command import Command 9 | from libpkpass.password import PasswordEntry 10 | from libpkpass.errors import CliArgumentError, LegacyImportFormatError, FileOpenError 11 | 12 | 13 | class Import(Command): 14 | #################################################################### 15 | """This Class implements the cli functionality of import""" 16 | #################################################################### 17 | name = "import" 18 | description = "Import passwords that you have saved to a file" 19 | selected_args = Command.selected_args + [ 20 | "pwfile", 21 | "stdin", 22 | "nopassphrase", 23 | "card_slot", 24 | "nocrypto", 25 | ] 26 | 27 | def _run_command_execution(self): 28 | #################################################################### 29 | """Run function for class.""" 30 | #################################################################### 31 | try: 32 | contents = "" 33 | with open(self.args["pwfile"], "r", encoding="ASCII") as fcontents: 34 | contents = fcontents.read().strip() 35 | if self.args["nocrypto"]: 36 | self._file_handler(contents) 37 | else: 38 | passwd = getpass.getpass("Please enter the password for the file: ") 39 | passwords = contents.split("\n") 40 | for password in tqdm(passwords): 41 | self._file_handler(sk_decrypt_string(password, passwd)) 42 | except IOError as err: 43 | raise FileOpenError( 44 | self.args["pwfile"], "No such file or directory" 45 | ) from err 46 | # necessary for print statement 47 | yield "" 48 | 49 | def _file_handler(self, string): 50 | #################################################################### 51 | """This function handles the contents of a file""" 52 | #################################################################### 53 | try: 54 | self._yaml_file(safe_load(string)) 55 | except (TypeError, scanner.ScannerError, KeyError): 56 | try: 57 | self._flat_file(string.strip().split("\n")) 58 | except TypeError as err: 59 | raise LegacyImportFormatError from err 60 | 61 | def _flat_file(self, passwords): 62 | #################################################################### 63 | """This function handles the simple key:value pair""" 64 | #################################################################### 65 | LOGGER.info( 66 | "Flat password file detected, using 'imported' as description \ 67 | you can manually change the description in the file if you would like" 68 | ) 69 | for password in tqdm(passwords): 70 | psplit = password.split(":") 71 | fname = psplit[0].strip() 72 | pvalue = psplit[1].strip() 73 | self.args["pwname"] = fname 74 | self.create_or_update_pass(pvalue, "imported", self.args["identity"]) 75 | 76 | def _yaml_file(self, password): 77 | #################################################################### 78 | """This function handles the yaml format of pkpass""" 79 | #################################################################### 80 | uid = self.iddb.id["name"] 81 | pwstore = self.args["pwstore"] 82 | 83 | self.args["pwname"] = password["metadata"]["name"] 84 | plaintext_str = password["recipients"][uid]["encrypted_secret"] 85 | full_path = os.path.join(pwstore, password["metadata"]["name"]) 86 | self.args["overwrite"] = True 87 | plist = list(password["recipients"]) 88 | if os.path.isfile(full_path): 89 | existing_password = PasswordEntry() 90 | existing_password.read_password_data(full_path) 91 | self.args["overwrite"] = False 92 | password["metadata"] = existing_password["metadata"] 93 | plist += list(existing_password["recipients"]) 94 | 95 | description = password["metadata"]["description"] 96 | authorizer = password["metadata"]["authorizer"] 97 | self.create_or_update_pass(plaintext_str, description, authorizer, plist) 98 | 99 | def _validate_args(self): 100 | #################################################################### 101 | """Ensure arguments are appropriate for this command""" 102 | #################################################################### 103 | for argument in ["pwfile", "keypath"]: 104 | if argument not in self.args or self.args[argument] is None: 105 | raise CliArgumentError(f"'{argument}' is a required argument") 106 | -------------------------------------------------------------------------------- /libpkpass/passworddb.py: -------------------------------------------------------------------------------- 1 | """This Module defines what a passworddb should look like""" 2 | from os import walk, path, makedirs 3 | from multiprocessing import Manager, cpu_count, Pool 4 | from fnmatch import fnmatch 5 | from pylibyaml import monkey_patch_pyyaml # pylint: disable=unused-import 6 | from yaml import safe_load, dump 7 | from libpkpass.password import PasswordEntry 8 | from libpkpass.errors import PasswordIOError 9 | 10 | 11 | class PasswordDB: 12 | #################################################################### 13 | """Password database object. Gets and retrieves password entries from places 14 | passwords are stored""" 15 | #################################################################### 16 | 17 | def __init__(self, mode="Filesystem"): 18 | self.mode = mode 19 | self.pwdb = {} 20 | self.ignore = "*requirements.txt" 21 | 22 | def __repr__(self): 23 | return f"{self.__class__}({self.__dict__})" 24 | 25 | def __str__(self): 26 | return f"{self.__dict__}" 27 | 28 | def __sizeof__(self): 29 | return len(self.pwdb) 30 | 31 | def load_password_data(self, password_id, pwdb=None): 32 | #################################################################### 33 | """Load and return password from wherever it may be stored""" 34 | #################################################################### 35 | if fnmatch(password_id, self.ignore): 36 | return None 37 | if password_id not in self.pwdb.keys() and self.mode == "Filesystem": 38 | self.pwdb[password_id] = self.read_password_data_from_file(password_id) 39 | if pwdb is not None: 40 | pwdb[password_id] = self.pwdb[password_id] 41 | return self.pwdb[password_id] 42 | 43 | def load_from_directory(self, pwstore): 44 | #################################################################### 45 | """Load all passwords from directory""" 46 | #################################################################### 47 | with Manager() as manager: 48 | pool = Pool(cpu_count()) 49 | pwdb = manager.dict() 50 | for fpath, _, files in walk(pwstore): 51 | pool.apply_async(self.parallel_loader, args=(files, fpath, pwdb)) 52 | pool.close() 53 | pool.join() 54 | self.pwdb = dict(pwdb) 55 | 56 | def parallel_loader(self, files, fpath, pwdb): 57 | #################################################################### 58 | """Function to allow multiprocessing to be utilize to read passwords""" 59 | #################################################################### 60 | for passwordname in files: 61 | passwordpath = path.join(fpath, passwordname) 62 | self.load_password_data(passwordpath, pwdb) 63 | 64 | def save_password_data(self, password_id, overwrite=False): 65 | #################################################################### 66 | """Store a password to wherever it may be stored""" 67 | #################################################################### 68 | if self.mode == "Filesystem": 69 | self.write_password_data_to_file( 70 | self.pwdb[password_id], password_id, overwrite 71 | ) 72 | 73 | def read_password_data_from_file(self, filename): 74 | #################################################################### 75 | """Open a password file, load passwords and read metadata""" 76 | #################################################################### 77 | try: 78 | with open(filename, "r", encoding="ASCII") as fname: 79 | password_data = safe_load(fname) 80 | password_entry = PasswordEntry() 81 | password_entry.metadata = password_data["metadata"] 82 | password_entry.recipients = password_data["recipients"] 83 | if "escrow" in password_data: 84 | password_entry.escrow = password_data["escrow"] 85 | return password_entry 86 | except (OSError, IOError, TypeError) as err: 87 | raise PasswordIOError( 88 | f"Error reading '{filename}' perhaps a path error for the db, or malformed file" 89 | ) from err 90 | 91 | def write_password_data_to_file(self, password_data, filename, overwrite=False): 92 | #################################################################### 93 | """Write password data and metadata to the appropriate password file""" 94 | #################################################################### 95 | try: 96 | if not path.isdir(path.dirname(filename)): 97 | makedirs(path.dirname(filename)) 98 | with open(filename, "w+", encoding="ASCII") as fname: 99 | passdata = { 100 | key: value for key, value in password_data.todict().items() if value 101 | } 102 | fname.write(dump(passdata, default_flow_style=False)) 103 | except (OSError, IOError) as err: 104 | raise PasswordIOError(f"Error creating '{filename}'") from err 105 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Configuration file for RTD""" 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'PkPass' 23 | author = u'Noah Ginsburg, Ryan Adamson' 24 | 25 | HERE = os.path.abspath(os.path.dirname(__file__)) 26 | THERE = os.path.join(HERE, "..", "..") 27 | 28 | VERSION = "2.2.7" 29 | 30 | # The short X.Y version 31 | version = VERSION 32 | # The full version, including alpha/beta/rc tags 33 | release = VERSION 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # If your documentation needs a minimal Sphinx version, state it here. 38 | # 39 | # needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 43 | # ones. 44 | extensions = [ 45 | 'sphinx.ext.autodoc', 46 | 'sphinx.ext.doctest', 47 | 'sphinx.ext.coverage', 48 | 'sphinx.ext.viewcode', 49 | 'sphinx.ext.githubpages', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path. 74 | exclude_patterns = [] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = None 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = 'sphinx_rtd_theme' 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | # html_theme_options = {} 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | 98 | # Custom sidebar templates, must be a dictionary that maps document names 99 | # to template names. 100 | # 101 | # The default sidebars (for documents that don't match any pattern) are 102 | # defined by theme itself. Builtin themes are using these templates by 103 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 104 | # 'searchbox.html']``. 105 | # 106 | # html_sidebars = {} 107 | 108 | 109 | # -- Options for HTMLHelp output --------------------------------------------- 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'PkPassdoc' 113 | 114 | 115 | # -- Options for LaTeX output ------------------------------------------------ 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'PkPass.tex', u'PkPass Documentation', 140 | u'Noah Ginsburg, Ryan Adamson', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'pkpass', u'PkPass Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ---------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'PkPass', u'PkPass Documentation', 161 | author, 'PkPass', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | # -- Options for Epub output ------------------------------------------------- 167 | 168 | # Bibliographic Dublin Core info. 169 | epub_title = project 170 | 171 | # The unique identifier of the text. This can be a ISBN number 172 | # or the project homepage. 173 | # 174 | # epub_identifier = '' 175 | 176 | # A unique identification for the text. 177 | # 178 | # epub_uid = '' 179 | 180 | # A list of files that should not be packed into the epub file. 181 | epub_exclude_files = ['search.html'] 182 | 183 | 184 | # -- Extension configuration ------------------------------------------------- 185 | -------------------------------------------------------------------------------- /docs/source/Configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Password Repository 5 | ------------------- 6 | Passwords are created on the file system, so any destination may be specified. For passwords that need to be distributed to other users, convention suggests putting these into a hierarchy with the root in 'passwords'. To make the repository as flat as possible, the top level will contain mostly groupings of passwords, with the next level containing the passwords themselves. 7 | Examples of groups may include "security-team", "database-users", "passwords/general", etc. It is up to each organization to determine the best hierarchy for storing passwords. The 'list' command and 'showall' commands will crawl the hierarchy starting at the root regardless of structure. 8 | 9 | You may distribute passwords to a specified group defined in your pkpassrc file. These groups may be arbitrary 10 | 11 | .. code-block:: bash 12 | 13 | databaseadmins: db1, db2,db3 14 | secadmins: admin1, admin2 , admin3 15 | groups: secadmins, databaseadmins 16 | 17 | you may also specify on the command line which groups to use: ``pkpass.py distribute password -g secadmins`` 18 | 19 | Cert Repository 20 | --------------- 21 | Certs are read into PkPass and are used in many of the processes. This can be presented to pkpass as a directory structure, repository, or 22 | by means of it's ``connector`` functionality. 23 | 24 | CA Bundle 25 | --------- 26 | The CA bundle is used to verify valid certs 27 | 28 | Arguments 29 | --------- 30 | The RC file (location ~/.pkpassrc, ~/.pkpassrc.yaml, or ~/.pkpassrc.yml) can take the majority of PkPass's arguments so that you do not need to pass them through. The only ones that should not be relied upon to work properly 31 | are arguments with 'store_true' or 'store_false' attributes. The following arguments should work in a pkpassrc file 32 | 33 | .. code-block:: bash 34 | 35 | cabundle 36 | card_slot 37 | certpath 38 | color 39 | connect 40 | escrow_users 41 | groups 42 | identity 43 | keypath 44 | min_escrow 45 | pwstore 46 | rules 47 | rules_map 48 | theme_map 49 | time 50 | users 51 | 52 | These along with user-defined groups should all work in an RC file. 53 | 54 | Special Treatment for Non-piv accounts/credentials 55 | -------------------------------------------------- 56 | There are some capabilities built into pkpass.py to manage passwords with rsa keys and x509 certificates without using smart card authentication. These 57 | keys still need to be signed by a CA in the CA bundle. 58 | Create a keypair: 59 | 60 | This will create an unsigned keypair. We really want it to create a certificate request in the future 61 | 62 | .. code-block:: bash 63 | 64 | openssl req -newkey rsa:4096 -keyout local.key -x509 -out local.cert 65 | 66 | As long as the private and public keys are in directories that pkpass can find, distribution to those identities works exactly the same. Keys must be named 'username.key'. For user foo, the private key must be named 'foo.key' and reside in the keypath directory. 67 | 68 | Behalf of functionality 69 | ----------------------- 70 | To utilize the functionality for showing a password on behalf of another user you need to create a password that is the private key of this user. Then when you issue a show command you specify the username with the `-b` flag 71 | 72 | Example: 73 | 74 | .. code-block:: bash 75 | 76 | pkpass show password_i_dont_have_direct_access_to -b rsa_user 77 | 78 | the argument `rsa_user` needs to be both the username and the password name for the password that store's this user's rsa key 79 | 80 | Populate other data stores 81 | -------------------------- 82 | Currently Pkpass can populate puppet-eyaml given appropriate configurations: 83 | 84 | It is suggested to have a `~/.eyaml/config.yaml` setup with `pkcs7_public_key:` defined at the highest level of that file. 85 | 86 | To completely configure this integration on the pkpass side please add values to your rc file that looks similar to the following 87 | 88 | .. code-block:: yaml 89 | 90 | populate: 91 | # puppet_eyaml is the definition for the `type` 92 | puppet_eyaml: 93 | # `bin` is the location of the binary for `eyaml` 94 | bin: /opt/puppetlabs/pdk/share/cache/ruby/2.5.0/bin/eyaml 95 | # `directory` is the directory of your puppet repo 96 | directory: ~/git/puppet 97 | passwords: 98 | # This level entry (`ops/password`) represents a pkpass password name 99 | ops/password: 100 | # This level entry (`data/team/security.yaml`) represents the rest of the file path for the heira file 101 | data/team/security.yaml: 102 | # The following list represents the keys that need to be replaced in the heira file 103 | - some::server::password 104 | - some:other::server 105 | 106 | 107 | To populate kubernetes you need a similar block 108 | Currently pkpass can only generate a single encrypted value per secret. It places the value stored in pkpass in the map where it's name is matched. 109 | 110 | in the following example you will see this, so for `testpass` pkpass will decrypt `testpass` and place the value of that password in `data/password` because in the configuration file the value of `data/password` is `testpass` 111 | 112 | Pkpass will then base64 encode all values in the `data` map and dump it as a yaml file in where `output` is defined, in this case `/tmp/secrets.yaml` 113 | 114 | .. code-block:: yaml 115 | 116 | populate: 117 | kubernetes: 118 | output: /tmp/secrets.yaml 119 | passwords: 120 | testpass: 121 | - apiVersion: v1 122 | type: Opaque 123 | metadata: 124 | name: test 125 | namespace: testing 126 | data: 127 | password: testpass 128 | username: someuser 129 | - apiVersion: v1 130 | type: Opaque 131 | metadata: 132 | name: test 133 | namespace: testing2 134 | data: 135 | password: testpass 136 | username: someuser 137 | 138 | 139 | It is not recommended to store the kubernetes output file anywhere, since kubernetes secrets are just base64 encoded, they are not secure! 140 | 141 | other data endpoints may be requested 142 | -------------------------------------------------------------------------------- /libpkpass/commands/verifyinstall.py: -------------------------------------------------------------------------------- 1 | """This is used to check the os requirements""" 2 | from os import path 3 | from platform import python_version 4 | from re import search 5 | from subprocess import check_output, CalledProcessError 6 | from shutil import which 7 | from libpkpass.commands.command import Command 8 | from libpkpass.errors import BadBackendError 9 | from libpkpass._version import get_versions 10 | 11 | class VerifyInstall(Command): 12 | #################################################################### 13 | """This class is used as a command object and parses information passed through 14 | the CLI to show passwords that have been distributed to users""" 15 | #################################################################### 16 | name = "verifyinstall" 17 | description = "verify required software is present" 18 | selected_args = Command.selected_args + [ 19 | "pwstore", 20 | "keypath", 21 | "noverify", 22 | "card_slot", 23 | "connect", 24 | ] 25 | 26 | def _run_command_execution(self): 27 | #################################################################### 28 | """Run function for class.""" 29 | #################################################################### 30 | yield from print_messages(check_required_software, "installed software check", SCBackend=self.args["SCBackend"]) 31 | yield from print_messages( 32 | check_passdb, 33 | "passdb check", 34 | cabundle=path.realpath(self.args["cabundle"]), 35 | pwstore=path.realpath(self.args["pwstore"]), 36 | keypath=path.realpath(self.args["keypath"]) 37 | if self.args["keypath"] 38 | else None, 39 | certpath=path.realpath(self.args["certpath"]) 40 | if self.args["certpath"] 41 | else None, 42 | connect=self.args["connect"], 43 | ) 44 | 45 | def _validate_args(self): 46 | pass 47 | 48 | def get_backend(SCBackend): 49 | if SCBackend == "opensc": 50 | return(SCBackend) 51 | elif SCBackend == "yubi": 52 | return(SCBackend) 53 | raise BadBackendError(SCBackend) 54 | 55 | 56 | 57 | def check_exists(name): 58 | #################################################################### 59 | """Check whether a program exists in path""" 60 | #################################################################### 61 | path=which(name) 62 | if path is not None: 63 | return path 64 | 65 | def check_exists_brew(name): 66 | whichcmd="brew --prefix --installed "+ name + " 2> /dev/null" 67 | try: 68 | path=check_output(whichcmd, shell=True).decode('utf-8').strip() 69 | #print(path) 70 | except CalledProcessError: 71 | path=None 72 | return path 73 | 74 | def print_messages(func, msg, **kwargs): 75 | yield f"Starting {msg}" 76 | yield func(**kwargs) 77 | 78 | def check_brew(): 79 | if which("brew"): 80 | return True 81 | else: 82 | return False 83 | #may need an exception here to catch which not existing 84 | 85 | def check_required_software(**kwargs): 86 | print("Using Python Version "+python_version()) 87 | print("Using Pkpass Version: "+get_versions()["version"]) 88 | SCBackend=get_backend(kwargs['SCBackend']) 89 | print("Using SCBackend: "+SCBackend) 90 | if SCBackend=="yubi": 91 | required_tools = { 92 | "ssl (openssl or libressl)": ["openssl", "libressl"], 93 | "yubico-piv-tool": ["yubico-piv-tool", "libp11"], 94 | } 95 | elif SCBackend=="opensc": 96 | required_tools = { 97 | "pkcs15-tool (available via opensc)": ["pkcs15-tool", "opensc"], 98 | "ssl (openssl or libressl)": ["openssl", "libressl"], 99 | } 100 | not_found = [] 101 | found = [] 102 | paths = [] 103 | brew=check_brew() 104 | for key, value in required_tools.items(): 105 | found_tool = False 106 | for tool in value: 107 | if brew: 108 | brewexists=check_exists_brew(tool) 109 | if brewexists: 110 | found.append(tool) 111 | found_tool = True 112 | print(tool+" is installed with brew") 113 | paths.append(brewexists) 114 | else: 115 | exists=check_exists(tool) 116 | if exists: 117 | found.append(tool) 118 | found_tool = True 119 | print(tool+" is installed") 120 | paths.append(exists) 121 | if not found_tool: 122 | not_found.append(key) 123 | matches = dict(zip(found,paths)) 124 | if not_found: 125 | return "The following packages were not found: \n\t%s" % "\n\t".join(not_found) 126 | if brew and SCBackend=="yubi": 127 | check_links(matches) 128 | return "Successful installed software check" 129 | 130 | def check_links(software): 131 | software['local']="/usr/local" 132 | for package,path in software.items(): 133 | pathlib=path+"/lib" 134 | needspkcs=["openssl","libp11","local"] 135 | needslibykcs=["yubico-piv-tool","local"] 136 | checkdir=check_output(["ls","-l",pathlib]).decode('utf-8').strip() 137 | if search("engines-3", checkdir): 138 | if search("pkcs11.dylib", check_output(["ls","-l",pathlib+"/engines-3"]).decode('utf-8').strip()): 139 | print("pkcs11.dylib exists in "+pathlib+"/engines-3") 140 | elif package in needspkcs: 141 | print("Required packages are installed, however no file was detected at "+pathlib+"/engines-3/pkcs11.dylib . A link may be needed.") 142 | if search("libykcs11.dylib", checkdir): 143 | print("libykcs11.dylib exists in "+pathlib) 144 | elif package in needslibykcs: 145 | print("Required packages are installed, however no file was detected at "+pathlib+"libykcs11.dylib . A link may be needed.") 146 | 147 | 148 | 149 | def check_passdb(cabundle, pwstore, certpath=None, keypath=None, connect=None): 150 | ret_msg = [] 151 | if not path.isfile(cabundle): 152 | ret_msg.append("Cabundle is not a file") 153 | if not path.isdir(pwstore): 154 | ret_msg.append("pwstore is not a directory") 155 | if connect and certpath: 156 | ret_msg.append("certpath or keypath is defined while using a connector") 157 | if certpath and not path.isdir(certpath): 158 | ret_msg.append(f"certpath is not a directory {certpath}") 159 | if keypath and not path.isdir(keypath): 160 | ret_msg.append(f"Keypath is not a directory: {keypath}") 161 | ret_msg.append("Completed passdb check") 162 | return "\n".join(ret_msg) 163 | -------------------------------------------------------------------------------- /libpkpass/identities.py: -------------------------------------------------------------------------------- 1 | """This Module handles the identitydb object""" 2 | from os import path, listdir 3 | from datetime import datetime 4 | from pem import parse_file 5 | from sqlalchemy.orm import sessionmaker 6 | from tqdm import tqdm 7 | from libpkpass.crypto import ( 8 | pk_verify_chain, 9 | get_cert_fingerprint, 10 | get_cert_subject, 11 | get_cert_issuer, 12 | get_cert_enddate, 13 | get_cert_issuerhash, 14 | get_cert_subjecthash, 15 | ) 16 | from libpkpass.errors import FileOpenError, CliArgumentError 17 | from libpkpass.models.recipient import Recipient 18 | from libpkpass.models.cert import Cert 19 | from libpkpass.util import create_or_update 20 | 21 | 22 | class IdentityDB: 23 | ########################################################################## 24 | """User database class. Contains information about the identities of and 25 | things pertinent to recipients and their groups and keys.""" 26 | ########################################################################## 27 | 28 | def __init__(self): 29 | self.extensions = {"certificate": [".cert", ".crt"], "key": ".key"} 30 | self.cabundle = "" 31 | self.args = {} 32 | self.session = None 33 | self.id = None 34 | 35 | def __repr__(self): 36 | return f"{self.__class__}({self.__dict__})" 37 | 38 | def __str__(self): 39 | return f"{self.__dict__}" 40 | 41 | def _load_certs_from_external(self, connection_map, usedb=True): 42 | ####################################################################### 43 | """Load certificates from external via use of plugin""" 44 | ####################################################################### 45 | if "base_directory" in connection_map and connection_map["base_directory"]: 46 | del connection_map["base_directory"] 47 | for key, value in connection_map.items(): 48 | connector = getattr(__import__(key.lower(), fromlist=[key]), key)(value) 49 | if usedb: 50 | with open(self.args["db"]["path"], "wb") as db_path: 51 | db_path.write(connector.get_db()) 52 | else: 53 | certs = connector.list_certificates() 54 | print("Loading certs into database") 55 | for name, certlist in tqdm(certs.items()): 56 | self.load_db(name, certlist) 57 | break 58 | 59 | def _load_from_directory(self, fpath, filetype): 60 | ####################################################################### 61 | """Helper function to read in (keys|certs) and store them correctly""" 62 | ####################################################################### 63 | try: 64 | print("Loading certs into database") 65 | for fname in tqdm(listdir(fpath)): 66 | if fname.endswith(tuple(self.extensions[filetype])): 67 | uid = fname.split(".")[0] 68 | filepath = path.join(fpath, fname) 69 | if filetype == "certificate": 70 | self.load_db( 71 | uid, certlist=[x.as_text() for x in parse_file(filepath)] 72 | ) 73 | elif filetype == "key": 74 | session = sessionmaker(bind=self.args["db"]["engine"])() 75 | create_or_update( 76 | session, 77 | Recipient, 78 | unique_identifiers=["name"], 79 | **{ 80 | "name": uid, 81 | "key": filepath, 82 | }, 83 | ) 84 | session.commit() 85 | except OSError as error: 86 | raise FileOpenError(fpath, str(error.strerror)) from error 87 | 88 | def load_certs_from_directory( 89 | self, certpath, connectmap=None, nocache=False, usedb=True 90 | ): 91 | ####################################################################### 92 | """Read in all x509 certificates from directory and name them as found""" 93 | ####################################################################### 94 | session = sessionmaker(bind=self.args["db"]["engine"])() 95 | if not session.query(Recipient).first() or nocache: 96 | if connectmap: 97 | self._load_certs_from_external(connectmap, usedb=usedb) 98 | if certpath: 99 | self._load_from_directory(certpath, "certificate") 100 | 101 | def load_db(self, identity, certlist=None): 102 | ####################################################################### 103 | """Read in all rsa keys from directory and name them as found""" 104 | ####################################################################### 105 | try: 106 | session = sessionmaker(bind=self.args["db"]["engine"])() 107 | recipient = create_or_update( 108 | session, Recipient, dont_update=["key"], **{"name": identity} 109 | ) 110 | for cert in certlist: 111 | cert = cert.encode() 112 | cert_dict = {} 113 | cert_dict["cert_bytes"] = cert 114 | cert_dict["verified"] = pk_verify_chain(cert, self.cabundle) 115 | cert_dict["fingerprint"] = get_cert_fingerprint(cert) 116 | cert_dict["subject"] = get_cert_subject(cert) 117 | cert_dict["issuer"] = get_cert_issuer(cert) 118 | cert_dict["enddate"] = datetime.strptime( 119 | get_cert_enddate(cert), "%b %d %H:%M:%S %Y %Z" 120 | ) 121 | cert_dict["issuerhash"] = get_cert_issuerhash(cert) 122 | cert_dict["subjecthash"] = get_cert_subjecthash(cert) 123 | cert = create_or_update( 124 | session, Cert, unique_identifiers=["fingerprint"], **cert_dict 125 | ) 126 | if cert not in recipient.certs: 127 | recipient.certs.append(cert) 128 | session.commit() 129 | except KeyError as err: 130 | raise CliArgumentError( 131 | f"Error: Recipient '{identity}' is not in the recipient database" 132 | ) from err 133 | 134 | def verify_identity(self, identity): 135 | session = sessionmaker(bind=self.args["db"]["engine"])() 136 | recipient = session.query(Recipient).filter(Recipient.name == identity).first() 137 | if not recipient: 138 | return False 139 | for cert in ( 140 | session.query(Cert).filter(Cert.recipients.contains(recipient)).all() 141 | ): 142 | if not cert.verified: 143 | return False 144 | return True 145 | 146 | def load_keys_from_directory(self, fpath): 147 | ####################################################################### 148 | """Read in all rsa keys from directory and name them as found""" 149 | ####################################################################### 150 | if path.isdir(fpath): 151 | self._load_from_directory(fpath, "key") 152 | # We used to print a warning on else here, but it seems unnecessary; 153 | # if a user is using PIVs/Smartcards, this warning could just annoy 154 | # them needlessly 155 | -------------------------------------------------------------------------------- /libpkpass/commands/populate.py: -------------------------------------------------------------------------------- 1 | """This module allows for the population of external password stores""" 2 | from os import path, remove 3 | from re import finditer 4 | from base64 import standard_b64encode 5 | from ruamel.yaml import YAML 6 | from yaml import dump 7 | from libpkpass import LOGGER 8 | from libpkpass.commands.show import Show 9 | from libpkpass.crypto import puppet_password 10 | from libpkpass.password import PasswordEntry 11 | from libpkpass.errors import CliArgumentError 12 | from libpkpass.models.recipient import Recipient 13 | 14 | 15 | class Populate(Show): 16 | #################################################################### 17 | """This class implements the CLI functionality of password store integration""" 18 | #################################################################### 19 | 20 | name = "populate" 21 | description = ( 22 | "Populate external resource with password, currently supports: puppet_eyaml" 23 | ) 24 | selected_args = [ 25 | "pwname", 26 | "cabundle", 27 | "card_slot", 28 | "nopassphrase", 29 | "all", 30 | "certpath", 31 | "color", 32 | "identity", 33 | "keypath", 34 | "nocache", 35 | "noverify", 36 | "pwstore", 37 | "quiet", 38 | "theme_map", 39 | "type", 40 | "value", 41 | "verbosity", 42 | ] 43 | 44 | def _run_command_execution(self): 45 | #################################################################### 46 | """Run function for class.""" 47 | #################################################################### 48 | # currently only support puppet 49 | pop_type = self.args["type"] 50 | if pop_type not in ["puppet_eyaml", "kubernetes"]: 51 | raise CliArgumentError(f"'{pop_type}' is an unsupported population type") 52 | 53 | if pop_type == "kubernetes": 54 | yield from self._handle_kubernetes() 55 | elif self.args["all"]: 56 | for password in self.args["populate"][pop_type]["passwords"].keys(): 57 | pwentry = PasswordEntry() 58 | self._decrypt_wrapper(self.args["pwstore"], pwentry, password, pop_type) 59 | 60 | elif ( 61 | self.args["pwname"] 62 | not in self.args["populate"][pop_type]["passwords"].keys() 63 | ): 64 | raise CliArgumentError( 65 | f"'{self.args['pwname']}' doesn't have a mapping in {pop_type}" 66 | ) 67 | else: 68 | password = PasswordEntry() 69 | yield from self._decrypt_wrapper( 70 | self.args["pwstore"], password, self.args["pwname"], pop_type 71 | ) 72 | 73 | def _handle_kubernetes(self): 74 | #################################################################### 75 | """This function creates a file containing kube secrets""" 76 | #################################################################### 77 | k_conf = self.args["populate"]["kubernetes"] 78 | plaintext = {} 79 | for password in k_conf["needed"]: 80 | pwentry = PasswordEntry() 81 | pwentry.read_password_data(path.join(self.args["pwstore"], password)) 82 | plaintext_pw = self._decrypt_password_entry(pwentry) 83 | plaintext[password] = plaintext_pw 84 | if "output" in k_conf: 85 | if self.args["pwstore"] in k_conf["output"]: 86 | raise CliArgumentError( 87 | "Kubernetes output file should not exist in password store" 88 | ) 89 | if path.isfile(k_conf["output"]): 90 | remove(k_conf["output"]) 91 | for args in k_conf["passwords"]: 92 | data = self._kubernetes_match_loop(args, plaintext) 93 | kube_map = { 94 | "kind": "Secret", 95 | "apiVersion": args["apiVersion"], 96 | "metadata": args["metadata"], 97 | "data": data, 98 | "type": args["type"], 99 | } 100 | if self.args["value"] or "output" not in k_conf: 101 | yield f"---\n{dump(kube_map)}" 102 | else: 103 | with open(k_conf["output"], "a", encoding="ASCII") as fname: 104 | print(f"---\n{dump(kube_map)}", file=fname) 105 | 106 | def _kubernetes_match_loop(self, args, plaintext): 107 | #################################################################### 108 | """This matches ${example} inside the arg defs and generates a 109 | dictionary of b64 encoded values with the actual password values 110 | placed instead of ${}""" 111 | #################################################################### 112 | data = {} 113 | for key, value in args["data"].items(): 114 | new_pw = str(value) 115 | match_iter = finditer(r"\$\{([^${]*)\}", new_pw) 116 | if not match_iter: 117 | data[key] = standard_b64encode(new_pw.encode("ASCII")).decode() 118 | else: 119 | for match_values in match_iter: 120 | match_value = match_values.group(1) 121 | new_pw = new_pw.replace( 122 | f"%{{{match_value}}}", plaintext[match_value] 123 | ) 124 | data[key] = standard_b64encode("".join(new_pw).encode("ASCII")).decode() 125 | return data 126 | 127 | def _handle_puppet(self, plaintext_pw, pwname): 128 | directory = path.expanduser(self.args["populate"]["puppet_eyaml"]["directory"]) 129 | if not path.isdir(directory): 130 | raise CliArgumentError(f"'{directory}' is not a directory") 131 | puppet_bin = path.expanduser(self.args["populate"]["puppet_eyaml"]["bin"]) 132 | if not path.isfile(puppet_bin): 133 | raise CliArgumentError(f"'{puppet_bin}' is not a file") 134 | pkcs7_pass = puppet_password(puppet_bin, plaintext_pw) 135 | if self.args["value"]: 136 | yield pkcs7_pass 137 | else: 138 | for hiera_file, names in self.args["populate"]["puppet_eyaml"]["passwords"][ 139 | pwname 140 | ].items(): 141 | with open( 142 | path.join(directory, hiera_file), "r", encoding="ASCII" 143 | ) as data_file: 144 | yield f"Updating: {hiera_file}" 145 | yaml = YAML() 146 | yaml.indent(mapping=2, sequence=4, offset=2) 147 | yaml.preserve_quotes = True 148 | hiera_yaml = yaml.load(data_file.read()) 149 | for name in names: 150 | yield f"Updating: {name}" 151 | hiera_yaml[name] = pkcs7_pass 152 | with open( 153 | path.join(directory, hiera_file), "w", encoding="ASCII" 154 | ) as data_file: 155 | yaml.dump(hiera_yaml, data_file) 156 | 157 | def _validate_args(self): 158 | for argument in ["keypath", "type"]: 159 | if argument not in self.args or self.args[argument] is None: 160 | raise CliArgumentError(f"'{argument}' is a required argument") 161 | 162 | def _decrypt_wrapper(self, directory, password, pwname, pop_type): 163 | #################################################################### 164 | """Decide whether to decrypt normally or for escrow""" 165 | #################################################################### 166 | password.read_password_data(path.join(directory, pwname)) 167 | plaintext_pw = self._decrypt_password_entry(password) 168 | if pop_type == "puppet_eyaml": 169 | yield from self._handle_puppet(plaintext_pw, pwname) 170 | 171 | def _decrypt_password_entry(self, password): 172 | #################################################################### 173 | """This decrypts a given password entry""" 174 | #################################################################### 175 | plaintext_pw = password.decrypt_entry( 176 | identity=self.iddb.id, 177 | passphrase=self.passphrase, 178 | card_slot=self.args["card_slot"], 179 | SCBackend=self.args["SCBackend"], 180 | PKCS11_module_path=self.args["PKCS11_module_path"], 181 | ) 182 | distributor = password.recipients[self.iddb.id["name"]]["distributor"] 183 | if not self.args["noverify"]: 184 | result = password.verify_entry( 185 | self.iddb.id["name"], 186 | self.iddb, 187 | distributor, 188 | self.iddb.session.query(Recipient) 189 | .filter(Recipient.name == distributor) 190 | .first() 191 | .certs, 192 | ) 193 | if not result["sigOK"]: 194 | LOGGER.warning( 195 | "Could not verify that %s correctly signed your password entry.", 196 | result["distributor"], 197 | ) 198 | if not result["certOK"]: 199 | LOGGER.warning( 200 | "Could not verify the certificate authenticity of user '%s'.", 201 | result["distributor"], 202 | ) 203 | return plaintext_pw 204 | -------------------------------------------------------------------------------- /libpkpass/commands/show.py: -------------------------------------------------------------------------------- 1 | """This module is used to process the decryption of keys""" 2 | from os import path, unlink, walk 3 | from fnmatch import fnmatch 4 | from tempfile import gettempdir 5 | from libpkpass import LOGGER 6 | from libpkpass.commands.command import Command 7 | from libpkpass.password import PasswordEntry 8 | from libpkpass.errors import ( 9 | PasswordIOError, 10 | CliArgumentError, 11 | NotARecipientError, 12 | DecryptionError, 13 | ) 14 | from libpkpass.models.recipient import Recipient 15 | 16 | 17 | class Show(Command): 18 | #################################################################### 19 | """This class is used as a command object and parses information passed through 20 | the CLI to show passwords that have been distributed to users""" 21 | #################################################################### 22 | name = "show" 23 | description = "Display a password" 24 | selected_args = Command.selected_args + [ 25 | "all", 26 | "behalf", 27 | "card_slot", 28 | "ignore_decrypt", 29 | "keypath", 30 | "nopassphrase", 31 | "noverify", 32 | "pwname", 33 | "pwstore", 34 | "recovery", 35 | "stdin", 36 | ] 37 | 38 | def _run_command_execution(self): 39 | #################################################################### 40 | """Run function for class.""" 41 | #################################################################### 42 | password = PasswordEntry() 43 | if "behalf" in self.args and self.args["behalf"]: 44 | yield from self._behalf_prep(password) 45 | else: 46 | yield from self._show_wrapper(password) 47 | 48 | def _show_wrapper(self, password): 49 | #################################################################### 50 | """Wrapper for show to allow for on behalf of""" 51 | #################################################################### 52 | if self.args["all"]: 53 | try: 54 | yield from self._walk_dir(password) 55 | except DecryptionError as err: 56 | raise err 57 | elif self.args["pwname"] is None: 58 | raise PasswordIOError("No password supplied") 59 | else: 60 | yield from self._decrypt_wrapper( 61 | self.args["pwstore"], password, self.args["pwname"] 62 | ) 63 | 64 | def _behalf_prep(self, password): 65 | #################################################################### 66 | """Create necessary temporary file for rsa key""" 67 | #################################################################### 68 | password.read_password_data( 69 | path.join(self.args["pwstore"], self.args["behalf"]) 70 | ) 71 | # allows the password to be stored outside the root of the password directory 72 | self.args["behalf"] = self.args["behalf"].split("/")[-1] 73 | temp_key = path.join(gettempdir(), f'{self.args["behalf"]}.key') 74 | plaintext_pw = password.decrypt_entry( 75 | identity=self.iddb.id, 76 | passphrase=self.passphrase, 77 | card_slot=self.args["card_slot"], 78 | SCBackend=self.args["SCBackend"], 79 | PKCS11_module_path=self.args["PKCS11_module_path"], 80 | ) 81 | with open(temp_key, "w", encoding="ASCII") as fname: 82 | fname.write( 83 | "%s\n%s\n%s" 84 | % ( 85 | "-----BEGIN RSA PRIVATE KEY-----", 86 | plaintext_pw.replace("-----BEGIN RSA PRIVATE KEY-----", "") 87 | .replace(" -----END RSA PRIVATE KEY----", "") 88 | .replace(" ", "\n") 89 | .strip(), 90 | "-----END RSA PRIVATE KEY-----", 91 | ) 92 | ) 93 | self.args["identity"] = self.args["behalf"] 94 | self.args["key_path"] = temp_key 95 | yield from self._show_wrapper(password) 96 | unlink(temp_key) 97 | 98 | def _walk_dir(self, password): 99 | #################################################################### 100 | """Walk our directory searching for passwords""" 101 | #################################################################### 102 | # walk returns root, dirs, and files we just need files 103 | for root, _, pwnames in walk(self.args['pwstore']): 104 | trim_root = root.replace(self.args["pwstore"], "").lstrip("/") 105 | for pwname in pwnames: 106 | if self.args["pwname"] is None or fnmatch( 107 | path.join(trim_root, pwname), self.args["pwname"] 108 | ): 109 | try: 110 | yield from self._decrypt_wrapper(root, password, pwname) 111 | except DecryptionError as err: 112 | if self.args['ignore_decrypt']: 113 | LOGGER.err(err) 114 | continue 115 | raise 116 | except (NotARecipientError, TypeError): 117 | continue 118 | 119 | def _handle_escrow_show(self, password): 120 | #################################################################### 121 | """This populates the user's escrow as passwords""" 122 | #################################################################### 123 | if password.escrow: 124 | if self.iddb.id["name"] in password.escrow["recipients"].keys(): 125 | return password.escrow["recipients"][self.iddb.id["name"]] 126 | return None 127 | 128 | def _decrypt_wrapper(self, directory, password, pwname): 129 | #################################################################### 130 | """Decide whether to decrypt normally or for escrow""" 131 | #################################################################### 132 | if directory and password and pwname: 133 | password.read_password_data(path.join(directory, pwname)) 134 | try: 135 | if self.args["recovery"]: 136 | myescrow = self._handle_escrow_show(password) 137 | if myescrow: 138 | distributor = myescrow["distributor"] 139 | password["recipients"][self.iddb.id["name"]] = myescrow 140 | yield self._decrypt_password_entry(password, distributor) 141 | else: 142 | distributor = password.recipients[self.iddb.id["name"]][ 143 | "distributor" 144 | ] 145 | yield self._decrypt_password_entry(password, distributor) 146 | except KeyError as err: 147 | raise NotARecipientError( 148 | f"Identity '{self.iddb.id['name']}' is not on the recipient list for password '{pwname}'" 149 | ) from err 150 | 151 | def _decrypt_password_entry(self, password, distributor): 152 | #################################################################### 153 | """This decrypts a given password entry""" 154 | #################################################################### 155 | plaintext_pw = password.decrypt_entry( 156 | identity=self.iddb.id, 157 | passphrase=self.passphrase, 158 | card_slot=self.args["card_slot"], 159 | SCBackend=self.args["SCBackend"], 160 | PKCS11_module_path=self.args["PKCS11_module_path"], 161 | ) 162 | dist_obj = ( 163 | self.iddb.session.query(Recipient) 164 | .filter(Recipient.name == distributor) 165 | .first() 166 | ) 167 | if (dist_obj and dist_obj.certs) and not self.args["noverify"]: 168 | result = password.verify_entry( 169 | self.iddb.id["name"], self.iddb, distributor, dist_obj.certs 170 | ) 171 | if not result["sigOK"]: 172 | LOGGER.warning( 173 | "Could not verify that %s correctly signed your password entry.", 174 | result["distributor"], 175 | ) 176 | if not result["certOK"]: 177 | LOGGER.warning( 178 | "Could not verify the certificate authenticity of user '%s'.", 179 | result["distributor"], 180 | ) 181 | 182 | return f"{self.color_print(password.metadata['name'], 'first_level')}: {plaintext_pw}" 183 | 184 | def _validate_args(self): 185 | for argument in ["keypath"]: 186 | if argument not in self.args or self.args[argument] is None: 187 | raise CliArgumentError(f"'{argument}' is a required argument") 188 | 189 | def _pre_check(self): 190 | """Ensure our password exists before asking for authentication""" 191 | for root, _, pwnames in walk(self.args['pwstore']): 192 | trim_root = root.replace(self.args["pwstore"], "").lstrip("/") 193 | for pwname in pwnames: 194 | if self.args["pwname"] is None or fnmatch( 195 | path.join(trim_root, pwname), self.args["pwname"] 196 | ): 197 | return True 198 | raise PasswordIOError( 199 | f"{path.join(self.args['pwstore'], self.args['pwname'])} does not exist" 200 | ) 201 | -------------------------------------------------------------------------------- /libpkpass/commands/arguments.py: -------------------------------------------------------------------------------- 1 | """This Module defines the arguments that argparse will accept from the CLI""" 2 | from json import loads 3 | 4 | ARGUMENTS = { 5 | ############################################################################ 6 | # Data structure containing all arguments a user could pass into pkpass 7 | # This helps us de-dupe our work and keep args consistent between 8 | # subcommands. We use this with *args and **kwargs constructs 9 | ############################################################################ 10 | "all": { 11 | "args": ["-a", "--all"], 12 | "kwargs": { 13 | "help": "Show all available password to the given user, if a pwname is supplied filtering will be done case-insensitivey based on the filename", 14 | "action": "store_true", 15 | }, 16 | }, 17 | "authorizer": { 18 | "args": ["--authorizer"], 19 | "kwargs": { 20 | "help": "The person or account authorizing the creation of this secret", 21 | "type": str, 22 | }, 23 | }, 24 | "behalf": { 25 | "args": ["-b", "--behalf"], 26 | "kwargs": { 27 | "help": "Show passwords for a user using a password as its private key", 28 | "type": str, 29 | }, 30 | }, 31 | "cabundle": { 32 | "args": ["--cabundle"], 33 | "kwargs": {"type": str, "help": "Path to CA certificate bundle file"}, 34 | }, 35 | "card_slot": { 36 | "args": ["-c", "--card_slot"], 37 | "kwargs": { 38 | "type": str, 39 | "help": "The slot number of the card that should be used", 40 | }, 41 | }, 42 | "certpath": { 43 | "args": ["--certpath"], 44 | "kwargs": { 45 | "type": str, 46 | "help": "Path to directory containing public keys. Certificates must end in '.cert'", 47 | }, 48 | }, 49 | "color": { 50 | "args": ["--color"], 51 | "kwargs": {"type": str, "help": "Disable color or not, accepts true/false"}, 52 | }, 53 | "connect": { 54 | "args": ["--connect"], 55 | "kwargs": { 56 | "type": loads, 57 | "help": "Connection string for the api to retrieve certs", 58 | }, 59 | }, 60 | "description": { 61 | "args": ["-d", "--description"], 62 | "kwargs": {"help": "A description of this secret", "type": str}, 63 | }, 64 | "escrow_users": { 65 | "args": ["-e", "--escrow_users"], 66 | "kwargs": { 67 | "help": "Escrow users list is a comma sepearated list of recovery users that each get part of a key", 68 | "type": str, 69 | }, 70 | }, 71 | "filter": { 72 | "args": ["-f", "--filter"], 73 | "kwargs": {"help": "Reduce output of commands to matching items", "type": str}, 74 | }, 75 | "groups": { 76 | "args": ["-g", "--groups"], 77 | "kwargs": {"type": str, "help": "Comma seperated list of recipient groups"}, 78 | }, 79 | "identity": { 80 | "args": ["-i", "--identity"], 81 | "kwargs": { 82 | "type": str, 83 | "help": "Override identity of user running the program", 84 | }, 85 | }, 86 | "ignore_decrypt": { 87 | "args": ["-I", "--ignore-decrypt"], 88 | "kwargs": { 89 | "action": "store_true", 90 | "help": "Ignore decryption errors during show all process", 91 | }, 92 | }, 93 | "keypath": { 94 | "args": ["--keypath"], 95 | "kwargs": { 96 | "type": str, 97 | "help": "Path to directory containing private keys. Keys must end in '.key'", 98 | }, 99 | }, 100 | "long_escrow": { 101 | "args": ["-l", "--long-escrow"], 102 | "kwargs": { 103 | "action": "store_true", 104 | "help": "Long list passwords that have been escrowed, only shows escrowed passwords", 105 | }, 106 | }, 107 | "min_escrow": { 108 | "args": ["-m", "--min_escrow"], 109 | "kwargs": { 110 | "type": int, 111 | "help": "Minimum number of users required to unlock escrowed password", 112 | }, 113 | }, 114 | "nocache": { 115 | "args": ["--no-cache"], 116 | "kwargs": { 117 | "action": "store_true", 118 | "help": "if using a connector, pull the certs again", 119 | }, 120 | }, 121 | "noescrow": { 122 | "args": ["--noescrow"], 123 | "kwargs": { 124 | "action": "store_true", 125 | "help": "Do not use escrow functionality, ignore defaults in rc file", 126 | }, 127 | }, 128 | "nocrypto": { 129 | "args": ["--nocrypto"], 130 | "kwargs": { 131 | "action": "store_true", 132 | "help": "Do not use a password for import/export files", 133 | }, 134 | }, 135 | "nopassphrase": { 136 | "args": ["--nopassphrase", "--nopin"], 137 | "kwargs": { 138 | "action": "store_true", 139 | "help": "Do not prompt for a pin/passphrase", 140 | }, 141 | }, 142 | "nosign": { 143 | "args": ["--nosign"], 144 | "kwargs": { 145 | "action": "store_true", 146 | "help": "Do not digitally sign the password information that you are generating", 147 | }, 148 | }, 149 | "noverify": { 150 | "args": ["--noverify"], 151 | "kwargs": { 152 | "action": "store_true", 153 | "help": "Do not verify certificates and signatures", 154 | }, 155 | }, 156 | "overwrite": { 157 | "args": ["--overwrite"], 158 | "kwargs": { 159 | "action": "store_true", 160 | "help": "Overwrite a password that already exists", 161 | }, 162 | }, 163 | "pwfile": { 164 | "args": ["pwfile"], 165 | "kwargs": { 166 | "type": str, 167 | "help": "path to the import/export file", 168 | "nargs": "?", 169 | "default": None, 170 | }, 171 | }, 172 | "pwname": { 173 | "args": ["pwname"], 174 | "kwargs": { 175 | "type": str, 176 | "help": "Name of the password. Ex: passwords/team/infrastructure/root", 177 | "nargs": "?", 178 | "default": None, 179 | }, 180 | }, 181 | "pwstore": { 182 | "args": ["--pwstore", "--srcpwstore"], 183 | "kwargs": { 184 | "type": str, 185 | "help": 'Path to the source password store. Defaults to "./passwords"', 186 | }, 187 | }, 188 | "quiet": { 189 | "args": ["-q", "--quiet"], 190 | "kwargs": { 191 | "action": "store_const", 192 | "const": -1, 193 | "default": 0, 194 | "dest": "verbosity", 195 | "help": "quiet output (show errors only)", 196 | }, 197 | }, 198 | "recovery": { 199 | "args": ["-r", "--recovery"], 200 | "kwargs": { 201 | "action": "store_true", 202 | "help": "Work with passwords distributed through escrow functionality", 203 | }, 204 | }, 205 | "rename": { 206 | "args": ["rename"], 207 | "kwargs": { 208 | "type": str, 209 | "help": "New name of the password.", 210 | "nargs": "?", 211 | "default": None, 212 | }, 213 | }, 214 | "rules": { 215 | "args": ["-R", "--rules"], 216 | "kwargs": {"type": str, "help": "Key of rules to use from provided rules map"}, 217 | }, 218 | "rules_map": { 219 | "args": ["--rules-map"], 220 | "kwargs": { 221 | "type": loads, 222 | "help": "Map of rules used for automated generation of passwords", 223 | }, 224 | }, 225 | "stdin": { 226 | "args": ["--stdin"], 227 | "kwargs": { 228 | "action": "store_true", 229 | "help": "Take all password input from stdin instead of from a user input prompt", 230 | }, 231 | }, 232 | "time": { 233 | "args": ["-t", "--time"], 234 | "kwargs": { 235 | "type": int, 236 | "help": "Number of seconds to keep password in paste buffer", 237 | }, 238 | }, 239 | "theme_map": { 240 | "args": ["--theme-map"], 241 | "kwargs": {"type": loads, "help": "Map of colors to use for colorized output"}, 242 | }, 243 | "type": { 244 | "args": ["--type"], 245 | "kwargs": {"type": str, "help": "Type of password integration used"}, 246 | }, 247 | "users": { 248 | "args": ["-u", "--users"], 249 | "kwargs": {"type": str, "help": "Comma seperated list of recipients"}, 250 | }, 251 | "value": { 252 | "args": ["--value"], 253 | "kwargs": { 254 | "action": "store_true", 255 | "help": "Don't update files directly, just dump value onto screen", 256 | }, 257 | }, 258 | "verbosity": { 259 | "args": ["-v", "--verbose"], 260 | "kwargs": { 261 | "action": "count", 262 | "dest": "verbosity", 263 | "default": 0, 264 | "help": "verbose output (repeat for increased verbosity)", 265 | }, 266 | }, 267 | "SCBackend": { 268 | "args": ["--scbackend"], 269 | "kwargs": { 270 | "type": str, 271 | "default": "opensc", 272 | "help": "SC backend to use: opensc or yubi", 273 | }, 274 | }, 275 | "PKCS11_module_path": { 276 | "args": ["--PKCS11-module-path"], 277 | "kwargs": { 278 | "type": str, 279 | "default": "/usr/local/lib/libykcs11.dylib", 280 | "help": "Path to yubi PKCS11 module", 281 | }, 282 | }, 283 | } 284 | -------------------------------------------------------------------------------- /test/passwords/test: -------------------------------------------------------------------------------- 1 | escrow: 2 | metadata: 3 | creator: r1 4 | minimum_escrow: 2 5 | recipients: 6 | r2: 7 | distributor: r1 8 | distributor_hash: d939a67c 9 | encrypted_secrets: 10 | 76:9C:E5:54:E2:D6:B3:66:BA:CF:C3:D1:F7:0E:B5:10:A6:09:20:3D: 11 | derived_key: !!binary | 12 | TC01Z0FIS3hhNXdVbGVxVHpCc1pIVW0zMEI2UzBhRFNqUnEyRW1kSnBCYTQ4UW9wTWRJTy0yaFd0 13 | WnRlaDFWUmxIdDVhRnN6ZnpKVGRHZ2F1UUtuU01QeWdDVDE0S2ZLUkVTOXJUQWhOdU9hNzZGUDVk 14 | T0RRaFpIR1RZa21iSWwyVkZDQkktUk10UjlOUU1GR0I5V1VKc2lLZkxJaW9LbmVPNFJMaHduMmRm 15 | OXUwM2E5Q20tLVdENWFBWTZGZEJOSVFxMTVPakxmdklCNWJaRUhJRU1XUlRhdU5pSjgtdDJTRUNE 16 | OFVmYnVrQ1YyVG5DMmRyNl82UDRJVjd3YnhnWjh5YzV1cHlLcUxPUXZqX3FwYXM5V1hfMXpoQzlE 17 | cENsRUJwdW5GU0NLVktEaFo2QzJhOTRYU0xQM3IxdnhhR0xsZUQ4U0lSY0pWRFpEbzU5SVVsNjI1 18 | U3pHWVFKb3NQR2VNS2NDNnRIRG50WnQwejVVcUpBUjBTUmwzejFMY0s4R0VSRFZpeHVqeW1VV004 19 | amQ4TDZTM0JraVJ0TXJyeTBtNGcxZ2hndG5vTHVCWVNXejNPV0t4bndVWW9RRUx0Nkc3VlZ4TWF0 20 | SEZ0VzMzYUdhQVFEWEpISTVJU2VHS0pnbHJXOTVqdTZDcy15WWtpdXRob0daa2xRTUIzZzZzZDk0 21 | dlNVeWdxMk5TTUNGaWJKVG9qRlBHMy1kU3BrX002QXhxc09GaXM5SHJpd2FVSHNIazJ0VFVFRFdv 22 | aFdSYS1sX1dlR0UwTmlLVVlTeU02b3RzSmVUWi1VMC04TGVsMVo1SElxNnpHWkhhVTRxb0ZGY3NC 23 | TnBBQ1ZLdUlBY3BqQmlaS21uOWN1c1hReTEzYnlMdE12c3FMWFVvcTIzNlhEU3BDY0xsUlg0UXM9 24 | encrypted_secret: !!binary | 25 | Z0FBQUFBQmg1eEtmeFBkWlh3VWx0cnN3UnhLMHprRXQ0bFUxUWtqVzZUTUZaLWtCeTAtckdVN0dt 26 | YXdtZHlWTnpzZDBCNEpzUWNtamFlQ2k5Q1BnYzZ1dmNXY2p1OHJYUnc9PQ== 27 | recipient_hash: 7c850ec8 28 | encryption_algorithm: rsautl 29 | signature: !!binary | 30 | S2R1cDc0X1hJV0NEN2s5eC1iSUlOU0NNZzdYWW9PQ2d0M1BjRFBoZGpQSmFMYi0wbXZlckc1X1d4 31 | cFFlTEttVkNxMHFGSHAxU3pzU05OX0lnZm84S3hmNl9FeWc3NEkxaVMxdEdMMHNoYXJob3RfUFFH 32 | RndMV01RYnhpWVNNRURyRXI0NFY1ZHZiRG1XczNUX3JXN3g2aXZ6cjBUOElpZUN6b0dXaFBLTl9V 33 | MHlJSDN1Y1N4Q1ZKaHVtZHMtRDRsUDFRNHVubW92dDRpcGktdTdLSHU4d0MyMmpad1gtdmtzb3J2 34 | ZUV3aFUzQ0xRMHN4RXJwNXlsSHZaeXFaeGFtdFd1alBaN1RiLWdNWGRoLXRFekFHSGJUZ0dLRlFt 35 | N1ZOSENpZEFCbkc4UDlxXzBvZmk1S29aUnNwOTNoS3l3M002WWZHT1pKZUc5bkNDSGhCMzhka2p5 36 | b3VKbV8zVHFtQzdXZHhReXNyVU54UHlaNDNjRHd3Ykd2VHNqTjQyUXZFUjVwWkZYNGFOdXBvZW9t 37 | a0tUU3d6V0dtWHJkS2IteUpDR3VUTlhCR3kwM3Z0bExzRUh4YTZHdEZTLVFaSDBWa2REcG1JRW8x 38 | VEJXaDlZQWRXQzVmSHdUdHNITThoRC1NWEUyQ0NkQ0J3aUJYNElRSVZQSURsSUk1YkZoMXlJS080 39 | cXF5ZGlQZHNGX2ppTk5fTXFPZmJJYml2NWRuczlrbmZaVUp1MUdTQnFia2dGN2huVzAzWDlvN1Uw 40 | VlJ3MDc2VnZCdEo5V3AwWHpRUGw0VURjUVVSZGU5dXpKZHV0RV9pUVRjMHRpVUhZNmFyaEhfVEVr 41 | amtaR2oteTRZZm03cm1XMHVRUjJhalhtaUtJejlBR0lOVDB3Q3RidXRzeWpBOU50MG93QjJjaVU9 42 | timestamp: '1642533535.50' 43 | r3: 44 | distributor: r1 45 | distributor_hash: d939a67c 46 | encrypted_secrets: 47 | FD:F0:6F:5E:8E:D0:41:04:1A:30:FE:B4:9F:5C:F1:82:66:38:99:A9: 48 | derived_key: !!binary | 49 | Vk1hbWJUVnBSQ1Z4Y3FyelZWcUZxLVN2clZ4b2pkRHdQTVZLN094blMyMVhJRmYybXZyeUJia01o 50 | Q0pCZnNFa2pKTFAwbzJPMnFXSlNad2MzRExEZW4zWHdxZTFKTm9WOHR2bHQydDRYQWF4ZFpkY1pO 51 | elFzN1d4SEtyM3JBN05nb1l1MUlnVWN6UVhleUUwY3FlMkMzVHcyakQtcGRhdlpMck54VlRVclRk 52 | T2daT2lHbjhieUdGWWRidVVYWXFoOGZlYlM2a3NKWkNrcFdpNVdQcUFjNTgtTVpYZkE3MGdQUEU0 53 | SHhud21FUkVmNUJHNFp2S1pXbjVKamFjSnpQaGRvSGtGYUdXZFM4S0ZqTUNiTzVwT2NUbGdvcHpL 54 | dWVqMXVBRUtDRkNpWlliN0tHbnpuOWF4WGgxVFpVek9wVFVuc0lkY1c2N1hpOERwS2s5VFR5dkJn 55 | TWdFM21nbGh2Q3ZGU0NXdWhOTnJKZmxIbnBoZ3RqcGpPaFdRczhYekhWZW5IeEM0d2d6NEc0Z0x3 56 | Q2JHcWkwODRNZ1FlRDI5V1d1VjFON0RMLVBpeW5wUmtqTVhQVDFUdVJzWTlJc3VkbDVONEY0MzI1 57 | OW9SVTgzRnJzVmdnMmdyTXBFd0Y1aU9pNTRWMk9uZVU0V2hwV0Y1bTNWdUlpcW96dC1ETUk3ZVRC 58 | SU02aGZaVWZiUmJJNW9pS25EUUVDaHBlOTk2QXAyZ01nakJYMTMzdHQ2WXpPNHFjdFR4bXBERWN0 59 | cFd1MkY0ekxGR1RmTG43Wm9vMmFvc0tjdmVpemlZWWFwdEFpc3YtM1FKMS0tcnBnaE9GYU50cFN5 60 | YVdRZmZ4TUZNM3ZtaXBOV05IZEZsbVhvM3ZUUDlYN2VKay1nTG1aSUxuSGdVX3dId0dQbktQek09 61 | encrypted_secret: !!binary | 62 | Z0FBQUFBQmg1eEtmdjVHU0ZGMGs4c3pwSXVQVDRGY2hxYmQzbF9VVDhCYURRN3EwUUpvTDQtVm5v 63 | WkVCU0dienBFdy1EV0w0UXBTcW9zYXR1bWd3Z2FBTFRreUxQRVN4dnc9PQ== 64 | recipient_hash: b779ae6e 65 | encryption_algorithm: rsautl 66 | signature: !!binary | 67 | bzE5dGluZTZTTVFwUFNZREowb3h0aThPSTMyU283OUNDc05ZWWlrYThxZE1DYi1FSTk3NWVOaE1B 68 | TE5FckxzQVljQmVwdGd6SXJYM2JMek5zU2ZvVFg4LVl3T0lMd2JybzdlUTF2TDNnajJhQzhZVWpw 69 | dXJhb2JhZTItSGlkN0prbWtQbUtEOXJUdE14dDRYQlVBX3lRU05UM1dDZ1c5SFhQd1BIQmxOc0Nv 70 | RzVaMXVrS0UxN2NkcmlUcElxa3BtRVhiQzN1ZkJrUmNJWkRvN2xVUmxJWklKSG5lYTlQdXUyWkRD 71 | RUNZZHJYVjNaUnBEcmQ4UVo2YjJucXFZeGVHaU9iMF9weWNPTFFpSFdTQU5lSWZyRVYyamZJYnVl 72 | blVqM1JKX083eTBmOWJacVEtSkVoT2ZEWjhuRWJrQUpNSkRETG5uVTl3dVVROW05allwTmFfT3lk 73 | R0xKaHNrSXZUN2Q3TVk1RkdNdGI2T0hOcDAyZmJrMmpzVnJYeDlLTHFuWEswSm9VcThTSG9FNnB4 74 | d2t1Sk8yWTNYTEctZ1BHVEdnY01kaWdXbTlUTF9zUGlqR1ZvMnBwdXQ4cG5aMWMtd3c4Mk9LVlBo 75 | eHJpVUJvU1pqVXNzVUJsZW9yeG42dkdwbmczeHFDNjVYVWRyX1FPVUFDc2htSzZBYUNqT0ZxNG1y 76 | TGFLbFV3Z2pkdjB1UjFGYTNuNi1mdXotSEY0cmE5S1duVm9WcEd4RENvQUFTTWZ1X1BIbFVkSWRK 77 | aDNwTGxUUlNaUXZabExfTzFBSmRwaVp2ZUVSUllieDIzb0lWbHBFRUNjVXFQUnJOdVRPRllMVHJj 78 | OElzQjZhaXBMRzJhY29XRTBEenlfSTZoUnRJWVNfdXZGRjhBTWtxN1RUSlM4Z3p0TmxNcTNaWkE9 79 | timestamp: '1642533535.66' 80 | r4: 81 | distributor: r1 82 | distributor_hash: d939a67c 83 | encrypted_secrets: 84 | 67:4E:DC:87:39:80:28:03:E9:17:02:3B:1C:18:E0:52:6F:62:F0:4E: 85 | derived_key: !!binary | 86 | M2ppZ2VLa1k5dVBjLU9oM19YR0VSdEFiSlBrOXhmUG1VRW5DZ1NtMzV0a25VaXNtT1hDdFoyZVk1 87 | UVZxSjVZZTAzdDg2WklxdTc4eDJXNWVsYWFfZDRSWkd5Vlh4UmlwXzVBdWxzTk5yWDNZMkRmWXZn 88 | bU14akFlcGdFTTdtYjRRYzNqNkFZazlGTnZpRjJ6NjJCb2Vkc3FpWHQtc2ZUa05wVVZkRmZqUWRK 89 | WFFXRjdtVlFFWDlKZmVYaXNPUnEzV0hTWEp4Z3BMN2JRc0ZhQjVMeU5LRUc2V0VoRTZxQ0lXN1FR 90 | cjJmTE02cFpWazBHZWJoSjFVOEpzN1hmbHpkNjJEUDJDTGxQd0pEQXB1SUEzMXF1dm1TZlU3anFG 91 | N1FEbjRNYk5jWjFiTUVEdlVUdGNnS1pPMkV5aWtTR2hjT29sVENzbi1tdE1PQThta2RJWFoxdXpW 92 | M0ZaazI4U19SSXd5V3o2dHhxLWctUTFNT1A2WTVidzRuaEZvbUFJSUNuS005M0lCamVCZzIwdkFo 93 | Qi1rTm1JSjZ6RzBjdWhJd3ZMVFRSNV91VDF4OGladnVOTVlNT3poU2xUNlFVSDF2TXowRzZqOVFP 94 | Rk9HcnpnZXhjczlkWnRITkJrVEwwRVFLMWJMYlVWa1piRXpMLVBmTFRwWXlQeGxVdUVnQ0g5bzMt 95 | SDRWTzZMN0tUcWl1cWFscGUtNGlrbUJrVHdKVjdQTExLX2RBSWNEejdsM2hyTzYtNHVSNlhMOFFF 96 | d0hsTURELVA2Vm9ocE9OeEdYd2pFRnk4cm1NSmNaRlc0cnpUVldtM05HSTNaSkVUQWE0YXhnUlZz 97 | bzJLTElQSENfVVA2NXJrVXgwakw5ekYtMUdiT1JMb1lBTmhnOWJzQ1lkWldSTk1UbHFwSWs0Rkk9 98 | encrypted_secret: !!binary | 99 | Z0FBQUFBQmg1eEtmNHR1Q3AzQUp5XzRreVRySnp6TWdjTm5oTEJoNUFYcDJFcXVzUktvWmFTT1JH 100 | c3ZrdklzUWxrSDBCMWZNb1k4MVBoS25jeU9wYmpsMzM1WHNkMnh4Mmc9PQ== 101 | recipient_hash: 1c1e4a12 102 | encryption_algorithm: rsautl 103 | signature: !!binary | 104 | V0szVURpN1JwRlNQYnVLVWstSHBqT0tHSnpzS0ROaW9GQm50MmQyemJFN3M4R3lBSHFYZ2ZZNDdQ 105 | Tzk5bEVHQXJWSGZyZWM0WW5PaVVNR1Z3dlBVYWNwa29pem5pTC1QSTlkSTkzcDJTVmtYYkY0QnV1 106 | dkhjTTNqMHNNQW15bjZHY1JCRkdQcjJXMUpTckZ1THhhUE9tZmhpSWFucFk5RmdxckdPRzlWZWl6 107 | RzRham1Pbi1wNEVkNkNVUEljRzhYMjllYnFnWXUxSm1jWnpxdzVkYVhsSjBRT1Jqc0tpd1NZa3dm 108 | NVIyR1B4azdLRE44cFIwVkJ4Q1JQY1RNZkQ5Q3hldWN1UGRjZl9PNkMwYmFudnRBb09JNUFaXzk5 109 | ZFJXZEoxRDFYZkZMNE9aNzE0elVKWjkzS3NwcGIyQ0ZJU1FRTHVtbGtJUTVSekw5aW5JcVRqWWoy 110 | MlJZOUU2LXlMSklkUHZNTkp0QXFOMjNvV25KUzc2bHhKd0w2QWpSS2otVjVMak01cTh6QlNMV1pD 111 | eVAzNkJIWmJRb2N0M3RIcnI1Y25jSTR2WmotS01lcGVrR1JqY3d3bGFsMmI2Qk4xWmdobG1aeHdS 112 | YzZORWFwWEFDYVB4VmxXbkdQeVRtNEJVVjNrVDE3dmMxQTl4WUZGdWNTOG9KVUdYMWJKX1VPR0FY 113 | VEI1SVo0bDZGR0ZhYWFidnRSRkNmdW1wMktCci1OMFI4VGUwMS1JSmtUeTZ6RXVyRC1hWjA1Ul9Q 114 | RXp1OHpjcmNUbzVsY0dqNTg3aUxuX1VRRHMwRWY5ekNnd0JOVENQamdoSVFBWE9QYldmMGxkV3N6 115 | dnpucWVyVmpRZ1FucXFjbWMxRHdtVWZlVERzOVVzaHd5SWpHTmo4UF9KeHg4UlJRMks0d1dRTWc9 116 | timestamp: '1642533535.82' 117 | metadata: 118 | authorizer: y 119 | creator: r1 120 | description: y 121 | name: test 122 | schemaVersion: v2 123 | signature: null 124 | recipients: 125 | r1: 126 | distributor: r1 127 | distributor_hash: d939a67c 128 | encrypted_secrets: 129 | 01:AA:D5:23:82:24:08:5D:14:A0:3B:11:29:D1:09:52:72:E6:7F:80: 130 | derived_key: !!binary | 131 | ZWZndmV4MzN5UzRTTG1wSWFncHY3WFg4eXFaaV8wZ2VGLUZ4YVdIcldObXN0SGZ6d08xcUpic1BQ 132 | dDdRQUYwLVpXcG52czlNaXBueVBrbDFEZ3ZRTnluVHZNLXBGWUJoSnZrN19HSV90dURBWnhaN1dy 133 | ZXRmZkRxWDlkOVhBSTNlOHh4U2xnQ0RBRUZIMjgyR0pTYzZTOFlDT2JPY2NFTFdHN3NKZjk0Mmd2 134 | a0N0Q25fZHNhSjhlMjJLRU9lOWEzeHM3ZW1fX19QVGFpdUsyODBwcWE0cEVmNFdNV0ZubVNNd1VW 135 | eXJSSzR6eWt1SU5zNFhQNFdfYTRVUmc1dkMybVdtV0hnaU1mZ2dmNEhTVExueHNpeU9GbFVfNjd2 136 | eUFOUGhWV0xvSlhUNHJaY2VVcG5HQTR5QTl1WXJYVFA1ZFM5eno3bndzVWM2dzNja2JjY2xZRWhs 137 | WGd6MlRGTWMxaTh5SzVicDJwLTU3VFYzXzZHZ0hnVWx6X2RqODd3aElrblNsb2wyanlSOVlqQWhV 138 | TjMwWFpVNGZoM04wWFMyZWRnaEhFcjZzaXN6QmFncTRnbEZxVExOSURXbUVBUDVDaV9RNk5fYkZL 139 | ZnBqOEViTk45Vl9GSHRDZTBXR1hXYThQb2hnMG0wUUkxVkoycTVGRTlpVW5OYlhFbUIzcWRTS2E0 140 | aWZkZ2g4YnlMYzd5ZEhYWktqZW1QQjg0TzJqT05Kei03Zkw3TkdTc1AtSUM2V3VhTWdrVFMyM3FO 141 | aEpjZzIyNzE0ckdZZlo0UzRiRWItNWtfQ1RtMkFmSXpiT3dmZ1AzdlZ5cHhJZlk1Z1dBRWU3aFFD 142 | MzJ5N0hLbktFWEJ1R0tXQzI5c0ZWYkdyUkFiOWkwUmYxZ1RTM1haRkZYcy1fMmpxSG9BOHdDQWc9 143 | encrypted_secret: !!binary | 144 | Z0FBQUFBQmg1eEtmMDIyd0pBZlk4RXFRbkVaSFJUZ1JBdDNnbXBZcnN2XzVVeGRaX1Q5dTVZeUNM 145 | ODg4cFI1VENXSFJPSGxmR0xFRUlRLUxxLWxWODZfaFFVYlo4N1dJQXc9PQ== 146 | recipient_hash: 63b96030 147 | encryption_algorithm: rsautl 148 | signature: !!binary | 149 | aVF6NDZpRUsydmJZNElsRFlYd291VVJFelV0SzQydGp3TzROeDVPemE3UzNnYU9XZG9SQ0Jlbmlz 150 | cGZHNkx1NktkdVhtOGdYVHhGTWVKcE9HemFWZUlMZzhyZlpHeUQya1ZMeGxoSllxWDdFMmNWTmJX 151 | R2hrczdGMTZmTWFxRnNVVGxzeGJvaWZUWHBxSF9jaUQ5OXk1NjhyV1dpZGhoOEQzZXBEMlRDYWdL 152 | UmRUcFFubHA4b3dwTVZRaGNwdVNpYWc3UkdyTm1xUVJCYmpGWTJsVG1kUG9NTWhKUU8yNGlpN1lm 153 | YnBlOU1DMENocENSVnFPMmF2dkJEWG9NN3NGQS1SNHpFTlczeWptaFJfQjR6NEZ4UXJDaFVBcF9u 154 | cXRRYkpPQnFqeGtER2ZxZThnZjl0LVpNcWMya28tN2dHd3pPdmY0dEpTcXM0RXdXTVdldzFseXhE 155 | R1MtdFpuWDNobGMzUzBYUnpCRkFSaWxIRVZsaGJqZExCOUpQbTA3al9DOHA2NDhTQ2dxa3JPTW9O 156 | eHM5TTNmWk1Hc25VbEhLWWZDNHRxUGZhdVpFY2dqanpZdGRLZENZQXY3WUdlOVh0b2Q4OVBfRU9O 157 | anoza3lVTTZrTk51dEd4RVdKZTB0Z29DZTNnWjhxdFBtQXA5Zk14YVg1dHlpb0ptQVRmSC00LXFO 158 | US1HMHR0dDE2V2NZVWlhR0lzWGNBT1JDbVpBOG5ub1VtenQyMFNISHFlVzhOX0pYRS11RzBscTk5 159 | NjVQQ0hfeWp6dlFncHNNYi1UaUQ4cjJRN2VXSWh0ZEVvSVMzQXh5eUJiMDVyd3QwUkI0NFU1Uldf 160 | Z21iQ0VsZWFGMUM4d0N0UVc4U2YxWUxEaFhBZzRDaThvLVVQbmR5ZzA5TlZ4c0lQSVVVUE96Mjg9 161 | timestamp: '1642533535.32' 162 | --------------------------------------------------------------------------------