├── 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 |
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 |  [](https://pkpass.readthedocs.io/en/latest/?badge=latest) [](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 |
--------------------------------------------------------------------------------