From f113b43cbb82203cd625059b3da7ac47e269c7bf Mon Sep 17 00:00:00 2001 From: "Hungenberg Alexander, INA-DNA-INF" Date: Mon, 29 Jun 2020 22:10:27 +0200 Subject: [PATCH] initial commit --- .github/workflows/pythonrelease.yml | 26 ++ .github/workflows/pythontest.yml | 26 ++ .gitignore | 4 + README.md | 58 +++++ poetry.lock | 302 ++++++++++++++++++++++++ pyproject.toml | 31 +++ src/secrets_tool/__init__.py | 0 src/secrets_tool/__main__.py | 74 ++++++ src/secrets_tool/cipher.py | 56 +++++ src/secrets_tool/handlers/__init__.py | 0 src/secrets_tool/handlers/generic.py | 15 ++ src/secrets_tool/handlers/yaml.py | 91 +++++++ src/secrets_tool/handlers/yamlcompat.py | 76 ++++++ 13 files changed, 759 insertions(+) create mode 100644 .github/workflows/pythonrelease.yml create mode 100644 .github/workflows/pythontest.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/secrets_tool/__init__.py create mode 100644 src/secrets_tool/__main__.py create mode 100644 src/secrets_tool/cipher.py create mode 100644 src/secrets_tool/handlers/__init__.py create mode 100644 src/secrets_tool/handlers/generic.py create mode 100644 src/secrets_tool/handlers/yaml.py create mode 100644 src/secrets_tool/handlers/yamlcompat.py diff --git a/.github/workflows/pythonrelease.yml b/.github/workflows/pythonrelease.yml new file mode 100644 index 0000000..455e985 --- /dev/null +++ b/.github/workflows/pythonrelease.yml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Poetry + run: | + pip install poetry + - name: Build and publish + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + run: | + poetry version ${GITHUB_REF/refs\/tags\/v/} + poetry build + + poetry publish \ No newline at end of file diff --git a/.github/workflows/pythontest.yml b/.github/workflows/pythontest.yml new file mode 100644 index 0000000..5127af2 --- /dev/null +++ b/.github/workflows/pythontest.yml @@ -0,0 +1,26 @@ +name: Tests + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Poetry and Dependencies + run: | + pip install poetry + poetry install + - name: Lint with flake8 + run: | + poetry run flake8 . --count --max-complexity=10 --max-line-length=120 --statistics + working-directory: ./src + - name: Static Typechecking with MyPy + run: | + poetry run mypy -p secrets_tool + working-directory: ./src diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dee5d4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +**/.mypy_cache +src/secrets_tool.egg-info +dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4978cb --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Secrets Tool +This is a small tool which helps to encrypt secrets that must be committed to a Git repository. + +It has the advantage to natively support partial encryption of YAML files. This is of great advantage, as it allows to see the YAML file structure even when some of its contents are encrypted (your PR reviewers and diff tools will thank you) + +## Prerequisites +* Python >= 3.7 +* Having the following packages installed: `pip install ruamel.yaml cryptography` + +## Usage +The tool reads a list of files to encrypt/decrypt from a `.gitignore` file. In there it will only consider files that are sorrounded by a comment block as in the following example: + +``` +# BEGIN ENCRYPTED +kaas-rubik-stage/values.yaml +# END ENCRYPTED +``` + +Run the tool by giving the `.gitignore` file as an argument, together with either a `encrypt` or `decrypt` command: + +``` +cd +python -m utils.secrets_tool k8s_helm/.gitignore encrypt +``` + +## Syntax +The tool provides different encryption handlers for all kind of file types. +* `yaml` for YAML files that are used by tools which are okay having a `!decrypted` tag in front of strings +* `yamlcompat` for tools that don't like the additional 'encryption marker' tag. +* `generic` for all other file types. It encrypts the complete file. + +The desired encryption handler is inferred from the filetype - or it can be given explicitly in the gitignore file using the `# type:` hint: + +``` +# BEGIN ENCRYPTED +kaas-rubik-stage/values.yaml + +# type: yaml +kaas-rubik-stage/values2.txt +# END ENCRYPTED +``` + +### yamlcompat +This encryption handler can encrypt individual YAML keys without relying on 'parser visible' changes in the YAML file structure. +Instead of marking the desired keys directly in the file, they are listed in the .gitignore file using a `# data: ` comment: + +``` +# BEGIN ENCRYPTED +kaas-rubik-stage/values.yaml + +# type: yamlcompat +# data: splunk.apiToken +# data: splunk.host +kaas-rubik-stage/values2.yaml +# END ENCRYPTED +``` + +*WARNING* It is recommended to use the normal YAML handler whenever possible. When using the yamlcompat module, you split up your encryption logic over multiple files, which might lead to errors (especially on fragile YAML files that contain unnamed structures - like lists) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0158b6d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,302 @@ +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = false +python-versions = "*" +version = "1.14.0" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "main" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +name = "cryptography" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "2.9.2" + +[package.dependencies] +cffi = ">=1.8,<1.11.3 || >1.11.3" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +idna = ["idna (>=2.1)"] +pep8test = ["flake8", "flake8-import-order", "pep8-naming"] +test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] + +[[package]] +category = "dev" +description = "the modular source code checker: pep8 pyflakes and co" +name = "flake8" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.3" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "Optional static typing for Python" +name = "mypy" +optional = false +python-versions = ">=3.5" +version = "0.782" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.20" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + +[[package]] +category = "main" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +name = "ruamel.yaml" +optional = false +python-versions = "*" +version = "0.16.10" + +[package.dependencies] +[package.dependencies."ruamel.yaml.clib"] +python = "<3.9" +version = ">=0.1.2" + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +category = "main" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +marker = "platform_python_implementation == \"CPython\" and python_version < \"3.9\"" +name = "ruamel.yaml.clib" +optional = false +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4.2" + +[metadata] +content-hash = "7131b3c6dcb0be7ab91171564dbce2f3711304b33f3c50da8c040258791a8513" +python-versions = "^3.8" + +[metadata.files] +cffi = [ + {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"}, + {file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"}, + {file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"}, + {file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"}, + {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"}, + {file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"}, + {file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"}, + {file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"}, + {file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"}, + {file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"}, + {file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"}, + {file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"}, + {file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"}, + {file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"}, + {file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"}, + {file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"}, + {file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"}, + {file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"}, + {file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"}, + {file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"}, + {file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"}, + {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"}, +] +cryptography = [ + {file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"}, + {file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"}, + {file = "cryptography-2.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365"}, + {file = "cryptography-2.9.2-cp27-cp27m-win32.whl", hash = "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"}, + {file = "cryptography-2.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55"}, + {file = "cryptography-2.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270"}, + {file = "cryptography-2.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf"}, + {file = "cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d"}, + {file = "cryptography-2.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785"}, + {file = "cryptography-2.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b"}, + {file = "cryptography-2.9.2-cp35-cp35m-win32.whl", hash = "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae"}, + {file = "cryptography-2.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b"}, + {file = "cryptography-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6"}, + {file = "cryptography-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3"}, + {file = "cryptography-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b"}, + {file = "cryptography-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e"}, + {file = "cryptography-2.9.2-cp38-cp38-win32.whl", hash = "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0"}, + {file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"}, + {file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"}, +] +flake8 = [ + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy = [ + {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, + {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"}, + {file = "mypy-0.782-cp35-cp35m-win_amd64.whl", hash = "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d"}, + {file = "mypy-0.782-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd"}, + {file = "mypy-0.782-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"}, + {file = "mypy-0.782-cp36-cp36m-win_amd64.whl", hash = "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406"}, + {file = "mypy-0.782-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86"}, + {file = "mypy-0.782-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707"}, + {file = "mypy-0.782-cp37-cp37m-win_amd64.whl", hash = "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308"}, + {file = "mypy-0.782-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc"}, + {file = "mypy-0.782-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea"}, + {file = "mypy-0.782-cp38-cp38-win_amd64.whl", hash = "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b"}, + {file = "mypy-0.782-py3-none-any.whl", hash = "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d"}, + {file = "mypy-0.782.tar.gz", hash = "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.16.10-py2.py3-none-any.whl", hash = "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b"}, + {file = "ruamel.yaml-0.16.10.tar.gz", hash = "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win32.whl", hash = "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win32.whl", hash = "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win32.whl", hash = "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win32.whl", hash = "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070"}, + {file = "ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30"}, + {file = "ruamel.yaml.clib-0.2.0.tar.gz", hash = "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, + {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, + {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..406512c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[tool.poetry] +name = "secrets_tool" +description = "A lightweight tool to easily encrypt/decrypt secrets inside a repository" +authors = ["Alexander Hungenberg "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/defreng/secrets-tool" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +# Version is set by GitHub action workflow (on release) +version = "0.0.1+local" + +[tool.poetry.dependencies] +python = "^3.8" +cryptography = "^2.9.2" +"ruamel.yaml" = "^0.16.10" + +[tool.poetry.scripts] +secrets_tool = "secrets_tool.__main__:main" + + +[tool.poetry.dev-dependencies] +mypy = "^0.782" +flake8 = "^3.8.3" +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/src/secrets_tool/__init__.py b/src/secrets_tool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/secrets_tool/__main__.py b/src/secrets_tool/__main__.py new file mode 100644 index 0000000..3f393bc --- /dev/null +++ b/src/secrets_tool/__main__.py @@ -0,0 +1,74 @@ +import argparse +import re +from pathlib import Path + +from .handlers.generic import GenericFileHandler +from .handlers.yaml import YamlFileHandler +from .handlers.yamlcompat import YamlCompatFileHandler + + +HANDLERS = { + 'yaml': YamlFileHandler, + 'yamlcompat': YamlCompatFileHandler, + 'generic': GenericFileHandler, +} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('filename') + parser.add_argument('command', choices=('e', 'encrypt', 'd', 'decrypt')) + + args = parser.parse_args() + + secrets_file_locations = ( + Path(Path.home(), '.tap-rubik-key'), + Path(Path.home(), '.secrets-tool-key'), + ) + + for path in secrets_file_locations: + if path.exists(): + key = path.read_bytes().strip()[:32] + break + else: + raise Exception('Could not find file with your secret key.') + + gitignore_filepath = Path(args.filename) + base_path = gitignore_filepath.parent + + gi_content = gitignore_filepath.read_text('ascii') + gi_content_match = re.search(r'(?s)# BEGIN ENCRYPTED\n(.*)\n# END ENCRYPTED', gi_content) + + if gi_content_match is None: + raise Exception("Couldn't find a # BEGIN/END ENCRYPTED section in the provided .gitignore file") + gi_content_match = gi_content_match.group(1) + + statement_expr = r'^(?:# type: (?P\w+)\n(?P(?:# data: .+\n)*))?^(?P[^#\n].*)$' + for statement in re.finditer(statement_expr, gi_content_match, flags=re.MULTILINE): + data_raw = statement.group('data_raw') + data = [raw.strip() for raw in data_raw.split('# data: ') if len(raw) > 0] if data_raw is not None else None + + filepath = base_path / statement.group('filepath') + + type_ = statement.group('type') + if type_ is None: + if filepath.suffix in ('.yml', '.yaml'): + type_ = 'yaml' + else: + type_ = 'generic' + handler = HANDLERS[type_] + + if args.command in ('e', 'encrypt'): + target_path = Path(str(filepath) + '.enc') + + handler(filepath, data).dump_encrypted(target_path, key) + print(f'ENCRYPTED {filepath.resolve()} (into {target_path.resolve()})') + elif args.command in ('d', 'decrypt'): + source_path = Path(str(filepath) + '.enc') + + handler(source_path, data).dump_decrypted(filepath, key) + print(f'DECRYPTED {source_path.resolve()} (into {filepath.resolve()})') + + +if __name__ == '__main__': + main() diff --git a/src/secrets_tool/cipher.py b/src/secrets_tool/cipher.py new file mode 100644 index 0000000..6af1dfa --- /dev/null +++ b/src/secrets_tool/cipher.py @@ -0,0 +1,56 @@ +""" +Implementing custom encryption, as the default "fernet" method from the cryptography package +uses random initialization numbers, which cause a different encrypted string for identical content all the time. + +We don't need message authentication in our case, and changing strings would be annoying in Git. +""" +import base64 + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + + +def encrypt(data: bytes, key: bytes, iv_bytes: bytes) -> bytes: + """ + data can be bytes of arbitrary length + key must be 32 bytes long + iv_bytes can be of arbitrary length, as they will be hashed + """ + iv_digest = hashes.Hash(hashes.SHA256(), default_backend()) + iv_digest.update(iv_bytes) + iv = iv_digest.finalize()[:16] + + algorithm = algorithms.AES(key) + mode = modes.CBC(iv) + + cipher = Cipher(algorithm, mode=mode, backend=default_backend()) + encryptor = cipher.encryptor() + + padder = padding.PKCS7(algorithm.block_size).padder() + to_encrypt = padder.update(iv + data) + padder.finalize() + + encrypted = encryptor.update(to_encrypt) + encryptor.finalize() + return base64.b64encode(encrypted) + + +def decrypt(data: bytes, key: bytes) -> bytes: + """ + data should be encrypted binary, encoded as base64 - as it is produced by 'encrypt()' + key must be 32 bytes long + """ + + data = base64.b64decode(data) + iv = data[:16] + message = data[16:] + + algorithm = algorithms.AES(key) + mode = modes.CBC(iv) + + cipher = Cipher(algorithm, mode=mode, backend=default_backend()) + decryptor = cipher.decryptor() + decrypted_padded = decryptor.update(message) + decryptor.finalize() + + unpadder = padding.PKCS7(algorithm.block_size).unpadder() + decrypted = unpadder.update(decrypted_padded) + unpadder.finalize() + return decrypted diff --git a/src/secrets_tool/handlers/__init__.py b/src/secrets_tool/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/secrets_tool/handlers/generic.py b/src/secrets_tool/handlers/generic.py new file mode 100644 index 0000000..2b589cc --- /dev/null +++ b/src/secrets_tool/handlers/generic.py @@ -0,0 +1,15 @@ +from pathlib import Path +from typing import List + +from .. import cipher + + +class GenericFileHandler: + def __init__(self, filepath: Path, data: List[str]): + self.filepath = filepath + + def dump_decrypted(self, target: Path, key: bytes): + target.write_bytes(cipher.decrypt(self.filepath.read_bytes(), key)) + + def dump_encrypted(self, target: Path, key: bytes): + target.write_bytes(cipher.encrypt(self.filepath.read_bytes(), key, str(self.filepath).encode('utf8'))) diff --git a/src/secrets_tool/handlers/yaml.py b/src/secrets_tool/handlers/yaml.py new file mode 100644 index 0000000..62354d8 --- /dev/null +++ b/src/secrets_tool/handlers/yaml.py @@ -0,0 +1,91 @@ +""" +Classes to encrypt and decrypt YAML files + +- Encryption operation will look for fields with the "!decrypted" hint and replace them with an encrypted version of + their content. This will be marked with the "!encrypted" hint +- Decryption operation will look for fields with the "!encrypted" hint and replace them with a decrypted version of + their content. This will be marked with the "!decrypted" hint +""" +from pathlib import Path +from typing import List + +import ruamel.yaml + +from .. import cipher + + +class DecryptedString: + yaml_tag = '!decrypted' + + def __init__(self, data: str): + self.data = data + + @classmethod + def from_encrypted(cls, data: str, key): + return cls(cipher.decrypt(data.encode('ascii'), key).decode('utf8')) + + @classmethod + def from_yaml(cls, constructor, node): + return cls(node.value) + + @classmethod + def to_yaml(cls, representer, node): + return representer.represent_scalar(cls.yaml_tag, node.data) + + +class EncryptedString: + yaml_tag = '!encrypted' + + def __init__(self, data: str): + self.data = data + + @classmethod + def from_decrypted(cls, data: str, key: bytes, iv: bytes): + return cls(cipher.encrypt(data.encode('utf8'), key, iv).decode('ascii')) + + @classmethod + def from_yaml(cls, constructor, node): + return cls(node.value) + + @classmethod + def to_yaml(cls, representer, node): + return representer.represent_scalar(cls.yaml_tag, node.data) + + +class YamlFileHandler: + def __init__(self, filepath: Path, data: List[str]): + self.yaml = ruamel.yaml.YAML() + self.yaml.register_class(DecryptedString) + self.yaml.register_class(EncryptedString) + + self.filepath = filepath + + def dump_decrypted(self, target: Path, key: bytes): + tree = self.yaml.load(self.filepath) + + self._walk_item(tree, EncryptedString, + lambda enc_string, iv: DecryptedString.from_encrypted(enc_string.data, key)) + + self.yaml.dump(tree, target) + + def dump_encrypted(self, target: Path, key: bytes): + tree = self.yaml.load(self.filepath) + + self._walk_item(tree, DecryptedString, + lambda dec_string, iv: EncryptedString.from_decrypted(dec_string.data, key, iv.encode('utf8'))) + + self.yaml.dump(tree, target) + + def _walk_item(self, item, type_, callback, path=''): + if isinstance(item, dict): + for key in item.keys(): + if isinstance(item[key], type_): + item[key] = callback(item[key], path + f'.{key}') + else: + self._walk_item(item[key], type_, callback, path=path + f'.{key}') + elif isinstance(item, list): + for i in range(len(item)): + if isinstance(item[i], type_): + item[i] = callback(item[i], path + f'.{i}') + else: + self._walk_item(item[i], type_, callback, path=path + f'.{i}') diff --git a/src/secrets_tool/handlers/yamlcompat.py b/src/secrets_tool/handlers/yamlcompat.py new file mode 100644 index 0000000..fb7882d --- /dev/null +++ b/src/secrets_tool/handlers/yamlcompat.py @@ -0,0 +1,76 @@ +""" +Classes to encrypt and decrypt YAML files. + +This 'compatibility' handler doesn't need additional tags in the YAML files. Instead, the desired paths +to en- and decrypt are given in the .gitignore file + +The desired paths for encryption MUST refer to a string field. +""" +from pathlib import Path +from typing import List + +import ruamel.yaml + +from .. import cipher + + +def getitem(obj, path: str): + if len(path) == 0: + return obj + + path_parts = path.split('.', maxsplit=1) + key = path_parts[0] + remainder = path_parts[1] if len(path_parts) > 1 else '' + + if isinstance(obj, dict): + return getitem(obj[key], remainder) + if isinstance(obj, list): + return getitem(obj[int(key)], remainder) + else: + ValueError('Cant read object', obj) + + +def setitem(root, path, value): + if path.find('.') < 0: + obj = root + key = path + else: + obj = getitem(root, path.rsplit('.', maxsplit=1)[0]) + key = path.rsplit('.', maxsplit=1)[1] + + if isinstance(obj, dict): + obj[key] = value + elif isinstance(obj, list): + obj[int(key)] = value + else: + ValueError('Cant write object', obj) + + +class YamlCompatFileHandler: + def __init__(self, filepath: Path, data: List[str]): + self.yaml = ruamel.yaml.YAML() + + self.filepath = filepath + self.data = data + + def dump_decrypted(self, target: Path, key: bytes): + tree = self.yaml.load(self.filepath) + + for path in self.data: + encrypted = getitem(tree, path) + decrypted = cipher.decrypt(encrypted.encode('ascii'), key).decode('utf8') + + setitem(tree, path, decrypted) + + self.yaml.dump(tree, target) + + def dump_encrypted(self, target: Path, key: bytes): + tree = self.yaml.load(self.filepath) + + for path in self.data: + decrypted = getitem(tree, path) + encrypted = cipher.encrypt(decrypted.encode('utf8'), key, path.encode('ascii')).decode('ascii') + + setitem(tree, path, encrypted) + + self.yaml.dump(tree, target)