diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..19d2f48 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +layout pyenv $(cat runtimes.txt) diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2bcd70e --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 88 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..dc8f064 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..adac1d2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + name: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - run: | + pip install -r requirements/test.txt + pip install -e . + pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faab669 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.egg-info +.direnv +.tox +__pycache__ +dist +venv + +# Add the following files to the allowlist +# under various searching tools. +!.envrc +!.flake8 +!.github +!.gitignore +!.pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6094b39 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,49 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-added-large-files # Prevent giant files from being committed + - id: check-ast # Simply check whether the files parse as valid python. + - id: check-builtin-literals # Require literal syntax when initializing empty or zero Python builtin types. + - id: check-byte-order-marker # forbid files which have a UTF-8 byte-order marker + - id: check-case-conflict # Check for files that would conflict in case-insensitive filesystems + - id: check-docstring-first # Checks a common error of defining a docstring after code. + - id: check-executables-have-shebangs # Ensures that (non-binary) executables have a shebang. + - id: check-json # This hook checks json files for parseable syntax. + - id: check-merge-conflict # Check for files that contain merge conflict strings. + - id: check-shebang-scripts-are-executable # Ensures that (non-binary) files with a shebang are executable. + - id: check-symlinks # Checks for symlinks which do not point to anything. + - id: check-toml # This hook checks toml files for parseable syntax. + - id: check-vcs-permalinks # Ensures that links to vcs websites are permalinks. + - id: check-xml # This hook checks xml files for parseable syntax. + - id: check-yaml # This hook checks yaml files for parseable syntax. + - id: debug-statements # Check for debugger imports and py37+ `breakpoint()` calls in python source. + - id: destroyed-symlinks # Detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to. + - id: detect-private-key # Detects the presence of private keys + - id: end-of-file-fixer # Ensures that a file is either empty, or ends with one newline. + - id: fix-byte-order-marker # Removes UTF-8 byte order marker + - id: forbid-new-submodules # Prevent addition of new git submodules + - id: mixed-line-ending # Replaces or checks mixed line ending + - id: requirements-txt-fixer # Sorts entries in requirements.txt + - id: sort-simple-yaml # Sorts simple YAML files which consist only of top-level keys, preserving comments and blocks. + - id: trailing-whitespace # This hook trims trailing whitespace. + + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + + - repo: https://github.com/timothycrosley/isort + rev: 5.10.1 + hooks: + - id: isort + + - repo: https://gitlab.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.931 + hooks: + - id: mypy diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..4d271d3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Joshua Taylor Eppinette + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cb4292 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Python SSSD LDAP Auth + +_A Python package which supports deobfuscating LDAP passwords +contained in (System Security Services Daemon) sssd.conf files._ + +## Inspiration + +- [Michael Ludvig](https://github.com/mludvig)'s [sss_deobfuscate](https://github.com/mludvig/sss_deobfuscate) script. +- [SSSD](https://github.com/SSSD/sssd)'s [/src/util/crypto/libcrypto/crypto_obfuscate.c](https://github.com/SSSD/sssd/blob/master/src/util/crypto/libcrypto/crypto_obfuscate.c) source file. + +## Features + +- Type Hints / Editor Completion +- Readable +- Fully Tested +- Python 3.6 - 3.10 Support + +## Install + +```sh +$ pip install sssdldapauth +``` + +## Usage + +```python +from sssdldapauth import deobfuscate + +password = deobfuscate("") +``` + +## Development + +### Required Software + +Refer to the links provided below to install these development dependencies: + +- [direnv](https://direnv.net) +- [git](https://git-scm.com/) +- [pyenv](https://github.com/pyenv/pyenv#installation) + +### Getting Started + +**Setup** + +```sh +$ .." + $ git tag v.. + $ git push origin main --tags + ``` + +4. Convert the tag to a release in GitHub with the history entry as the + description. + +**Build** + +```sh +$ python -m build +``` + +**Upload** + +``` +$ twine upload dist/* +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..69d974e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.black] +target-version = ["py310"] + +[tool.isort] +profile = "black" +sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..1690fba --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,9 @@ +-r ./publish.txt +-r ./test.txt +black==22.3.0 +flake8==4.0.1 +isort==5.10.1 +mypy==0.931 +pre-commit==2.17.0 +pytest-watch==4.2.0 +tox==3.24.5 diff --git a/requirements/publish.txt b/requirements/publish.txt new file mode 100644 index 0000000..df372b3 --- /dev/null +++ b/requirements/publish.txt @@ -0,0 +1,2 @@ +build==0.7.0 +twine==3.8.0 diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..4a46ff6 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1 @@ +pytest==7.0.0 diff --git a/runtimes.txt b/runtimes.txt new file mode 100644 index 0000000..ac18024 --- /dev/null +++ b/runtimes.txt @@ -0,0 +1 @@ +3.6.15 3.7.12 3.8.11 3.9.10 3.10.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1fdc02e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,20 @@ +[metadata] +name = sssdldapauth +version = attr: sssdldapauth.__version__ +description = Supports deobfuscating LDAP passwords contained in (System Security Services Daemon) sssd.conf files. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/jteppinette/python-sssd-ldap-auth +author = Joshua Taylor Eppinette +author_email = jteppinette@jteppinette.com +license = MIT + +[options] +packages = find: +python_requires = >= 3.6 +include_package_data = True +install_requires= + cryptography + +[options.package_data] +sssdldapauth = py.typed diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/sssdldapauth/__init__.py b/sssdldapauth/__init__.py new file mode 100644 index 0000000..2c37a88 --- /dev/null +++ b/sssdldapauth/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa + +from sssdldapauth.deobfuscate import deobfuscate + +__version__ = "0.0.0" diff --git a/sssdldapauth/deobfuscate.py b/sssdldapauth/deobfuscate.py new file mode 100644 index 0000000..3ff4242 --- /dev/null +++ b/sssdldapauth/deobfuscate.py @@ -0,0 +1,71 @@ +import base64 +import struct +from operator import itemgetter +from typing import Optional, Tuple, Union + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +METHODS = [ + { + "algo": algorithms.AES, + "mode": modes.CBC, + "key_len": 32, + "iv_len": 16, + } +] + +PopResult = Tuple[bytes, Union[bytes, Tuple]] + + +def pop(binary: bytes, length: int, unpack: Optional[str] = None) -> PopResult: + """ + Return the remainding binary data and the requested binary segment. + + If the unpack keyword arguent is provided, then the requested segment + will be unpacked accordingly. i.e. The unpack argument will be used as + the unpack format. When using the unpack functionality, the second + tuple index will be a tuple of unpacked items. + """ + if len(binary) < length: + raise Exception("requested segment too large") + + segment = binary[:length] + remainder = binary[length:] + + if unpack: + return remainder, struct.unpack(unpack, segment) + + return remainder, segment + + +def deobfuscate(token: str) -> str: + """ + Consume an obfuscated token and return the unobfuscated password. + """ + binary = base64.b64decode(token) + + binary, (method, ciphertext_len) = pop(binary, 4, "HH") + + try: + properties = METHODS[method] + except IndexError: + raise Exception("unsupported method") + + algo, mode, key_len, iv_len = itemgetter("algo", "mode", "key_len", "iv_len")( + properties + ) + + binary, encryption_key = pop(binary, key_len) + binary, iv = pop(binary, iv_len) + binary, ciphertext = pop(binary, ciphertext_len) + + cipher = Cipher(algo(encryption_key), mode(iv), backend=default_backend()) + decryptor = cipher.decryptor() + decrypted = decryptor.update(ciphertext) + decryptor.finalize() + + # Anything after \x00 can be thrown away. + password_binary = decrypted.split(b"\x00")[0] + + # UTF-8 passwords are supported (and any subset encodings e.g. ASCII). + return password_binary.decode("utf-8") diff --git a/sssdldapauth/py.typed b/sssdldapauth/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_deobfuscate.py b/tests/test_deobfuscate.py new file mode 100644 index 0000000..0a3e6eb --- /dev/null +++ b/tests/test_deobfuscate.py @@ -0,0 +1,19 @@ +import pytest + +from sssdldapauth.deobfuscate import deobfuscate + +OBFUSCATED = ( + "AAAQABagVAjf9KgUyIxTw3A+HUfbig7N1+L0qtY4xAULt2GY" + "HFc1B3CBWGAE9ArooklBkpxQtROiyCGDQH+VzLHYmiIAAQID" +) +DEOBFUSCATED = "Passw0rd" + + +def test_valid(): + assert deobfuscate(OBFUSCATED) == DEOBFUSCATED + + +@pytest.mark.parametrize("token", ["invalid", "", None]) +def test_invalid(token): + with pytest.raises(Exception): + deobfuscate(token) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c94c5be --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py36,py37,py38,py39,py310 + +[testenv] +deps = pytest +commands = pytest {posargs}