├── .flake8 ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── __init__.py ├── generate_readme.rst.py ├── pyproject.toml ├── scripts ├── __init__.py └── snmpv3_hashgen.py ├── setup.cfg ├── setup.py ├── snmpv3_hashgen ├── __init__.py └── hashgen.py ├── tests ├── __init__.py └── tests.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 160 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.platform }} 8 | strategy: 9 | max-parallel: 4 10 | matrix: 11 | platform: 12 | - ubuntu-latest 13 | python-version: [ 3.6, 3.7, 3.8 ] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip setuptools wheel tox nose2 PyYAML toml 24 | - name: Test with tox 25 | run: tox 26 | -------------------------------------------------------------------------------- /.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 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # mkdocs documentation 97 | /site 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.4.1] - 2020-07-12 9 | 10 | This release has no functional changes 11 | 12 | ### Changed 13 | 14 | - Changed casing of YAML 15 | - Changed how the runtime version is detected 16 | 17 | ## [0.4.0] - 2020-07-12 18 | 19 | ### Added 20 | 21 | - Added a changelog 22 | - Added some automated testing using Github actions 23 | 24 | ### Changed 25 | 26 | - Switched to Semver - 0.4.0 didn't change the API, just the versioning scheme. 27 | 28 | ## [0.3] - 2020-07-12 29 | 30 | ### Added 31 | 32 | - YAML and TOML included as output options if the relevant libraries are installed 33 | - Untested support for RFC 7630 hashes (SHA-2 224, 256, 384 and 512) 34 | 35 | ### Changed 36 | 37 | - Switched the default username to 'librenms' 38 | 39 | ### Fixed 40 | 41 | - Method binding issue preventing use of md5 42 | - Kdf was hard coded to sha1 43 | 44 | ## [0.2] - 2017-04-13 45 | 46 | ### Fixed 47 | 48 | - Confusion between authPriv, authpriv, auth, priv and none 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.rst 3 | include *.py 4 | include LICENSE 5 | include CHANGELOG.md 6 | exclude tox.ini 7 | exclude pyproject.toml 8 | exclude generate_readme.rst.py 9 | recursive-exclude .vscode * 10 | recursive-exclude .tox * 11 | recursive-exclude dist * 12 | recursive-exclude tests * 13 | global-exclude __pycache__ 14 | global-exclude *~ 15 | global-exclude *.pyc 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A script to generate SNMPv3 keys as detailed by rfc3414 (passphrases expanded with a kdf, then hashed with the engine id). All key generation is done using the new cryptographically secure _secrets_ library. 2 | 3 | As I wrote this with ESXi in mind, it also emits a string suitable for configuring the SNMP daemon via esxcli/PowerCLI, but the hashes are standard and compatible with other SNMPv3 implementations. 4 | 5 | With no arguments, it will generate an authentication and privacy passphrase with associated random engine ID in text form. `--json` will format the output as json. 6 | 7 | The script is fully idempotent; if you take the parameters it generates randomly and re-enter them, you will get the same output a second time. 8 | 9 | Dependencies 10 | ============ 11 | Runtime: Python 3.6 or greater. 12 | 13 | Sample Output 14 | ============= 15 | 16 | Standard 17 | -------- 18 | 19 | ``` 20 | User: observium 21 | Auth: gaYA82XVtNaf3WLwRgoIs544ghP6f80S / f78359764ca382922fa382cf884e588031de575a 22 | Priv: H5XEtRpxXVaGzXU5i2rFwPnYGr8SEzTp / 31a001a56a225fdfc1916bd60190405a1aa22ff0 23 | Engine: 7ae1b0ff0aa2f3950566d3de2274d05a 24 | ESXi USM String: observium/f78359764ca382922fa382cf884e588031de575a/31a001a56a225fdfc1916bd60190405a1aa22ff0/authpriv 25 | SR-OS Config: configure system security user observium snmp authentication md5 f78359764ca382922fa382cf884e588031de575a privacy aes-128-cfb-key 31a001a56a225fdfc1916bd60190405a1aa22ff0 26 | ``` 27 | 28 | JSON 29 | ---- 30 | 31 | ``` 32 | { 33 | "user": "observium", 34 | "engine": "b2a50167b7c8512ddfc9d5765a3490af", 35 | "phrases": { 36 | "auth": "71rOhjfj6QVSy2mw5tBo7PueZ8KWSv60", 37 | "priv": "xwsvzht8NEcuwAlEpUKzMxKFWeH72sK9" 38 | }, 39 | "hashes": { 40 | "auth": "fa0d5249293404502f9953b9514d0636a96c2cbc", 41 | "priv": "cccbdcfa603817df340514ecc22dfae8c4c412e8" 42 | }, 43 | "esxi": "observium/fa0d5249293404502f9953b9514d0636a96c2cbc/cccbdcfa603817df340514ecc22dfae8c4c412e8/authpriv", 44 | "sros": "configure system security user observium snmp authentication md5 f78359764ca382922fa382cf884e588031de575a privacy aes-128-cfb-key 31a001a56a225fdfc1916bd60190405a1aa22ff0"} 45 | ``` 46 | 47 | If a YAML or TOML library is installed, you also use the `--yaml` and `--toml` arguments respectively. 48 | 49 | It should go without saying, but **DO NOT** use the engine id or passphrases in the samples. 50 | 51 | Usage 52 | ===== 53 | 54 | ``` 55 | usage: snmpv3-hashgen [-h] [--auth AUTH] [--priv PRIV] [--engine ENGINE] [--user USER] [--mode {auth,priv,none}] [--hash {md5,sha1,sha224,sha256,sha384,sha512}] [--json | --yaml | --toml] 56 | 57 | Convert an SNMPv3 auth or priv passphrase to hashes. 58 | 59 | optional arguments: 60 | -h, --help show this help message and exit 61 | --auth AUTH Authentication passphrase to be derived as utf8 string 62 | --priv PRIV Privacy passphrase to be derived as utf8 string 63 | --engine ENGINE Engine ID as hex string 64 | --user USER SNMPv3 USM username (default "librenms") 65 | --mode {auth,priv,none} 66 | SNMPv3 mode (default "priv") 67 | --hash {md5,sha1,sha224,sha256,sha384,sha512} 68 | Hash algorithm to use (default "sha1") 69 | --json Emit output as json 70 | --yaml Emit output as yaml 71 | --toml Emit output as toml 72 | 73 | RFC 7630 defines no test data for sha[2-9]{3} - these should be considered experimental. 74 | Report bugs at https://github.com/TheMysteriousX/SNMPv3-Hash-Generator/issues 75 | ``` 76 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A script to generate SNMPv3 keys as detailed by rfc3414 (passphrases 2 | expanded with a kdf, then hashed with the engine id). All key generation 3 | is done using the new cryptographically secure *secrets* library. 4 | 5 | As I wrote this with ESXi in mind, it also emits a string suitable for 6 | configuring the SNMP daemon via esxcli/PowerCLI, but the hashes are 7 | standard and compatible with other SNMPv3 implementations. 8 | 9 | With no arguments, it will generate an authentication and privacy 10 | passphrase with associated random engine ID in text form. ``--json`` 11 | will format the output as json. 12 | 13 | The script is fully idempotent; if you take the parameters it generates 14 | randomly and re-enter them, you will get the same output a second time. 15 | 16 | Dependencies 17 | ============ 18 | 19 | Runtime: Python 3.6 or greater. 20 | 21 | Sample Output 22 | ============= 23 | 24 | Standard 25 | -------- 26 | 27 | :: 28 | 29 | User: observium 30 | Auth: gaYA82XVtNaf3WLwRgoIs544ghP6f80S / f78359764ca382922fa382cf884e588031de575a 31 | Priv: H5XEtRpxXVaGzXU5i2rFwPnYGr8SEzTp / 31a001a56a225fdfc1916bd60190405a1aa22ff0 32 | Engine: 7ae1b0ff0aa2f3950566d3de2274d05a 33 | ESXi USM String: observium/f78359764ca382922fa382cf884e588031de575a/31a001a56a225fdfc1916bd60190405a1aa22ff0/authpriv 34 | SR-OS Config: configure system security user observium snmp authentication md5 f78359764ca382922fa382cf884e588031de575a privacy aes-128-cfb-key 31a001a56a225fdfc1916bd60190405a1aa22ff0 35 | 36 | JSON 37 | ---- 38 | 39 | :: 40 | 41 | { 42 | "user": "observium", 43 | "engine": "b2a50167b7c8512ddfc9d5765a3490af", 44 | "phrases": { 45 | "auth": "71rOhjfj6QVSy2mw5tBo7PueZ8KWSv60", 46 | "priv": "xwsvzht8NEcuwAlEpUKzMxKFWeH72sK9" 47 | }, 48 | "hashes": { 49 | "auth": "fa0d5249293404502f9953b9514d0636a96c2cbc", 50 | "priv": "cccbdcfa603817df340514ecc22dfae8c4c412e8" 51 | }, 52 | "esxi": "observium/fa0d5249293404502f9953b9514d0636a96c2cbc/cccbdcfa603817df340514ecc22dfae8c4c412e8/authpriv", 53 | "sros": "configure system security user observium snmp authentication md5 f78359764ca382922fa382cf884e588031de575a privacy aes-128-cfb-key 31a001a56a225fdfc1916bd60190405a1aa22ff0" 54 | } 55 | 56 | If a YAML or TOML library is installed, you also use the ``--yaml`` and 57 | ``--toml`` arguments respectively. 58 | 59 | It should go without saying, but **DO NOT** use the engine id or 60 | passphrases in the samples. 61 | 62 | Usage 63 | ===== 64 | 65 | :: 66 | 67 | usage: snmpv3-hashgen [-h] [--auth AUTH] [--priv PRIV] [--engine ENGINE] [--user USER] [--mode {auth,priv,none}] [--hash {md5,sha1,sha224,sha256,sha384,sha512}] [--json | --yaml | --toml] 68 | 69 | Convert an SNMPv3 auth or priv passphrase to hashes. 70 | 71 | optional arguments: 72 | -h, --help show this help message and exit 73 | --auth AUTH Authentication passphrase to be derived as utf8 string 74 | --priv PRIV Privacy passphrase to be derived as utf8 string 75 | --engine ENGINE Engine ID as hex string 76 | --user USER SNMPv3 USM username (default "librenms") 77 | --mode {auth,priv,none} 78 | SNMPv3 mode (default "priv") 79 | --hash {md5,sha1,sha224,sha256,sha384,sha512} 80 | Hash algorithm to use (default "sha1") 81 | --json Emit output as json 82 | --yaml Emit output as yaml 83 | --toml Emit output as toml 84 | 85 | RFC 7630 defines no test data for sha[2-9]{3} - these should be considered experimental. 86 | Report bugs at https://github.com/TheMysteriousX/SNMPv3-Hash-Generator/issues 87 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMysteriousX/SNMPv3-Hash-Generator/1e62e186ee93c269f387f78f21bf57c515f413be/__init__.py -------------------------------------------------------------------------------- /generate_readme.rst.py: -------------------------------------------------------------------------------- 1 | import pypandoc 2 | 3 | output = pypandoc.convert_file("README.md", "rst") 4 | 5 | with open("README.rst", "w+") as f: 6 | f.write(output) 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 160 3 | 4 | [tool.pylint] 5 | max-line-length = 160 6 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMysteriousX/SNMPv3-Hash-Generator/1e62e186ee93c269f387f78f21bf57c515f413be/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/snmpv3_hashgen.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import sys 4 | 5 | if sys.version_info < (3, 6): 6 | print("Python 3.6 or higher is required, please see https://www.python.org/ or your OS package repository", file=sys.stderr) 7 | sys.exit(2) 8 | 9 | import argparse 10 | import json 11 | 12 | from snmpv3_hashgen import Hashgen 13 | 14 | help_text = """ 15 | Convert an SNMPv3 auth or priv passphrase to hashes. 16 | """ 17 | 18 | epilog_text = """ 19 | RFC 7630 defines no test data for sha[2-9]{3} - these should be considered experimental. 20 | Report bugs at https://github.com/TheMysteriousX/SNMPv3-Hash-Generator/issues 21 | """ 22 | 23 | parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=help_text, epilog=epilog_text,) 24 | parser.add_argument("--auth", type=str, help="Authentication passphrase to be derived as utf8 string") 25 | parser.add_argument("--priv", type=str, help="Privacy passphrase to be derived as utf8 string") 26 | parser.add_argument("--engine", type=str, help="Engine ID as hex string") 27 | parser.add_argument("--user", type=str, help='SNMPv3 USM username (default "librenms")') 28 | parser.add_argument("--mode", type=str, choices=["auth", "priv", "none"], help='SNMPv3 mode (default "priv")') 29 | parser.add_argument("--hash", type=str, choices=["md5", "sha1", "sha224", "sha256", "sha384", "sha512"], help='Hash algorithm to use (default "sha1")') 30 | 31 | fmt = parser.add_mutually_exclusive_group() 32 | fmt.add_argument("--json", action="store_true", help="Emit output as json") 33 | fmt.add_argument("--yaml", action="store_true", help="Emit output as yaml") 34 | fmt.add_argument("--toml", action="store_true", help="Emit output as toml") 35 | 36 | 37 | def format_esxi(user, Kul_auth, Kul_priv, mode, hash): 38 | if mode == "priv": 39 | return f"{user}/{hash(Kul_auth)}/{hash(Kul_priv)}/{mode}" 40 | elif mode == "auth": 41 | return f"{user}/{hash(Kul_auth)}/-/{mode}" 42 | else: 43 | return f"{user}/-/-/{mode}" 44 | 45 | def format_sros(user, Kul_auth, Kul_priv, mode, hash): 46 | hashmode = "sha" if hash.keywords['name'] == "sha1" else hash.keywords['name'] 47 | 48 | if mode == "priv" and (hashmode == "sha" or hashmode == "md5"): 49 | return f"configure system security user {user} snmp authentication {hashmode} {hash(Kul_auth)} privacy aes-128-cfb-key {hash(Kul_priv)}" 50 | elif mode == "auth" and (hashmode == "sha" or hashmode == "md5"): 51 | return f"configure system security user {user} snmp authentication {hashmode} {hash(Kul_auth)}" 52 | else: 53 | return f"unsupported hash algorithm" 54 | 55 | def main(*args, **kwargs): 56 | # Argument setup 57 | args = parser.parse_args() 58 | 59 | if args.priv and not args.auth: 60 | print("Error: privacy passphrase supplied without auth passphrase", file=sys.stderr) 61 | sys.exit(3) 62 | 63 | user = "librenms" if not args.user else args.user 64 | mode = "priv" if not args.mode else args.mode 65 | auth = Hashgen.random_string() if not args.auth else args.auth 66 | priv = Hashgen.random_string() if not args.priv else args.priv 67 | engine = Hashgen.random_engine() if not args.engine else args.engine 68 | hash = Hashgen.algs["sha1"] if not args.hash else Hashgen.algs[args.hash] 69 | 70 | # Derive Kul from passphrases 71 | try: 72 | Kul_auth = Hashgen.derive_msg(auth, engine, hash) if "none" not in mode else None 73 | Kul_priv = Hashgen.derive_msg(priv, engine, hash) if "priv" in mode else None 74 | except ValueError: 75 | print("Error: Engine ID seems invalid; ensure that it is entered as a hex character string", file=sys.stderr) 76 | sys.exit(1) 77 | 78 | esxi = format_esxi(user, Kul_auth, Kul_priv, mode, hash) 79 | sros = format_sros(user, Kul_auth, Kul_priv, mode, hash) 80 | output = { 81 | "user": user, 82 | "engine": engine, 83 | "phrases": {"auth": auth if "none" not in mode else None, "priv": priv if "priv" in mode else None}, 84 | "hashes": {"auth": hash(Kul_auth) if "auth" in mode or "priv" in mode else None, "priv": hash(Kul_priv) if "priv" in mode else None}, 85 | "esxi": esxi, 86 | "sros": sros 87 | } 88 | 89 | if args.json: 90 | print(json.dumps(output)) 91 | elif args.yaml: 92 | try: 93 | import yaml 94 | 95 | print(yaml.dump({"snmpv3": output}, explicit_start=True)) 96 | except ImportError: 97 | print("YAML output requires a YAML library") 98 | print("Try running pip3 install PyYAML") 99 | sys.exit(4) 100 | elif args.toml: 101 | try: 102 | import toml 103 | 104 | print(toml.dumps(output)) 105 | except ImportError: 106 | print("TOML output requires a TOML library") 107 | print("Try running pip3 install toml") 108 | sys.exit(5) 109 | else: 110 | print(f"User: {user}") 111 | if "none" not in mode: 112 | print(f"Auth: {auth} / {hash(Kul_auth)}") 113 | if "priv" in mode: 114 | print(f"Priv: {priv} / {hash(Kul_priv)}") 115 | print(f"Engine: {engine}") 116 | print(f"ESXi USM String: {esxi}") 117 | print(f"SR-OS Config: {sros}") 118 | 119 | 120 | if __name__ == "__main__": 121 | main() 122 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | 6 | long_desc = open("README.md").read() 7 | 8 | if os.path.exists("README.rst"): 9 | long_desc = open("README.rst").read() 10 | 11 | setup( 12 | name="SNMPv3 Hash Generator", 13 | version="0.4.1", 14 | packages=find_packages(), 15 | entry_points={"console_scripts": ["snmpv3-hashgen=scripts.snmpv3_hashgen:main"]}, 16 | license="Apache Software License", 17 | long_description=long_desc, 18 | extras_require={"YAML": ["PyYAML"], "TOML": ["toml"]}, 19 | author="Adam Bishop", 20 | author_email="adam@omega.org.uk", 21 | description="Generates SNMPv3 hashes as described in rfc3414 suitable for use with ESXi and other SNMP daemons", 22 | url="https://github.com/TheMysteriousX/SNMPv3-Hash-Generator", 23 | classifiers=["Development Status :: 4 - Beta", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.6"], 24 | python_requires='>=3.6', 25 | ) 26 | -------------------------------------------------------------------------------- /snmpv3_hashgen/__init__.py: -------------------------------------------------------------------------------- 1 | from .hashgen import Hashgen 2 | -------------------------------------------------------------------------------- /snmpv3_hashgen/hashgen.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import string 3 | import secrets 4 | 5 | from itertools import repeat 6 | from functools import partial 7 | 8 | P_LEN = 32 9 | E_LEN = 16 10 | 11 | 12 | class Hashgen(object): 13 | @staticmethod 14 | def hash(bytes, alg=hashlib.sha1, name=None, raw=False): 15 | digest = alg(bytes).digest() 16 | return digest if raw else digest.hex() 17 | 18 | @staticmethod 19 | def expand(substr, target_len): 20 | reps = target_len // len(substr) + 1 # approximation; worst case: overrun = l + len(s) 21 | return "".join(list(repeat(substr, reps)))[:target_len] 22 | 23 | @staticmethod 24 | def kdf(password, alg=None): 25 | alg = Hashgen.algs["sha1"] if alg is None else alg 26 | 27 | data = Hashgen.expand(password, 1048576).encode("utf-8") 28 | return alg(data, raw=True) 29 | 30 | @staticmethod 31 | def random_string(len=P_LEN, alphabet=(string.ascii_letters + string.digits)): 32 | return "".join(secrets.choice(alphabet) for _ in range(len)) 33 | 34 | @staticmethod 35 | def random_engine(len=E_LEN): 36 | return secrets.token_hex(len) 37 | 38 | @staticmethod 39 | def derive_msg(passphrase, engine, alg): 40 | # Parameter derivation á la rfc3414 41 | Ku = Hashgen.kdf(passphrase, alg) 42 | E = bytearray.fromhex(engine) 43 | 44 | return b"".join([Ku, E, Ku]) 45 | 46 | 47 | # Define available hash algorithms 48 | Hashgen.algs = { 49 | "md5": partial(Hashgen.hash, alg=hashlib.md5, name='md5'), 50 | "sha1": partial(Hashgen.hash, alg=hashlib.sha1, name='sha1'), 51 | "sha224": partial(Hashgen.hash, alg=hashlib.sha224, name='sha224'), 52 | "sha256": partial(Hashgen.hash, alg=hashlib.sha256, name='sha256'), 53 | "sha384": partial(Hashgen.hash, alg=hashlib.sha384, name='sha384'), 54 | "sha512": partial(Hashgen.hash, alg=hashlib.sha512, name='sha512'), 55 | } 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheMysteriousX/SNMPv3-Hash-Generator/1e62e186ee93c269f387f78f21bf57c515f413be/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from snmpv3_hashgen.hashgen import Hashgen 4 | 5 | # flake8: noqa: E501 6 | 7 | 8 | class TestLibrary(unittest.TestCase): 9 | kdf_maplesyrups = { 10 | "md5": "9faf3283884e92834ebc9847d8edd963", # RFC 3413 §A.3.1 11 | "sha1": "9fb5cc0381497b3793528939ff788d5d79145211", # RFC 3413 §A.3.2 12 | "sha224": "282a5867ee9aac639ad59df9572c7d3ac0fbc13a905b6df07dbbf00b", #  Unverified 13 | "sha256": "ab51014d1e077f6017df2b12bee5f5aa72993177e9bb569c4dff5a4ca0b4afac", # Unverified 14 | "sha384": "e06eccdf2c68a06ed034723c9c26e0db3b669e1e2efed49150b55377a2e98f383c86fb836857444654b287c93f51ff64", # Unverified 15 | "sha512": "7e4396de5aadc77be853819b98c9406265b3a9c37cc3176569847a4e4f6fba63dd3a73d04924d31a63f95a601f9385af6be4ed1b37f87d040f7c6ed6f8d38a91", # Unverified 16 | } 17 | 18 | final_maplesyrups = { 19 | "md5": "526f5eed9fcce26f8964c2930787d82b", # RFC 3413 §A.3.1 20 | "sha1": "6695febc9288e36282235fc7151f128497b38f3f", # RFC 3413 §A.3.2 21 | "sha224": "0bd8827c6e29f8065e08e09237f177e410f69b90e1782be682075674", # Unverified 22 | "sha256": "8982e0e549e866db361a6b625d84cccc11162d453ee8ce3a6445c2d6776f0f8b", # Unverified 23 | "sha384": "3b298f16164a11184279d5432bf169e2d2a48307de02b3d3f7e2b4f36eb6f0455a53689a3937eea07319a633d2ccba78", # Unverified 24 | "sha512": "22a5a36cedfcc085807a128d7bc6c2382167ad6c0dbc5fdff856740f3d84c099ad1ea87a8db096714d9788bd544047c9021e4229ce27e4c0a69250adfcffbb0b", # Unverified 25 | } 26 | 27 | test_string = "maplesyrup" 28 | engine_id = "000000000000000000000002" 29 | 30 | def test_kdf(self): 31 | for alg, fptr in Hashgen.algs.items(): 32 | self.assertEqual(Hashgen.kdf(self.test_string, fptr).hex(), self.kdf_maplesyrups[alg]) 33 | 34 | def test_concat(self): 35 | for alg, fptr in Hashgen.algs.items(): 36 | self.assertEqual(Hashgen.derive_msg(self.test_string, self.engine_id, fptr).hex(), "".join([self.kdf_maplesyrups[alg], self.engine_id, self.kdf_maplesyrups[alg]])) 37 | 38 | def test_final(self): 39 | for alg, fptr in Hashgen.algs.items(): 40 | self.assertEqual(fptr(Hashgen.derive_msg(self.test_string, self.engine_id, fptr)), self.final_maplesyrups[alg]) 41 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3 3 | 4 | [testenv] 5 | deps = nose2 6 | commands = nose2 --------------------------------------------------------------------------------