├── .coveragerc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitleaks.toml ├── .pre-commit-config.yaml ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.ln ├── README.md ├── build_helpers ├── lib.sh └── run-ci.sh ├── pyproject.toml ├── requirements-test.txt ├── src └── dpapi_ng │ ├── __init__.py │ ├── _asn1.py │ ├── _blob.py │ ├── _client.py │ ├── _crypto.py │ ├── _dns.py │ ├── _epm.py │ ├── _gkdi.py │ ├── _pkcs7.py │ ├── _rpc │ ├── __init__.py │ ├── _auth.py │ ├── _bind.py │ ├── _client.py │ ├── _pdu.py │ ├── _request.py │ └── _verification.py │ ├── _security_descriptor.py │ └── _version.py └── tests ├── __init__.py ├── _rpc ├── __init__.py ├── test_bind.py ├── test_pdu.py ├── test_request.py └── test_verification.py ├── conftest.py ├── data ├── dpapi_ng_blob ├── ecdh_key ├── ffc_dh_key ├── ffc_dh_parameters ├── group_key_envelope ├── kdf_sha1_dh.json ├── kdf_sha1_ecdh_p256.json ├── kdf_sha1_ecdh_p384.json ├── kdf_sha1_nonce.json ├── kdf_sha256_dh.json ├── kdf_sha256_ecdh_p256.json ├── kdf_sha256_ecdh_p384.json ├── kdf_sha256_nonce.json ├── kdf_sha384_dh.json ├── kdf_sha384_ecdh_p256.json ├── kdf_sha384_ecdh_p384.json ├── kdf_sha384_nonce.json ├── kdf_sha512_dh.json ├── kdf_sha512_ecdh_p256.json ├── kdf_sha512_ecdh_p384.json ├── kdf_sha512_nonce.json └── seed_key.json ├── integration ├── README.md ├── Vagrantfile ├── ansible.cfg ├── files │ ├── ConvertFrom-DpapiNgBlob.ps1 │ ├── ConvertTo-DpapiNgBlob.ps1 │ ├── New-KdsRootKey.ps1 │ ├── generate_seed_keys.py │ ├── sp800_56a_concat.ps1 │ └── sp800_56a_concat.py ├── inventory.yml ├── main.yml ├── requirements.yml ├── run_test.yml ├── templates │ ├── krb5.conf.j2 │ └── test_integration.py └── tests.yml ├── test_asn1.py ├── test_blob.py ├── test_client.py ├── test_crypto.py ├── test_epm.py ├── test_gkdi.py └── test_security_descriptor.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | src/dpapi_ng 4 | **/dist-packages/dpapi_ng 5 | **/site-packages/dpapi_ng 6 | [run] 7 | omit = 8 | src/dpapi_ng/_version.py -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test dpapi-ng 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - CHANGELOG.md 8 | - LICENSE 9 | - README.md 10 | 11 | pull_request: 12 | branches: 13 | - main 14 | paths-ignore: 15 | - CHANGELOG.md 16 | - LICENSE 17 | - README.md 18 | 19 | release: 20 | types: 21 | - published 22 | 23 | jobs: 24 | build: 25 | name: build project 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.13" 33 | 34 | - name: Build sdist and wheel 35 | run: | 36 | python -m pip install build 37 | python -m build --sdist --wheel 38 | 39 | - uses: actions/upload-artifact@v4 40 | with: 41 | name: artifact 42 | path: ./dist/* 43 | 44 | test: 45 | name: test 46 | needs: 47 | - build 48 | 49 | runs-on: ${{ matrix.os }} 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | os: 54 | - ubuntu-latest 55 | python-version: 56 | - 3.9 57 | - '3.10' 58 | - '3.11' 59 | - '3.12' 60 | - '3.13' 61 | python-arch: 62 | - x86 63 | - x64 64 | 65 | exclude: 66 | - os: ubuntu-latest 67 | python-arch: x86 68 | 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - name: Set up Python ${{ matrix.python-version }} 73 | uses: actions/setup-python@v5 74 | with: 75 | python-version: ${{ matrix.python-version }} 76 | architecture: ${{ matrix.python-arch }} 77 | 78 | - uses: actions/download-artifact@v4 79 | with: 80 | name: artifact 81 | path: ./dist 82 | 83 | - name: Extract OS name 84 | shell: bash 85 | run: | 86 | echo NAME=$( echo '${{ matrix.os }}' | tr '-' ' ' | awk '{print $1}' ) 87 | echo "name=${NAME}" >> $GITHUB_OUTPUT 88 | id: os 89 | 90 | - name: Test 91 | shell: bash 92 | run: | 93 | if [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then 94 | build_helpers/run-ci.sh 95 | else 96 | sudo -E build_helpers/run-ci.sh 97 | fi 98 | env: 99 | PYTEST_ADDOPTS: --color=yes 100 | 101 | - name: Upload Test Results 102 | if: always() 103 | uses: actions/upload-artifact@v4 104 | with: 105 | name: Unit Test Results (${{ matrix.os }} ${{ matrix.python-version }} ${{ matrix.python-arch }}) 106 | path: ./junit/test-results.xml 107 | 108 | - name: Upload Coverage Results 109 | if: always() && !startsWith(github.ref, 'refs/tags/v') 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: Coverage Results (${{ matrix.os }} ${{ matrix.python-version }} ${{ matrix.python-arch }}) 113 | path: ./coverage.xml 114 | 115 | - name: Upload Coverage to codecov 116 | if: always() 117 | uses: codecov/codecov-action@v5 118 | with: 119 | files: ./coverage.xml 120 | flags: ${{ steps.os.outputs.name }},py${{ matrix.python-version }},${{ matrix.python-arch }} 121 | token: ${{ secrets.CODECOV_TOKEN }} 122 | 123 | publish: 124 | name: publish 125 | needs: 126 | - test 127 | runs-on: ubuntu-latest 128 | permissions: 129 | # IMPORTANT: this permission is mandatory for trusted publishing 130 | id-token: write 131 | 132 | steps: 133 | - uses: actions/download-artifact@v4 134 | with: 135 | name: artifact 136 | path: ./dist 137 | 138 | - name: Publish 139 | if: startsWith(github.ref, 'refs/tags/v') 140 | uses: pypa/gh-action-pypi-publish@release/v1 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | test-results.xml 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # IDEA Files 106 | .idea 107 | *~ 108 | 109 | # Visual Studio Code Files 110 | .vscode/launch.json 111 | 112 | # MacOS File 113 | .DS_Store 114 | 115 | # Vagrant 116 | .vagrant/ 117 | 118 | # Cython 119 | *.c 120 | *.pyd 121 | 122 | # vim 123 | *.swp 124 | 125 | ### Custom entries ### 126 | tests/integration/.vagrant 127 | tests/integration/artifact.zip 128 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | [allowlist] 2 | description = "Global Allowlist" 3 | 4 | # Ignore based on any subset of the file path 5 | paths = [ 6 | # Ignore test data 7 | '''tests\/integration\/files\/sp800_56a_concat\.ps1''', 8 | ] 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 24.10.0 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://github.com/PyCQA/isort 8 | rev: 5.13.2 9 | hooks: 10 | - id: isort 11 | 12 | - repo: https://github.com/pre-commit/mirrors-mypy 13 | rev: v1.13.0 14 | hooks: 15 | - id: mypy 16 | exclude: ^setup.py|build/ 17 | additional_dependencies: 18 | - cryptography >= 3.4.4 19 | - dnspython >= 2.0.0 20 | - pytest 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.black-formatter", 4 | "ms-python.isort", 5 | "ms-python.mypy-type-checker", 6 | "ms-python.vscode-pylance", 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [ 3 | 80, 4 | 120 5 | ], 6 | "files.trimTrailingWhitespace": true, 7 | "editor.trimAutoWhitespace": true, 8 | "black-formatter.importStrategy": "fromEnvironment", 9 | "isort.importStrategy": "fromEnvironment", 10 | "mypy-type-checker.importStrategy": "fromEnvironment", 11 | "python.analysis.extraPaths": [ 12 | "src", 13 | ], 14 | "[python]": { 15 | "editor.defaultFormatter": "ms-python.black-formatter", 16 | "editor.formatOnSave": true, 17 | "editor.codeActionsOnSave": { 18 | "source.organizeImports": "explicit" 19 | }, 20 | }, 21 | "python.testing.pytestArgs": [ 22 | "tests" 23 | ], 24 | "python.testing.unittestEnabled": false, 25 | "python.testing.pytestEnabled": true, 26 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 - TBD 4 | 5 | + Dropped end of life Python versions 3.7, and 3.8 6 | + Added explicit support for Python 3.12, and 3.13 7 | + Added mininum version for the `cryptography` dep at `>= 3.4.4` 8 | 9 | ## 0.2.0 - 2023-06-02 10 | 11 | + Added functions to encrypt data using DPAPI-NG: 12 | + `async_ncrypt_protect_secret` 13 | + `ncrypt_protect_secret` 14 | + Fixed packing of DH and ECDH key structures with small integer values 15 | 16 | ## 0.1.1 - 2023-05-16 17 | 18 | + Fix up RPC stub data unpacking with no padding data - https://github.com/jborean93/dpapi-ng/issues/2 19 | 20 | ## 0.1.0 - 2023-05-09 21 | 22 | Initial release of `dpapi-ng` 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jordan Borean, Red Hat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.ln: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGELOG.md 3 | include requirements-test.txt 4 | exclude .coverage 5 | exclude .gitignore 6 | exclude .pre-commit-config.yaml 7 | recursive-include build_helpers * 8 | recursive-include tests * 9 | recursive-exclude tests *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dpapi_ng - Python DPAPI-NG De-/Encryption Library 2 | 3 | [![Test workflow](https://github.com/jborean93/dpapi-ng/actions/workflows/ci.yml/badge.svg)](https://github.com/jborean93/dpapi-ng/actions/workflows/ci.yml) 4 | [![codecov](https://codecov.io/gh/jborean93/dpapi-ng/branch/main/graph/badge.svg?token=UEA7VoocS5)](https://codecov.io/gh/jborean93/dpapi-ng) 5 | [![PyPI version](https://badge.fury.io/py/dpapi-ng.svg)](https://badge.fury.io/py/dpapi-ng) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jborean93/dpapi-ng/blob/main/LICENSE) 7 | 8 | Library for [DPAPI NG](https://learn.microsoft.com/en-us/windows/win32/seccng/cng-dpapi), also known as CNG DPAPI, de- and encryption in Python. 9 | It is designed to replicate the behaviour of [NCryptUnprotectSecret](https://learn.microsoft.com/en-us/windows/win32/api/ncryptprotect/nf-ncryptprotect-ncryptunprotectsecret) and [NCryptProtectSecret](https://learn.microsoft.com/en-us/windows/win32/api/ncryptprotect/nf-ncryptprotect-ncryptprotectsecret). 10 | This can be used on non-Windows hosts to de-/encrypt DPAPI NG protected secrets, like PFX user protected password, or LAPS encrypted password. 11 | It can either decrypt any DPAPI NG blobs using an offline copy of the domain's root key or de-/encrypt by using the credentials of the supplied user to retrieve the required information over RPC. 12 | 13 | Currently only these protection descriptors are supported: 14 | 15 | |Type|Purpose| 16 | |-|-| 17 | |SID|Only the SID user or members of the SID group can decrypt the secret| 18 | 19 | This implements the [MS-GKDI Group Key Distribution Protocol](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gkdi/943dd4f6-6b80-4a66-8594-80df6d2aad0a). 20 | 21 | ## Requirements 22 | 23 | * CPython 3.9+ 24 | * [cryptography >= 3.1](https://pypi.org/project/cryptography/) 25 | * [dnspython >= 2.0.0](https://pypi.org/project/dnspython/) 26 | * [pyspnego >= 0.9.0](https://pypi.org/project/pyspnego/) 27 | 28 | ## How to Install 29 | 30 | To install dpapi-ng with all the basic features, run 31 | 32 | ```bash 33 | python -m pip install dpapi-ng 34 | ``` 35 | 36 | ### Kerberos Authentication 37 | 38 | Kerberos authentication support won't be installed by default as it relies on system libraries and a valid compiler to be present. 39 | The krb5 library and compiler can be installed by installing these packages: 40 | 41 | ```bash 42 | # Debian/Ubuntu 43 | apt-get install gcc python3-dev libkrb5-dev 44 | 45 | # Centos/RHEL 46 | yum install gcc python-devel krb5-devel 47 | 48 | # Fedora 49 | dnf install gcc python-devel krb5-devel 50 | 51 | # Arch Linux 52 | pacman -S gcc krb5 53 | ``` 54 | 55 | Once installed, the Kerberos Python extras can be installed with 56 | 57 | ```bash 58 | python -m pip install dpapi-ng[kerberos] 59 | ``` 60 | 61 | Kerberos also needs to be configured to talk to the domain but that is outside the scope of this page. 62 | 63 | ### From Source 64 | 65 | ```bash 66 | git clone https://github.com/jborean93/dpapi-ng.git 67 | cd dpapi-ng 68 | pip install -e . 69 | ``` 70 | 71 | ## Examples 72 | 73 | There is both a sync and asyncio API available to de-/encrypt a blob. 74 | 75 | ```python 76 | import dpapi_ng 77 | 78 | 79 | ### DECRYPTION ### 80 | dpapi_ng_blob = b"..." 81 | decrypted_blob = dpapi_ng.ncrypt_unprotect_secret(dpapi_ng_blob) 82 | 83 | # async equivalent to the above 84 | decrypted_blob = await dpapi_ng.async_ncrypt_unprotect_secret(dpapi_ng_blob) 85 | 86 | 87 | ### ENCRYPTION ### 88 | data = b"..." 89 | target_sid = "S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-XXXX" 90 | dpapi_ng_blob = dpapi_ng.ncrypt_protect_secret(data, target_sid) 91 | 92 | # async equivalent to the above 93 | dpapi_ng_blob = await dpapi_ng.async_ncrypt_protect_secret(data, target_sid) 94 | ``` 95 | 96 | To decrypt the blob, the key specified in the blob needs to be retrieved from the domain controller the blob was generated by. 97 | To encrypt a blob, the group key of the target SID specified needs to be retrieved. 98 | For decryption, the domain controller hostname is automatically retrieved through an DNS `SRV` lookup of `_ldap._tcp.dc._msdcs.{domain_name}` with the domain name found in the DPAPI-NG blob or with the value specified in the `server` kwarg. 99 | For encryption, the domain controller hostname is either determined using the `domain_name` kwarg for a DNS `SRV` lookup, the `server` kwarg directly or by using the system's search domain (if available). 100 | It will attempt to authenticate with the current user identifier which on Linux will only exist if `kinit` has already been called to retrieve a user's ticket. 101 | Otherwise if no identity is available, the `username` and `password` kwargs can be used to specify a custom user. 102 | 103 | The following kwargs can be used for `ncrypt_unprotect_secret`, `async_ncrypt_unprotect_secret`, `ncrypt_protect_secret` and `async_ncrypt_protect_secret`. 104 | 105 | * `server`: Use this server as the RPC target if a key needs to be retrieved 106 | * `username`: The username to authenticate as for the RPC connection 107 | * `password`: The password to authenticate with for the RPC connection 108 | * `auth_protocol`: The authentication protocol (`negotiate`, `kerberos`, `ntlm`) to use for the RPC connection 109 | * `cache`: A cache to store keys retrieved for future operation 110 | 111 | In addition to that, `ncrypt_protect_secret` and `async_ncrypt_protect_secret` take the following additional kwarg. 112 | 113 | * `domain_name`: The name of the domain/forest to use when looking up the RPC target to use when retrieving the key info 114 | * `root_key_identifier`: The root key identifier UUID, necessary in order to make the cache work for these functions 115 | 116 | It is also possible to encrypt and decrypt the DPAPI-NG blob by providing the root key stored in the domain. 117 | This can either be retrieved using an offline attack or through an LDAP query if running as a Domain Admin user. 118 | To retrieve the domain root keys using PowerShell the following can be run: 119 | 120 | ```powershell 121 | $configurationContext = (Get-ADRootDSE).configurationNamingContext 122 | $getParams = @{ 123 | LDAPFilter = '(objectClass=msKds-ProvRootKey)' 124 | SearchBase = "CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,$configurationContext" 125 | SearchScope = 'OneLevel' 126 | Properties = @( 127 | 'cn' 128 | 'msKds-KDFAlgorithmID' 129 | 'msKds-KDFParam' 130 | 'msKds-SecretAgreementAlgorithmID' 131 | 'msKds-SecretAgreementParam' 132 | 'msKds-PrivateKeyLength' 133 | 'msKds-PublicKeyLength' 134 | 'msKds-RootKeyData' 135 | ) 136 | } 137 | Get-ADObject @getParams | ForEach-Object { 138 | [PSCustomObject]@{ 139 | Version = 1 140 | RootKeyId = [Guid]::new($_.cn) 141 | KdfAlgorithm = $_.'msKds-KDFAlgorithmID' 142 | KdfParameters = [System.Convert]::ToBase64String($_.'msKds-KDFParam') 143 | SecretAgreementAlgorithm = $_.'msKds-SecretAgreementAlgorithmID' 144 | SecretAgreementParameters = [System.Convert]::ToBase64String($_.'msKds-SecretAgreementParam') 145 | PrivateKeyLength = $_.'msKds-PrivateKeyLength' 146 | PublicKeyLength = $_.'msKds-PublicKeyLength' 147 | RootKeyData = [System.Convert]::ToBase64String($_.'msKds-RootKeyData') 148 | } 149 | } 150 | ``` 151 | 152 | The following `ldapsearch` command can be used outside of Windows: 153 | 154 | ```bash 155 | ldapsearch \ 156 | -b 'CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,DC=domain,DC=test' \ 157 | -s one \ 158 | '(objectClass=msKds-ProvRootKey)' \ 159 | cn \ 160 | msKds-KDFAlgorithmID \ 161 | msKds-KDFParam \ 162 | msKds-SecretAgreementAlgorithmID \ 163 | msKds-SecretAgreementParam \ 164 | msKds-PrivateKeyLength \ 165 | msKds-PublicKeyLength \ 166 | msKds-RootKeyData 167 | ``` 168 | 169 | _Note: ldapsearch will most likely need the -H and user bind information to succeed._ 170 | 171 | The information retrieved there can be stored in a cache and used for subsequent `ncrypt_protect_secret` and `ncrypt_unprotect_secret` calls: 172 | 173 | ```python 174 | import uuid 175 | 176 | import dpapi_ng 177 | 178 | cache = dpapi_ng.KeyCache() 179 | 180 | root_key_id = uuid.UUID("76ec8b2d-d444-4f67-9db7-2f62b4358b35") 181 | cache.load_key( 182 | b"...", # msKds-RootKeydata 183 | root_key_id, # cn 184 | version=1, 185 | kdf_algorithm="SP800_108_CTR_HMAC", # msKds-KDFAlgorithmID 186 | kdf_parameters=b"...", # msKds-KDFParam 187 | secret_algorithm="DH", # mskds-SecretAgreementAlgorithmID 188 | secret_parameters=b"...", # msKds-SecretAgreementParam 189 | private_key_length=512, # msKds-PrivateKeyLength 190 | public_key_length=2048, # msKds-PublicKeyLength 191 | ) 192 | 193 | dpapi_ng.ncrypt_unprotect_secret(b"...", cache=cache) 194 | ``` 195 | 196 | Currently the `SP800_108_CTR_HMAC` KDF algorithm and `DH`, `ECDH_P256`, and `ECDH_P384` secret agreement algorithms have been tested to work. 197 | The `ECDH_P521` secret agreement algorithm should also work but has been untested as a test environment cannot be created with it right now. 198 | 199 | ## Special Thanks 200 | 201 | I would like to thank the following people (GitHub or Twitter handles in brackets) for their help on this project: 202 | 203 | * Georg Sieber (@schorschii) for implementing encryption support 204 | * Grzegorz Tworek (@0gtweet) and Michał Grzegorzewski for providing more information on the internal BCrypt* API workflow used in DPAPI-NG 205 | * Marc-André Moreau (@awakecoding) for their help with reverse engineering some of the Windows APIs and talking through some theories 206 | * SkelSec (@SkelSec) for help on the RPC calls and being available as a general sounding board for my theories 207 | * Steve Syfuhs (@SteveSyfuhs) for connecting me with some Microsoft engineers to help understand some undocumented logic 208 | 209 | Without their patience and knowledge this probably would not have been possible. 210 | -------------------------------------------------------------------------------- /build_helpers/lib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | lib::setup::system_requirements() { 4 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 5 | echo "::group::Installing System Requirements" 6 | fi 7 | 8 | echo "No system requirements required" 9 | 10 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 11 | echo "::endgroup::" 12 | fi 13 | } 14 | 15 | lib::setup::python_requirements() { 16 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 17 | echo "::group::Installing Python Requirements" 18 | fi 19 | 20 | echo "Installing dpapi-ng" 21 | if [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then 22 | DIST_LINK_PATH="$( echo "${PWD}/dist" | sed -e 's/^\///' -e 's/\//\\/g' -e 's/^./\0:/' )" 23 | else 24 | DIST_LINK_PATH="${PWD}/dist" 25 | fi 26 | 27 | # Getting the version is important so that pip prioritises our local dist 28 | python -m pip install build 29 | PACKAGE_VERSION="$( python -c "import build.util; print(build.util.project_wheel_metadata('.').get('Version'))" )" 30 | 31 | python -m pip install dpapi-ng=="${PACKAGE_VERSION}" \ 32 | --find-links "file://${DIST_LINK_PATH}" \ 33 | --verbose 34 | 35 | echo "Installing dev dependencies" 36 | python -m pip install -r requirements-test.txt 37 | 38 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 39 | echo "::endgroup::" 40 | fi 41 | } 42 | 43 | lib::sanity::run() { 44 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 45 | echo "::group::Running Sanity Checks" 46 | fi 47 | 48 | python -m black . --check 49 | python -m isort . --check-only 50 | python -m mypy . 51 | 52 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 53 | echo "::endgroup::" 54 | fi 55 | } 56 | 57 | lib::tests::run() { 58 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 59 | echo "::group::Running Tests" 60 | fi 61 | 62 | python -m pytest \ 63 | -v \ 64 | --junitxml junit/test-results.xml \ 65 | --cov dpapi_ng \ 66 | --cov-branch \ 67 | --cov-report xml \ 68 | --cov-report term-missing 69 | 70 | if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then 71 | echo "::endgroup::" 72 | fi 73 | } 74 | -------------------------------------------------------------------------------- /build_helpers/run-ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | source ./build_helpers/lib.sh 4 | lib::setup::system_requirements 5 | lib::setup::python_requirements 6 | lib::sanity::run 7 | lib::tests::run -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 61.0.0", # Support for setuptools config in pyproject.toml 4 | ] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "dpapi-ng" 9 | description = "DPAPI NG decryption for Python" 10 | readme = "README.md" 11 | requires-python = ">=3.9" 12 | license = {file = "LICENSE"} 13 | authors = [ 14 | { name = "Jordan Borean", email = "jborean93@gmail.com" } 15 | ] 16 | keywords = ["dpapi", "dpapi-ng", "laps"] 17 | classifiers = [ 18 | "Development Status :: 4 - Beta", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Programming Language :: Python :: 3.13" 26 | ] 27 | dependencies = [ 28 | "dnspython >= 2.0.0", # Needed for DC lookup support 29 | "cryptography >= 3.4.4", # py.typed added, runtime technically need 3.1 but this makes it simpler for testing 30 | "pyspnego >= 0.9.0", # Needed for DCE support 31 | ] 32 | dynamic = ["version"] 33 | 34 | [project.optional-dependencies] 35 | kerberos = [ 36 | "pyspnego[kerberos] >= 0.9.0" 37 | ] 38 | 39 | [project.urls] 40 | homepage = "https://github.com/jborean93/dpapi-ng" 41 | 42 | [tool.setuptools] 43 | include-package-data = true 44 | 45 | [tool.setuptools.dynamic] 46 | version = {attr = "dpapi_ng._version.__version__"} 47 | 48 | [tool.setuptools.packages.find] 49 | where = ["src"] 50 | 51 | [tool.setuptools.package-data] 52 | sansldap = ["py.typed"] 53 | 54 | [tool.black] 55 | line-length = 120 56 | include = '\.pyi?$' 57 | exclude = ''' 58 | /( 59 | \.git 60 | | \.hg 61 | | \.mypy_cache 62 | | \.tox 63 | | \.venv 64 | | _build 65 | | buck-out 66 | | build 67 | | dist 68 | )/ 69 | ''' 70 | 71 | [tool.isort] 72 | profile = "black" 73 | 74 | [tool.mypy] 75 | exclude = "setup.py|build/|docs/|tests/integration/" 76 | mypy_path = "$MYPY_CONFIG_FILE_DIR/src" 77 | show_error_codes = true 78 | show_column_numbers = true 79 | disallow_any_unimported = true 80 | disallow_untyped_calls = true 81 | disallow_untyped_defs = true 82 | disallow_incomplete_defs = true 83 | check_untyped_defs = true 84 | disallow_untyped_decorators = true 85 | warn_redundant_casts = true 86 | warn_unused_ignores = true 87 | 88 | [tool.pytest.ini_options] 89 | asyncio_mode = "auto" 90 | testpaths = "tests" 91 | junit_family = "xunit2" 92 | norecursedirs = "tests/integration" 93 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | black == 24.10.0 2 | isort == 5.13.2 3 | mypy == 1.13.0 4 | pre-commit 5 | pytest 6 | pytest-asyncio 7 | pytest-cov 8 | -------------------------------------------------------------------------------- /src/dpapi_ng/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | from ._client import ( 7 | KeyCache, 8 | async_ncrypt_protect_secret, 9 | async_ncrypt_unprotect_secret, 10 | ncrypt_protect_secret, 11 | ncrypt_unprotect_secret, 12 | ) 13 | 14 | __all__ = [ 15 | "KeyCache", 16 | "async_ncrypt_protect_secret", 17 | "async_ncrypt_unprotect_secret", 18 | "ncrypt_protect_secret", 19 | "ncrypt_unprotect_secret", 20 | ] 21 | -------------------------------------------------------------------------------- /src/dpapi_ng/_blob.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import dataclasses 7 | import enum 8 | import typing as t 9 | import uuid 10 | 11 | from ._asn1 import ASN1Reader, ASN1Writer 12 | from ._pkcs7 import ( 13 | AlgorithmIdentifier, 14 | ContentInfo, 15 | EncryptedContentInfo, 16 | EnvelopedData, 17 | KEKIdentifier, 18 | KEKRecipientInfo, 19 | OtherKeyAttribute, 20 | ) 21 | from ._security_descriptor import ace_to_bytes, sd_to_bytes 22 | 23 | 24 | @dataclasses.dataclass(frozen=True) 25 | class KeyIdentifier: 26 | """Key Identifier. 27 | 28 | This contains the key identifier info that can be used by MS-GKDI GetKey 29 | to retrieve the group key seed values. This structure is not defined 30 | publicly by Microsoft but it closely matches the :class:`GroupKeyEnvelope` 31 | structure. 32 | 33 | Args: 34 | version: The version of the structure, should be 1 35 | flags: Flags describing the values inside the structure 36 | l0: The L0 index of the key 37 | l1: The L1 index of the key 38 | l2: The L2 index of the key 39 | root_key_identifier: The key identifier 40 | key_info: If is_public_key this is the public key, else it is the key 41 | KDF context value. 42 | domain_name: The domain name of the server in DNS format. 43 | forest_name: The forest name of the server in DNS format. 44 | """ 45 | 46 | version: int 47 | magic: bytes = dataclasses.field(init=False, repr=False, default=b"\x4B\x44\x53\x4B") 48 | flags: int 49 | l0: int 50 | l1: int 51 | l2: int 52 | root_key_identifier: uuid.UUID 53 | key_info: bytes 54 | domain_name: str 55 | forest_name: str 56 | 57 | @property 58 | def is_public_key(self) -> bool: 59 | return bool(self.flags & 1) 60 | 61 | def pack(self) -> bytes: 62 | b_domain_name = (self.domain_name + "\00").encode("utf-16-le") 63 | b_forest_name = (self.forest_name + "\00").encode("utf-16-le") 64 | 65 | return b"".join( 66 | [ 67 | self.version.to_bytes(4, byteorder="little"), 68 | self.magic, 69 | self.flags.to_bytes(4, byteorder="little"), 70 | self.l0.to_bytes(4, byteorder="little"), 71 | self.l1.to_bytes(4, byteorder="little"), 72 | self.l2.to_bytes(4, byteorder="little"), 73 | self.root_key_identifier.bytes_le, 74 | len(self.key_info).to_bytes(4, byteorder="little"), 75 | len(b_domain_name).to_bytes(4, byteorder="little"), 76 | len(b_forest_name).to_bytes(4, byteorder="little"), 77 | self.key_info, 78 | b_domain_name, 79 | b_forest_name, 80 | ] 81 | ) 82 | 83 | @classmethod 84 | def unpack( 85 | cls, 86 | data: t.Union[bytes, bytearray, memoryview], 87 | ) -> KeyIdentifier: 88 | view = memoryview(data) 89 | 90 | version = int.from_bytes(view[:4], byteorder="little") 91 | 92 | if view[4:8].tobytes() != cls.magic: 93 | raise ValueError(f"Failed to unpack {cls.__name__} as magic identifier is invalid") 94 | 95 | flags = int.from_bytes(view[8:12], byteorder="little") 96 | l0_index = int.from_bytes(view[12:16], byteorder="little") 97 | l1_index = int.from_bytes(view[16:20], byteorder="little") 98 | l2_index = int.from_bytes(view[20:24], byteorder="little") 99 | root_key_identifier = uuid.UUID(bytes_le=view[24:40].tobytes()) 100 | key_info_len = int.from_bytes(view[40:44], byteorder="little") 101 | domain_len = int.from_bytes(view[44:48], byteorder="little") 102 | forest_len = int.from_bytes(view[48:52], byteorder="little") 103 | view = view[52:] 104 | 105 | key_info = view[:key_info_len].tobytes() 106 | view = view[key_info_len:] 107 | 108 | # Take away 2 for the final null padding 109 | domain = view[: domain_len - 2].tobytes().decode("utf-16-le") 110 | view = view[domain_len:] 111 | 112 | forest = view[: forest_len - 2].tobytes().decode("utf-16-le") 113 | view = view[forest_len:] 114 | 115 | return KeyIdentifier( 116 | version=version, 117 | flags=flags, 118 | l0=l0_index, 119 | l1=l1_index, 120 | l2=l2_index, 121 | root_key_identifier=root_key_identifier, 122 | key_info=key_info, 123 | domain_name=domain, 124 | forest_name=forest, 125 | ) 126 | 127 | 128 | class ProtectionDescriptorType(enum.Enum): 129 | SID = "1.3.6.1.4.1.311.74.1.1" 130 | KEY_FILE = "1.3.6.1.4.1.311.74.1.2" # KeyFile in UF8String type 131 | SDDL = "1.3.6.1.4.1.311.74.1.5" 132 | LOCAL = "1.3.6.1.4.1.311.74.1.8" 133 | 134 | 135 | @dataclasses.dataclass(frozen=True) 136 | class ProtectionDescriptor: 137 | type: ProtectionDescriptorType 138 | value: str 139 | 140 | def get_target_sd(self) -> bytes: 141 | raise NotImplementedError() # pragma: nocover 142 | 143 | def pack(self) -> bytes: 144 | writer = ASN1Writer() 145 | 146 | with writer.push_sequence() as w: 147 | w.write_object_identifier(self.type.value) 148 | 149 | with w.push_sequence() as w1: 150 | with w1.push_sequence() as w2: 151 | with w2.push_sequence() as w3: 152 | w3.write_utf8_string(self.type.name) 153 | w3.write_utf8_string(self.value) 154 | 155 | return writer.get_data() 156 | 157 | @classmethod 158 | def parse( 159 | cls, 160 | value: str, 161 | ) -> ProtectionDescriptor: 162 | # Currently only the SID type is supported 163 | return SIDDescriptor(value) 164 | 165 | @classmethod 166 | def unpack( 167 | cls, 168 | data: t.Union[bytes, bytearray, memoryview], 169 | ) -> ProtectionDescriptor: 170 | reader = ASN1Reader(data).read_sequence() 171 | content_type = reader.read_object_identifier() 172 | 173 | reader = reader.read_sequence().read_sequence().read_sequence() 174 | value_type = reader.read_utf8_string() 175 | value = reader.read_utf8_string() 176 | 177 | if content_type == ProtectionDescriptorType.SID.value and value_type == "SID": 178 | return SIDDescriptor(value) 179 | 180 | else: 181 | raise ValueError(f"DPAPI-NG protection descriptor type {content_type} '{value_type}' is unsupported") 182 | 183 | 184 | @dataclasses.dataclass(frozen=True) 185 | class SIDDescriptor(ProtectionDescriptor): 186 | type: ProtectionDescriptorType = dataclasses.field(init=False, default=ProtectionDescriptorType.SID) 187 | 188 | def get_target_sd(self) -> bytes: 189 | # Build the target security descriptor from the SID passed in. This SD 190 | # contains an ACE per target user with a mask of 0x3 and a final ACE of 191 | # the current user with a mask of 0x2. When viewing this over the wire 192 | # the current user is set as S-1-1-0 (World) and the owner/group is 193 | # S-1-5-18 (SYSTEM). 194 | return sd_to_bytes( 195 | owner="S-1-5-18", 196 | group="S-1-5-18", 197 | dacl=[ace_to_bytes(self.value, 3), ace_to_bytes("S-1-1-0", 2)], 198 | ) 199 | 200 | 201 | @dataclasses.dataclass 202 | class DPAPINGBlob: 203 | MICROSOFT_SOFTWARE_OID = "1.3.6.1.4.1.311.74.1" 204 | 205 | """DPAPI NG Blob. 206 | 207 | The unpacked DPAPI NG blob that contains the information needed to decrypt 208 | the encrypted content. The key identifier and protection descriptor can be 209 | used to generate the KEK. The KEK is used to decrypt the encrypted CEK. The 210 | CEK can be used to decrypt the encrypted contents. 211 | 212 | Args: 213 | key_identifier: The key identifier for the KEK. 214 | protection_descriptor: The protection descriptor that protects the key. 215 | enc_cek: The encrypted CEK. 216 | enc_cek_algorithm: The encrypted CEK algorithm OID. 217 | enc_cek_parameters: The encrypted CEK algorithm parameters. 218 | enc_content: The encrypted content. 219 | enc_content_algorithm: The encrypted content algorithm OID. 220 | enc_content_parameters: The encrypted content parameters. 221 | """ 222 | 223 | key_identifier: KeyIdentifier 224 | protection_descriptor: ProtectionDescriptor 225 | enc_cek: bytes 226 | enc_cek_algorithm: str 227 | enc_cek_parameters: t.Optional[bytes] 228 | enc_content: bytes 229 | enc_content_algorithm: str 230 | enc_content_parameters: t.Optional[bytes] 231 | 232 | def pack( 233 | self, 234 | blob_in_envelope: bool = True, 235 | ) -> bytes: 236 | """Pack the DPAPI-NG Blob 237 | 238 | Packs the DPAPI-NG blob into a byte string. 239 | 240 | Args: 241 | blob_in_envelope: True to store the encrypted blob in the 242 | EnvelopedData structure (NCryptProtectSecret general), False to 243 | append the encrypted blob after the EnvelopedData structure 244 | (LAPS style). 245 | 246 | Returns: 247 | bytes: The DPAPI NG Blob data. 248 | """ 249 | writer = ASN1Writer() 250 | 251 | recipient_info = KEKRecipientInfo( 252 | version=4, 253 | kekid=KEKIdentifier( 254 | key_identifier=self.key_identifier.pack(), 255 | other=OtherKeyAttribute( 256 | key_attr_id=DPAPINGBlob.MICROSOFT_SOFTWARE_OID, 257 | key_attr=self.protection_descriptor.pack(), 258 | ), 259 | ), 260 | key_encryption_algorithm=AlgorithmIdentifier( 261 | self.enc_cek_algorithm, 262 | self.enc_cek_parameters, 263 | ), 264 | encrypted_key=self.enc_cek, 265 | ) 266 | 267 | enveloped_data = EnvelopedData( 268 | version=2, 269 | recipient_infos=[recipient_info], 270 | encrypted_content_info=EncryptedContentInfo( 271 | content_type=EnvelopedData.CONTENT_TYPE_DATA_OID, 272 | algorithm=AlgorithmIdentifier( 273 | algorithm=self.enc_content_algorithm, 274 | parameters=self.enc_content_parameters, 275 | ), 276 | content=self.enc_content if blob_in_envelope else b"", 277 | ), 278 | ) 279 | writer = ASN1Writer() 280 | enveloped_data.pack(writer) 281 | 282 | content_info = ContentInfo( 283 | content_type=EnvelopedData.CONTENT_TYPE_ENVELOPED_DATA_OID, 284 | content=writer.get_data(), 285 | ) 286 | writer = ASN1Writer() 287 | content_info.pack(writer) 288 | 289 | return b"".join( 290 | [ 291 | writer.get_data(), 292 | b"" if blob_in_envelope else self.enc_content, 293 | ] 294 | ) 295 | 296 | @classmethod 297 | def unpack( 298 | cls, 299 | data: t.Union[bytes, bytearray, memoryview], 300 | ) -> DPAPINGBlob: 301 | view = memoryview(data) 302 | header = ASN1Reader(view).peek_header() 303 | content_info = ContentInfo.unpack(view[: header.tag_length + header.length], header=header) 304 | remaining_data = view[header.tag_length + header.length :] 305 | 306 | if content_info.content_type != EnvelopedData.CONTENT_TYPE_ENVELOPED_DATA_OID: 307 | raise ValueError(f"DPAPI-NG blob content type '{content_info.content_type}' is unsupported") 308 | enveloped_data = EnvelopedData.unpack(content_info.content) 309 | 310 | if ( 311 | enveloped_data.version != 2 312 | or len(enveloped_data.recipient_infos) != 1 313 | or not isinstance(enveloped_data.recipient_infos[0], KEKRecipientInfo) 314 | or enveloped_data.recipient_infos[0].version != 4 315 | ): 316 | raise ValueError(f"DPAPI-NG blob is not in the expected format") 317 | 318 | kek_info = enveloped_data.recipient_infos[0] 319 | key_identifier = KeyIdentifier.unpack(kek_info.kekid.key_identifier) 320 | 321 | if not kek_info.kekid.other or kek_info.kekid.other.key_attr_id != DPAPINGBlob.MICROSOFT_SOFTWARE_OID: 322 | raise ValueError("DPAPI-NG KEK Id is not in the expected format") 323 | 324 | protection_descriptor = ProtectionDescriptor.unpack(kek_info.kekid.other.key_attr or b"") 325 | 326 | # Some DPAPI blobs don't include the content in the PKCS7 payload but 327 | # just append after the blob. 328 | enc_content = enveloped_data.encrypted_content_info.content or remaining_data.tobytes() 329 | 330 | return DPAPINGBlob( 331 | key_identifier=key_identifier, 332 | protection_descriptor=protection_descriptor, 333 | enc_cek=kek_info.encrypted_key, 334 | enc_cek_algorithm=kek_info.key_encryption_algorithm.algorithm, 335 | enc_cek_parameters=kek_info.key_encryption_algorithm.parameters, 336 | enc_content=enc_content, 337 | enc_content_algorithm=enveloped_data.encrypted_content_info.algorithm.algorithm, 338 | enc_content_parameters=enveloped_data.encrypted_content_info.algorithm.parameters, 339 | ) 340 | -------------------------------------------------------------------------------- /src/dpapi_ng/_crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import enum 7 | import os 8 | import typing as t 9 | 10 | from cryptography.hazmat.primitives import hashes, keywrap 11 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM 12 | from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash 13 | from cryptography.hazmat.primitives.kdf.kbkdf import KBKDFHMAC, CounterLocation, Mode 14 | 15 | from ._asn1 import ASN1Reader 16 | 17 | 18 | class AlgorithmOID(str, enum.Enum): 19 | """OIDs for cryptographic algorithms.""" 20 | 21 | AES256_WRAP = "2.16.840.1.101.3.4.1.45" 22 | AES256_GCM = "2.16.840.1.101.3.4.1.46" 23 | 24 | 25 | def cek_decrypt( 26 | algorithm: str, 27 | parameters: t.Optional[bytes], 28 | kek: bytes, 29 | value: bytes, 30 | ) -> bytes: 31 | if algorithm == AlgorithmOID.AES256_WRAP: 32 | return keywrap.aes_key_unwrap(kek, value) 33 | 34 | else: 35 | raise NotImplementedError(f"Unknown cek encryption algorithm OID '{algorithm}'") 36 | 37 | 38 | def cek_encrypt( 39 | algorithm: str, 40 | parameters: t.Optional[bytes], 41 | kek: bytes, 42 | value: bytes, 43 | ) -> bytes: 44 | if algorithm == AlgorithmOID.AES256_WRAP: 45 | return keywrap.aes_key_wrap(kek, value) 46 | 47 | else: 48 | raise NotImplementedError(f"Unknown cek encryption algorithm OID '{algorithm}'") 49 | 50 | 51 | def cek_generate( 52 | algorithm: str, 53 | ) -> t.Tuple[bytes, bytes]: 54 | if algorithm == AlgorithmOID.AES256_WRAP: 55 | cek = AESGCM.generate_key(256) 56 | cek_iv = os.urandom(12) 57 | return cek, cek_iv 58 | 59 | else: 60 | raise NotImplementedError(f"Unknown cek encryption algorithm OID '{algorithm}'") 61 | 62 | 63 | def content_decrypt( 64 | algorithm: str, 65 | parameters: t.Optional[bytes], 66 | cek: bytes, 67 | value: bytes, 68 | ) -> bytes: 69 | if algorithm == AlgorithmOID.AES256_GCM: 70 | if not parameters: 71 | raise ValueError("Expecting parameters for AES256 GCM decryption but received none.") 72 | 73 | reader = ASN1Reader(parameters).read_sequence() 74 | iv = reader.read_octet_string() 75 | 76 | cipher = AESGCM(cek) 77 | return cipher.decrypt(iv, value, None) 78 | 79 | else: 80 | raise NotImplementedError(f"Unknown content encryption algorithm OID '{algorithm}'") 81 | 82 | 83 | def content_encrypt( 84 | algorithm: str, 85 | parameters: t.Optional[bytes], 86 | cek: bytes, 87 | value: bytes, 88 | ) -> bytes: 89 | if algorithm == AlgorithmOID.AES256_GCM: 90 | if not parameters: 91 | raise ValueError("Expecting parameters for AES256 GCM encryption but received none.") 92 | 93 | reader = ASN1Reader(parameters).read_sequence() 94 | iv = reader.read_octet_string() 95 | 96 | cipher = AESGCM(cek) 97 | return cipher.encrypt(iv, value, None) 98 | 99 | else: 100 | raise NotImplementedError(f"Unknown content encryption algorithm OID '{algorithm}'") 101 | 102 | 103 | def kdf( 104 | algorithm: hashes.HashAlgorithm, 105 | secret: bytes, 106 | label: bytes, 107 | context: bytes, 108 | length: int, 109 | ) -> bytes: 110 | # KDF(HashAlg, KI, Label, Context, L) 111 | # where KDF is SP800-108 in counter mode. 112 | kdf = KBKDFHMAC( 113 | algorithm=algorithm, 114 | mode=Mode.CounterMode, 115 | length=length, 116 | label=label, 117 | context=context, 118 | # MS-SMB2 uses the same KDF function and my implementation that 119 | # sets a value of 4 seems to work so assume that's the case here. 120 | rlen=4, 121 | llen=4, 122 | location=CounterLocation.BeforeFixed, 123 | fixed=None, 124 | ) 125 | return kdf.derive(secret) 126 | 127 | 128 | def kdf_concat( 129 | algorithm: hashes.HashAlgorithm, 130 | shared_secret: bytes, 131 | algorithm_id: bytes, 132 | party_uinfo: bytes, 133 | party_vinfo: bytes, 134 | length: int, 135 | ) -> bytes: 136 | otherinfo = b"".join([algorithm_id, party_uinfo, party_vinfo]) 137 | return ConcatKDFHash( 138 | algorithm, 139 | length=length, 140 | otherinfo=otherinfo, 141 | ).derive(shared_secret) 142 | -------------------------------------------------------------------------------- /src/dpapi_ng/_dns.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import typing as t 7 | 8 | import dns.asyncresolver 9 | import dns.rdtypes 10 | import dns.resolver 11 | 12 | 13 | class SrvRecord(t.NamedTuple): 14 | target: str 15 | port: int 16 | weight: int 17 | priority: int 18 | 19 | 20 | def _get_highest_answer( 21 | answer: dns.resolver.Answer, 22 | ) -> SrvRecord: 23 | answers: t.List[SrvRecord] = [] 24 | for a in answer: 25 | answers.append( 26 | SrvRecord( 27 | # The trailing . causes errors on Windows and the SPN lookup. 28 | target=str(a.target).rstrip("."), # type: ignore[attr-defined] 29 | port=a.port, # type: ignore[attr-defined] 30 | weight=a.weight, # type: ignore[attr-defined] 31 | priority=a.priority, # type: ignore[attr-defined] 32 | ) 33 | ) 34 | 35 | # Sorts the array by lowest priority then highest weight. 36 | return sorted(answers, key=lambda a: (a.priority, -a.weight))[0] 37 | 38 | 39 | async def async_lookup_dc( 40 | domain_name: t.Optional[str] = None, 41 | ) -> SrvRecord: 42 | """Lookup DC for domain name 43 | 44 | Attempts to lookup LDAP server based on the domain name specified or the 45 | system's search domain if available. This is done through an SRV lookup for 46 | '_ldap._tcp.dc._msdcs.{domain_name}'. 47 | 48 | Args: 49 | domain_name: The domain to lookup the DC for. 50 | 51 | Returns: 52 | SrvRecord: The SRV record result. 53 | 54 | Raises: 55 | dns.exception.DNSException: DNS lookup error. 56 | """ 57 | 58 | if domain_name: 59 | record = f"_ldap._tcp.dc._msdcs.{domain_name}" 60 | else: 61 | record = f"_ldap._tcp.dc._msdcs" 62 | 63 | answers = await dns.asyncresolver.resolve(record, "SRV", search=True) 64 | return _get_highest_answer(answers) 65 | 66 | 67 | def lookup_dc( 68 | domain_name: t.Optional[str] = None, 69 | ) -> SrvRecord: 70 | """Lookup DC for domain name 71 | 72 | Attempts to lookup LDAP server based on the domain name specified or the 73 | system's search domain if available. This is done through an SRV lookup for 74 | '_ldap._tcp.dc._msdcs.{domain_name}'. 75 | 76 | Args: 77 | domain_name: The domain to lookup the DC for. 78 | 79 | Returns: 80 | SrvRecord: The SRV record result. 81 | 82 | Raises: 83 | dns.exception.DNSException: DNS lookup error. 84 | """ 85 | 86 | if domain_name: 87 | record = f"_ldap._tcp.dc._msdcs.{domain_name}" 88 | else: 89 | record = f"_ldap._tcp.dc._msdcs" 90 | 91 | answers = dns.resolver.resolve(record, "SRV", search=True) 92 | return _get_highest_answer(answers) 93 | -------------------------------------------------------------------------------- /src/dpapi_ng/_rpc/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | from ._bind import ( 7 | AlterContext, 8 | AlterContextResponse, 9 | Bind, 10 | BindAck, 11 | BindNak, 12 | BindTimeFeatureNegotiation, 13 | ContextElement, 14 | ContextResult, 15 | ContextResultCode, 16 | SyntaxId, 17 | bind_time_feature_negotiation, 18 | ) 19 | from ._client import ( 20 | NDR, 21 | NDR64, 22 | AsyncRpcClient, 23 | SyncRpcClient, 24 | async_create_rpc_connection, 25 | create_rpc_connection, 26 | ) 27 | from ._pdu import ( 28 | AuthenticationLevel, 29 | CharacterRep, 30 | DataRep, 31 | Fault, 32 | FaultFlags, 33 | FloatingPointRep, 34 | IntegerRep, 35 | PacketFlags, 36 | PacketType, 37 | PDUHeader, 38 | SecTrailer, 39 | SecurityProvider, 40 | ) 41 | from ._request import Request, Response 42 | from ._verification import ( 43 | Command, 44 | CommandBitmask, 45 | CommandFlags, 46 | CommandHeader2, 47 | CommandPContext, 48 | CommandType, 49 | VerificationTrailer, 50 | ) 51 | 52 | __all__ = [ 53 | "NDR", 54 | "NDR64", 55 | "AlterContext", 56 | "AlterContextResponse", 57 | "AsyncRpcClient", 58 | "AuthenticationLevel", 59 | "Bind", 60 | "BindAck", 61 | "BindNak", 62 | "BindTimeFeatureNegotiation", 63 | "CharacterRep", 64 | "Command", 65 | "CommandBitmask", 66 | "CommandFlags", 67 | "CommandHeader2", 68 | "CommandPContext", 69 | "CommandType", 70 | "ContextElement", 71 | "ContextResult", 72 | "ContextResultCode", 73 | "DataRep", 74 | "Fault", 75 | "FaultFlags", 76 | "FloatingPointRep", 77 | "IntegerRep", 78 | "PacketFlags", 79 | "PacketType", 80 | "PDUHeader", 81 | "SecTrailer", 82 | "SecurityProvider", 83 | "SyncRpcClient", 84 | "SyntaxId", 85 | "Request", 86 | "Response", 87 | "VerificationTrailer", 88 | "async_create_rpc_connection", 89 | "bind_time_feature_negotiation", 90 | "create_rpc_connection", 91 | ] 92 | -------------------------------------------------------------------------------- /src/dpapi_ng/_rpc/_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import typing as t 7 | 8 | import spnego 9 | import spnego.iov 10 | 11 | from ._pdu import AuthenticationLevel, SecTrailer, SecurityProvider 12 | 13 | 14 | class AuthenticationProvider: 15 | def __init__( 16 | self, 17 | username: t.Optional[str] = None, 18 | password: t.Optional[str] = None, 19 | hostname: str = "unspecified", 20 | protocol: str = "negotiate", 21 | ) -> None: 22 | self.ctx = spnego.client( 23 | username, 24 | password, 25 | hostname=hostname, 26 | service="host", 27 | protocol=protocol, 28 | context_req=spnego.ContextReq.default | spnego.ContextReq.dce_style, 29 | ) 30 | self.provider = { 31 | "negotiate": SecurityProvider.RPC_C_AUTHN_GSS_NEGOTIATE, 32 | "ntlm": SecurityProvider.RPC_C_AUTHN_WINNT, 33 | "kerberos": SecurityProvider.RPC_C_AUTHN_GSS_KERBEROS, 34 | }[protocol] 35 | self._header_length = 0 36 | 37 | @property 38 | def complete(self) -> bool: 39 | return self.ctx.complete 40 | 41 | def step( 42 | self, 43 | in_token: t.Optional[bytes] = None, 44 | ) -> SecTrailer: 45 | out_token = self.ctx.step(in_token) or b"" 46 | return SecTrailer( 47 | type=self.provider, 48 | level=AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY, 49 | pad_length=0, 50 | context_id=0, 51 | auth_value=out_token, 52 | ) 53 | 54 | def get_empty_trailer(self, pad_length: int) -> SecTrailer: 55 | header_length = self._header_length = self._header_length or self.ctx.query_message_sizes().header 56 | return SecTrailer( 57 | type=self.provider, 58 | level=AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY, 59 | pad_length=pad_length, 60 | context_id=0, 61 | auth_value=b"\x00" * header_length, 62 | ) 63 | 64 | def wrap( 65 | self, 66 | header: bytes, 67 | body: bytes, 68 | trailer: bytes, 69 | sign_header: bool, 70 | ) -> bytes: 71 | sign_buffer_type = spnego.iov.BufferType.sign_only if sign_header else spnego.iov.BufferType.data_readonly 72 | res = self.ctx.wrap_iov( 73 | [ 74 | (sign_buffer_type, header), 75 | body, 76 | (sign_buffer_type, trailer), 77 | spnego.iov.BufferType.header, 78 | ], 79 | encrypt=True, 80 | qop=None, 81 | ) 82 | 83 | return b"".join( 84 | [ 85 | header, 86 | res.buffers[1].data or b"", 87 | trailer, 88 | res.buffers[3].data or b"", 89 | ] 90 | ) 91 | 92 | def unwrap( 93 | self, 94 | header: bytes, 95 | body: bytes, 96 | trailer: bytes, 97 | signature: bytes, 98 | sign_header: bool, 99 | ) -> bytes: 100 | sign_buffer_type = spnego.iov.BufferType.sign_only if sign_header else spnego.iov.BufferType.data_readonly 101 | res = self.ctx.unwrap_iov( 102 | [ 103 | (sign_buffer_type, header), 104 | body, 105 | (sign_buffer_type, trailer), 106 | (spnego.iov.BufferType.header, signature), 107 | ], 108 | ) 109 | 110 | return res.buffers[1].data or b"" 111 | -------------------------------------------------------------------------------- /src/dpapi_ng/_rpc/_bind.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import dataclasses 7 | import enum 8 | import typing as t 9 | import uuid 10 | 11 | from ._pdu import PDU, PacketType, PDUHeader, SecTrailer, register_pdu 12 | 13 | 14 | class BindTimeFeatureNegotiation(enum.IntFlag): 15 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/cef529cc-77b5-4794-85dc-91e1467e80f0 16 | NONE = 0x00 17 | SECURITY_CONTEXT_MULTIPLEXING = 0x01 18 | KEEP_CONNECTION_ON_ORPHAN = 0x02 19 | 20 | 21 | @dataclasses.dataclass(frozen=True) 22 | class SyntaxId: 23 | uuid: uuid.UUID 24 | version: int 25 | version_minor: int 26 | 27 | def pack(self) -> bytes: 28 | return b"".join( 29 | [ 30 | self.uuid.bytes_le, 31 | self.version.to_bytes(2, byteorder="little"), 32 | self.version_minor.to_bytes(2, byteorder="little"), 33 | ] 34 | ) 35 | 36 | @classmethod 37 | def unpack( 38 | cls, 39 | data: t.Union[bytes, bytearray, memoryview], 40 | ) -> SyntaxId: 41 | view = memoryview(data) 42 | 43 | return cls( 44 | uuid=uuid.UUID(bytes_le=view[:16].tobytes()), 45 | version=int.from_bytes(view[16:18], byteorder="little"), 46 | version_minor=int.from_bytes(view[18:20], byteorder="little"), 47 | ) 48 | 49 | 50 | @dataclasses.dataclass(frozen=True) 51 | class ContextElement: 52 | # https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm 53 | context_id: int 54 | abstract_syntax: SyntaxId 55 | transfer_syntaxes: t.List[SyntaxId] 56 | 57 | def pack(self) -> bytes: 58 | return b"".join( 59 | [ 60 | self.context_id.to_bytes(2, byteorder="little"), 61 | len(self.transfer_syntaxes).to_bytes(2, byteorder="little"), 62 | self.abstract_syntax.pack(), 63 | b"".join([t.pack() for t in self.transfer_syntaxes]), 64 | ] 65 | ) 66 | 67 | @classmethod 68 | def unpack( 69 | cls, 70 | data: t.Union[bytes, bytearray, memoryview], 71 | ) -> ContextElement: 72 | view = memoryview(data) 73 | 74 | context_id = int.from_bytes(view[:2], byteorder="little") 75 | num_transfers = int.from_bytes(view[2:4], byteorder="little") 76 | abstract_syntax = SyntaxId.unpack(view[4:]) 77 | view = view[24:] 78 | transfer_syntaxes = [] 79 | for _ in range(num_transfers): 80 | transfer_syntaxes.append(SyntaxId.unpack(view)) 81 | view = view[20:] 82 | 83 | return cls( 84 | context_id=context_id, 85 | abstract_syntax=abstract_syntax, 86 | transfer_syntaxes=transfer_syntaxes, 87 | ) 88 | 89 | 90 | class ContextResultCode(enum.IntEnum): 91 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/8df5c4d4-364d-468c-81fe-ec94c1b40917 92 | ACCEPTANCE = 0 93 | USER_REJECTION = 1 94 | PROVIDER_REJECTION = 2 95 | NEGOTIATE_ACK = 3 # MS-RPCE extension 96 | 97 | 98 | @dataclasses.dataclass(frozen=True) 99 | class ContextResult: 100 | # https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm 101 | result: ContextResultCode 102 | reason: int 103 | syntax: uuid.UUID 104 | syntax_version: int 105 | 106 | def pack(self) -> bytes: 107 | return b"".join( 108 | [ 109 | self.result.to_bytes(2, byteorder="little"), 110 | self.reason.to_bytes(2, byteorder="little"), 111 | self.syntax.bytes_le, 112 | self.syntax_version.to_bytes(4, byteorder="little"), 113 | ] 114 | ) 115 | 116 | @classmethod 117 | def unpack( 118 | cls, 119 | data: t.Union[bytes, bytearray, memoryview], 120 | ) -> ContextResult: 121 | view = memoryview(data) 122 | 123 | return cls( 124 | result=ContextResultCode(int.from_bytes(view[:2], byteorder="little")), 125 | reason=int.from_bytes(view[2:4], byteorder="little"), 126 | syntax=uuid.UUID(bytes_le=view[4:20].tobytes()), 127 | syntax_version=int.from_bytes(view[20:24], byteorder="little"), 128 | ) 129 | 130 | 131 | @dataclasses.dataclass(frozen=True) 132 | @register_pdu(PacketType.BIND) 133 | class Bind(PDU): 134 | # https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm 135 | max_xmit_frag: int 136 | max_recv_frag: int 137 | assoc_group: int 138 | contexts: t.List[ContextElement] 139 | 140 | def pack(self) -> bytes: 141 | return b"".join( 142 | [ 143 | self.header.pack(), 144 | self.max_xmit_frag.to_bytes(2, byteorder="little"), 145 | self.max_recv_frag.to_bytes(2, byteorder="little"), 146 | self.assoc_group.to_bytes(4, byteorder="little"), 147 | len(self.contexts).to_bytes(4, byteorder="little"), 148 | b"".join(c.pack() for c in self.contexts), 149 | self.sec_trailer.pack() if self.sec_trailer else b"", 150 | ] 151 | ) 152 | 153 | @classmethod 154 | def _unpack( 155 | cls, 156 | data: t.Union[bytes, bytearray, memoryview], 157 | header: PDUHeader, 158 | sec_trailer: t.Optional[SecTrailer], 159 | ) -> Bind: 160 | view = memoryview(data) 161 | 162 | max_xmit_frag = int.from_bytes(view[:2], byteorder="little") 163 | max_recv_frag = int.from_bytes(view[2:4], byteorder="little") 164 | assoc_group = int.from_bytes(view[4:8], byteorder="little") 165 | 166 | num_contexts = view[8] 167 | view = view[12:] 168 | contexts = [] 169 | for _ in range(num_contexts): 170 | c = ContextElement.unpack(view) 171 | contexts.append(c) 172 | view = view[24 + (len(c.transfer_syntaxes) * 20) :] 173 | 174 | return cls( 175 | header=header, 176 | sec_trailer=sec_trailer, 177 | max_xmit_frag=max_xmit_frag, 178 | max_recv_frag=max_recv_frag, 179 | assoc_group=assoc_group, 180 | contexts=contexts, 181 | ) 182 | 183 | 184 | @dataclasses.dataclass(frozen=True) 185 | @register_pdu(PacketType.BIND_ACK) 186 | class BindAck(PDU): 187 | # https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm 188 | max_xmit_frag: int 189 | max_recv_frag: int 190 | assoc_group: int 191 | sec_addr: str 192 | results: t.List[ContextResult] 193 | 194 | def pack(self) -> bytes: 195 | b_sec_addr = b"" 196 | if self.sec_addr: 197 | b_sec_addr = self.sec_addr.encode("utf-8") + b"\x00" 198 | sec_addr_len = len(b_sec_addr) 199 | padding = -(2 + sec_addr_len) % 4 200 | b_result = b"".join([r.pack() for r in self.results]) 201 | 202 | return b"".join( 203 | [ 204 | self.header.pack(), 205 | self.max_xmit_frag.to_bytes(2, byteorder="little"), 206 | self.max_recv_frag.to_bytes(2, byteorder="little"), 207 | self.assoc_group.to_bytes(4, byteorder="little"), 208 | sec_addr_len.to_bytes(2, byteorder="little"), 209 | b_sec_addr, 210 | b"\x00" * padding, 211 | len(self.results).to_bytes(4, byteorder="little"), 212 | b_result, 213 | self.sec_trailer.pack() if self.sec_trailer else b"", 214 | ] 215 | ) 216 | 217 | @classmethod 218 | def _unpack( 219 | cls, 220 | data: t.Union[bytes, bytearray, memoryview], 221 | header: PDUHeader, 222 | sec_trailer: t.Optional[SecTrailer], 223 | ) -> BindAck: 224 | view = memoryview(data) 225 | 226 | max_xmit_frag = int.from_bytes(view[:2], byteorder="little") 227 | max_recv_frag = int.from_bytes(view[2:4], byteorder="little") 228 | assoc_group = int.from_bytes(view[4:8], byteorder="little") 229 | sec_addr_len = int.from_bytes(view[8:10], byteorder="little") 230 | sec_addr = view[10 : 10 + sec_addr_len - 1].tobytes().decode("utf-8") 231 | padding = -(2 + sec_addr_len) % 4 232 | view = view[10 + sec_addr_len + padding :] 233 | 234 | num_result = view[0] 235 | view = view[4:] 236 | results = [] 237 | for _ in range(num_result): 238 | results.append(ContextResult.unpack(view)) 239 | view = view[24:] 240 | 241 | return cls( 242 | header=header, 243 | sec_trailer=sec_trailer, 244 | max_xmit_frag=max_xmit_frag, 245 | max_recv_frag=max_recv_frag, 246 | assoc_group=assoc_group, 247 | sec_addr=sec_addr, 248 | results=results, 249 | ) 250 | 251 | 252 | @dataclasses.dataclass(frozen=True) 253 | @register_pdu(PacketType.BIND_NAK) 254 | class BindNak(PDU): 255 | # https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm 256 | reject_reason: int 257 | versions: t.List[tuple[int, int]] 258 | 259 | def pack(self) -> bytes: 260 | protocols = [v[0].to_bytes(1, byteorder="little") + v[1].to_bytes(1, byteorder="little") for v in self.versions] 261 | b_versions = b"".join( 262 | [ 263 | len(protocols).to_bytes(1, byteorder="little"), 264 | b"".join(protocols), 265 | ] 266 | ) 267 | padding = -(2 + len(b_versions)) % 4 268 | 269 | return b"".join( 270 | [ 271 | self.header.pack(), 272 | self.reject_reason.to_bytes(2, byteorder="little"), 273 | b_versions, 274 | b"\x00" * padding, 275 | ] 276 | ) 277 | 278 | @classmethod 279 | def _unpack( 280 | cls, 281 | data: t.Union[bytes, bytearray, memoryview], 282 | header: PDUHeader, 283 | sec_trailer: t.Optional[SecTrailer], 284 | ) -> BindNak: 285 | view = memoryview(data) 286 | 287 | reject_reason = int.from_bytes(view[:2], byteorder="little") 288 | versions = [] 289 | num_versions = view[2] 290 | 291 | view = view[3:] 292 | for _ in range(num_versions): 293 | versions.append((view[0], view[1])) 294 | view = view[2:] 295 | 296 | return cls( 297 | header=header, 298 | sec_trailer=None, 299 | reject_reason=reject_reason, 300 | versions=versions, 301 | ) 302 | 303 | 304 | @dataclasses.dataclass(frozen=True) 305 | @register_pdu(PacketType.ALTER_CONTEXT) 306 | class AlterContext(Bind): 307 | @classmethod 308 | def _unpack( 309 | cls, 310 | data: t.Union[bytes, bytearray, memoryview], 311 | header: PDUHeader, 312 | sec_trailer: t.Optional[SecTrailer], 313 | ) -> AlterContext: 314 | return Bind._unpack.__func__(cls, data, header, sec_trailer) # type: ignore[attr-defined] 315 | 316 | 317 | @dataclasses.dataclass(frozen=True) 318 | @register_pdu(PacketType.ALTER_CONTEXT_RESP) 319 | class AlterContextResponse(BindAck): 320 | @classmethod 321 | def _unpack( 322 | cls, 323 | data: t.Union[bytes, bytearray, memoryview], 324 | header: PDUHeader, 325 | sec_trailer: t.Optional[SecTrailer], 326 | ) -> AlterContextResponse: 327 | return BindAck._unpack.__func__(cls, data, header, sec_trailer) # type: ignore[attr-defined] 328 | 329 | 330 | def bind_time_feature_negotiation( 331 | flags: BindTimeFeatureNegotiation = BindTimeFeatureNegotiation.NONE, 332 | ) -> SyntaxId: 333 | """Creates the Bind Time Feature Negotiation Syntax value from the flags specified.""" 334 | return SyntaxId( 335 | uuid=uuid.UUID(fields=(0x6CB71C2C, 0x9812, 0x4540, flags, 0, 0)), 336 | version=1, 337 | version_minor=0, 338 | ) 339 | -------------------------------------------------------------------------------- /src/dpapi_ng/_rpc/_pdu.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import dataclasses 7 | import enum 8 | import typing as t 9 | 10 | 11 | class IntegerRep(enum.IntEnum): 12 | BIG_ENDIAN = 0 13 | LITTLE_ENDIAN = 1 14 | 15 | 16 | class CharacterRep(enum.IntEnum): 17 | ASCII = 0 18 | EBCDIC = 1 19 | 20 | 21 | class FloatingPointRep(enum.IntEnum): 22 | IEEE = 0 23 | VAX = 1 24 | CRAY = 2 25 | IBM = 3 26 | 27 | 28 | class PacketType(enum.IntEnum): 29 | REQUEST = 0 30 | PING = 1 31 | RESPONSE = 2 32 | FAULT = 3 33 | WORKING = 4 34 | NOCALL = 5 35 | REJECT = 6 36 | ACK = 7 37 | CL_CANCEL = 8 38 | FACK = 9 39 | CANCEL_ACK = 10 40 | BIND = 11 41 | BIND_ACK = 12 42 | BIND_NAK = 13 43 | ALTER_CONTEXT = 14 44 | ALTER_CONTEXT_RESP = 15 45 | SHUTDOWN = 17 46 | CO_CANCEL = 18 47 | ORPHANED = 19 48 | 49 | 50 | class PacketFlags(enum.IntFlag): 51 | NONE = 0x00 52 | PFC_FIRST_FRAG = 0x01 53 | PFC_LAST_FRAG = 0x02 54 | PFC_PENDING_CANCEL = 0x04 55 | PFC_SUPPORT_HEADER_SIGN = 0x04 # MS-RPCE extension used in Bind/AlterContext 56 | PFC_RESERVED_1 = 0x08 57 | PFC_CONC_MPX = 0x10 58 | PFC_DID_NOT_EXECUTE = 0x20 59 | PFC_MAYBE = 0x40 60 | PFC_OBJECT_UUID = 0x80 61 | 62 | 63 | @dataclasses.dataclass(frozen=True) 64 | class DataRep: 65 | # https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm 66 | byte_order: IntegerRep = IntegerRep.LITTLE_ENDIAN 67 | character: CharacterRep = CharacterRep.ASCII 68 | floating_point: FloatingPointRep = FloatingPointRep.IEEE 69 | 70 | def pack(self) -> bytes: 71 | first_octet = self.byte_order << 4 | self.character 72 | return b"".join( 73 | [ 74 | first_octet.to_bytes(1, byteorder="little"), 75 | self.floating_point.to_bytes(1, byteorder="little"), 76 | b"\x00\x00", 77 | ] 78 | ) 79 | 80 | @classmethod 81 | def unpack( 82 | cls, 83 | data: t.Union[bytes, bytearray, memoryview], 84 | ) -> DataRep: 85 | view = memoryview(data) 86 | 87 | return cls( 88 | byte_order=IntegerRep((view[0] & 0b11110000) >> 4), 89 | character=CharacterRep(view[0] & 0b00001111), 90 | floating_point=FloatingPointRep(view[1]), 91 | ) 92 | 93 | 94 | @dataclasses.dataclass(frozen=True) 95 | class PDUHeader: 96 | # https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm 97 | version: int 98 | version_minor: int 99 | packet_type: PacketType 100 | packet_flags: PacketFlags 101 | data_rep: DataRep 102 | frag_len: int 103 | auth_len: int 104 | call_id: int 105 | 106 | def pack(self) -> bytes: 107 | return b"".join( 108 | [ 109 | self.version.to_bytes(1, byteorder="little"), 110 | self.version_minor.to_bytes(1, byteorder="little"), 111 | self.packet_type.to_bytes(1, byteorder="little"), 112 | self.packet_flags.to_bytes(1, byteorder="little"), 113 | self.data_rep.pack(), 114 | self.frag_len.to_bytes(2, byteorder="little"), 115 | self.auth_len.to_bytes(2, byteorder="little"), 116 | self.call_id.to_bytes(4, byteorder="little"), 117 | ] 118 | ) 119 | 120 | @classmethod 121 | def unpack( 122 | cls, 123 | data: t.Union[bytes, bytearray, memoryview], 124 | ) -> PDUHeader: 125 | view = memoryview(data) 126 | 127 | return cls( 128 | version=view[0], 129 | version_minor=view[1], 130 | packet_type=PacketType(view[2]), 131 | packet_flags=PacketFlags(view[3]), 132 | data_rep=DataRep.unpack(view[4:8]), 133 | frag_len=int.from_bytes(view[8:10], byteorder="little"), 134 | auth_len=int.from_bytes(view[10:12], byteorder="little"), 135 | call_id=int.from_bytes(view[12:16], byteorder="little"), 136 | ) 137 | 138 | 139 | class SecurityProvider(enum.IntEnum): 140 | RPC_C_AUTHN_NONE = 0x00 141 | RPC_C_AUTHN_GSS_NEGOTIATE = 0x09 142 | RPC_C_AUTHN_WINNT = 0x0A 143 | RPC_C_AUTHN_GSS_SCHANNEL = 0x0E 144 | RPC_C_AUTHN_GSS_KERBEROS = 0x10 145 | RPC_C_AUTHN_NETLOGON = 0x44 146 | RPC_C_AUTHN_DEFAULT = 0xFF 147 | 148 | 149 | class AuthenticationLevel(enum.IntEnum): 150 | RPC_C_AUTHN_LEVEL_DEFAULT = 0x00 151 | RPC_C_AUTHN_LEVEL_NONE = 0x01 152 | RPC_C_AUTHN_LEVEL_CONNECT = 0x02 153 | RPC_C_AUTHN_LEVEL_CALL = 0x03 154 | RPC_C_AUTHN_LEVEL_PKT = 0x04 155 | RPC_C_AUTHN_LEVEL_PKT_INTEGRITY = 0x05 156 | RPC_C_AUTHN_LEVEL_PKT_PRIVACY = 0x06 157 | 158 | 159 | @dataclasses.dataclass(frozen=True) 160 | class SecTrailer: 161 | # https://pubs.opengroup.org/onlinepubs/9629399/chap13.htm 162 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/ab45c6a5-951a-4096-b805-7347674dc6ab 163 | type: SecurityProvider 164 | level: AuthenticationLevel 165 | pad_length: int 166 | context_id: int 167 | auth_value: bytes 168 | 169 | def pack(self) -> bytes: 170 | return b"".join( 171 | [ 172 | self.type.to_bytes(1, byteorder="little"), 173 | self.level.to_bytes(1, byteorder="little"), 174 | self.pad_length.to_bytes(1, byteorder="little"), 175 | b"\x00", # Auth-Rsrvd 176 | self.context_id.to_bytes(4, byteorder="little"), 177 | self.auth_value, 178 | ] 179 | ) 180 | 181 | @classmethod 182 | def unpack( 183 | cls, 184 | data: t.Union[bytes, bytearray, memoryview], 185 | ) -> SecTrailer: 186 | view = memoryview(data) 187 | 188 | return cls( 189 | type=SecurityProvider(view[0]), 190 | level=AuthenticationLevel(view[1]), 191 | pad_length=view[2], 192 | context_id=int.from_bytes(view[4:8], byteorder="little"), 193 | auth_value=view[8:].tobytes(), 194 | ) 195 | 196 | 197 | T = t.TypeVar("T") 198 | 199 | _PACKET_TYPE_REGISTRY: t.Dict[PacketType, t.Callable[[memoryview, PDUHeader, t.Optional[SecTrailer]], PDU]] = {} 200 | 201 | 202 | @dataclasses.dataclass(frozen=True) 203 | class PDU: 204 | header: PDUHeader 205 | sec_trailer: t.Optional[SecTrailer] 206 | 207 | def pack(self) -> bytes: 208 | raise NotImplementedError() # pragma: nocover 209 | 210 | @classmethod 211 | def unpack( 212 | cls, 213 | data: t.Union[bytes, bytearray, memoryview], 214 | ) -> PDU: 215 | view = memoryview(data) 216 | 217 | header = PDUHeader.unpack(view) 218 | view = view[16 : header.frag_len] 219 | 220 | sec_trailer = None 221 | if header.auth_len: 222 | sec_trailer = SecTrailer.unpack(view[-(header.auth_len + 8) :]) 223 | view = view[: -(header.auth_len + 8)] 224 | 225 | return _PACKET_TYPE_REGISTRY[header.packet_type]( 226 | view, 227 | header, 228 | sec_trailer, 229 | ) 230 | 231 | 232 | def register_pdu(packet_type: PacketType) -> t.Callable[[T], T]: 233 | def wrap(cls: T) -> T: 234 | _PACKET_TYPE_REGISTRY[packet_type] = getattr(cls, "_unpack") 235 | return cls 236 | 237 | return wrap 238 | 239 | 240 | class FaultFlags(enum.IntFlag): 241 | NONE = 0x00 242 | EXTENDED_ERROR_PRESENT = 0x01 243 | 244 | 245 | @dataclasses.dataclass(frozen=True) 246 | @register_pdu(PacketType.FAULT) 247 | class Fault(PDU): 248 | # https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm 249 | alloc_hint: int 250 | context_id: int 251 | cancel_count: int 252 | status: int 253 | flags: FaultFlags # Extension of MS-RPCE 254 | stub_data: bytes 255 | 256 | def pack(self) -> bytes: 257 | return b"".join( 258 | [ 259 | self.header.pack(), 260 | self.alloc_hint.to_bytes(4, byteorder="little"), 261 | self.context_id.to_bytes(2, byteorder="little"), 262 | self.cancel_count.to_bytes(1, byteorder="little"), 263 | self.flags.to_bytes(1, byteorder="little"), 264 | self.status.to_bytes(4, byteorder="little"), 265 | b"\x00\x00\x00\x00", # alignment padding 266 | self.stub_data, 267 | self.sec_trailer.pack() if self.sec_trailer else b"", 268 | ] 269 | ) 270 | 271 | @classmethod 272 | def _unpack( 273 | cls, 274 | data: t.Union[bytes, bytearray, memoryview], 275 | header: PDUHeader, 276 | sec_trailer: t.Optional[SecTrailer], 277 | ) -> Fault: 278 | view = memoryview(data) 279 | 280 | return cls( 281 | header=header, 282 | sec_trailer=sec_trailer, 283 | alloc_hint=int.from_bytes(view[:4], byteorder="little"), 284 | context_id=int.from_bytes(view[4:6], byteorder="little"), 285 | cancel_count=view[6], 286 | flags=FaultFlags(view[7]), 287 | status=int.from_bytes(view[8:12], byteorder="little"), 288 | stub_data=view[16:].tobytes(), 289 | ) 290 | -------------------------------------------------------------------------------- /src/dpapi_ng/_rpc/_request.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import dataclasses 7 | import typing as t 8 | import uuid 9 | 10 | from ._pdu import PDU, PacketFlags, PacketType, PDUHeader, SecTrailer, register_pdu 11 | 12 | 13 | @dataclasses.dataclass(frozen=True) 14 | @register_pdu(PacketType.REQUEST) 15 | class Request(PDU): 16 | alloc_hint: int 17 | context_id: int 18 | opnum: int 19 | obj: t.Optional[uuid.UUID] 20 | stub_data: bytes 21 | 22 | def pack(self) -> bytes: 23 | return b"".join( 24 | [ 25 | self.header.pack(), 26 | self.alloc_hint.to_bytes(4, byteorder="little"), 27 | self.context_id.to_bytes(2, byteorder="little"), 28 | self.opnum.to_bytes(2, byteorder="little"), 29 | self.obj.bytes_le if self.obj else b"", 30 | self.stub_data, 31 | self.sec_trailer.pack() if self.sec_trailer else b"", 32 | ] 33 | ) 34 | 35 | @classmethod 36 | def _unpack( 37 | cls, 38 | data: t.Union[bytes, bytearray, memoryview], 39 | header: PDUHeader, 40 | sec_trailer: t.Optional[SecTrailer], 41 | ) -> Request: 42 | view = memoryview(data) 43 | 44 | alloc_hint = int.from_bytes(view[:4], byteorder="little") 45 | context_id = int.from_bytes(view[4:6], byteorder="little") 46 | opnum = int.from_bytes(view[6:8], byteorder="little") 47 | 48 | view = view[8:] 49 | obj = None 50 | if header.packet_flags & PacketFlags.PFC_OBJECT_UUID: 51 | obj = uuid.UUID(bytes_le=view[:16].tobytes()) 52 | view = view[16:] 53 | 54 | return cls( 55 | header=header, 56 | sec_trailer=sec_trailer, 57 | alloc_hint=alloc_hint, 58 | context_id=context_id, 59 | opnum=opnum, 60 | obj=obj, 61 | stub_data=view.tobytes(), 62 | ) 63 | 64 | 65 | @dataclasses.dataclass(frozen=True) 66 | @register_pdu(PacketType.RESPONSE) 67 | class Response(PDU): 68 | alloc_hint: int 69 | context_id: int 70 | cancel_count: int 71 | stub_data: bytes 72 | 73 | def pack(self) -> bytes: 74 | return b"".join( 75 | [ 76 | self.header.pack(), 77 | self.alloc_hint.to_bytes(4, byteorder="little"), 78 | self.context_id.to_bytes(2, byteorder="little"), 79 | self.cancel_count.to_bytes(1, byteorder="little"), 80 | b"\x00", # reserved 81 | self.stub_data, 82 | self.sec_trailer.pack() if self.sec_trailer else b"", 83 | ] 84 | ) 85 | 86 | @classmethod 87 | def _unpack( 88 | cls, 89 | data: t.Union[bytes, bytearray, memoryview], 90 | header: PDUHeader, 91 | sec_trailer: t.Optional[SecTrailer], 92 | ) -> Response: 93 | view = memoryview(data) 94 | 95 | return cls( 96 | header=header, 97 | sec_trailer=sec_trailer, 98 | alloc_hint=int.from_bytes(view[:4], byteorder="little"), 99 | context_id=int.from_bytes(view[4:6], byteorder="little"), 100 | cancel_count=view[6], 101 | stub_data=view[8:].tobytes(), 102 | ) 103 | -------------------------------------------------------------------------------- /src/dpapi_ng/_rpc/_verification.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import dataclasses 7 | import enum 8 | import typing as t 9 | 10 | from ._bind import SyntaxId 11 | from ._pdu import DataRep, PacketType 12 | 13 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/0e9fea61-1bff-4478-9bfe-a3b6d8b64ac3 14 | 15 | 16 | class CommandType(enum.IntEnum): 17 | SEC_VT_COMMAND_BITMASK_1 = 0x0001 18 | SEC_VT_COMMAND_PCONTEXT = 0x0002 19 | SEC_VT_COMMAND_HEADER2 = 0x0003 20 | 21 | @classmethod 22 | def _missing_(cls, value: object) -> t.Optional[enum.Enum]: 23 | new_member = int.__new__(cls) 24 | new_member._name_ = f"CommandType Unknown 0x{value:04X}" 25 | new_member._value_ = value # type: ignore[assignment] 26 | return cls._value2member_map_.setdefault(value, new_member) 27 | 28 | 29 | class CommandFlags(enum.IntFlag): 30 | NONE = 0x0000 31 | SEC_VT_COMMAND_END = 0x4000 32 | SEC_VT_MUST_PROCESS_COMMAND = 0x8000 33 | 34 | 35 | @dataclasses.dataclass(frozen=True) 36 | class Command: 37 | command: CommandType 38 | flags: CommandFlags 39 | value: bytes 40 | 41 | def pack(self) -> bytes: 42 | return b"".join( 43 | [ 44 | (self.command.value | self.flags.value).to_bytes(2, byteorder="little"), 45 | len(self.value).to_bytes(2, byteorder="little"), 46 | self.value, 47 | ] 48 | ) 49 | 50 | @classmethod 51 | def unpack( 52 | cls, 53 | data: t.Union[bytes, bytearray, memoryview], 54 | ) -> Command: 55 | view = memoryview(data) 56 | 57 | cmd_field = int.from_bytes(view[:2], byteorder="little") 58 | command_type = CommandType(cmd_field & 0x3FFF) 59 | command_flags = CommandFlags(cmd_field & 0xC000) 60 | command_length = int.from_bytes(view[2:4], byteorder="little") 61 | value = view[4 : 4 + command_length].tobytes() 62 | 63 | unpack_func = _COMMAND_TYPE_REGISTRY.get(command_type, None) 64 | if unpack_func: 65 | cmd = unpack_func(command_flags, value) 66 | object.__setattr__(cmd, "value", value) 67 | return cmd 68 | 69 | else: 70 | return cls(command_type, command_flags, value) 71 | 72 | 73 | T = t.TypeVar("T") 74 | _COMMAND_TYPE_REGISTRY: t.Dict[CommandType, t.Callable[[CommandFlags, bytes], Command]] = {} 75 | 76 | 77 | def register_cmd(cls: T) -> T: 78 | _COMMAND_TYPE_REGISTRY[getattr(cls, "command").default] = getattr(cls, "_unpack") 79 | return cls 80 | 81 | 82 | @dataclasses.dataclass(frozen=True) 83 | class _KnownCommand(Command): 84 | value: bytes = dataclasses.field(init=False, repr=False, default=b"") 85 | 86 | 87 | @dataclasses.dataclass(frozen=True) 88 | @register_cmd 89 | class CommandBitmask(_KnownCommand): 90 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/35d7781d-6c5b-46b2-9083-3d53f98bef0d 91 | CLIENT_SUPPORT_HEADER_SIGNING: int = dataclasses.field(init=False, repr=False, default=0x00000001) 92 | 93 | command: CommandType = dataclasses.field(init=False, default=CommandType.SEC_VT_COMMAND_BITMASK_1) 94 | bits: int 95 | 96 | def pack(self) -> bytes: 97 | return Command(self.command, self.flags, self.bits.to_bytes(4, byteorder="little")).pack() 98 | 99 | @classmethod 100 | def _unpack( 101 | cls, 102 | flags: CommandFlags, 103 | value: bytes, 104 | ) -> CommandBitmask: 105 | return cls( 106 | flags=flags, 107 | bits=int.from_bytes(value, byteorder="little"), 108 | ) 109 | 110 | 111 | @dataclasses.dataclass(frozen=True) 112 | @register_cmd 113 | class CommandPContext(_KnownCommand): 114 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/41e3cf7a-3b42-470c-9d27-c4e047ac6445 115 | command: CommandType = dataclasses.field(init=False, default=CommandType.SEC_VT_COMMAND_PCONTEXT) 116 | interface_id: SyntaxId 117 | transfer_syntax: SyntaxId 118 | 119 | def pack(self) -> bytes: 120 | value = self.interface_id.pack() + self.transfer_syntax.pack() 121 | return Command(self.command, self.flags, value).pack() 122 | 123 | @classmethod 124 | def _unpack( 125 | cls, 126 | flags: CommandFlags, 127 | value: bytes, 128 | ) -> CommandPContext: 129 | interface_id = SyntaxId.unpack(value) 130 | transfer_syntax = SyntaxId.unpack(value[20:]) 131 | return cls( 132 | flags=flags, 133 | interface_id=interface_id, 134 | transfer_syntax=transfer_syntax, 135 | ) 136 | 137 | 138 | @dataclasses.dataclass(frozen=True) 139 | @register_cmd 140 | class CommandHeader2(_KnownCommand): 141 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/0a108fbd-c848-4755-9e15-6c4df1c35134 142 | command: CommandType = dataclasses.field(init=False, default=CommandType.SEC_VT_COMMAND_HEADER2) 143 | packet_type: PacketType 144 | data_rep: DataRep 145 | call_id: int 146 | context_id: int 147 | opnum: int 148 | 149 | def pack(self) -> bytes: 150 | value = b"".join( 151 | [ 152 | self.packet_type.to_bytes(1, byteorder="little"), 153 | b"\x00\x00\x00", # Reserved 154 | self.data_rep.pack(), 155 | self.call_id.to_bytes(4, byteorder="little"), 156 | self.context_id.to_bytes(2, byteorder="little"), 157 | self.opnum.to_bytes(2, byteorder="little"), 158 | ] 159 | ) 160 | return Command(self.command, self.flags, value).pack() 161 | 162 | @classmethod 163 | def _unpack( 164 | cls, 165 | flags: CommandFlags, 166 | value: bytes, 167 | ) -> CommandHeader2: 168 | view = memoryview(value) 169 | 170 | return cls( 171 | flags=flags, 172 | packet_type=PacketType(view[0]), 173 | data_rep=DataRep.unpack(view[4:8]), 174 | call_id=int.from_bytes(view[8:12], byteorder="little"), 175 | context_id=int.from_bytes(view[12:14], byteorder="little"), 176 | opnum=int.from_bytes(view[14:16], byteorder="little"), 177 | ) 178 | 179 | 180 | @dataclasses.dataclass(frozen=True) 181 | class VerificationTrailer: 182 | signature: bytes = dataclasses.field(init=False, default=b"\x8A\xE3\x13\x71\x02\xF4\x36\x71") 183 | commands: t.List[Command] 184 | 185 | def pack(self) -> bytes: 186 | return b"".join( 187 | [ 188 | self.signature, 189 | b"".join(c.pack() for c in self.commands), 190 | ] 191 | ) 192 | 193 | @classmethod 194 | def unpack( 195 | cls, 196 | data: t.Union[bytes, bytearray, memoryview], 197 | ) -> VerificationTrailer: 198 | view = memoryview(data) 199 | 200 | if view[:8].tobytes() != cls.signature: 201 | raise ValueError(f"Failed to unpack {cls.__name__} as signature header is invalid") 202 | 203 | view = view[8:] 204 | commands = [] 205 | while True: 206 | cmd = Command.unpack(view) 207 | commands.append(cmd) 208 | view = view[4 + len(cmd.value) :] 209 | if cmd.flags & CommandFlags.SEC_VT_COMMAND_END: 210 | break 211 | 212 | return cls(commands=commands) 213 | -------------------------------------------------------------------------------- /src/dpapi_ng/_security_descriptor.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import re 7 | import typing as t 8 | 9 | 10 | def sid_to_bytes(sid: str) -> bytes: 11 | sid_pattern = re.compile(r"^S-(\d)-(\d+)(?:-\d+){1,15}$") 12 | sid_match = sid_pattern.match(sid) 13 | if not sid_match: 14 | raise ValueError(f"Input string '{sid}' is not a valid SID string") 15 | 16 | sid_split = sid.split("-") 17 | revision = int(sid_split[1]) 18 | authority = int(sid_split[2]) 19 | 20 | data = bytearray(authority.to_bytes(8, byteorder="big")) 21 | data[0] = revision 22 | data[1] = len(sid_split) - 3 23 | 24 | for idx in range(3, len(sid_split)): 25 | sub_auth = int(sid_split[idx]) 26 | data += sub_auth.to_bytes(4, byteorder="little") 27 | 28 | return bytes(data) 29 | 30 | 31 | def ace_to_bytes(sid: str, access_mask: int) -> bytes: 32 | b_sid = sid_to_bytes(sid) 33 | 34 | return b"".join( 35 | [ 36 | b"\x00\x00", # AceType, AceFlags - ACCESS_ALLOWED_ACE_TYPE 37 | (8 + len(b_sid)).to_bytes(2, byteorder="little"), 38 | access_mask.to_bytes(4, byteorder="little"), 39 | b_sid, 40 | ] 41 | ) 42 | 43 | 44 | def acl_to_bytes(aces: t.List[bytes]) -> bytes: 45 | ace_data = b"".join(aces) 46 | 47 | return b"".join( 48 | [ 49 | b"\x02\x00", # AclRevision, Sbz1 - ACL_REVISION 50 | (8 + len(ace_data)).to_bytes(2, byteorder="little"), 51 | len(aces).to_bytes(2, byteorder="little"), 52 | b"\x00\x00", # Sbz1 53 | ace_data, 54 | ] 55 | ) 56 | 57 | 58 | def sd_to_bytes( 59 | owner: str, 60 | group: str, 61 | sacl: t.Optional[t.List[bytes]] = None, 62 | dacl: t.Optional[t.List[bytes]] = None, 63 | ) -> bytes: 64 | control = 0b10000000 << 8 # Self-Relative 65 | 66 | # While MS-DTYP state there is no required order for the dynamic data, it 67 | # is important that the raw bytes are exactly what Microsoft uses on the 68 | # server side when it computes the seed key values. Luckily the footnote 69 | # give the correct order the MS-GKDI expects: Sacl, Dacl, Owner, Group 70 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/11e1608c-6169-4fbc-9c33-373fc9b224f4#Appendix_A_72 71 | dynamic_data = bytearray() 72 | current_offset = 20 # Length of the SD header bytes 73 | 74 | sacl_offset = 0 75 | if sacl: 76 | sacl_bytes = acl_to_bytes(sacl) 77 | sacl_offset = current_offset 78 | current_offset += len(sacl_bytes) 79 | 80 | control |= 0b00010000 # SACL Present 81 | dynamic_data += sacl_bytes 82 | 83 | dacl_offset = 0 84 | if dacl: 85 | dacl_bytes = acl_to_bytes(dacl) 86 | dacl_offset = current_offset 87 | current_offset += len(dacl_bytes) 88 | 89 | control |= 0b00000100 # DACL Present 90 | dynamic_data += dacl_bytes 91 | 92 | owner_bytes = sid_to_bytes(owner) 93 | owner_offset = current_offset 94 | current_offset += len(owner_bytes) 95 | dynamic_data += owner_bytes 96 | 97 | group_bytes = sid_to_bytes(group) 98 | group_offset = current_offset 99 | dynamic_data += group_bytes 100 | 101 | return b"".join( 102 | [ 103 | b"\x01\x00", # Revision and Sbz1 104 | control.to_bytes(2, byteorder="little"), 105 | owner_offset.to_bytes(4, byteorder="little"), 106 | group_offset.to_bytes(4, byteorder="little"), 107 | sacl_offset.to_bytes(4, byteorder="little"), 108 | dacl_offset.to_bytes(4, byteorder="little"), 109 | dynamic_data, 110 | ] 111 | ) 112 | -------------------------------------------------------------------------------- /src/dpapi_ng/_version.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | __version__ = "0.3.0" 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | -------------------------------------------------------------------------------- /tests/_rpc/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | -------------------------------------------------------------------------------- /tests/_rpc/test_pdu.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | from dpapi_ng._rpc import _pdu as pdu 7 | 8 | 9 | def test_pdu_header_pack() -> None: 10 | expected = b"\x05\x00\x0b\x03\x10\x00\x00\x00\xa0\x00\x00\x00\x01\x00\x00\x00" 11 | 12 | msg = pdu.PDUHeader( 13 | version=5, 14 | version_minor=0, 15 | packet_type=pdu.PacketType.BIND, 16 | packet_flags=pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG, 17 | data_rep=pdu.DataRep(), 18 | frag_len=160, 19 | auth_len=0, 20 | call_id=1, 21 | ) 22 | actual = msg.pack() 23 | assert actual == expected 24 | 25 | 26 | def test_pdu_header_unpack() -> None: 27 | data = b"\x05\x00\x0b\x03\x10\x00\x00\x00\xa0\x00\x00\x00\x01\x00\x00\x00" 28 | 29 | header = pdu.PDUHeader.unpack(data) 30 | assert header.version == 5 31 | assert header.version_minor == 0 32 | assert header.packet_type == pdu.PacketType.BIND 33 | assert header.packet_flags == pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG 34 | assert header.data_rep.byte_order == pdu.IntegerRep.LITTLE_ENDIAN 35 | assert header.data_rep.character == pdu.CharacterRep.ASCII 36 | assert header.data_rep.floating_point == pdu.FloatingPointRep.IEEE 37 | assert header.frag_len == 160 38 | assert header.auth_len == 0 39 | assert header.call_id == 1 40 | 41 | 42 | def test_sec_trailer_pack() -> None: 43 | expected = b"\x09\x06\x08\x00\x01\x00\x00\x00\x01" 44 | 45 | msg = pdu.SecTrailer( 46 | type=pdu.SecurityProvider.RPC_C_AUTHN_GSS_NEGOTIATE, 47 | level=pdu.AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY, 48 | pad_length=8, 49 | context_id=1, 50 | auth_value=b"\x01", 51 | ) 52 | actual = msg.pack() 53 | assert actual == expected 54 | 55 | 56 | def test_sec_trailer_unpack() -> None: 57 | data = b"\x09\x06\x08\x00\x01\x00\x00\x00\x01" 58 | 59 | sec_trailer = pdu.SecTrailer.unpack(data) 60 | assert sec_trailer.type == pdu.SecurityProvider.RPC_C_AUTHN_GSS_NEGOTIATE 61 | assert sec_trailer.level == pdu.AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY 62 | assert sec_trailer.pad_length == 8 63 | assert sec_trailer.context_id == 1 64 | assert sec_trailer.auth_value == b"\x01" 65 | 66 | 67 | def test_fault_pack() -> None: 68 | expected = b"\x05\x00\x03\x23\x10\x00\x00\x00\x20\x00\x00\x00\x01\x00\x00\x00\x20\x00\x00\x00\x00\x00\x00\x00\x03\x00\x01\x1c\x00\x00\x00\x00" 69 | 70 | msg = pdu.Fault( 71 | header=pdu.PDUHeader( 72 | version=5, 73 | version_minor=0, 74 | packet_type=pdu.PacketType.FAULT, 75 | packet_flags=pdu.PacketFlags.PFC_FIRST_FRAG 76 | | pdu.PacketFlags.PFC_LAST_FRAG 77 | | pdu.PacketFlags.PFC_DID_NOT_EXECUTE, 78 | data_rep=pdu.DataRep(), 79 | frag_len=32, 80 | auth_len=0, 81 | call_id=1, 82 | ), 83 | sec_trailer=None, 84 | alloc_hint=32, 85 | context_id=0, 86 | cancel_count=0, 87 | status=0x1C010003, 88 | flags=pdu.FaultFlags.NONE, 89 | stub_data=b"", 90 | ) 91 | actual = msg.pack() 92 | assert actual == expected 93 | 94 | 95 | def test_fault_unpack() -> None: 96 | data = b"\x05\x00\x03\x23\x10\x00\x00\x00\x20\x00\x00\x00\x01\x00\x00\x00\x20\x00\x00\x00\x00\x00\x00\x00\x03\x00\x01\x1c\x00\x00\x00\x00" 97 | 98 | msg = pdu.PDU.unpack(data) 99 | assert isinstance(msg, pdu.Fault) 100 | assert msg.header.version == 5 101 | assert msg.header.version_minor == 0 102 | assert msg.header.packet_type == pdu.PacketType.FAULT 103 | assert ( 104 | msg.header.packet_flags 105 | == pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG | pdu.PacketFlags.PFC_DID_NOT_EXECUTE 106 | ) 107 | assert msg.header.data_rep.byte_order == pdu.IntegerRep.LITTLE_ENDIAN 108 | assert msg.header.data_rep.character == pdu.CharacterRep.ASCII 109 | assert msg.header.data_rep.floating_point == pdu.FloatingPointRep.IEEE 110 | assert msg.header.frag_len == 32 111 | assert msg.header.auth_len == 0 112 | assert msg.header.call_id == 1 113 | assert msg.alloc_hint == 32 114 | assert msg.context_id == 0 115 | assert msg.cancel_count == 0 116 | assert msg.flags == pdu.FaultFlags.NONE 117 | assert msg.status == 0x1C010003 118 | assert msg.stub_data == b"" 119 | assert msg.sec_trailer is None 120 | -------------------------------------------------------------------------------- /tests/_rpc/test_request.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import uuid 7 | 8 | from dpapi_ng._rpc import _pdu as pdu 9 | from dpapi_ng._rpc import _request as request 10 | 11 | 12 | def test_request_pack() -> None: 13 | expected = ( 14 | b"\x05\x00\x00\x03\x10\x00\x00\x00" 15 | b"\x1c\x00\x00\x00\x02\x00\x00\x00" 16 | b"\x90\x00\x00\x00\x01\x00\x03\x00" 17 | b"\x01\x00\x00\x00" 18 | ) 19 | 20 | msg = request.Request( 21 | header=pdu.PDUHeader( 22 | version=5, 23 | version_minor=0, 24 | packet_type=pdu.PacketType.REQUEST, 25 | packet_flags=pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG, 26 | data_rep=pdu.DataRep(), 27 | frag_len=28, 28 | auth_len=0, 29 | call_id=2, 30 | ), 31 | sec_trailer=None, 32 | alloc_hint=144, 33 | context_id=1, 34 | opnum=3, 35 | obj=None, 36 | stub_data=b"\x01\x00\x00\x00", 37 | ) 38 | actual = msg.pack() 39 | assert actual == expected 40 | 41 | 42 | def test_request_unpack() -> None: 43 | data = ( 44 | b"\x05\x00\x00\x03\x10\x00\x00\x00" 45 | b"\x1c\x00\x00\x00\x02\x00\x00\x00" 46 | b"\x90\x00\x00\x00\x01\x00\x03\x00" 47 | b"\x01\x00\x00\x00" 48 | ) 49 | 50 | msg = pdu.PDU.unpack(data) 51 | assert isinstance(msg, request.Request) 52 | assert msg.header.version == 5 53 | assert msg.header.version_minor == 0 54 | assert msg.header.packet_type == pdu.PacketType.REQUEST 55 | assert msg.header.packet_flags == pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG 56 | assert msg.header.data_rep.byte_order == pdu.IntegerRep.LITTLE_ENDIAN 57 | assert msg.header.data_rep.character == pdu.CharacterRep.ASCII 58 | assert msg.header.data_rep.floating_point == pdu.FloatingPointRep.IEEE 59 | assert msg.header.frag_len == 28 60 | assert msg.header.auth_len == 0 61 | assert msg.header.call_id == 2 62 | assert msg.alloc_hint == 144 63 | assert msg.context_id == 1 64 | assert msg.opnum == 3 65 | assert msg.obj is None 66 | assert msg.stub_data == b"\x01\x00\x00\x00" 67 | assert msg.sec_trailer is None 68 | 69 | 70 | def test_request_pack_with_obj() -> None: 71 | expected = ( 72 | b"\x05\x00\x00\x83\x10\x00\x00\x00" 73 | b"\x2c\x00\x00\x00\x02\x00\x00\x00" 74 | b"\x90\x00\x00\x00\x01\x00\x03\x00" 75 | b"\xff\xff\xff\xff\xff\xff\xff\xff" 76 | b"\xff\xff\xff\xff\xff\xff\xff\xff" 77 | b"\x01\x00\x00\x00" 78 | ) 79 | 80 | msg = request.Request( 81 | header=pdu.PDUHeader( 82 | version=5, 83 | version_minor=0, 84 | packet_type=pdu.PacketType.REQUEST, 85 | packet_flags=pdu.PacketFlags.PFC_FIRST_FRAG 86 | | pdu.PacketFlags.PFC_LAST_FRAG 87 | | pdu.PacketFlags.PFC_OBJECT_UUID, 88 | data_rep=pdu.DataRep(), 89 | frag_len=44, 90 | auth_len=0, 91 | call_id=2, 92 | ), 93 | sec_trailer=None, 94 | alloc_hint=144, 95 | context_id=1, 96 | opnum=3, 97 | obj=uuid.UUID(bytes_le=b"\xff" * 16), 98 | stub_data=b"\x01\x00\x00\x00", 99 | ) 100 | actual = msg.pack() 101 | assert actual == expected 102 | 103 | 104 | def test_request_unpack_with_obj() -> None: 105 | data = ( 106 | b"\x05\x00\x00\x83\x10\x00\x00\x00" 107 | b"\x2c\x00\x00\x00\x02\x00\x00\x00" 108 | b"\x90\x00\x00\x00\x01\x00\x03\x00" 109 | b"\xff\xff\xff\xff\xff\xff\xff\xff" 110 | b"\xff\xff\xff\xff\xff\xff\xff\xff" 111 | b"\x01\x00\x00\x00" 112 | ) 113 | 114 | msg = pdu.PDU.unpack(data) 115 | assert isinstance(msg, request.Request) 116 | assert msg.header.version == 5 117 | assert msg.header.version_minor == 0 118 | assert msg.header.packet_type == pdu.PacketType.REQUEST 119 | assert ( 120 | msg.header.packet_flags 121 | == pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG | pdu.PacketFlags.PFC_OBJECT_UUID 122 | ) 123 | assert msg.header.data_rep.byte_order == pdu.IntegerRep.LITTLE_ENDIAN 124 | assert msg.header.data_rep.character == pdu.CharacterRep.ASCII 125 | assert msg.header.data_rep.floating_point == pdu.FloatingPointRep.IEEE 126 | assert msg.header.frag_len == 44 127 | assert msg.header.auth_len == 0 128 | assert msg.header.call_id == 2 129 | assert msg.alloc_hint == 144 130 | assert msg.context_id == 1 131 | assert msg.opnum == 3 132 | assert msg.obj == uuid.UUID(bytes_le=b"\xff" * 16) 133 | assert msg.stub_data == b"\x01\x00\x00\x00" 134 | assert msg.sec_trailer is None 135 | 136 | 137 | def test_request_pack_sec_trailer() -> None: 138 | expected = ( 139 | b"\x05\x00\x00\x03\x10\x00\x00\x00" 140 | b"\x38\x00\x04\x00\x02\x00\x00\x00" 141 | b"\x94\x00\x00\x00\x01\x00\x00\x00" 142 | b"\xba\x8b\xff\xf4\x6c\x22\x7f\x25" 143 | b"\xce\x5c\xd2\x57\x3f\x9c\xd7\xba" 144 | b"\x09\x06\x0c\x00\x00\x00\x00\x00" 145 | b"\x05\x04\x06\xff" 146 | ) 147 | 148 | msg = request.Request( 149 | header=pdu.PDUHeader( 150 | version=5, 151 | version_minor=0, 152 | packet_type=pdu.PacketType.REQUEST, 153 | packet_flags=pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG, 154 | data_rep=pdu.DataRep(), 155 | frag_len=56, 156 | auth_len=4, 157 | call_id=2, 158 | ), 159 | sec_trailer=pdu.SecTrailer( 160 | type=pdu.SecurityProvider.RPC_C_AUTHN_GSS_NEGOTIATE, 161 | level=pdu.AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY, 162 | pad_length=12, 163 | context_id=0, 164 | auth_value=b"\x05\x04\x06\xff", 165 | ), 166 | alloc_hint=148, 167 | context_id=1, 168 | opnum=0, 169 | obj=None, 170 | stub_data=b"\xba\x8b\xff\xf4\x6c\x22\x7f\x25\xce\x5c\xd2\x57\x3f\x9c\xd7\xba", 171 | ) 172 | actual = msg.pack() 173 | assert actual == expected 174 | 175 | 176 | def test_request_unpack_sec_trailer() -> None: 177 | data = ( 178 | b"\x05\x00\x00\x03\x10\x00\x00\x00" 179 | b"\x38\x00\x04\x00\x02\x00\x00\x00" 180 | b"\x94\x00\x00\x00\x01\x00\x00\x00" 181 | b"\xba\x8b\xff\xf4\x6c\x22\x7f\x25" 182 | b"\xce\x5c\xd2\x57\x3f\x9c\xd7\xba" 183 | b"\x09\x06\x0c\x00\x00\x00\x00\x00" 184 | b"\x05\x04\x06\xff" 185 | ) 186 | 187 | msg = pdu.PDU.unpack(data) 188 | assert isinstance(msg, request.Request) 189 | assert msg.header.version == 5 190 | assert msg.header.version_minor == 0 191 | assert msg.header.packet_type == pdu.PacketType.REQUEST 192 | assert msg.header.packet_flags == pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG 193 | assert msg.header.data_rep.byte_order == pdu.IntegerRep.LITTLE_ENDIAN 194 | assert msg.header.data_rep.character == pdu.CharacterRep.ASCII 195 | assert msg.header.data_rep.floating_point == pdu.FloatingPointRep.IEEE 196 | assert msg.header.frag_len == 56 197 | assert msg.header.auth_len == 4 198 | assert msg.header.call_id == 2 199 | assert msg.alloc_hint == 148 200 | assert msg.context_id == 1 201 | assert msg.opnum == 0 202 | assert msg.obj is None 203 | assert msg.stub_data == b"\xba\x8b\xff\xf4\x6c\x22\x7f\x25\xce\x5c\xd2\x57\x3f\x9c\xd7\xba" 204 | assert isinstance(msg.sec_trailer, pdu.SecTrailer) 205 | assert msg.sec_trailer.type == pdu.SecurityProvider.RPC_C_AUTHN_GSS_NEGOTIATE 206 | assert msg.sec_trailer.level == pdu.AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY 207 | assert msg.sec_trailer.pad_length == 12 208 | assert msg.sec_trailer.context_id == 0 209 | assert msg.sec_trailer.auth_value == b"\x05\x04\x06\xff" 210 | 211 | 212 | def test_response_pack() -> None: 213 | expected = ( 214 | b"\x05\x00\x02\x03\x10\x00\x00\x00" 215 | b"\x1c\x00\x00\x00\x02\x00\x00\x00" 216 | b"\x94\x00\x00\x00\x01\x00\x00\x00" 217 | b"\x00\x00\x00\x00" 218 | ) 219 | 220 | msg = request.Response( 221 | header=pdu.PDUHeader( 222 | version=5, 223 | version_minor=0, 224 | packet_type=pdu.PacketType.RESPONSE, 225 | packet_flags=pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG, 226 | data_rep=pdu.DataRep(), 227 | frag_len=28, 228 | auth_len=0, 229 | call_id=2, 230 | ), 231 | sec_trailer=None, 232 | alloc_hint=148, 233 | context_id=1, 234 | cancel_count=0, 235 | stub_data=b"\x00\x00\x00\x00", 236 | ) 237 | actual = msg.pack() 238 | assert actual == expected 239 | 240 | 241 | def test_response_unpack() -> None: 242 | data = ( 243 | b"\x05\x00\x02\x03\x10\x00\x00\x00" 244 | b"\x1c\x00\x00\x00\x02\x00\x00\x00" 245 | b"\x94\x00\x00\x00\x01\x00\x00\x00" 246 | b"\x00\x00\x00\x00" 247 | ) 248 | 249 | msg = pdu.PDU.unpack(data) 250 | assert isinstance(msg, request.Response) 251 | assert msg.header.version == 5 252 | assert msg.header.version_minor == 0 253 | assert msg.header.packet_type == pdu.PacketType.RESPONSE 254 | assert msg.header.packet_flags == pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG 255 | assert msg.header.data_rep.byte_order == pdu.IntegerRep.LITTLE_ENDIAN 256 | assert msg.header.data_rep.character == pdu.CharacterRep.ASCII 257 | assert msg.header.data_rep.floating_point == pdu.FloatingPointRep.IEEE 258 | assert msg.header.frag_len == 28 259 | assert msg.header.auth_len == 0 260 | assert msg.header.call_id == 2 261 | assert msg.alloc_hint == 148 262 | assert msg.context_id == 1 263 | assert msg.cancel_count == 0 264 | assert msg.stub_data == b"\x00\x00\x00\x00" 265 | assert msg.sec_trailer is None 266 | 267 | 268 | def test_response_pack_sec_trailer() -> None: 269 | expected = ( 270 | b"\x05\x00\x02\x03\x10\x00\x00\x00" 271 | b"\x28\x00\x04\x00\x02\x00\x00\x00" 272 | b"\x60\x00\x00\x00\x01\x00\x00\x00" 273 | b"\x9d\x08\xd7\x07\x09\x06\x00\x00" 274 | b"\x00\x00\x00\x00\x05\x04\x07\xff" 275 | ) 276 | 277 | msg = request.Response( 278 | header=pdu.PDUHeader( 279 | version=5, 280 | version_minor=0, 281 | packet_type=pdu.PacketType.RESPONSE, 282 | packet_flags=pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG, 283 | data_rep=pdu.DataRep(), 284 | frag_len=40, 285 | auth_len=4, 286 | call_id=2, 287 | ), 288 | sec_trailer=pdu.SecTrailer( 289 | type=pdu.SecurityProvider.RPC_C_AUTHN_GSS_NEGOTIATE, 290 | level=pdu.AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY, 291 | pad_length=0, 292 | context_id=0, 293 | auth_value=b"\x05\x04\x07\xff", 294 | ), 295 | alloc_hint=96, 296 | context_id=1, 297 | cancel_count=0, 298 | stub_data=b"\x9d\x08\xd7\x07", 299 | ) 300 | actual = msg.pack() 301 | assert actual == expected 302 | 303 | 304 | def test_response_unpack_sec_trailer() -> None: 305 | data = ( 306 | b"\x05\x00\x02\x03\x10\x00\x00\x00" 307 | b"\x28\x00\x04\x00\x02\x00\x00\x00" 308 | b"\x60\x00\x00\x00\x01\x00\x00\x00" 309 | b"\x9d\x08\xd7\x07\x09\x06\x00\x00" 310 | b"\x00\x00\x00\x00\x05\x04\x07\xff" 311 | ) 312 | 313 | msg = pdu.PDU.unpack(data) 314 | assert isinstance(msg, request.Response) 315 | assert msg.header.version == 5 316 | assert msg.header.version_minor == 0 317 | assert msg.header.packet_type == pdu.PacketType.RESPONSE 318 | assert msg.header.packet_flags == pdu.PacketFlags.PFC_FIRST_FRAG | pdu.PacketFlags.PFC_LAST_FRAG 319 | assert msg.header.data_rep.byte_order == pdu.IntegerRep.LITTLE_ENDIAN 320 | assert msg.header.data_rep.character == pdu.CharacterRep.ASCII 321 | assert msg.header.data_rep.floating_point == pdu.FloatingPointRep.IEEE 322 | assert msg.header.frag_len == 40 323 | assert msg.header.auth_len == 4 324 | assert msg.header.call_id == 2 325 | assert msg.alloc_hint == 96 326 | assert msg.context_id == 1 327 | assert msg.cancel_count == 0 328 | assert msg.stub_data == b"\x9d\x08\xd7\x07" 329 | assert isinstance(msg.sec_trailer, pdu.SecTrailer) 330 | assert msg.sec_trailer.type == pdu.SecurityProvider.RPC_C_AUTHN_GSS_NEGOTIATE 331 | assert msg.sec_trailer.level == pdu.AuthenticationLevel.RPC_C_AUTHN_LEVEL_PKT_PRIVACY 332 | assert msg.sec_trailer.pad_length == 0 333 | assert msg.sec_trailer.context_id == 0 334 | assert msg.sec_trailer.auth_value == b"\x05\x04\x07\xff" 335 | -------------------------------------------------------------------------------- /tests/_rpc/test_verification.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import uuid 7 | 8 | import pytest 9 | 10 | from dpapi_ng._rpc import _bind as bind 11 | from dpapi_ng._rpc import _pdu as pdu 12 | from dpapi_ng._rpc import _verification as verification 13 | 14 | 15 | def test_verification_trailer_pack() -> None: 16 | expected = ( 17 | b"\x8a\xe3\x13\x71\x02\xf4\x36\x71" 18 | b"\x02\x40\x28\x00\x60\x59\x78\xb9" 19 | b"\x4f\x52\xdf\x11\x8b\x6d\x83\xdc" 20 | b"\xde\xd7\x20\x85\x01\x00\x00\x00" 21 | b"\x33\x05\x71\x71\xba\xbe\x37\x49" 22 | b"\x83\x19\xb5\xdb\xef\x9c\xcc\x36" 23 | b"\x01\x00\x00\x00" 24 | ) 25 | 26 | msg = verification.VerificationTrailer( 27 | [ 28 | verification.CommandPContext( 29 | verification.CommandFlags.SEC_VT_COMMAND_END, 30 | bind.SyntaxId(uuid.UUID("b9785960-524f-11df-8b6d-83dcded72085"), 1, 0), 31 | bind.SyntaxId(uuid.UUID("71710533-beba-4937-8319-b5dbef9ccc36"), 1, 0), 32 | ) 33 | ] 34 | ) 35 | actual = msg.pack() 36 | assert actual == expected 37 | 38 | 39 | def test_verification_trailer_unpack() -> None: 40 | data = ( 41 | b"\x8a\xe3\x13\x71\x02\xf4\x36\x71" 42 | b"\x02\x40\x28\x00\x60\x59\x78\xb9" 43 | b"\x4f\x52\xdf\x11\x8b\x6d\x83\xdc" 44 | b"\xde\xd7\x20\x85\x01\x00\x00\x00" 45 | b"\x33\x05\x71\x71\xba\xbe\x37\x49" 46 | b"\x83\x19\xb5\xdb\xef\x9c\xcc\x36" 47 | b"\x01\x00\x00\x00" 48 | ) 49 | 50 | msg = verification.VerificationTrailer.unpack(data) 51 | assert len(msg.commands) == 1 52 | assert isinstance(msg.commands[0], verification.CommandPContext) 53 | assert msg.commands[0].command == verification.CommandType.SEC_VT_COMMAND_PCONTEXT 54 | assert msg.commands[0].flags == verification.CommandFlags.SEC_VT_COMMAND_END 55 | assert msg.commands[0].value 56 | assert msg.commands[0].interface_id == bind.SyntaxId(uuid.UUID("b9785960-524f-11df-8b6d-83dcded72085"), 1, 0) 57 | assert msg.commands[0].transfer_syntax == bind.SyntaxId(uuid.UUID("71710533-beba-4937-8319-b5dbef9ccc36"), 1, 0) 58 | 59 | 60 | def test_verification_trailer_unpack_invalid_signature() -> None: 61 | with pytest.raises(ValueError, match="Failed to unpack VerificationTrailer as signature header is invalid"): 62 | verification.VerificationTrailer.unpack(b"\x00") 63 | 64 | 65 | def test_verification_trailer_unpack_multiple_commands() -> None: 66 | data = b"\x8a\xe3\x13\x71\x02\xf4\x36\x71\x01\x00\x04\x00\x01\x00\x00\x00\x00\x60\x01\x00\x00" 67 | 68 | msg = verification.VerificationTrailer.unpack(data) 69 | assert len(msg.commands) == 2 70 | assert isinstance(msg.commands[0], verification.CommandBitmask) 71 | assert msg.commands[0].command == verification.CommandType.SEC_VT_COMMAND_BITMASK_1 72 | assert msg.commands[0].flags == verification.CommandFlags.NONE 73 | assert msg.commands[0].value == b"\x01\x00\x00\x00" 74 | assert msg.commands[0].bits == 1 75 | 76 | assert isinstance(msg.commands[1], verification.Command) 77 | assert msg.commands[1].command == verification.CommandType(0x2000) 78 | assert msg.commands[1].flags == verification.CommandFlags.SEC_VT_COMMAND_END 79 | assert msg.commands[1].value == b"\x00" 80 | 81 | 82 | def test_command_bitmask_pack() -> None: 83 | expected = b"\x01\x00\x04\x00\x01\x00\x00\x00" 84 | 85 | msg = verification.CommandBitmask(flags=verification.CommandFlags.NONE, bits=1) 86 | actual = msg.pack() 87 | assert actual == expected 88 | 89 | 90 | def test_command_bitmask_unpack() -> None: 91 | data = b"\x01\x00\x04\x00\x01\x00\x00\x00" 92 | 93 | msg = verification.Command.unpack(data) 94 | assert isinstance(msg, verification.CommandBitmask) 95 | assert msg.command == verification.CommandType.SEC_VT_COMMAND_BITMASK_1 96 | assert msg.flags == verification.CommandFlags.NONE 97 | assert msg.value == b"\x01\x00\x00\x00" 98 | assert msg.bits == 1 99 | 100 | 101 | def test_command_pcontext_pack() -> None: 102 | expected = ( 103 | b"\x02\x40\x28\x00\x60\x59\x78\xb9" 104 | b"\x4f\x52\xdf\x11\x8b\x6d\x83\xdc" 105 | b"\xde\xd7\x20\x85\x01\x00\x00\x00" 106 | b"\x33\x05\x71\x71\xba\xbe\x37\x49" 107 | b"\x83\x19\xb5\xdb\xef\x9c\xcc\x36" 108 | b"\x01\x00\x00\x00" 109 | ) 110 | 111 | msg = verification.CommandPContext( 112 | verification.CommandFlags.SEC_VT_COMMAND_END, 113 | bind.SyntaxId(uuid.UUID("b9785960-524f-11df-8b6d-83dcded72085"), 1, 0), 114 | bind.SyntaxId(uuid.UUID("71710533-beba-4937-8319-b5dbef9ccc36"), 1, 0), 115 | ) 116 | actual = msg.pack() 117 | assert actual == expected 118 | 119 | 120 | def test_command_pcontext_unpack() -> None: 121 | data = ( 122 | b"\x02\x40\x28\x00\x60\x59\x78\xb9" 123 | b"\x4f\x52\xdf\x11\x8b\x6d\x83\xdc" 124 | b"\xde\xd7\x20\x85\x01\x00\x00\x00" 125 | b"\x33\x05\x71\x71\xba\xbe\x37\x49" 126 | b"\x83\x19\xb5\xdb\xef\x9c\xcc\x36" 127 | b"\x01\x00\x00\x00" 128 | ) 129 | 130 | msg = verification.Command.unpack(data) 131 | assert isinstance(msg, verification.CommandPContext) 132 | assert msg.command == verification.CommandType.SEC_VT_COMMAND_PCONTEXT 133 | assert msg.flags == verification.CommandFlags.SEC_VT_COMMAND_END 134 | assert msg.value 135 | assert msg.interface_id == bind.SyntaxId(uuid.UUID("b9785960-524f-11df-8b6d-83dcded72085"), 1, 0) 136 | assert msg.transfer_syntax == bind.SyntaxId(uuid.UUID("71710533-beba-4937-8319-b5dbef9ccc36"), 1, 0) 137 | 138 | 139 | def test_command_header2_pack() -> None: 140 | expected = b"\x03\x80\x10\x00\x00\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x00\x02\x00\x03\x00" 141 | 142 | msg = verification.CommandHeader2( 143 | verification.CommandFlags.SEC_VT_MUST_PROCESS_COMMAND, 144 | pdu.PacketType.REQUEST, 145 | pdu.DataRep(), 146 | 1, 147 | 2, 148 | 3, 149 | ) 150 | actual = msg.pack() 151 | assert actual == expected 152 | 153 | 154 | def test_command_header2_unpack() -> None: 155 | data = b"\x03\x80\x10\x00\x00\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x00\x02\x00\x03\x00" 156 | 157 | msg = verification.Command.unpack(data) 158 | assert isinstance(msg, verification.CommandHeader2) 159 | assert msg.command == verification.CommandType.SEC_VT_COMMAND_HEADER2 160 | assert msg.flags == verification.CommandFlags.SEC_VT_MUST_PROCESS_COMMAND 161 | assert msg.value == b"\x00\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x00\x02\x00\x03\x00" 162 | assert msg.packet_type == pdu.PacketType.REQUEST 163 | assert msg.data_rep == pdu.DataRep() 164 | assert msg.call_id == 1 165 | assert msg.context_id == 2 166 | assert msg.opnum == 3 167 | 168 | 169 | def test_unknown_command_unpack() -> None: 170 | data = b"\x00\xA0\x02\x00\x00\x01" 171 | 172 | msg = verification.Command.unpack(data) 173 | assert isinstance(msg, verification.Command) 174 | assert msg.command == verification.CommandType(0x2000) 175 | assert msg.flags == verification.CommandFlags.SEC_VT_MUST_PROCESS_COMMAND 176 | assert msg.value == b"\x00\x01" 177 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import pathlib 7 | 8 | 9 | def get_test_data(name: str) -> bytes: 10 | test_path = pathlib.Path(__file__).parent / "data" / name 11 | return test_path.read_bytes() 12 | -------------------------------------------------------------------------------- /tests/data/dpapi_ng_blob: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jborean93/dpapi-ng/eac650f928af3eee494f7312131d540a81c5e8af/tests/data/dpapi_ng_blob -------------------------------------------------------------------------------- /tests/data/ecdh_key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jborean93/dpapi-ng/eac650f928af3eee494f7312131d540a81c5e8af/tests/data/ecdh_key -------------------------------------------------------------------------------- /tests/data/ffc_dh_key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jborean93/dpapi-ng/eac650f928af3eee494f7312131d540a81c5e8af/tests/data/ffc_dh_key -------------------------------------------------------------------------------- /tests/data/ffc_dh_parameters: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jborean93/dpapi-ng/eac650f928af3eee494f7312131d540a81c5e8af/tests/data/ffc_dh_parameters -------------------------------------------------------------------------------- /tests/data/group_key_envelope: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jborean93/dpapi-ng/eac650f928af3eee494f7312131d540a81c5e8af/tests/data/group_key_envelope -------------------------------------------------------------------------------- /tests/data/kdf_sha1_dh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "108e67ae-2ef9-d45e-4379-0141bb7a49d1", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000A0000000000000053004800410031000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "5DB81523771A683B89A3396AD0CFDE9D3560B29548537B058FD537180F44BC0F5DC739CC71E26B1DE045E3889EA0D3B857DAB8C4EA9F8758245B429496F956BC", 11 | "Data": "3082044006092A864886F70D010703A08204313082042D020102318203E6A28203E2020104308203A404820370010000004B44534B0300000069010000110000000D000000AE678E10F92E5ED443790141BB7A49D1080300001A0000001A000000444850420001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC4165983F5CBE95AB91290F3BEC83D68FB4CF786BD012F3B08F829258249DAE99380141F2EA7C30CEFB7B57201EE3A94C9DEF6035A93256B8E2320B88F1D7714958A9B1F9F609010D419FF03F49C69006D76F8AD9592C32786997CCA5BD30C3237CDFD2748CABB4EC6C384C4E9B66C712FDAB29D248D1BF9BEFEEFCE9C7678FCBA10FC18CA5907933D4B0406CC3BF8A60896D170D753B605F66CE9A0AAE2E5C38B796291EA70020B501B50BFD3B5BA2BCB9B8F9BC5B55765D76048FB676EB3AB1FAB4C7478509252B38F3EF958AA6885AAD5DDF134CE21647738F2132D15D32EDB2C61D2DB61A125FF72C113035A777F1E5403519EC32C871C8ADC4B4E4610EB0D7BFF640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D04286A583B6C7E04DC7BF51D63495DD070AC7D669E99A29EA75CFEB0E557729A24D17157002C9EEE61DE303E06092A864886F70D010701301E060960864801650304012E3011040C8C9ACD5FDC38290D9DC8434602011080116D2E3128FCE187EE331A291B95405617D8" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha1_ecdh_p256.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "9d4f5a1f-7be2-43df-750f-417befeab34f", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000A0000000000000053004800410031000000", 6 | "SecretAgreementAlgorithm": "ECDH_P256", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 256, 9 | "PublicKeyLength": 256, 10 | "RootKeyData": "45A844EB61B23889F5605EE02A25573DF08976ECE21C9510640BF2B1B2252BD7722F317AF9F13A6A032FA8CB3DDB102F2781836F8E2885537215598918930AA5", 11 | "Data": "3082017E06092A864886F70D010703A082016F3082016B02010231820124A28201200201043081E30481B0010000004B44534B0300000069010000110000000D0000001F5A4F9DE27BDF43750F417BEFEAB34F480000001A0000001A00000045434B312000000037CF806AC5C68C3F41009F0E15D214B906CE947250D83C07A22B593A04B9F4920C602F1DD5E28CA99B6C945D1B37ECE4640767C9B576225C48B5586E5C22FFC0640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D042882381708B9E9898AEA7680C8C636961293A80EE11A7CC2425B2D8B4C27B927D6518375F177404101303E06092A864886F70D010701301E060960864801650304012E3011040C747F9CC191016ACD49709E510201108011C7B73F4E1AA2E374F2FDBDFB549A87FCCD" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha1_ecdh_p384.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "a8e81ca3-aa31-3076-a2ef-ce049c8a7d7e", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000A0000000000000053004800410031000000", 6 | "SecretAgreementAlgorithm": "ECDH_P384", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 384, 9 | "PublicKeyLength": 384, 10 | "RootKeyData": "DDC380F2C137F6E35838987DB992A4F357FE5FE29D40AA899E161B930F7EC5EBBE0D1C339AF7F99269D64D3685CA3377577AE3466A48A15C5668B3A598A6AC15", 11 | "Data": "3082019F06092A864886F70D010703A08201903082018C02010231820145A2820141020104308201030481D0010000004B44534B0300000069010000110000000D000000A31CE8A831AA7630A2EFCE049C8A7D7E680000001A0000001A00000045434B33300000006EE84B444A64BA1347C2911375E54E7B512534BDB859063C953F541A8B48403F04D8976DC958CCF894F76612C77F58B0428F7D606F6CD649092D5E6DA8D1626E2F2B5BDA4E56C47D9AACBA8FFFF13E763834CD2000F9A208C6C94A079A84A590640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D04281CB67F4F8F0990B6E6ABD14EC2F92E373D62F681F703E5AF7A149EC9D914D322D531E99F624DB38A303E06092A864886F70D010701301E060960864801650304012E3011040C339799FC8A602D5E7F89F7300201108011E5C531D9002F455947C069E87A022612FD" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha1_nonce.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "108e67ae-2ef9-d45e-4379-0141bb7a49d1", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000A0000000000000053004800410031000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "5DB81523771A683B89A3396AD0CFDE9D3560B29548537B058FD537180F44BC0F5DC739CC71E26B1DE045E3889EA0D3B857DAB8C4EA9F8758245B429496F956BC", 11 | "Data": "3082017C06092A864886F70D010703A082016D3082016902010231820122A282011E0201043081E1048188010000004B44534B0200000069010000110000000D000000AE678E10F92E5ED443790141BB7A49D1200000001A0000001A0000003065B6088DFAF8D301136CBAE781341DE5B57FC1189A18A7E2EF460447DA46EF640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000305406092B0601040182374A013047060A2B0601040182374A01013039303730350C035349440C2E532D312D352D32312D313737333930393633322D323430343833393738302D333834313237343735362D31313034300B060960864801650304012D0428243976C6C8C8F4B50B5F119D53F07BEDA64199AAB12F779701F7C5C38D6306E899DD868D2C53EA2C303E06092A864886F70D010701301E060960864801650304012E3011040C1D2B5A26DCAFC63C52F392B602011080110A1FCD1A3EB4154F921818F2073B7C4C8E" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha256_dh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "2491e5f1-c935-27c4-22ba-b85f61b24768", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003200350036000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "8DBC68793FA3E97905BFA22AD7F7693A3CA0AB004FFD8A5EDD7618E2FEDA94F05E8FAD26BE4A87BCB222CB532B1393020395C4D6B8D96D6CBE651A0AD10FD0C7", 11 | "Data": "3082044006092A864886F70D010703A08204313082042D020102318203E6A28203E2020104308203A404820370010000004B44534B0300000069010000110000000D000000F1E5912435C9C42722BAB85F61B24768080300001A0000001A000000444850420001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC4165960A6F1F03FCA7572A4B0A9288EE0E89B6F74E6340BB94600C882767FA8F721EEDADBE99F15E146EFF569D15A6E227EB0725EA95F2BDE4843123857EFE2584CEFCCA8376B36A3F805F27116021EF6E68A97A89727C5FE536A17FD3E567928CEE38BFAF26A626337DE4018C6FE88A95312895376941FFE59903B5EEBF38A136556757DCC56E0CF6F6DCBA88B9D37B2D60F98F1CE43D7AF773B457755123A811469C9D85897829C8F7AFD9750ED5A93757077628842B1AD65035637363E22C7F9827EDC66D5D024F6D8DE90B2FE6AD18CB4607FE5D99AA0788E3ED69EC790D1A644A8407B463E9C22F669D6B54A07E1B80DC535D522F823A4114A998C7558FBE1D5640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D0428D851F1B0B98C4989A313A7E7AF5A5FCF51628EB52E0688A41E4235EA3D99EBC5ACD597D0B5ABCCBB303E06092A864886F70D010701301E060960864801650304012E3011040C72F7CFCF004E802AC6DBD4F0020110801199268D32E53CFFF3B0BA68DC5E6065369A" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha256_ecdh_p256.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "6d79ed3d-8a58-3f58-c963-ca860b23dfff", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003200350036000000", 6 | "SecretAgreementAlgorithm": "ECDH_P256", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 256, 9 | "PublicKeyLength": 256, 10 | "RootKeyData": "89670CB1F027FA0B4157FBF64B3026E31CE284134B4E0174A6CF7D6DCD1D4A9BC871C9051EE7BBB6E87F35F3DF1F5F6A4596E4E86CD33F7411B1120CD3260581", 11 | "Data": "3082017E06092A864886F70D010703A082016F3082016B02010231820124A28201200201043081E30481B0010000004B44534B0300000069010000110000000D0000003DED796D588A583FC963CA860B23DFFF480000001A0000001A00000045434B31200000009E7556164FDEB70ED4BD7CC7BE8A222285DBED529285D06E84098B53649ECFFAA04E2A90BF7DF70C026197E6E13157DC8DE6A05428308E2E89A25B96BC441B15640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D04287142857B5954A9BCB2E903C6724ED2BFBAEED91931BE4FEB842A561CEB8DBC5CDAB051E56C379004303E06092A864886F70D010701301E060960864801650304012E3011040C558E8824CE00E5F9949E2A4102011080113583185B81838BC7B70FA1ECE75C9A8EB2" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha256_ecdh_p384.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "9bd9cbad-3745-5c94-b637-1013242b8986", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003200350036000000", 6 | "SecretAgreementAlgorithm": "ECDH_P384", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 384, 9 | "PublicKeyLength": 384, 10 | "RootKeyData": "5AB92A9DA1B7BB40410EC1CBF65ACAF68AF817412246FB8146E377609E33A13A1DC08DCBAE147805E4A028906F5E4D9038A66A25521BC14A3165DA8091F172DC", 11 | "Data": "3082019F06092A864886F70D010703A08201903082018C02010231820145A2820141020104308201030481D0010000004B44534B0300000069010000110000000D000000ADCBD99B4537945CB6371013242B8986680000001A0000001A00000045434B3330000000549824E6C6DD72E1D3048E4299E9939A0D8986802E90AF9443661CAC38A29121DEB81A725CE68BB1141C9B019457EFBFCBBF119A78106F2FDD2CDE1B8F9EF7071016181E42003CACCE039D1CA0AECD601022D1EC670F2CCDA2153041CE1081D0640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D0428814806D68157A16239B247C55CEEF9646275CD2853BEC36B2AD54FCFC061653DB508CEFBE327C351303E06092A864886F70D010701301E060960864801650304012E3011040CD0ED62DB1D5A0A3D30D46170020110801140A939890340AD97A728761350592684D9" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha256_nonce.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "2491e5f1-c935-27c4-22ba-b85f61b24768", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003200350036000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "8DBC68793FA3E97905BFA22AD7F7693A3CA0AB004FFD8A5EDD7618E2FEDA94F05E8FAD26BE4A87BCB222CB532B1393020395C4D6B8D96D6CBE651A0AD10FD0C7", 11 | "Data": "3082017C06092A864886F70D010703A082016D3082016902010231820122A282011E0201043081E1048188010000004B44534B0200000069010000110000000D000000F1E5912435C9C42722BAB85F61B24768200000001A0000001A0000002E987E0B8E66C09210A0EA2B6C6A24437730848E3D6FE63E680E9D550B0191F9640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000305406092B0601040182374A013047060A2B0601040182374A01013039303730350C035349440C2E532D312D352D32312D313737333930393633322D323430343833393738302D333834313237343735362D31313034300B060960864801650304012D0428CBC3C528F43C10A4DC923B1A09393C475AFA06C5E5AD23131EE3AE7E7D0F85A71322A922EF9240AB303E06092A864886F70D010701301E060960864801650304012E3011040C5D54633DB67E78D36B95DCF20201108011DED4A98C45EF6BFA18A01566155D115361" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha384_dh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "a0accaa8-0bbc-c616-4437-c35e7b95e9eb", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003300380034000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "F20B5E861F43682148E42B497F5851078EFE609FC3F41F4C7167A2B572C38872E34D38BE34F1918136492DC6EE95C3691A0DC5A3D5217D6B5D191D3ACC18788F", 11 | "Data": "3082044006092A864886F70D010703A08204313082042D020102318203E6A28203E2020104308203A404820370010000004B44534B0300000069010000110000000D000000A8CAACA0BC0B16C64437C35E7B95E9EB080300001A0000001A000000444850420001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC4165938D4E579F14C9F8DF4F42448BDB4E0039F00EC0E7EDB68CAFB5B124F63159714F541F52153A04BF9A162407242066D7D3FF09139E5D4678AC1077B420D0D44C9A6A0CDBF5AC3E035FF281B95C2377C85472A85A313FED2B36FF571A2D4DB080F0EACA1A5BBAA7EE7AE98F02DAC956AF479BB8E3D50702368A92A059432AB6DCB8C7931C208C4F94DE58D9B3CF5060637B9DE23DC2A3A9459BE698C80FBA402FCBBA7EFBB53510F7506AD30DAC50E61A5BABB193EB86B0F345AE47F7AE603A286B3D72E9C5CA02F730269C74D4FBB012AF94A949F2B9A10178EB0A459CFE2D25D798B7F65919C928F48B43D3651CB08DBEE508AD659C17397CD3F76C3045F5589640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D04287FF28816EAE131221094075640DDB472FDCFF1B4236301943BE02BEF5EE8F9A5309C635FCFBDA42B303E06092A864886F70D010701301E060960864801650304012E3011040C874D80F3579212A63A0A134B02011080111D05EA57E7C69243EC936ADE9E6568AFDD" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha384_ecdh_p256.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "76db058e-b6b5-d824-02ce-8603fd4ea2de", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003300380034000000", 6 | "SecretAgreementAlgorithm": "ECDH_P256", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 256, 9 | "PublicKeyLength": 256, 10 | "RootKeyData": "AE8E71FBA41E864B0F1B3824C1F6AB06C1F1939FDE8F951C625DFDC7AA1BA6BB5E48BD1422302F33F8DDDBCDE5862AE82EA8B97DB8219204B55AF6057FAC901B", 11 | "Data": "3082017E06092A864886F70D010703A082016F3082016B02010231820124A28201200201043081E30481B0010000004B44534B0300000069010000110000000D0000008E05DB76B5B624D802CE8603FD4EA2DE480000001A0000001A00000045434B3120000000A673932C9F121C5C0C7F25EE6392B8DE50E056B3C40F4D492B743BAD6AD4A1FFAB4F8E2E692884E436817D13E64C499FBE9F1DE047AE0C36C0697FD5AE089538640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D04288907C17C064D6E2C77F1E64DFB5A17F0E6C73E1BA1983E25AFD9F2851B39431164255453C20B5F34303E06092A864886F70D010701301E060960864801650304012E3011040C3179767482CD6DF5DBB507D60201108011FDA1126E95453E3DAF5B78259400FC168F" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha384_ecdh_p384.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "16b9698d-975b-55a0-c01b-746cf2795812", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003300380034000000", 6 | "SecretAgreementAlgorithm": "ECDH_P384", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 384, 9 | "PublicKeyLength": 384, 10 | "RootKeyData": "30FD11FD5DA0E23B31818D37C8B30155B81C1A0075342C4E87ED176CB52E5EEE2F6BB3FD09A79551FAA0D1B3A1BB2CEC0AA99725FCE51851CD9B6758169A954B", 11 | "Data": "3082019F06092A864886F70D010703A08201903082018C02010231820145A2820141020104308201030481D0010000004B44534B0300000069010000110000000D0000008D69B9165B97A055C01B746CF2795812680000001A0000001A00000045434B3330000000919367614F06563FE89CA486E7C5CDF63FA097F5A6DB43E9B030EF674C2BE61D4C293BF6A8A86F1C18F00273443E1D72905F0287DF6D4C0F532CBAA6641F5C69FB89F689A8818FA108FAAECED55C2C370D9F67D043FE7190816FE7D1A71E10FA640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D0428A0B9CC2783B5B86B3AC27A0DD28395AEA6E4079A15F6CD9CA95EEB5B3D75D8B3C3F6D0023E0269FE303E06092A864886F70D010701301E060960864801650304012E3011040C265D90A4298DC2795185C3800201108011D54A278F523721336A8A36E02EE7FD8151" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha384_nonce.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "a0accaa8-0bbc-c616-4437-c35e7b95e9eb", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003300380034000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "F20B5E861F43682148E42B497F5851078EFE609FC3F41F4C7167A2B572C38872E34D38BE34F1918136492DC6EE95C3691A0DC5A3D5217D6B5D191D3ACC18788F", 11 | "Data": "3082017C06092A864886F70D010703A082016D3082016902010231820122A282011E0201043081E1048188010000004B44534B0200000069010000110000000D000000A8CAACA0BC0B16C64437C35E7B95E9EB200000001A0000001A000000DAB42FA6D326784286158C8BE3BA8A54ABB0F70A67ED3F911BF820DCB185A54C640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000305406092B0601040182374A013047060A2B0601040182374A01013039303730350C035349440C2E532D312D352D32312D313737333930393633322D323430343833393738302D333834313237343735362D31313034300B060960864801650304012D042831A65950F9B8699B85457F044BF24CF394E83BFECA1C6A7AA16561DAA7E2FAFABD79B2EFD3D458F2303E06092A864886F70D010701301E060960864801650304012E3011040CBCA50A6770C06812CFEE450C0201108011E9DB9262FE1EB0480A584AA1D3C67C6FB0" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha512_dh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "2e1b932a-4e21-ced3-0b7b-8815aff8335d", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003500310032000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "9F48CF96AE350DD017E2922D05235C8B926600A1D18B77DB7C2B4ED72816863871AFC7F35D1E0584635AD3652B5F3FD8AC775D7311F3AF50828BE3F9AC477BE5", 11 | "Data": "3082044006092A864886F70D010703A08204313082042D020102318203E6A28203E2020104308203A404820370010000004B44534B0300000069010000110000000D0000002A931B2E214ED3CE0B7B8815AFF8335D080300001A0000001A000000444850420001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC416594332ADF3B8666880976F5F0766D22ED4A198AE6FD5E967A372B340CDB6C459C74AE0E6D8CC9BD77684FE85B7F5AF93E9AD56F50CE99FA6CB4BC71F9E2ACEE8EC04349368A7422B0755790721CF2C5BB9E46B9C3B8794812EB6A024F8B7A1A524BED7FCF6A1665600E5492EBC979932C736314C84AC4A2CA331A59ECBCDD9E98000F20914EA34C32B0DFF3ADC0B27E2B0D065A307F38B4E7CA12CDD52B62B603250B2882633D97F28A2C7ED1A9B1C5FF06A4BC53772F77B2A02C239C27C99F61213623582B0B307D1E4ADAD737B5D3B463D9CC8DB134DC7C316622A7BBE2B38CEEBD5A3D67CDF0D713100AF0D77F0BBF42A950B91350F34DD8BA000ED01C55EAE640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D04289E97A18FBC010EEDA09FD4955CCE466358CE487F329BCE7535589FD367C302F79BF2347C0BD42EEF303E06092A864886F70D010701301E060960864801650304012E3011040C035F1A4967EFF84054C54D7602011080115F4ECF25ED8A16598E6A7B6345918E26BF" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha512_ecdh_p256.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "af562727-f449-177c-196e-72137e0202b0", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003500310032000000", 6 | "SecretAgreementAlgorithm": "ECDH_P256", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 256, 9 | "PublicKeyLength": 256, 10 | "RootKeyData": "0E76508EB290154C6F7B2E44773627F70E0BDA881D4CF8A9963B630FB934E2EDACB92A4130E96980EAD6A7E5D7D144378739CF277EDAB6B1F168669267503E05", 11 | "Data": "3082017E06092A864886F70D010703A082016F3082016B02010231820124A28201200201043081E30481B0010000004B44534B0300000069010000110000000D000000272756AF49F47C17196E72137E0202B0480000001A0000001A00000045434B31200000005AFBDDBB412E39B367302AEDF04A8F8D184D9801FA4A560BF35AB0FD83E5C93CB5EE98D8B938672442C1CFCDFAADADA31A014776F9B72C2CA9F06E5B6DDA2218640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000302E06092B0601040182374A013021060A2B0601040182374A010130133011300F0C035349440C08532D312D352D3138300B060960864801650304012D0428B0CF0666C0387BCDEC4091D03BAFFC0C1C244CF9C0487E0C5934F1ECFA2291153DF0865F963E8316303E06092A864886F70D010701301E060960864801650304012E3011040C61E5C4607A1256F8798445D402011080115E45F628DEFF83BFE41D08F14BB70D0CB2" 12 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha512_ecdh_p384.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "1bc9cb9e-a69e-c8eb-0a92-db2514af086f", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003500310032000000", 6 | "SecretAgreementAlgorithm": "ECDH_P384", 7 | "SecretAgreementParameters": "", 8 | "PrivateKeyLength": 384, 9 | "PublicKeyLength": 384, 10 | "RootKeyData": "07CB6B0D11155C3E3A0782308BF898F626554457EA0FDB02FCAD846448B54FD8903FD7347584551F53EF1AFDEDB93549F74743DB21C279F25EF64AE272CA0DF6", 11 | "Data": "308201C506092A864886F70D010703A08201B6308201B20201023182016BA2820167020104308201290481D0010000004B44534B0300000069010000110000000D0000009ECBC91B9EA6EBC80A92DB2514AF086F680000001A0000001A00000045434B3330000000519FB25DB5692BADEE44DE4044EC0F0AAF4BD86F9D3F1967031AD2E7B7A656EEE7E114EFFCF0D83E682C246F3E04119FED903CE810FC060F6DCA3E4901E20DFA5EE82AE9DCB237102892E8A2997BFF0CC9C755541F066E83550FD200B3D4B50E640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000305406092B0601040182374A013047060A2B0601040182374A01013039303730350C035349440C2E532D312D352D32312D313737333930393633322D323430343833393738302D333834313237343735362D31313034300B060960864801650304012D04289961EC2244AFCF7D3A75C9926439D25C29A2D774C089336C4CFBE496511B74A24E301A37EB92C0AB303E06092A864886F70D010701301E060960864801650304012E3011040C6BB207920DC0FDABCB4FAB0A0201108011843C1241C03169BAB765986D113B5D6715", 12 | "Data1": "3082017C06092A864886F70D010703A082016D3082016902010231820122A282011E0201043081E1048188010000004B44534B0200000069010000110000000D0000009ECBC91B9EA6EBC80A92DB2514AF086F200000001A0000001A00000030B6E9C4FFCB8A7C68023A3E75975910A3E2D0EF18541DA7EFA6988641025741640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000305406092B0601040182374A013047060A2B0601040182374A01013039303730350C035349440C2E532D312D352D32312D313737333930393633322D323430343833393738302D333834313237343735362D31313034300B060960864801650304012D0428070483F6E96B8EC93B9BCDD3099FF524C73D519D2B306DEF7150AC5C02976DA406743840F1C71C4D303E06092A864886F70D010701301E060960864801650304012E3011040C9DA3017EB9DFEA6AA32ECE0F020110801124F4DE1F11231AB38128A4DF5C89980564" 13 | } -------------------------------------------------------------------------------- /tests/data/kdf_sha512_nonce.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "RootKeyId": "2e1b932a-4e21-ced3-0b7b-8815aff8335d", 4 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 5 | "KdfParameters": "00000000010000000E000000000000005300480041003500310032000000", 6 | "SecretAgreementAlgorithm": "DH", 7 | "SecretAgreementParameters": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 8 | "PrivateKeyLength": 512, 9 | "PublicKeyLength": 2048, 10 | "RootKeyData": "9F48CF96AE350DD017E2922D05235C8B926600A1D18B77DB7C2B4ED72816863871AFC7F35D1E0584635AD3652B5F3FD8AC775D7311F3AF50828BE3F9AC477BE5", 11 | "Data": "3082017C06092A864886F70D010703A082016D3082016902010231820122A282011E0201043081E1048188010000004B44534B0200000069010000110000000D0000002A931B2E214ED3CE0B7B8815AFF8335D200000001A0000001A00000017F4016A608FE281C23F69BA8697AD10B3CC2C1F4F621B314D9FE16024BC1F97640070006100700069006E0067002E0074006500730074000000640070006100700069006E0067002E0074006500730074000000305406092B0601040182374A013047060A2B0601040182374A01013039303730350C035349440C2E532D312D352D32312D313737333930393633322D323430343833393738302D333834313237343735362D31313034300B060960864801650304012D04289175BA7C89921301F9ED15FC57DF7B3DC23DB2E0C8D9E6BB439B51A89D864A82995E19D8578D9ECE303E06092A864886F70D010701301E060960864801650304012E3011040C80DFB7BBC56714B928F519B202011080113E7D319EA0B9FF07551D85D47AD13A0213" 12 | } -------------------------------------------------------------------------------- /tests/data/seed_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "RootKeyId": "c1f25b76-d435-4015-1259-b14e5cca4e00", 3 | "SecurityDescriptor": "0100048054000000600000000000000014000000020040000200000000002400030000000105000000000005150000001A084482EEE8B0C8E6E8BD524F0400000000140002000000010100000000000100000000010100000000000512000000010100000000000512000000", 4 | "Version": 1, 5 | "Flags": 0, 6 | "L0": 361, 7 | "L1": 19, 8 | "L2": 6, 9 | "KdfAlgorithm": "SP800_108_CTR_HMAC", 10 | "KdfParameters": "00000000010000000E000000000000005300480041003500310032000000", 11 | "SecretAlgorithm": "ECDH_P256", 12 | "SecretParameters": "", 13 | "PrivateKeyLength": 256, 14 | "PublicKeyLength": 256, 15 | "DomainName": "", 16 | "ForestName": "", 17 | "L1Key": "", 18 | "L2Key": "45434B312000000025A4AEE37431C4EC7728F4CEF8D43731D85B007C2D4034F06F50B6E33E67D1278F82C6D2DA432A5AC180901509B9A01EF572E7931AF2CF98218EA63A8BF93936" 19 | } -------------------------------------------------------------------------------- /tests/integration/README.md: -------------------------------------------------------------------------------- 1 | # dpapi-ng Integration Environment 2 | 3 | This contains a Vagrantfile and Ansible playbook that can be used to setup an AD environment to test sansldap with more complex scenarios. 4 | The plan is to expand this environment setup to test out edge case scenarios that cannot be done through CI. 5 | 6 | To set up the environment run the following: 7 | 8 | ```bash 9 | ansible-galaxy collection install -r requirements.yml 10 | 11 | vagrant up 12 | 13 | ansible-playbook main.yml -vv 14 | ``` 15 | 16 | Before running `main.yml`, download the `artifact` zip from the GitHub Actions workflow to test. 17 | This zip should be placed in the same directory as the playbook as `artifact.zip`. 18 | 19 | The command to run the tests is: 20 | 21 | ```bash 22 | ansible-playbook tests.yml -vv 23 | ``` 24 | 25 | The tests will run for the root key algorithms `DH`, `ECDH_P256`, and `ECDH_P384`. 26 | To run the tests for just one algorithm add `--tags ` like `--tags DH`. 27 | -------------------------------------------------------------------------------- /tests/integration/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | require 'yaml' 5 | 6 | inventory = YAML.load_file('inventory.yml') 7 | 8 | Vagrant.configure("2") do |config| 9 | inventory['all']['children'].each do |group,group_details| 10 | group_details['hosts'].each do |server,details| 11 | 12 | config.vm.define server do |srv| 13 | srv.vm.box = details['vagrant_box'] 14 | srv.vm.hostname = server 15 | srv.vm.network :private_network, 16 | :ip => details['ansible_host'], 17 | :libvirt__domain_name => inventory['all']['vars']['domain_name'], 18 | :libvirt__network_name => 'dpapi_ng' 19 | 20 | srv.vm.provider :libvirt do |l| 21 | l.memory = 4096 22 | l.cpus = 2 23 | end 24 | 25 | if group == "linux" 26 | srv.vm.provision "shell", inline: <<-SHELL 27 | sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config 28 | systemctl restart sshd.service 29 | SHELL 30 | end 31 | end 32 | 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /tests/integration/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = inventory.yml 3 | retry_files_enabled = False 4 | stdout_callback = yaml 5 | -------------------------------------------------------------------------------- /tests/integration/files/ConvertFrom-DpapiNgBlob.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Module Ctypes 2 | 3 | [CmdletBinding()] 4 | param( 5 | [Parameter(Mandatory)] 6 | [byte[]] 7 | $InputObject 8 | ) 9 | 10 | $ncrypt = New-CtypesLib ncrypt.dll 11 | 12 | $outData = [IntPtr]::Zero 13 | $descriptorHandle = [IntPtr]::Zero 14 | try { 15 | $outDataLength = 0 16 | 17 | $res = $ncrypt.NCryptUnprotectSecret( 18 | [ref]$descriptorHandle, 19 | 0x40, # NCRYPT_SILENT_FLAG, 20 | $ncrypt.MarshalAs($InputObject, 'LPArray'), 21 | $InputObject.Length, 22 | $null, 23 | $null, 24 | [ref]$outData, 25 | [ref]$outDataLength) 26 | if ($res) { 27 | throw [System.ComponentModel.Win32Exception]$res 28 | } 29 | 30 | $rawValue = [byte[]]::new($outDataLength) 31 | [System.Runtime.InteropServices.Marshal]::Copy($outData, $rawValue, 0, $rawValue.Length) 32 | $rawValue 33 | } 34 | finally { 35 | if ($outData -ne [IntPtr]::Zero) { 36 | [System.Runtime.InteropServices.Marshal]::FreeHGlobal($outData) 37 | $outData = [IntPtr]::Zero 38 | } 39 | if ($descriptorHandle -ne [IntPtr]::Zero) { 40 | $ncrypt.Returns([void]).NCryptCloseProtectionDescriptor($descriptorHandle) 41 | $descriptorHandle = [IntPtr]::Zero 42 | } 43 | } -------------------------------------------------------------------------------- /tests/integration/files/ConvertTo-DpapiNgBlob.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Module Ctypes 2 | 3 | [CmdletBinding()] 4 | param ( 5 | [Parameter(Mandatory)] 6 | [byte[]] 7 | $InputObject, 8 | 9 | [Parameter()] 10 | [string] 11 | $ProtectionDescriptor 12 | ) 13 | 14 | $ncrypt = New-CtypesLib ncrypt.dll 15 | 16 | if (-not $ProtectionDescriptor) { 17 | $ProtectionDescriptor = "SID=$([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value)" 18 | } 19 | 20 | $descriptor = [IntPtr]::Zero 21 | $res = $ncrypt.NCryptCreateProtectionDescriptor( 22 | $ncrypt.MarshalAs($ProtectionDescriptor, "LPWStr"), 23 | 0, 24 | [ref]$descriptor) 25 | if ($res) { 26 | throw [System.ComponentModel.Win32Exception]$res 27 | } 28 | 29 | $blob = [IntPtr]::Zero 30 | $blobLength = 0 31 | $res = $ncrypt.NCryptProtectSecret( 32 | $descriptor, 33 | 0x40, # NCRYPT_SILENT_FLAG 34 | $ncrypt.MarshalAs($InputObject, 'LPArray'), 35 | $InputObject.Length, 36 | $null, 37 | $null, 38 | [ref]$blob, 39 | [ref]$blobLength) 40 | if ($res) { 41 | throw [System.ComponentModel.Win32Exception]$res 42 | } 43 | 44 | try { 45 | $encBlob = [byte[]]::new($blobLength) 46 | [System.Runtime.InteropServices.Marshal]::Copy($blob, $encBlob, 0, $encBlob.Length) 47 | $encBlob 48 | } 49 | finally { 50 | [System.Runtime.InteropServices.Marshal]::FreeHGlobal($blob) 51 | } 52 | -------------------------------------------------------------------------------- /tests/integration/files/New-KdsRootKey.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter()] 4 | [ValidateSet("SHA1", "SHA256", "SHA384", "SHA512")] 5 | [string] 6 | $KdfHashAlgorithm = "SHA512", 7 | 8 | [Parameter()] 9 | [ValidateSet("DH", "ECDH_P256", "ECDH_P384")] 10 | [string] 11 | $SecretAgreementAlgorithm = "DH" 12 | ) 13 | 14 | $ErrorActionPreference = 'Stop' 15 | 16 | $kdfHashName = [System.Text.Encoding]::Unicode.GetBytes($KdfHashAlgorithm) 17 | 18 | $kdsParams = @{ 19 | KdfParameters = [byte[]]@( 20 | @(0, 0, 0, 0, 1, 0, 0, 0) 21 | [System.BitConverter]::GetBytes(($kdfHashName.Length + 2)) 22 | @(0, 0, 0, 0) 23 | $kdfHashName 24 | @(0, 0) 25 | ) 26 | } 27 | 28 | $kdsParams.SecretAgreementAlgorithm = $SecretAgreementAlgorithm 29 | if ($SecretAgreementAlgorithm -eq "DH") { 30 | $keyLength = 256 31 | $fieldOrder = [System.Numerics.BigInteger]::Parse("17125458317614137930196041979257577826408832324037508573393292981642667139747621778802438775238728592968344613589379932348475613503476932163166973813218698343816463289144185362912602522540494983090531497232965829536524507269848825658311420299335922295709743267508322525966773950394919257576842038771632742044142471053509850123605883815857162666917775193496157372656195558305727009891276006514000409365877218171388319923896309377791762590614311849642961380224851940460421710449368927252974870395873936387909672274883295377481008150475878590270591798350563488168080923804611822387520198054002990623911454389104774092183").ToByteArray($true, $true) 32 | $generator = [System.Numerics.BigInteger]::Parse("8041367327046189302693984665026706374844608289874374425728797669509435881459140662650215832833471328470334064628508692231999401840332046192569287351991689963279656892562484773278584208040987631569628520464069532361274047374444344996651832979378318849943741662110395995778429270819222431610927356005913836932462099770076239554042855287138026806960470277326229482818003962004453764400995790974042663675692120758726145869061236443893509136147942414445551848162391468541444355707785697825741856849161233887307017428371823608125699892904960841221593344499088996021883972185241854777608212592397013510086894908468466292313").ToByteArray($true, $true) 33 | 34 | $kdsParams.SecretAgreementParameters = [byte[]]@( 35 | [System.BitConverter]::GetBytes(12 + $fieldOrder.Length + $generator.Length) 36 | @(0x44, 0x48, 0x50, 0x4D) 37 | [System.BitConverter]::GetBytes($keyLength) 38 | $fieldOrder 39 | $generator 40 | ) 41 | $kdsParams.SecretAgreementPrivateKeyLength = 512 42 | $kdsParams.SecretAgreementPublicKeyLength = 2048 43 | } 44 | else { 45 | # ECDH_P521 is also meant to work but I keep on getting errors setting it 46 | $kdsParams.SecretAgreementAlgorithm = $SecretAgreementAlgorithm 47 | $kdsParams.SecretAgreementParameters = $null 48 | $kdsParams.SecretAgreementPublicKeyLength = $kdsParams.SecretAgreementPrivateKeyLength = switch ($SecretAgreementAlgorithm) { 49 | ECDH_P256 { 256 } 50 | ECDH_P384 { 384 } 51 | } 52 | } 53 | $null = Set-KdsConfiguration @kdsParams 54 | 55 | $newKey = Add-KdsRootKey -EffectiveImmediately 56 | $cryptoKeysPath = "$env:LOCALAPPDATA\Microsoft\Crypto\KdsKey" 57 | if (Test-Path -LiteralPath $cryptoKeysPath) { 58 | Get-ChildItem -LiteralPath $cryptoKeysPath | Remove-Item -Force -Recurse 59 | } 60 | Restart-Service -Name KdsSvc 61 | 62 | $configurationContext = (Get-ADRootDSE).configurationNamingContext 63 | $kdsBase = "CN=Group Key Distribution Service,CN=Services,$configurationContext" 64 | $getParams = @{ 65 | LDAPFilter = "(&(cn=$($newKey.Guid))(objectClass=msKds-ProvRootKey))" 66 | SearchBase = "CN=Master Root Keys,$kdsBase" 67 | SearchScope = 'OneLevel' 68 | Properties = @( 69 | 'cn' 70 | 'msKds-KDFAlgorithmID' 71 | 'msKds-KDFParam' 72 | 'msKds-SecretAgreementAlgorithmID' 73 | 'msKds-SecretAgreementParam' 74 | 'msKds-PrivateKeyLength' 75 | 'msKds-PublicKeyLength' 76 | 'msKds-RootKeyData' 77 | ) 78 | } 79 | Get-ADObject @getParams | ForEach-Object { 80 | $secretParams = if ($_.'msKds-SecretAgreementParam') { 81 | $_.'msKds-SecretAgreementParam' 82 | } 83 | else { 84 | , [byte[]]::new(0) 85 | } 86 | [PSCustomObject]@{ 87 | Version = 1 88 | RootKeyId = [Guid]::new($_.cn) 89 | KdfAlgorithm = $_.'msKds-KDFAlgorithmID' 90 | KdfParameters = [System.Convert]::ToHexString($_.'msKds-KDFParam') 91 | SecretAgreementAlgorithm = $_.'msKds-SecretAgreementAlgorithmID' 92 | SecretAgreementParameters = [System.Convert]::ToHexString($secretParams) 93 | PrivateKeyLength = $_.'msKds-PrivateKeyLength' 94 | PublicKeyLength = $_.'msKds-PublicKeyLength' 95 | RootKeyData = [System.Convert]::ToHexString($_.'msKds-RootKeyData') 96 | } 97 | } | ConvertTo-Json 98 | -------------------------------------------------------------------------------- /tests/integration/files/generate_seed_keys.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import uuid 3 | 4 | from cryptography.hazmat.primitives import hashes 5 | 6 | from dpapi_ng import _crypto as crypto 7 | from dpapi_ng import _gkdi as gkdi 8 | 9 | algorithm = hashes.SHA512() 10 | key_id = uuid.UUID("2e1b932a-4e21-ced3-0b7b-8815aff8335d") 11 | 12 | # This should be the L1 seed key for (L0, 31, -1) 13 | l1_key = base64.b16decode( 14 | "60E0A81F93164F5DC3ABE981E1EE54C1A6B9B0EDB6FF8274642758D29BBC66559D11F1871A82A6E3F232C42490D7C41C6AD2B8B189FE2752A88CEC2EA4B2021C" 15 | ) 16 | l0 = 361 17 | l1 = 31 18 | l2 = 31 19 | l2_key = b"" 20 | 21 | while l1 >= 0: 22 | print(f"({l0}, {l1:<2}, -1) {base64.b16encode(l1_key).decode()}") 23 | 24 | while l2 >= 0: 25 | if l2 == 31: 26 | l2_key = crypto.kdf( 27 | algorithm, 28 | l1_key, 29 | gkdi.KDS_SERVICE_LABEL, 30 | gkdi.compute_kdf_context(key_id, l0, l1, l2), 31 | 64, 32 | ) 33 | 34 | else: 35 | l2_key = crypto.kdf( 36 | algorithm, 37 | l2_key, 38 | gkdi.KDS_SERVICE_LABEL, 39 | gkdi.compute_kdf_context(key_id, l0, l1, l2), 40 | 64, 41 | ) 42 | 43 | print(f"({l0}, {l1:<2}, {l2:<2}) {base64.b16encode(l2_key).decode()}") 44 | l2 -= 1 45 | 46 | l2 = 31 47 | l1 -= 1 48 | l1_key = crypto.kdf( 49 | algorithm, 50 | l1_key, 51 | gkdi.KDS_SERVICE_LABEL, 52 | gkdi.compute_kdf_context(key_id, l0, l1, -1), 53 | 64, 54 | ) 55 | -------------------------------------------------------------------------------- /tests/integration/files/sp800_56a_concat.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import dataclasses 3 | import hashlib 4 | import sys 5 | import typing as t 6 | 7 | from cryptography.hazmat.primitives import hashes 8 | from cryptography.hazmat.primitives.asymmetric import ec 9 | from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash 10 | 11 | 12 | @dataclasses.dataclass(frozen=True) 13 | class FFCDHKey: 14 | # MS-GKDI 2.2.3.1 FFC DH Key: 15 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gkdi/f8770f01-036d-4bf6-a4cf-1bd0e3913404 16 | 17 | magic: bytes = dataclasses.field(init=False, repr=False, default=b"\x44\x48\x50\x42") 18 | key_length: int 19 | field_order: int 20 | generator: int 21 | public_key: int 22 | 23 | @classmethod 24 | def unpack( 25 | cls, 26 | data: t.Union[bytes, bytearray, memoryview], 27 | ) -> "FFCDHKey": 28 | view = memoryview(data) 29 | 30 | if view[:4].tobytes() != cls.magic: 31 | raise ValueError(f"Failed to unpack {cls.__name__} as magic identifier is invalid") 32 | 33 | key_length = int.from_bytes(view[4:8], byteorder="little") 34 | 35 | field_order = view[8 : 8 + key_length].tobytes() 36 | view = view[8 + key_length :] 37 | 38 | generator = view[:key_length].tobytes() 39 | view = view[key_length:] 40 | 41 | public_key = view[:key_length].tobytes() 42 | 43 | return FFCDHKey( 44 | key_length=key_length, 45 | field_order=int.from_bytes(field_order, byteorder="big"), 46 | generator=int.from_bytes(generator, byteorder="big"), 47 | public_key=int.from_bytes(public_key, byteorder="big"), 48 | ) 49 | 50 | 51 | @dataclasses.dataclass(frozen=True) 52 | class ECDHKey: 53 | # MS-GKDI 2.2.3.2 ECDH Key: 54 | # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gkdi/24876a37-9a92-4187-9052-222bb6f85d4a 55 | 56 | curve_name: str 57 | key_length: int 58 | x: int 59 | y: int 60 | 61 | @property 62 | def curve_and_hash(self) -> tuple[ec.EllipticCurve, hashes.HashAlgorithm]: 63 | return { 64 | "P256": (ec.SECP256R1(), hashes.SHA256()), 65 | "P384": (ec.SECP384R1(), hashes.SHA384()), 66 | "P521": (ec.SECP521R1(), hashes.SHA512()), 67 | }[self.curve_name] 68 | 69 | @classmethod 70 | def unpack( 71 | cls, 72 | data: t.Union[bytes, bytearray, memoryview], 73 | ) -> "ECDHKey": 74 | view = memoryview(data) 75 | 76 | curve_id = int.from_bytes(view[:4], byteorder="little") 77 | curve = { 78 | 0x314B4345: "P256", 79 | 0x334B4345: "P384", 80 | 0x354B4345: "P521", 81 | }.get(curve_id, None) 82 | if not curve: 83 | raise ValueError(f"Failed to unpack {cls.__name__} with unknown curve 0x{curve_id:08X}") 84 | 85 | length = int.from_bytes(view[4:8], byteorder="little") 86 | 87 | x = view[8 : 8 + length].tobytes() 88 | view = view[8 + length :] 89 | 90 | y = view[:length].tobytes() 91 | 92 | return ECDHKey( 93 | curve_name=curve, 94 | key_length=length, 95 | x=int.from_bytes(x, byteorder="big"), 96 | y=int.from_bytes(y, byteorder="big"), 97 | ) 98 | 99 | 100 | scenarios = { 101 | "DH": { 102 | "KeyLength": 2048, 103 | "SecretParams": "0C0200004448504D0001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", 104 | "PublicKey": "444850420001000087A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A15973FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659535CC9DB0F3BE1D18BA5D691DCBD7ADFC2A3F331E8875264BDB99B71F0DD0715ED1002DFFDC00BEA4A252738BFD027B283C0A61C3BA7A060732B2DBEC520BCA23941810CBC555A4C69F45F35C05EE02E71E3ACB6ED5B9B55F0DC408E13640CDC58A04900E73018ADBD7D5840DD29CB6482AF75483C22AF35A48AD0D166FADED4C1C58F749CD130BDB4726938FC6A90E17726D75B2284592AA292B52A97807B80355705794340702333C9558EC671DD206D9C796BC26953D7F7261776E69A2DA8496E3AD04877D645571BBCCE655CD57C53BFE3406B457B807BB497B79C99D0766DD3D19B594E98D5B685302171A02313DA5BE5F5F6D1B98BC6B9BF5B68992C1C", 105 | "PrivateKey": "8844C8622AF1CC0D2ACF6581CD4F03DBDA3C97C08E37C97CA6E8C30C0B4E27926C6726FF6EF74EFA6D7F9AF818564A73D511A661E37CB41D07098B0F4D0D5B80", 106 | "ExpectedSecretAgreement": "2F3ED6CECAFBDF3E386240FCFB5B499310015243651BC97AEFF9EE23E760100E3A825814CCBD7B064339B9105512CEA22D6E9C0FEFBBDADCC26E01BD8F286BD9993F0068B3CFEF9113311BBAA7B37D05462E2B740259CC211E75260708706A98B3FB967C45109FDABF6589312490B6F03AA65F0E4C882317865C55708916D82B962912909F6ED6E85EDEB7CB2CC1AC8C812A390A23B4E2DB645D17BF7417BF9F176CE807366A46A5797E7F5828B64389ADAEDDED099A9BC634C037315D0389B008C5D3CB966AF458FC524F8C7FBF814DD93117979F5410952510BED6B5BA4A7DBCA10FFDC4B7A5709C223C9A1AF9C9B36BF92883333417A3606CC59A200B12D1", 107 | "ExpectedSecret": "CAF41BA47887FB04AC7B80B6FF1BD15C5135500372B889A143C3AAC7D695955E", 108 | }, 109 | "ECDH_P256": { 110 | "KeyLength": 256, 111 | "SecretParams": "", 112 | "PublicKey": "45434B31200000005AFBDDBB412E39B367302AEDF04A8F8D184D9801FA4A560BF35AB0FD83E5C93CB5EE98D8B938672442C1CFCDFAADADA31A014776F9B72C2CA9F06E5B6DDA2218", 113 | "PrivateKey": "B65D20E0916BE7C6A9F865826432C4F3B5347FAA07271D675C065EE2BA34AA13", 114 | "ExpectedSecretAgreement": "98E13228F67F865CB9A699679F37C394BCA0DF718AF71C9F9E97B7108C16D74B", 115 | "ExpectedSecret": "37208B86DC931C2F5FB4E5A0295B877D0AE98B8F4CE79F6B407B5926B519AF6C", 116 | }, 117 | "ECDH_P384": { 118 | "KeyLength": 384, 119 | "SecretParams": "", 120 | "PublicKey": "45434B3330000000519FB25DB5692BADEE44DE4044EC0F0AAF4BD86F9D3F1967031AD2E7B7A656EEE7E114EFFCF0D83E682C246F3E04119FED903CE810FC060F6DCA3E4901E20DFA5EE82AE9DCB237102892E8A2997BFF0CC9C755541F066E83550FD200B3D4B50E", 121 | "PrivateKey": "1D94E9DE911B17981356E4464B691FDAB12AE822ECC152C2D786CD060CA32255ACE7B1EB0F3644AAE19AF5F75D03FFC3", 122 | "ExpectedSecretAgreement": "2441392B082A8A8AD52BE6707517DA5CF19A629B4AF177B19FF03D13BF6CBA2D0A06A98259C16F6A3CDDD29EE50B72FC", 123 | "ExpectedSecret": "8A10120B4DD74F21DA5C79917E993DBC5DE1A8346112A33300C8F30CCC77E5F1DEAD404BBDA472E6CB42D6A448872425", 124 | }, 125 | } 126 | 127 | scenario = sys.argv[1] 128 | key_length = scenarios[scenario]["KeyLength"] 129 | secret_params = base64.b16decode(str(scenarios[scenario]["SecretParams"])) 130 | public_key = base64.b16decode(str(scenarios[scenario]["PublicKey"])) 131 | private_key = base64.b16decode(str(scenarios[scenario]["PrivateKey"])) 132 | expected_secret_agreement = base64.b16decode(str(scenarios[scenario]["ExpectedSecretAgreement"])) 133 | expected_secret = base64.b16decode(str(scenarios[scenario]["ExpectedSecret"])) 134 | 135 | secret_hash_algorithm: hashes.HashAlgorithm 136 | if scenario == "DH": 137 | dh_pub_key = FFCDHKey.unpack(public_key) 138 | shared_secret_int = pow( 139 | dh_pub_key.public_key, 140 | int.from_bytes(private_key, byteorder="big"), 141 | dh_pub_key.field_order, 142 | ) 143 | shared_secret = shared_secret_int.to_bytes(dh_pub_key.key_length, byteorder="big") 144 | secret_hash_algorithm = hashes.SHA256() 145 | 146 | else: 147 | ecdh_pub_key_info = ECDHKey.unpack(public_key) 148 | curve, secret_hash_algorithm = ecdh_pub_key_info.curve_and_hash 149 | 150 | ecdh_pub_key = ec.EllipticCurvePublicNumbers(ecdh_pub_key_info.x, ecdh_pub_key_info.y, curve).public_key() 151 | ecdh_private = ec.derive_private_key( 152 | int.from_bytes(private_key, byteorder="big"), 153 | curve, 154 | ) 155 | shared_secret = ecdh_private.exchange(ec.ECDH(), ecdh_pub_key) 156 | 157 | 158 | shared_secret1 = hashlib.sha256(shared_secret).digest() 159 | print(f"Actual Secret Agreement : {base64.b16encode(shared_secret).decode()}") 160 | print(f"Expected Secret Agreement: {base64.b16encode(expected_secret_agreement).decode()}") 161 | 162 | algorithm_id = "SHA512\0" 163 | party_uinfo = "KDS public key\0" 164 | party_vinfo = "KDS service\0" 165 | 166 | otherinfo = f"{algorithm_id}{party_uinfo}{party_vinfo}".encode("utf-16-le") 167 | actual = ConcatKDFHash( 168 | secret_hash_algorithm, 169 | length=secret_hash_algorithm.digest_size, 170 | otherinfo=otherinfo, 171 | ).derive(shared_secret) 172 | 173 | print(f"Actual Secret : {base64.b16encode(actual).decode()}") 174 | print(f"Expected Secret: {base64.b16encode(expected_secret).decode()}") 175 | -------------------------------------------------------------------------------- /tests/integration/inventory.yml: -------------------------------------------------------------------------------- 1 | all: 2 | children: 3 | windows: 4 | hosts: 5 | DC01: 6 | ansible_host: 192.168.3.10 7 | vagrant_box: jborean93/WindowsServer2022 8 | APP01: 9 | ansible_host: 192.168.3.11 10 | vagrant_box: jborean93/WindowsServer2022 11 | vars: 12 | ansible_connection: psrp 13 | ansible_port: 5985 14 | python_interpreters: 15 | - C:\Program Files\Python39 16 | - C:\Program Files (x86)\Python39-32 17 | - C:\Program Files\Python310 18 | - C:\Program Files (x86)\Python310-32 19 | - C:\Program Files\Python311 20 | - C:\Program Files (x86)\Python311-32 21 | - C:\Program Files\Python312 22 | - C:\Program Files (x86)\Python312-32 23 | - C:\Program Files\Python313 24 | - C:\Program Files (x86)\Python313-32 25 | 26 | linux: 27 | hosts: 28 | DEBIAN11-MIT: 29 | ansible_host: 192.168.3.12 30 | vagrant_box: generic/debian11 31 | krb_provider: mit 32 | krb_packages: 33 | - krb5-user 34 | - libkrb5-dev 35 | DEBIAN11-HEIMDAL: 36 | ansible_host: 192.168.3.13 37 | vagrant_box: generic/debian11 38 | krb_provider: heimdal 39 | krb_packages: 40 | - heimdal-clients 41 | - heimdal-dev 42 | 43 | vars: 44 | ansible_ssh_common_args: -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no 45 | 46 | vars: 47 | ansible_user: vagrant 48 | ansible_password: vagrant 49 | 50 | domain_name: dpaping.test 51 | domain_username: vagrant-domain 52 | domain_username2: vagrant-domain2 53 | domain_password: VagrantPass1 54 | domain_user_upn: '{{ domain_username }}@{{ domain_name | upper }}' 55 | domain_user_upn2: '{{ domain_username2 }}@{{ domain_name | upper }}' 56 | -------------------------------------------------------------------------------- /tests/integration/main.yml: -------------------------------------------------------------------------------- 1 | - name: setup common Windows information 2 | hosts: windows 3 | gather_facts: false 4 | 5 | tasks: 6 | - name: get network connection names 7 | ansible.windows.win_powershell: 8 | script: | 9 | $ErrorActionPreference = 'Stop' 10 | $Ansible.Changed = $false 11 | 12 | Get-CimInstance -ClassName Win32_NetworkAdapter -Filter "Netenabled='True'" | ForEach-Object -Process { 13 | $config = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index='$($_.Index)'" 14 | if ($config.IPAddress -contains '{{ ansible_host }}') { 15 | $_.NetConnectionID 16 | } 17 | } 18 | register: raw_connection_name 19 | 20 | - set_fact: 21 | public_connection_name: '{{ raw_connection_name.output[0] }}' 22 | 23 | - name: install ctypes PowerShell module 24 | ansible.windows.win_powershell: 25 | script: | 26 | $ErrorActionPreference = 'Stop' 27 | 28 | $ctypesPath = 'C:\Program Files\WindowsPowerShell\Modules\Ctypes' 29 | if (Test-Path -LiteralPath $ctypesPath) { 30 | $Ansible.Changed = $false 31 | } 32 | else { 33 | Install-Module -Name Ctypes -Scope AllUsers -Force 34 | } 35 | 36 | - name: copy across scripts for testing 37 | ansible.windows.win_copy: 38 | src: '{{ item }}' 39 | dest: C:\temp\{{ item }} 40 | loop: 41 | - ConvertFrom-DpapiNgBlob.ps1 42 | - ConvertTo-DpapiNgBlob.ps1 43 | - New-KdsRootKey.ps1 44 | 45 | - name: create domain controller 46 | hosts: DC01 47 | gather_facts: false 48 | 49 | tasks: 50 | - name: set the DNS for the internal adapters to localhost 51 | ansible.windows.win_dns_client: 52 | adapter_names: 53 | - '{{ public_connection_name }}' 54 | dns_servers: 55 | - 127.0.0.1 56 | 57 | - name: ensure domain exists and DC is promoted as a domain controller 58 | microsoft.ad.domain: 59 | dns_domain_name: '{{ domain_name }}' 60 | safe_mode_password: '{{ domain_password }}' 61 | reboot: true 62 | 63 | - name: create domain username 64 | microsoft.ad.user: 65 | name: '{{ domain_username }}' 66 | upn: '{{ domain_user_upn }}' 67 | description: '{{ domain_username }} Domain Account' 68 | password: '{{ domain_password }}' 69 | password_never_expires: true 70 | update_password: when_changed 71 | groups: 72 | set: 73 | - Domain Admins 74 | - Domain Users 75 | - Enterprise Admins 76 | state: present 77 | 78 | - name: create domain username for other test 79 | microsoft.ad.user: 80 | name: '{{ domain_username2 }}' 81 | upn: '{{ domain_user_upn2 }}' 82 | description: '{{ domain_username }} Domain Account' 83 | password: '{{ domain_password }}' 84 | password_never_expires: true 85 | update_password: when_changed 86 | groups: 87 | set: 88 | - Domain Admins 89 | - Domain Users 90 | - Enterprise Admins 91 | state: present 92 | 93 | - name: join Windows host to domain 94 | hosts: APP01 95 | gather_facts: false 96 | 97 | tasks: 98 | - name: set the DNS for the private adapter to point to the DC 99 | ansible.windows.win_dns_client: 100 | adapter_names: 101 | - '{{ public_connection_name }}' 102 | dns_servers: 103 | - '{{ hostvars["DC01"]["ansible_host"] }}' 104 | 105 | - name: join host to domain 106 | microsoft.ad.membership: 107 | dns_domain_name: '{{ domain_name }}' 108 | domain_admin_user: '{{ domain_user_upn }}' 109 | domain_admin_password: '{{ domain_password }}' 110 | state: domain 111 | reboot: true 112 | 113 | - name: test out domain user logon 114 | ansible.windows.win_whoami: 115 | register: become_res 116 | failed_when: become_res.upn != domain_user_upn 117 | become: true 118 | become_method: runas 119 | vars: 120 | ansible_become_user: '{{ domain_user_upn }}' 121 | ansible_become_pass: '{{ domain_password }}' 122 | 123 | # Use the following to get a snaphot of programs installed and their product_ids 124 | # 'SOFTWARE', 'SOFTWARE\Wow6432Node' | ForEach-Object { 125 | # $getParams = @{ 126 | # Path = "HKLM:\$_\Microsoft\Windows\CurrentVersion\Uninstall\*" 127 | # Name = 'DisplayName' 128 | # ErrorAction = 'SilentlyContinue' 129 | # } 130 | # Get-ItemProperty @getParams | Select-Object -Property @( 131 | # @{ N = 'Name'; E = { $_.DisplayName } }, 132 | # @{ N = 'AppId'; E = { $_.PSChildName } } 133 | # ) 134 | # } | Where-Object { $_.Name -like 'Python * Standard Library *' } | 135 | # Sort-Object { [Version]$_.Name.Split(' ')[1] }, Name 136 | 137 | - name: install Python interpreters 138 | ansible.windows.win_package: 139 | path: '{{ item.url }}' 140 | arguments: '{{ item.arguments }}' 141 | product_id: '{{ item.product_id }}' 142 | state: present 143 | with_items: 144 | - url: https://www.python.org/ftp/python/3.9.13/python-3.9.13.exe 145 | product_id: '{E23C472D-F346-4D47-A909-9D48E5D7252F}' 146 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 147 | - url: https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe 148 | product_id: '{90A30DAB-6FD8-4CF8-BB8B-C0DB21C69F20}' 149 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 150 | - url: https://www.python.org/ftp/python/3.10.11/python-3.10.11.exe 151 | product_id: '{2627E7A3-6630-4858-8151-D91D1AF62F8E}' 152 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 153 | - url: https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe 154 | product_id: '{6532871D-1F76-408C-ABD0-63C732137351}' 155 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 156 | - url: https://www.python.org/ftp/python/3.11.9/python-3.11.9.exe 157 | product_id: '{89D284CB-6250-4C7A-88DD-56A7CE162ACD}' 158 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 159 | - url: https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe 160 | product_id: '{9AFDC691-40E5-4B15-835F-9A524AC4672C}' 161 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 162 | - url: https://www.python.org/ftp/python/3.12.8/python-3.12.8.exe 163 | product_id: '{7F7E778C-EE41-4573-A6A0-CD38F116A464}' 164 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 165 | - url: https://www.python.org/ftp/python/3.12.8/python-3.12.8-amd64.exe 166 | product_id: '{DF8A5B68-D4BE-4580-9777-DA1A5EBA9843}' 167 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 168 | - url: https://www.python.org/ftp/python/3.13.1/python-3.13.1.exe 169 | product_id: '{CF9B1D77-9343-43AC-AB82-C8E7F1973411}' 170 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 171 | - url: https://www.python.org/ftp/python/3.13.1/python-3.13.1-amd64.exe 172 | product_id: '{29A3DBE6-A3D3-42C9-9338-A321F61C897A}' 173 | arguments: /quiet InstallAllUsers=1 Shortcuts=0 174 | 175 | - name: ensure virtualenv package is installed for each Python install 176 | ansible.windows.win_command: 177 | argv: 178 | - '{{ item }}\python.exe' 179 | - -m 180 | - pip 181 | - install 182 | - virtualenv 183 | args: 184 | creates: '{{ item }}\Scripts\virtualenv.exe' 185 | with_items: '{{ python_interpreters }}' 186 | 187 | - name: create virtualenv for each Python install 188 | ansible.windows.win_command: 189 | argv: 190 | - '{{ item }}\python.exe' 191 | - -m 192 | - virtualenv 193 | - C:\temp\venv\{{ item | win_basename }} 194 | args: 195 | creates: c:\temp\venv\{{ item | win_basename }} 196 | with_items: '{{ python_interpreters }}' 197 | 198 | - name: copy across wheel artifacts 199 | ansible.windows.win_copy: 200 | src: artifact.zip 201 | dest: C:\temp\wheels.zip 202 | 203 | - name: ensure wheel dir exists 204 | ansible.windows.win_file: 205 | path: C:\temp\wheels 206 | state: directory 207 | 208 | - name: extract wheel from archive 209 | community.windows.win_unzip: 210 | src: C:\temp\wheels.zip 211 | dest: C:\temp\wheels 212 | 213 | - name: get dpapi_ng artifact sdist filename 214 | ansible.windows.win_find: 215 | paths: C:\temp\wheels 216 | patterns: 'dpapi_ng-*.tar.gz' 217 | use_regex: false 218 | register: dpapi_ng_sdist_file 219 | 220 | - name: verify sdist was found 221 | assert: 222 | that: 223 | - dpapi_ng_sdist_file.files | count == 1 224 | 225 | - name: get dpapi-ng artifact version 226 | set_fact: 227 | dpapi_ng_version: >- 228 | {{ dpapi_ng_sdist_file.files[0].filename | regex_replace('dpapi_ng-(?P.*)\.tar\.gz', '\g') }} 229 | 230 | - name: install dpapi-ng into virtualenv 231 | ansible.windows.win_command: 232 | argv: 233 | - c:\temp\venv\{{ item | win_basename }}\Scripts\python.exe 234 | - -m 235 | - pip 236 | - install 237 | - dpapi-ng=={{ dpapi_ng_version }} 238 | - pypsrp[kerberos]==1.0.0b1 239 | - pyspnego[kerberos] 240 | - pytest 241 | - pytest-asyncio 242 | - --find-links=C:/temp/wheels 243 | args: 244 | creates: c:\temp\venv\{{ item | win_basename }}\Lib\site-packages\dpapi_ng 245 | with_items: '{{ python_interpreters }}' 246 | 247 | - name: set up Linux host 248 | hosts: linux 249 | gather_facts: false 250 | become: true 251 | 252 | tasks: 253 | - name: install base packages 254 | ansible.builtin.apt: 255 | name: 256 | - gcc 257 | - make 258 | - python3 259 | - python3-dev 260 | - python3-venv 261 | - unzip 262 | - vim 263 | state: present 264 | 265 | - name: install kerberos packages 266 | ansible.builtin.apt: 267 | name: '{{ krb_packages }}' 268 | state: present 269 | 270 | - name: template krb5.conf file 271 | ansible.builtin.template: 272 | src: krb5.conf.j2 273 | dest: /etc/krb5.conf 274 | 275 | - name: setup DNS settings for eth0 adapter 276 | ansible.builtin.copy: 277 | content: | 278 | [Match] 279 | Name=eth0 280 | 281 | [Network] 282 | DHCP=ipv4 283 | dest: /etc/systemd/network/eth0.network 284 | register: eth0_networkd 285 | 286 | - name: setup DNS settings for eth1 adapter 287 | ansible.builtin.copy: 288 | content: | 289 | [Match] 290 | Name=eth1 291 | 292 | [Network] 293 | Address={{ ansible_host }}/24 294 | Gateway=192.168.3.1 295 | DNS={{ hostvars["DC01"]["ansible_host"] }} 296 | Domains=~{{ domain_name }} 297 | dest: /etc/systemd/network/eth1.network 298 | register: eth1_networkd 299 | 300 | - name: ensure resolv.conf is pointing to systemd 301 | ansible.builtin.file: 302 | src: /run/systemd/resolve/stub-resolv.conf 303 | dest: /etc/resolv.conf 304 | state: link 305 | force: true 306 | register: resolv_conf_repoint 307 | 308 | - name: start and enable the systemd DNS services 309 | ansible.builtin.service: 310 | name: '{{ item }}' 311 | enabled: True 312 | state: restarted 313 | when: >- 314 | eth0_networkd is changed or 315 | eth1_networkd is changed or 316 | resolv_conf_repoint is changed 317 | loop: 318 | - systemd-resolved 319 | - systemd-networkd 320 | 321 | - name: create user keytab - MIT 322 | ansible.builtin.command: ktutil 323 | args: 324 | chdir: ~/ 325 | creates: ~/user.keytab 326 | stdin: "addent -password -p {{ domain_user_upn }} -k 1 -e aes256-cts\n{{ domain_password }}\nwrite_kt user.keytab" 327 | become: false 328 | when: krb_provider == 'mit' 329 | 330 | - name: create user keytab - Heimdal 331 | ansible.builtin.command: >- 332 | ktutil 333 | --keytab=user.keytab 334 | add 335 | --principal={{ domain_user_upn }} 336 | --kvno=1 337 | --enctype=aes256-cts 338 | --password={{ domain_password }} 339 | args: 340 | chdir: ~/ 341 | creates: ~/user.keytab 342 | become: false 343 | when: krb_provider == 'heimdal' 344 | 345 | - name: ensure wheel dir exists 346 | ansible.builtin.file: 347 | path: ~/wheels 348 | state: directory 349 | become: false 350 | 351 | - name: extract wheel artifacts 352 | ansible.builtin.unarchive: 353 | src: artifact.zip 354 | dest: ~/wheels 355 | become: false 356 | 357 | - name: get dpapi-ng artifact sdist filename 358 | ansible.builtin.find: 359 | paths: ~/wheels 360 | patterns: 'dpapi_ng-*.tar.gz' 361 | recurse: false 362 | file_type: file 363 | become: false 364 | register: dpapi_ng_sdist_file 365 | 366 | - name: verify sdist was found 367 | assert: 368 | that: 369 | - dpapi_ng_sdist_file.files | count == 1 370 | 371 | - name: get dpapi-ng artifact version 372 | set_fact: 373 | dpapi_ng_version: >- 374 | {{ dpapi_ng_sdist_file.files[0].path | basename | regex_replace('dpapi_ng-(?P.*)\.tar\.gz', '\g') }} 375 | 376 | - name: create a virtualenv for each Python interpreter 377 | ansible.builtin.pip: 378 | name: 379 | - dpapi-ng=={{ dpapi_ng_version }} 380 | - pypsrp[kerberos]==1.0.0b1 381 | - pyspnego[kerberos] 382 | - pytest 383 | - pytest-asyncio 384 | virtualenv: ~/venv/dpapi-ng 385 | virtualenv_command: /usr/bin/python3 -m venv 386 | extra_args: --find-links file:///{{ dpapi_ng_sdist_file.files[0].path | dirname }} 387 | become: false 388 | -------------------------------------------------------------------------------- /tests/integration/requirements.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | - ansible.windows 3 | - community.windows 4 | - microsoft.ad 5 | -------------------------------------------------------------------------------- /tests/integration/run_test.yml: -------------------------------------------------------------------------------- 1 | - name: run integration tests on Windows 2 | hosts: APP01 3 | gather_facts: false 4 | tags: 5 | - windows 6 | 7 | tasks: 8 | - name: run integration tests 9 | ansible.windows.win_command: 10 | argv: 11 | - C:\temp\venv\{{ item | win_basename }}\Scripts\python.exe 12 | - -m 13 | - pytest 14 | - C:\temp\test_integration.py 15 | - -v 16 | with_items: '{{ python_interpreters }}' 17 | become: yes 18 | become_method: runas 19 | vars: 20 | ansible_become_user: '{{ domain_user_upn }}' 21 | ansible_become_pass: '{{ domain_password }}' 22 | 23 | - name: run integration tests on Linux 24 | hosts: linux 25 | gather_facts: false 26 | tags: 27 | - linux 28 | 29 | tasks: 30 | - name: run integration tests 31 | ansible.builtin.command: 32 | argv: 33 | - ~/venv/dpapi-ng/bin/python 34 | - -m 35 | - pytest 36 | - ~/test_integration.py 37 | - -v 38 | environment: 39 | KRB5CCNAME: /tmp/krb5.ccache 40 | -------------------------------------------------------------------------------- /tests/integration/templates/krb5.conf.j2: -------------------------------------------------------------------------------- 1 | [libdefaults] 2 | default_realm = {{ domain_name | upper }} 3 | {% if krb_provider == 'mit' %} 4 | dns_canonicalize_hostname = false 5 | {% endif %} -------------------------------------------------------------------------------- /tests/integration/templates/test_integration.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | import os 5 | import socket 6 | import typing as t 7 | 8 | import psrp 9 | import pytest 10 | 11 | import dpapi_ng 12 | 13 | DOMAIN_REALM = "{{ domain_name }}" 14 | DC_FQDN = f"dc01.{DOMAIN_REALM}" 15 | DC_IP = socket.gethostbyname(DC_FQDN) 16 | USERNAME1 = "{{ domain_username | lower }}" 17 | USERNAME2 = "{{ domain_username2 | lower }}" 18 | PASSWORD = "{{ domain_password }}" 19 | USER_UPN = f"{USERNAME1}@{DOMAIN_REALM.upper()}" 20 | 21 | GET_SID_SCRIPT = r"""[CmdletBinding()] 22 | param ( 23 | [Parameter(Mandatory)] 24 | [string] 25 | $UserName 26 | ) 27 | 28 | ([System.Security.Principal.NTAccount]$UserName).Translate([System.Security.Principal.SecurityIdentifier]).Value 29 | """ 30 | 31 | ENCRYPT_SCRIPT = r"""[CmdletBinding()] 32 | param ( 33 | [Parameter(Mandatory)] 34 | [string] 35 | $ProtectionDescriptor, 36 | 37 | [Parameter(Mandatory)] 38 | [byte[]] 39 | $InputObject 40 | ) 41 | 42 | $ErrorActionPreference = 'Stop' 43 | 44 | # Ensure we remove any cached key so the latest KDS root is selected 45 | $cryptoKeysPath = "$env:LOCALAPPDATA\Microsoft\Crypto\KdsKey" 46 | if (Test-Path -LiteralPath $cryptoKeysPath) { 47 | Get-ChildItem -LiteralPath $cryptoKeysPath | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue 48 | } 49 | 50 | [byte[]]$res = . "C:\temp\ConvertTo-DpapiNgBlob.ps1" -ProtectionDescriptor $ProtectionDescriptor -InputObject $InputObject 51 | , $res 52 | """ 53 | 54 | DECRYPT_SCRIPT = r"""[CmdletBinding()] 55 | param ( 56 | [Parameter(Mandatory)] 57 | [byte[]] 58 | $InputObject 59 | ) 60 | 61 | $ErrorActionPreference = 'Stop' 62 | 63 | [byte[]]$res = . "C:\temp\ConvertFrom-DpapiNgBlob.ps1" -InputObject $InputObject 64 | , $res 65 | """ 66 | 67 | wsman = psrp.WSManInfo(DC_FQDN) 68 | with psrp.SyncRunspacePool(wsman) as rp: 69 | ps = psrp.SyncPowerShell(rp) 70 | ps.add_script(GET_SID_SCRIPT).add_parameter("UserName", USERNAME1) 71 | USERNAME1_SID = ps.invoke()[0] 72 | 73 | ps = psrp.SyncPowerShell(rp) 74 | ps.add_script(GET_SID_SCRIPT).add_parameter("UserName", USERNAME2) 75 | USERNAME2_SID = ps.invoke()[0] 76 | 77 | 78 | def test_decrypt_sync_with_nonce() -> None: 79 | data = os.urandom(64) 80 | 81 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME1, password=PASSWORD) 82 | with psrp.SyncRunspacePool(wsman) as rp: 83 | ps = psrp.SyncPowerShell(rp) 84 | ps.add_script(ENCRYPT_SCRIPT).add_parameters( 85 | ProtectionDescriptor=f"SID={USERNAME1_SID}", 86 | InputObject=data, 87 | ) 88 | enc_blob = ps.invoke()[0] 89 | 90 | actual = dpapi_ng.ncrypt_unprotect_secret(enc_blob) 91 | assert actual == data 92 | 93 | 94 | @pytest.mark.asyncio 95 | async def test_decrypt_async_with_nonce() -> None: 96 | data = os.urandom(64) 97 | 98 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME1, password=PASSWORD) 99 | async with psrp.AsyncRunspacePool(wsman) as rp: 100 | ps = psrp.AsyncPowerShell(rp) 101 | ps.add_script(ENCRYPT_SCRIPT).add_parameters( 102 | ProtectionDescriptor=f"SID={USERNAME1_SID}", 103 | InputObject=data, 104 | ) 105 | enc_blob = (await ps.invoke())[0] 106 | 107 | actual = await dpapi_ng.async_ncrypt_unprotect_secret(enc_blob) 108 | assert actual == data 109 | 110 | 111 | def test_decrypt_sync_with_public_key() -> None: 112 | data = os.urandom(64) 113 | 114 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME2, password=PASSWORD) 115 | with psrp.SyncRunspacePool(wsman) as rp: 116 | ps = psrp.SyncPowerShell(rp) 117 | ps.add_script(ENCRYPT_SCRIPT).add_parameters( 118 | ProtectionDescriptor=f"SID={USERNAME1_SID}", 119 | InputObject=data, 120 | ) 121 | enc_blob = ps.invoke()[0] 122 | 123 | actual = dpapi_ng.ncrypt_unprotect_secret(enc_blob) 124 | assert actual == data 125 | 126 | 127 | @pytest.mark.asyncio 128 | async def test_decrypt_async_with_public_key() -> None: 129 | data = os.urandom(64) 130 | 131 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME2, password=PASSWORD) 132 | async with psrp.AsyncRunspacePool(wsman) as rp: 133 | ps = psrp.AsyncPowerShell(rp) 134 | ps.add_script(ENCRYPT_SCRIPT).add_parameters( 135 | ProtectionDescriptor=f"SID={USERNAME1_SID}", 136 | InputObject=data, 137 | ) 138 | enc_blob = (await ps.invoke())[0] 139 | 140 | actual = dpapi_ng.ncrypt_unprotect_secret(enc_blob) 141 | assert actual == data 142 | 143 | 144 | def test_encrypt_sync_as_authorised_user() -> None: 145 | data = os.urandom(64) 146 | 147 | kwargs: t.Dict[str, t.Any] = {} 148 | if os.name != "nt": 149 | kwargs["domain_name"] = DOMAIN_REALM 150 | 151 | blob = dpapi_ng.ncrypt_protect_secret(data, USERNAME1_SID, **kwargs) 152 | 153 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME1, password=PASSWORD) 154 | with psrp.SyncRunspacePool(wsman) as rp: 155 | ps = psrp.SyncPowerShell(rp) 156 | ps.add_script(DECRYPT_SCRIPT).add_argument(blob) 157 | actual = ps.invoke() 158 | 159 | assert not ps.had_errors 160 | assert actual == [data] 161 | 162 | 163 | @pytest.mark.asyncio 164 | async def test_encrypt_async_as_authorised_user() -> None: 165 | data = os.urandom(64) 166 | 167 | kwargs: t.Dict[str, t.Any] = {} 168 | if os.name != "nt": 169 | kwargs["domain_name"] = DOMAIN_REALM 170 | 171 | blob = dpapi_ng.ncrypt_protect_secret(data, USERNAME1_SID, **kwargs) 172 | 173 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME1, password=PASSWORD) 174 | async with psrp.AsyncRunspacePool(wsman) as rp: 175 | ps = psrp.AsyncPowerShell(rp) 176 | ps.add_script(DECRYPT_SCRIPT).add_argument(blob) 177 | actual = await ps.invoke() 178 | 179 | assert not ps.had_errors 180 | assert actual == [data] 181 | 182 | 183 | def test_encrypt_sync_as_unauthorised_user() -> None: 184 | data = os.urandom(64) 185 | 186 | kwargs: t.Dict[str, t.Any] = {} 187 | if os.name != "nt": 188 | kwargs["domain_name"] = DOMAIN_REALM 189 | 190 | blob = dpapi_ng.ncrypt_protect_secret(data, USERNAME2_SID, **kwargs) 191 | 192 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME2, password=PASSWORD) 193 | with psrp.SyncRunspacePool(wsman) as rp: 194 | ps = psrp.SyncPowerShell(rp) 195 | ps.add_script(DECRYPT_SCRIPT).add_argument(blob) 196 | actual = ps.invoke() 197 | 198 | assert not ps.had_errors 199 | assert actual == [data] 200 | 201 | 202 | @pytest.mark.asyncio 203 | async def test_encrypt_async_as_unauthorised_user() -> None: 204 | data = os.urandom(64) 205 | 206 | kwargs: t.Dict[str, t.Any] = {} 207 | if os.name != "nt": 208 | kwargs["domain_name"] = DOMAIN_REALM 209 | 210 | blob = dpapi_ng.ncrypt_protect_secret(data, USERNAME2_SID, **kwargs) 211 | 212 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME2, password=PASSWORD) 213 | async with psrp.AsyncRunspacePool(wsman) as rp: 214 | ps = psrp.AsyncPowerShell(rp) 215 | ps.add_script(DECRYPT_SCRIPT).add_argument(blob) 216 | actual = await ps.invoke() 217 | 218 | assert not ps.had_errors 219 | assert actual == [data] 220 | 221 | 222 | @pytest.mark.parametrize("protocol", ["negotiate", "negotiate-ntlm", "kerberos", "ntlm"]) 223 | def test_rpc_auth(protocol: str) -> None: 224 | data = os.urandom(64) 225 | 226 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME1, password=PASSWORD) 227 | with psrp.SyncRunspacePool(wsman) as rp: 228 | ps = psrp.SyncPowerShell(rp) 229 | ps.add_script(ENCRYPT_SCRIPT).add_parameters( 230 | ProtectionDescriptor=f"SID={USERNAME1_SID}", 231 | InputObject=data, 232 | ) 233 | enc_blob = ps.invoke()[0] 234 | 235 | server = None 236 | username = None 237 | password = None 238 | is_ntlm = protocol in ["negotiate-ntlm", "ntlm"] 239 | if protocol == "negotiate-ntlm": 240 | server = DC_IP 241 | protocol = "negotiate" 242 | 243 | if os.name != "nt" and is_ntlm: 244 | username = USER_UPN 245 | password = PASSWORD 246 | 247 | actual = dpapi_ng.ncrypt_unprotect_secret( 248 | enc_blob, 249 | server, 250 | username=username, 251 | password=password, 252 | auth_protocol=protocol, 253 | ) 254 | assert actual == data 255 | 256 | 257 | @pytest.mark.asyncio 258 | @pytest.mark.parametrize("protocol", ["negotiate", "negotiate-ntlm", "kerberos", "ntlm"]) 259 | async def test_rpc_auth_async(protocol: str) -> None: 260 | data = os.urandom(64) 261 | 262 | wsman = psrp.WSManInfo(DC_FQDN, auth="credssp", username=USERNAME1, password=PASSWORD) 263 | async with psrp.AsyncRunspacePool(wsman) as rp: 264 | ps = psrp.AsyncPowerShell(rp) 265 | ps.add_script(ENCRYPT_SCRIPT).add_parameters( 266 | ProtectionDescriptor=f"SID={USERNAME1_SID}", 267 | InputObject=data, 268 | ) 269 | enc_blob = (await ps.invoke())[0] 270 | 271 | server = None 272 | username = None 273 | password = None 274 | is_ntlm = protocol in ["negotiate-ntlm", "ntlm"] 275 | if protocol == "negotiate-ntlm": 276 | server = DC_IP 277 | protocol = "negotiate" 278 | 279 | if os.name != "nt" and is_ntlm: 280 | username = USER_UPN 281 | password = PASSWORD 282 | 283 | actual = await dpapi_ng.async_ncrypt_unprotect_secret( 284 | enc_blob, 285 | server, 286 | username=username, 287 | password=password, 288 | auth_protocol=protocol, 289 | ) 290 | assert actual == data 291 | -------------------------------------------------------------------------------- /tests/integration/tests.yml: -------------------------------------------------------------------------------- 1 | - name: setup Windows test files 2 | hosts: APP01 3 | gather_facts: false 4 | tags: 5 | - windows 6 | - DH 7 | - ECDH_P256 8 | - ECDH_P384 9 | 10 | tasks: 11 | - name: template out tests 12 | ansible.windows.win_template: 13 | src: test_integration.py 14 | dest: C:\temp\test_integration.py 15 | 16 | - name: setup Linux test files 17 | hosts: linux 18 | gather_facts: false 19 | tags: 20 | - linux 21 | - DH 22 | - ECDH_P256 23 | - ECDH_P384 24 | 25 | tasks: 26 | - name: template out tests 27 | ansible.builtin.template: 28 | src: test_integration.py 29 | dest: ~/test_integration.py 30 | 31 | - name: get Kerberos ticket 32 | ansible.builtin.command: 33 | argv: 34 | - kinit 35 | - -k 36 | - -t 37 | - ~/user.keytab 38 | - '{{ domain_user_upn }}' 39 | environment: 40 | KRB5CCNAME: /tmp/krb5.ccache 41 | 42 | - name: setup DH root key 43 | hosts: DC01 44 | gather_facts: false 45 | tags: 46 | - windows 47 | - linux 48 | - DH 49 | 50 | tasks: 51 | - name: Add DH KDS root key 52 | ansible.windows.win_powershell: 53 | parameters: 54 | KdfHashAlgorithm: SHA512 55 | SecretAgreementAlgorithm: DH 56 | script: '{{ lookup("file", "New-KdsRootKey.ps1") }}' 57 | executable: pwsh 58 | become: yes 59 | become_method: runas 60 | vars: 61 | ansible_become_user: '{{ domain_user_upn }}' 62 | ansible_become_pass: '{{ domain_password }}' 63 | 64 | - name: run DH integration tests 65 | import_playbook: run_test.yml 66 | tags: 67 | - DH 68 | 69 | - name: setup ECDH_P256 root key 70 | hosts: DC01 71 | gather_facts: false 72 | tags: 73 | - windows 74 | - linux 75 | - ECDH_P256 76 | 77 | tasks: 78 | - name: Add ECDH_P256 KDS root key 79 | ansible.windows.win_powershell: 80 | parameters: 81 | KdfHashAlgorithm: SHA512 82 | SecretAgreementAlgorithm: ECDH_P256 83 | script: '{{ lookup("file", "New-KdsRootKey.ps1") }}' 84 | executable: pwsh 85 | become: yes 86 | become_method: runas 87 | vars: 88 | ansible_become_user: '{{ domain_user_upn }}' 89 | ansible_become_pass: '{{ domain_password }}' 90 | 91 | - name: run ECDH_P256 integration tests 92 | import_playbook: run_test.yml 93 | tags: 94 | - ECDH_P256 95 | 96 | - name: setup ECDH_P384 root key 97 | hosts: DC01 98 | gather_facts: false 99 | tags: 100 | - windows 101 | - linux 102 | - ECDH_P384 103 | 104 | tasks: 105 | - name: Add ECDH_P384 KDS root key 106 | ansible.windows.win_powershell: 107 | parameters: 108 | KdfHashAlgorithm: SHA512 109 | SecretAgreementAlgorithm: ECDH_P384 110 | script: '{{ lookup("file", "New-KdsRootKey.ps1") }}' 111 | executable: pwsh 112 | become: yes 113 | become_method: runas 114 | vars: 115 | ansible_become_user: '{{ domain_user_upn }}' 116 | ansible_become_pass: '{{ domain_password }}' 117 | 118 | - name: run ECDH_P384 integration tests 119 | import_playbook: run_test.yml 120 | tags: 121 | - ECDH_P384 122 | -------------------------------------------------------------------------------- /tests/test_blob.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import uuid 7 | 8 | from dpapi_ng import _blob as blob 9 | 10 | from .conftest import get_test_data 11 | 12 | 13 | def test_blob_unpack() -> None: 14 | data = get_test_data("dpapi_ng_blob") 15 | 16 | msg = blob.DPAPINGBlob.unpack(data) 17 | assert msg.key_identifier.version == 1 18 | assert msg.key_identifier.flags == 3 19 | assert msg.key_identifier.is_public_key 20 | assert msg.key_identifier.l0 == 361 21 | assert msg.key_identifier.l1 == 16 22 | assert msg.key_identifier.l2 == 3 23 | assert msg.key_identifier.root_key_identifier == uuid.UUID("d778c271-9025-9a82-f6dc-b8960b8ad8c5") 24 | assert msg.key_identifier.key_info == get_test_data("ffc_dh_key") 25 | assert msg.key_identifier.domain_name == "domain.test" 26 | assert msg.key_identifier.forest_name == "domain.test" 27 | assert isinstance(msg.protection_descriptor, blob.SIDDescriptor) 28 | assert msg.protection_descriptor.value == "S-1-5-21-3337337973-3297078028-437386066-512" 29 | assert msg.enc_cek == ( 30 | b"\x89\x7F\xC4\x3F\x74\x8E\xFD\x09" 31 | b"\x57\x27\xDD\xE9\x8F\x4E\x1A\x6F" 32 | b"\xFB\x9D\x41\x63\xD3\x9F\xB3\x74" 33 | b"\xD0\x49\xC7\x3D\x89\x69\x0C\x7E" 34 | b"\xFA\x45\xE6\xBE\x11\x9E\x0D\x6B" 35 | ) 36 | assert msg.enc_cek_algorithm == "2.16.840.1.101.3.4.1.45" 37 | assert msg.enc_cek_parameters is None 38 | assert msg.enc_content == ( 39 | b"\xE4\xCD\xF6\x54\x72\x2A\x49\xD5" 40 | b"\x5F\x53\x08\x55\x0E\xC4\xE8\xAA" 41 | b"\xC6\xD0\xBE\x49\x51\x16\xF6\x13" 42 | b"\x2A\x4D\x59\x17\x9F\xD7\x13\x8E" 43 | b"\xC9\x4B\x53\x6E\x25\x11\xD5\xCA" 44 | b"\x0D\x37\x8D\xEC\x3C\x42\x3D\x55" 45 | b"\xC5\x0A\x60\xDC\x41\x8F\x90\x17" 46 | b"\x82\x48\x46\xE0\x2B\x62\x04\xC8" 47 | b"\xB3\x27\x3C\x9F\xC4\x43\x37\x63" 48 | b"\x94\x47\x3B\xF9\x7B\xDC\x55\x80" 49 | b"\x09\x51\xAD\xF9\x23\x8D\x8A\x02" 50 | b"\xFF\xE0\x38\xCD\x4D\x7B\x16\x01" 51 | b"\x2F\x7A\xE8\xB8\x79\x03\xE0\x50" 52 | b"\x00\xD8\xE3\x10\xDE\x1B\x2D\x1C" 53 | b"\xA3\x44\xB2\xF2\x67\x3A\x3D\x5A" 54 | b"\x5C\x4D\xE4\x63\x26\x4B\x95\x64" 55 | b"\xEB\x9E\xB0\x4C\x52\x71\x1C\x33" 56 | b"\xC5\xA7\xA9\x74\x0D\x66\x54\x88" 57 | b"\x55\xB6" 58 | ) 59 | assert msg.enc_content_algorithm == "2.16.840.1.101.3.4.1.46" 60 | assert msg.enc_content_parameters == b"\x30\x11\x04\x0C\x9E\x5B\x2E\x17\xC2\x3F\x04\xFC\x35\x25\xE1\x18\x02\x01\x10" 61 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import base64 7 | import json 8 | import os 9 | import typing as t 10 | import uuid 11 | 12 | import pytest 13 | from cryptography.hazmat.primitives.asymmetric import ec 14 | 15 | import dpapi_ng 16 | import dpapi_ng._client as client 17 | import dpapi_ng._gkdi as gkdi 18 | 19 | from .conftest import get_test_data 20 | 21 | # These scenarios were created with tests/integration/files/New-KdsRootKey.ps1 22 | # and tests/integration/files/ConvertTo-DpapiNgBlob.ps1 23 | 24 | 25 | def _load_root_key(scenario: str) -> tuple[bytes, dpapi_ng.KeyCache]: 26 | data = json.loads(get_test_data(f"{scenario}.json")) 27 | 28 | cache = dpapi_ng.KeyCache() 29 | cache.load_key( 30 | key=base64.b16decode(data["RootKeyData"]), 31 | root_key_id=uuid.UUID(data["RootKeyId"]), 32 | version=data["Version"], 33 | kdf_algorithm=data["KdfAlgorithm"], 34 | kdf_parameters=base64.b16decode(data["KdfParameters"]), 35 | secret_algorithm=data["SecretAgreementAlgorithm"], 36 | secret_parameters=base64.b16decode(data["SecretAgreementParameters"]), 37 | private_key_length=data["PrivateKeyLength"], 38 | public_key_length=data["PublicKeyLength"], 39 | ) 40 | 41 | return base64.b16decode(data["Data"]), cache 42 | 43 | 44 | @pytest.mark.parametrize( 45 | "scenario", 46 | [ 47 | "kdf_sha1_nonce", 48 | "kdf_sha256_nonce", 49 | "kdf_sha384_nonce", 50 | "kdf_sha512_nonce", 51 | ], 52 | ) 53 | def test_protect_secret(scenario: str) -> None: 54 | test_data = b"schorschii" 55 | test_protection_descriptor = "S-1-5-21-2185496602-3367037166-1388177638-1103" 56 | 57 | key_cache = _load_root_key(scenario)[1] 58 | test_root_key_identifier = list(key_cache._root_keys.keys())[0] 59 | 60 | encrypted = dpapi_ng.ncrypt_protect_secret( 61 | test_data, 62 | test_protection_descriptor, 63 | root_key_identifier=test_root_key_identifier, 64 | cache=key_cache, 65 | ) 66 | decrypted = dpapi_ng.ncrypt_unprotect_secret(encrypted, cache=key_cache) 67 | assert test_data == decrypted 68 | 69 | 70 | @pytest.mark.asyncio 71 | @pytest.mark.parametrize( 72 | "scenario", 73 | [ 74 | "kdf_sha1_nonce", 75 | "kdf_sha256_nonce", 76 | "kdf_sha384_nonce", 77 | "kdf_sha512_nonce", 78 | ], 79 | ) 80 | async def test_async_protect_secret(scenario: str) -> None: 81 | test_data = b"schorschii" 82 | test_protection_descriptor = "S-1-5-21-2185496602-3367037166-1388177638-1103" 83 | 84 | key_cache = _load_root_key(scenario)[1] 85 | test_root_key_identifier = list(key_cache._root_keys.keys())[0] 86 | 87 | encrypted = await dpapi_ng.async_ncrypt_protect_secret( 88 | test_data, 89 | test_protection_descriptor, 90 | root_key_identifier=test_root_key_identifier, 91 | cache=key_cache, 92 | ) 93 | decrypted = await dpapi_ng.async_ncrypt_unprotect_secret(encrypted, cache=key_cache) 94 | assert test_data == decrypted 95 | 96 | 97 | @pytest.mark.parametrize( 98 | "kdf_algo, secret_algo", 99 | [ 100 | ("SHA1", "DH"), 101 | ("SHA1", "ECDH_P256"), 102 | ("SHA1", "ECDH_P384"), 103 | ("SHA256", "DH"), 104 | ("SHA256", "ECDH_P256"), 105 | ("SHA256", "ECDH_P384"), 106 | ("SHA384", "DH"), 107 | ("SHA384", "ECDH_P256"), 108 | ("SHA384", "ECDH_P384"), 109 | ("SHA512", "DH"), 110 | ("SHA512", "ECDH_P256"), 111 | ("SHA512", "ECDH_P384"), 112 | ], 113 | ) 114 | def test_protect_secret_public_key( 115 | kdf_algo: str, 116 | secret_algo: str, 117 | monkeypatch: pytest.MonkeyPatch, 118 | ) -> None: 119 | test_data = b"schorschii" 120 | test_protection_descriptor = "S-1-5-21-2185496602-3367037166-1388177638-1103" 121 | 122 | private_key_length, public_key_length = { 123 | "DH": (512, 2048), 124 | "ECDH_P256": (256, 256), 125 | "ECDH_P384": (384, 384), 126 | "ECDH_P521": (521, 521), 127 | }[secret_algo] 128 | 129 | root_key_id = uuid.uuid4() 130 | key_cache = dpapi_ng.KeyCache() 131 | key_cache.load_key( 132 | os.urandom(64), 133 | root_key_id=root_key_id, 134 | version=1, 135 | kdf_parameters=gkdi.KDFParameters(kdf_algo).pack(), 136 | secret_algorithm=secret_algo, 137 | private_key_length=private_key_length, 138 | public_key_length=public_key_length, 139 | ) 140 | 141 | original_get_gke = client._get_protection_gke_from_cache 142 | 143 | def get_protection_gke( 144 | root_key_identifier: t.Optional[uuid.UUID], 145 | target_sd: bytes, 146 | cache: client.KeyCache, 147 | ) -> gkdi.GroupKeyEnvelope: 148 | gke = original_get_gke(root_key_identifier, target_sd, cache) 149 | assert gke 150 | 151 | private_key = gkdi.kdf( 152 | gkdi.KDFParameters.unpack(gke.kdf_parameters).hash_algorithm, 153 | gke.l2_key, 154 | gkdi.KDS_SERVICE_LABEL, 155 | (gke.secret_algorithm + "\0").encode("utf-16-le"), 156 | (gke.private_key_length // 8), 157 | ) 158 | 159 | if gke.secret_algorithm == "DH": 160 | secret_params = gkdi.FFCDHParameters.unpack(gke.secret_parameters) 161 | public_key = pow( 162 | secret_params.generator, 163 | int.from_bytes(private_key, byteorder="big"), 164 | secret_params.field_order, 165 | ) 166 | 167 | pub_key = gkdi.FFCDHKey( 168 | key_length=secret_params.key_length, 169 | field_order=secret_params.field_order, 170 | generator=secret_params.generator, 171 | public_key=public_key, 172 | ).pack() 173 | else: 174 | curve, curve_name = { 175 | "ECDH_P256": (ec.SECP256R1(), "P256"), 176 | "ECDH_P384": (ec.SECP384R1(), "P384"), 177 | "ECDH_P521": (ec.SECP521R1(), "P521"), 178 | }[gke.secret_algorithm] 179 | 180 | ecdh_private = ec.derive_private_key( 181 | int.from_bytes(private_key, byteorder="big"), 182 | curve, 183 | ) 184 | key_numbers = ecdh_private.public_key().public_numbers() 185 | 186 | pub_key = gkdi.ECDHKey( 187 | curve_name=curve_name, 188 | key_length=ecdh_private.key_size // 8, 189 | x=key_numbers.x, 190 | y=key_numbers.y, 191 | ).pack() 192 | 193 | object.__setattr__(gke, "flags", 1) 194 | object.__setattr__(gke, "l2_key", pub_key) 195 | 196 | return gke 197 | 198 | monkeypatch.setattr(client, "_get_protection_gke_from_cache", get_protection_gke) 199 | 200 | encrypted = dpapi_ng.ncrypt_protect_secret( 201 | test_data, 202 | test_protection_descriptor, 203 | root_key_identifier=root_key_id, 204 | cache=key_cache, 205 | ) 206 | decrypted = dpapi_ng.ncrypt_unprotect_secret(encrypted, cache=key_cache) 207 | assert test_data == decrypted 208 | 209 | 210 | @pytest.mark.parametrize( 211 | "scenario", 212 | [ 213 | "kdf_sha1_nonce", 214 | "kdf_sha256_nonce", 215 | "kdf_sha384_nonce", 216 | "kdf_sha512_nonce", 217 | "kdf_sha1_dh", 218 | "kdf_sha256_dh", 219 | "kdf_sha384_dh", 220 | "kdf_sha512_dh", 221 | "kdf_sha1_ecdh_p256", 222 | "kdf_sha256_ecdh_p256", 223 | "kdf_sha384_ecdh_p256", 224 | "kdf_sha512_ecdh_p256", 225 | "kdf_sha1_ecdh_p384", 226 | "kdf_sha256_ecdh_p384", 227 | "kdf_sha384_ecdh_p384", 228 | "kdf_sha512_ecdh_p384", 229 | ], 230 | ) 231 | def test_unprotect_secret( 232 | scenario: str, 233 | ) -> None: 234 | expected = b"\x00" 235 | 236 | data, key_cache = _load_root_key(scenario) 237 | 238 | actual = dpapi_ng.ncrypt_unprotect_secret(data, cache=key_cache) 239 | assert actual == expected 240 | 241 | 242 | @pytest.mark.asyncio 243 | @pytest.mark.parametrize( 244 | "scenario", 245 | [ 246 | "kdf_sha1_nonce", 247 | "kdf_sha256_nonce", 248 | "kdf_sha384_nonce", 249 | "kdf_sha512_nonce", 250 | "kdf_sha1_dh", 251 | "kdf_sha256_dh", 252 | "kdf_sha384_dh", 253 | "kdf_sha512_dh", 254 | "kdf_sha1_ecdh_p256", 255 | "kdf_sha256_ecdh_p256", 256 | "kdf_sha384_ecdh_p256", 257 | "kdf_sha512_ecdh_p256", 258 | "kdf_sha1_ecdh_p384", 259 | "kdf_sha256_ecdh_p384", 260 | "kdf_sha384_ecdh_p384", 261 | "kdf_sha512_ecdh_p384", 262 | ], 263 | ) 264 | async def test_async_unprotect_secret( 265 | scenario: str, 266 | ) -> None: 267 | expected = b"\x00" 268 | 269 | data, key_cache = _load_root_key(scenario) 270 | 271 | actual = await dpapi_ng.async_ncrypt_unprotect_secret(data, cache=key_cache) 272 | assert actual == expected 273 | -------------------------------------------------------------------------------- /tests/test_crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import typing as t 7 | import uuid 8 | 9 | import pytest 10 | from cryptography.hazmat.primitives import hashes 11 | 12 | from dpapi_ng import _crypto as crypto 13 | 14 | 15 | def test_cek_decrypt_invalid_algorithm() -> None: 16 | with pytest.raises(NotImplementedError, match="Unknown cek encryption algorithm OID '1.2'"): 17 | crypto.cek_decrypt("1.2", None, b"", b"") 18 | 19 | 20 | def test_content_decrypt_aes256_gcm_no_parameters() -> None: 21 | with pytest.raises(ValueError, match="Expecting parameters for AES256 GCM decryption but received none"): 22 | crypto.content_decrypt("2.16.840.1.101.3.4.1.46", None, b"", b"") 23 | 24 | 25 | def test_content_decrypt_invalid_algorithm() -> None: 26 | with pytest.raises(NotImplementedError, match="Unknown content encryption algorithm OID '1.2'"): 27 | crypto.content_decrypt("1.2", None, b"", b"") 28 | 29 | 30 | def test_cek_encrypt_invalid_algorithm() -> None: 31 | with pytest.raises(NotImplementedError, match="Unknown cek encryption algorithm OID '1.2'"): 32 | crypto.cek_encrypt("1.2", None, b"", b"") 33 | 34 | 35 | def test_content_encrypt_aes256_gcm_no_parameters() -> None: 36 | with pytest.raises(ValueError, match="Expecting parameters for AES256 GCM encryption but received none"): 37 | crypto.content_encrypt("2.16.840.1.101.3.4.1.46", None, b"", b"") 38 | 39 | 40 | def test_content_encrypt_invalid_algorithm() -> None: 41 | with pytest.raises(NotImplementedError, match="Unknown content encryption algorithm OID '1.2'"): 42 | crypto.content_encrypt("1.2", None, b"", b"") 43 | 44 | 45 | def test_cek_generate_invalid_algorithm() -> None: 46 | with pytest.raises(NotImplementedError, match="Unknown cek encryption algorithm OID '1.2'"): 47 | crypto.cek_generate("1.2") 48 | -------------------------------------------------------------------------------- /tests/test_security_descriptor.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) 2023, Jordan Borean (@jborean93) 2 | # MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 | 4 | from __future__ import annotations 5 | 6 | import re 7 | import typing as t 8 | import uuid 9 | 10 | import pytest 11 | 12 | from dpapi_ng import _security_descriptor as security_descriptor 13 | 14 | from .conftest import get_test_data 15 | 16 | 17 | def test_sid_to_bytes() -> None: 18 | expected = ( 19 | b"\x01\x05\x00\x00\x00\x00\x00\x05" 20 | b"\x15\x00\x00\x00\x1D\x93\x77\xF7" 21 | b"\x44\x35\x7A\xCC\x8C\xD3\x7B\xA9" 22 | b"\x50\x04\x00\x00" 23 | ) 24 | actual = security_descriptor.sid_to_bytes("S-1-5-21-4151808797-3430561092-2843464588-1104") 25 | 26 | assert actual == expected 27 | 28 | 29 | @pytest.mark.parametrize( 30 | "value", 31 | [ 32 | "S-1-5", 33 | "S-1-51", 34 | "S-1-5-", 35 | "Z-1-5-1", 36 | "S-1-5-1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16", 37 | ], 38 | ) 39 | def test_sid_to_bytes_invalid(value: str) -> None: 40 | expected = re.escape(f"Input string '{value}' is not a valid SID string") 41 | with pytest.raises(ValueError, match=expected): 42 | security_descriptor.sid_to_bytes(value) 43 | 44 | 45 | def test_sd_to_bytes_no_sacl() -> None: 46 | expected = ( 47 | b"\x01\x00\x04\x80\x30\x00\x00\x00" 48 | b"\x3C\x00\x00\x00\x00\x00\x00\x00" 49 | b"\x14\x00\x00\x00\x02\x00\x1C\x00" 50 | b"\x01\x00\x00\x00\x00\x00\x14\x00" 51 | b"\x01\x00\x00\x00\x01\x01\x00\x00" 52 | b"\x00\x00\x00\x05\x12\x00\x00\x00" 53 | b"\x01\x01\x00\x00\x00\x00\x00\x05" 54 | b"\x12\x00\x00\x00\x01\x01\x00\x00" 55 | b"\x00\x00\x00\x05\x12\x00\x00\x00" 56 | ) 57 | actual = security_descriptor.sd_to_bytes( 58 | "S-1-5-18", 59 | "S-1-5-18", 60 | dacl=[security_descriptor.ace_to_bytes("S-1-5-18", 1)], 61 | ) 62 | assert actual == expected 63 | 64 | 65 | def test_sd_to_bytes_no_dacl() -> None: 66 | expected = ( 67 | b"\x01\x00\x10\x80\x30\x00\x00\x00" 68 | b"\x3C\x00\x00\x00\x14\x00\x00\x00" 69 | b"\x00\x00\x00\x00\x02\x00\x1C\x00" 70 | b"\x01\x00\x00\x00\x00\x00\x14\x00" 71 | b"\x01\x00\x00\x00\x01\x01\x00\x00" 72 | b"\x00\x00\x00\x05\x12\x00\x00\x00" 73 | b"\x01\x01\x00\x00\x00\x00\x00\x05" 74 | b"\x12\x00\x00\x00\x01\x01\x00\x00" 75 | b"\x00\x00\x00\x05\x12\x00\x00\x00" 76 | ) 77 | actual = security_descriptor.sd_to_bytes( 78 | "S-1-5-18", 79 | "S-1-5-18", 80 | sacl=[security_descriptor.ace_to_bytes("S-1-5-18", 1)], 81 | ) 82 | assert actual == expected 83 | --------------------------------------------------------------------------------