{"id":23287184,"url":"https://github.com/blackm0re/pyotp2289","last_synced_at":"2025-08-21T17:32:45.284Z","repository":{"id":62582299,"uuid":"251597736","full_name":"blackm0re/pyotp2289","owner":"blackm0re","description":"A pure Python implementation of \"A One-Time Password System\" - RFC-2289","archived":false,"fork":false,"pushed_at":"2023-01-08T23:10:17.000Z","size":133,"stargazers_count":2,"open_issues_count":0,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-12-16T02:24:17.811Z","etag":null,"topics":["2289","cryptography","freebsd","otp","password","python","security","unix"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blackm0re.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-31T12:35:09.000Z","updated_at":"2023-10-20T10:47:59.000Z","dependencies_parsed_at":"2023-02-08T07:45:37.116Z","dependency_job_id":null,"html_url":"https://github.com/blackm0re/pyotp2289","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blackm0re%2Fpyotp2289","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blackm0re%2Fpyotp2289/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blackm0re%2Fpyotp2289/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blackm0re%2Fpyotp2289/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blackm0re","download_url":"https://codeload.github.com/blackm0re/pyotp2289/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230523683,"owners_count":18239447,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["2289","cryptography","freebsd","otp","password","python","security","unix"],"created_at":"2024-12-20T02:16:10.709Z","updated_at":"2024-12-20T02:16:11.257Z","avatar_url":"https://github.com/blackm0re.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pyotp2289\n\n*pyotp2289* is a pure Python 3 implementation of \"A One-Time Password System\" -\nRFC-2289.\n\nIt requires no additional libraries.\n\n## General\n\nThe main reason for writing this library was the need to login into my\nFreeBSD servers using [opiepasswd(1)](https://www.freebsd.org/cgi/man.cgi?query=opiepasswd\u0026sektion=1\u0026manpath=FreeBSD)\nas [described in the FreeBSD Handbook](https://docs.freebsd.org/en/books/handbook/security/#one-time-passwords).\n\nI decided to license the library under the\n[Simplified BSD License / 2-clause BSD license](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) and not under the\n(L)GPL-3 as I usually do.\n\nI hope that somebody will find it useful.\n\n## Installation\n\n### pip (pypi)\n\n   ```bash\n   pip install pyotp2289\n   ```\n\n### FreeBSD\n\n*pyotp2289* is included in the official ports-tree.\n\n   ```bash\n   pkg install security/py-pyotp2289\n   ```\n\n– or: \n\n   ```bash\n   cd /usr/ports/security/py-pyotp2289\n   make install clean\n   ```\n\n### Gentoo\n\n   ```bash\n   # add sgs' custom repository using app-eselect/eselect-repository\n   eselect repository add sgs\n\n   # ... or using app-portage/layman (obsolete)\n   layman -a sgs\n\n   emerge dev-python/pyotp2289\n   ```\n\n## Overview of RFC-2289\n\nRFC-2289 describes a one-time password authentication system (OTP):\n\n\"The system provides authentication for system access (login) and other\napplications requiring authentication that is secure against passive attacks\nbased on replaying captured reusable passwords. OTP evolved from the S/KEY\n(S/KEY is a trademark of Bellcore) One-Time Password System that was released\nby Bellcore.\"\n\n\"One form of attack on networked computing systems is eavesdropping on\nnetwork connections to obtain authentication information such as the\nlogin IDs and passwords of legitimate users. Once this information is\ncaptured, it can be used at a later time to gain access to the\nsystem. One-time password systems are designed to counter this type\nof attack, called a 'replay attack'.\"\n\nThe authentication system described in RFC-2289 \"uses a secret\npass-phrase to generate a sequence of one-time (single use)\npasswords.  With this system, the user's secret pass-phrase never\nneeds to cross the network at any time such as during authentication\nor during pass-phrase changes. Thus, it is not vulnerable to replay\nattacks.  Added security is provided by the property that no secret\ninformation need be stored on any system, including the server being\nprotected.\"\n\n\"There are two entities in the operation of the OTP one-time password\nsystem. The **generator** must produce the appropriate one-time password\nfrom the user's secret pass-phrase and from information provided in\nthe **challenge** from the **server**. The server must send a challenge that\nincludes the appropriate generation parameters to the generator, must\nverify the one-time password received, must store the last valid\none-time password it received, and must store the corresponding one-\ntime password sequence number. The server must also facilitate the\nchanging of the user's secret pass-phrase in a secure manner.\"\n\n\"The OTP system generator passes the user's secret pass-phrase, along\nwith a seed received from the server as part of the challenge,\nthrough multiple iterations of a secure hash function to produce a\none-time password. After each successful authentication, the number\nof secure hash function iterations is reduced by one.  Thus, a unique\nsequence of passwords is generated.  The server verifies the one-time\npassword received from the generator by computing the secure hash\nfunction once and comparing the result with the previously accepted\none-time password.\"\n\n\n## Examples\n\nWe define the two entities: *client* and *server*. The entire application of\nRFC-2289 consists of interactions between them.\n\n   ```python\n   import getpass  # client only\n\n   import otp2289  # client and server\n\n   # the server starts by picking:\n   # - algorithm (MD5 or SHA1) to use\n   # - seed - 1 to 16 alphanumeric characters. The seed must never be reused.\n   # - initial step - number (int) that will be decremented for each OTP.\n   # In FreeBSD, the following default values are used:\n   # - MD5\n   # - the first two letters of the hostname + 5 random digits for seed\n   # - initial step: 500\n\n   # the client receives those values, chooses a strong password and creates\n   # initialization digest (hash). The password 'This is a test.' will give you\n   # the same results as in the following example.\n   passwd_bytes = getpass.getpass().encode()  # Fetch the password as bytes\n   generator = otp2289.generator.OTPGenerator(passwd_bytes,\n                                              'TesT',\n                                              otp2289.OTP_ALGO_MD5)\n   digest = generator.generate_otp_hexdigest(500)\n   # digest is now: 0x2b8d82b6ac14346c\n   # the client sends it to the server\n\n   # the server creates the first state. Note that step is decremented by 1:\n   state = otp2289.server.OTPState(digest, 499, 'TesT', otp2289.OTP_ALGO_MD5)\n   # the state can be stored in a OTPStore container:\n   store = otp2289.server.OTPStore()\n   # key can be any str that can be used to reference the state (f.i username)\n   store.add_state('myusername', state)  # where key can be any str that can be\n   # OTPStore is provided only for convenience as it is not part of RFC-2289.\n   # The server can store states any way it wants. A normal dict is also fine.\n   # Once the initial state is set on the server, the client can authenticate.\n\n   # Upon authentication request (f.i. login), the server issues a challenge\n   # based on the state:\n   challenge = state.challenge_string  # challenge is now 'otp-md5 499 TesT '\n\n   # the client can now respond by using (or recreating) the same generator\n   # created earlier. RFC-2289 defines two types of responses:\n   # - hex (like '0x2b8d82b6ac14346c') - more suited for automation\n   # - tokens consisting of 6 short words - better when responding manually\n   hex_response = generator.generate_otp_hexdigest(499)  # '0x6323f96296a2526b'\n   token_response = generator.generate_otp_words(499)\n   # token_response is now: 'CANT JAW BITS NU LO PUP'\n   # a possible shortcut may be to use the challenge-string directly:\n   hex_response = generator.generate_otp_hexdigest_from_challenge(challenge)\n   token_response = generator.generate_otp_words_from_challenge(challenge)\n   # ... giving the same results.\n\n   # once the response is received, the server validates it by yet again using\n   # the current state:\n   result = state.response_validates(hex_response)\n   # or\n   result = state.response_validates(token_response)\n   # result should be True if the response matches the state, False if not\n   # in case of invalid response or response checksum doesn't match, a\n   # otp2289.server.OTPInvalidResponse exception is raised.\n\n   # once the state has successfully validated the corresponding response,\n   # the state **must never be used again** and a state corresponding to the\n   # \"next\" (498) step created.\n   state = state.get_next_state()\n\n   # the next authentication attempt...\n   challenge = state.challenge_string  # challenge is now 'otp-md5 498 TesT '\n   # ... and on the client side...\n   hex_response = generator.generate_otp_hexdigest_from_challenge(challenge)\n   # etc. etc...\n   ```\n\nPlease visit the\n[API documentation](http://gnulover.simeonov.no/docs/api/pyotp2289/latest/) for\na complete reference.\n\nIf you don't care about developing applications in Python and only care about\ngenerating one-time passwords (tokens / hex digests) and authenticating with\nexisting solutions (f.i. FreeBSD servers), *pyotp2289* comes with a simple CLI:\n\n   ```bash\n   python -m otp2289 --generate-otp-response -f token -i 498 -s TesT\n   ```\n\n... will prompt for password and generate a 6 words (token) response.\n\n   ```bash\n   python -m otp2289 --generate-otp-range -f token -i 498 -s TesT\n   ```\n\n... will prompt for password and generate a range of 4 one-time passwords\nstarting from (and including) 498.\n\n\n## Support and contributing\n\n*pyotp2289* is hosted on GitHub: https://github.com/blackm0re/pyotp2289\n\n\n## Author\n\nSimeon Simeonov - sgs @ LiberaChat\n\n\n## [License](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE)\n\nCopyright (c) 2020-2023 Simeon Simeonov\nAll rights reserved.\n\n[Licensed](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) under the BSD 2-clause.\nSPDX-License-Identifier: BSD-2-Clause-FreeBSD\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblackm0re%2Fpyotp2289","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblackm0re%2Fpyotp2289","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblackm0re%2Fpyotp2289/lists"}