├── CHANGES ├── INSTALL ├── LICENSE ├── Makefile.am ├── README ├── README.md ├── Release.txt ├── autogen.sh ├── base32.c ├── configure.ac ├── errinc.h ├── genotpurl.1 ├── genotpurl.c ├── hotp.c ├── md5q.c ├── mod_authn_otp.c ├── motp.c ├── otpdefs.h ├── otplock.1 ├── otplock.c ├── otptool.1 ├── otptool.c ├── phex.c └── users.sample /CHANGES: -------------------------------------------------------------------------------- 1 | Version Next 2 | 3 | - Refactor to avoid warnings and reduce code duplication (issue #49). 4 | 5 | Version 1.1.11 released June 27, 2024 6 | 7 | - Fixed build issue with libapr on some systems (issue #48) 8 | - Added otplock(1) utility (issue #38) 9 | 10 | Version 1.1.10 released May 4, 2022 11 | 12 | - Fixed bug in genotpurl on platforms with signed "char" (pr #46) 13 | - Added "-K" flag to genotpurl tool to specify key length (pr #43) 14 | 15 | Version 1.1.9 released June 22, 2019 16 | 17 | - Added genotpurl(1) utility 18 | - Added "-F" flag to otptool(1). 19 | 20 | Version 1.1.8 relesed October 30, 2017 21 | 22 | - Added PINFakeBasicAuth configuration option 23 | 24 | Version 1.1.7 (r147) released 17 May 2014 25 | 26 | - Fixed bug where users file could get deleted when using Apache worker MPM (issue #22) 27 | - Added "OTPAuthFallThrough" to allow fall through to other auth providers (issue #23) 28 | - Allow "logout" by sending empty password (issue #24) 29 | - Count PINs against OTPAuthMaxOTPFailure even when they have the wrong length 30 | 31 | Version 1.1.6 (r131) released 2 Apr 2013 32 | 33 | - Detect errors when writing to the new users.txt file 34 | - Fix (harmless) bug where new users.txt file was not being closed 35 | - Add -Werror configure flag to fail on compiler warnings 36 | 37 | Version 1.1.5 (r124) released 29 Nov 2012 38 | 39 | - Allow building on systems without strptime(3) (e.g., Windows) 40 | - Add support for Apache 2.4.x 41 | 42 | Version 1.1.4 (r106) released 27 Jun 2011 43 | 44 | - Always allow stale time-based offsets to re-synchronize (issue #14) 45 | - Added "OTPAuthMaxOTPFailure" to lock accounts after repeated wrong OTP's 46 | 47 | Version 1.1.3 (r94) released 28 Mar 2011 48 | 49 | - Fixed bug with "OTPAuthPINAuthProvider" and 50 | 51 | Version 1.1.2 (r87) released 22 Mar 2011 52 | 53 | - Added "OTPAuthPINAuthProvider" to allow alternate verification of PINs 54 | - Added "OTPAuthLogoutOnIPChange" flag to auto-logout on IP address change 55 | - Build fixes for Solaris 56 | 57 | Version 1.1.1 (r66) released 15 Aug 2010 58 | 59 | - Build fixes 60 | 61 | Version 1.1.0 (r44) released 23 Jun 2009 62 | 63 | - Moved time interval and #digits configuration into users file 64 | - Fixed bug in time based token synchronization at large offsets 65 | - Added support for the Mobile-OTP algorithm: http://motp.sourceforge.net/ 66 | - Added otptool(1) one-time password utility program. 67 | - Accept either decimal or hexadecimal values (basic auth only). 68 | 69 | Version 1.0.0 (r10) released 14 Jun 2009 70 | 71 | - Initial release 72 | 73 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | 2 | To build and install mod_authn_otp: 3 | 4 | 1. Ensure you have the following software packages installed: 5 | 6 | apache2-devel 7 | openssl-devel 8 | apache2-dev (Ubuntu) 9 | build-essential (Ubuntu) 10 | 11 | 2. If you are building from GITHUB SOURCE instead of distribution tarball: 12 | 13 | 2.1. Ensure you have the following software packages installed: 14 | 15 | autoconf 16 | automake 17 | 18 | 2.2 Run `./autogen.sh` 19 | 20 | 3. Run `./configure' 21 | 4. Run `make' 22 | 5. Run `make install' 23 | 24 | Once the module is installed on your system, you'll also need to do the 25 | following steps to add it to Apache: 26 | 27 | 1. Add the "mod_authn_otp" module to your Apache config. This usually 28 | involves adding a line looking something like this: 29 | 30 | LoadModule authn_otp_module /usr/lib64/apache2/mod_authn_otp.so 31 | 32 | There should already be a bunch of similar "LoadModule" lines nearby. 33 | 34 | 2. Enable the "mod_authn_otp" module, typically by running this command: 35 | 36 | a2enmod authn_otp 37 | 38 | 3. Restart Apache 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # mod_authn_otp - Apache module for one-time password authentication 4 | # 5 | # Copyright 2009 Archie L. Cobbs 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | all-local: module 21 | 22 | module: mod_authn_otp.c 23 | if test "$(srcdir)" != "."; then $(CP) $(srcdir)/mod_authn_otp.c .; fi 24 | $(APXS) -c -D_REENTRANT `echo $(GCC_WARN_FLAGS) | sed 's/ -/ -Wc,-/g'` -l crypto mod_authn_otp.c hotp.c motp.c phex.c md5q.c 25 | 26 | install-exec-local: module 27 | mkdir -p "$(DESTDIR)`$(APXS) -q LIBEXECDIR`" 28 | $(APXS) -S LIBEXECDIR="$(DESTDIR)`$(APXS) -q LIBEXECDIR`" -i mod_authn_otp.la 29 | 30 | install-exec-hook: 31 | test -z "$(STRIP)" || $(STRIP) $(DESTDIR)`$(APXS) -q LIBEXECDIR`/mod_authn_otp.so 32 | test -z "$(STRIP)" || $(STRIP) $(DESTDIR)$(bindir)/otptool 33 | test -z "$(STRIP)" || $(STRIP) $(DESTDIR)$(bindir)/otplock 34 | test -z "$(STRIP)" || $(STRIP) $(DESTDIR)$(bindir)/genotpurl 35 | 36 | bin_PROGRAMS= otptool otplock genotpurl 37 | 38 | noinst_HEADERS= otpdefs.h errinc.h 39 | 40 | man_MANS= otptool.1 otplock.1 genotpurl.1 41 | 42 | otptool_SOURCES= otptool.c hotp.c motp.c phex.c md5q.c 43 | 44 | otplock_SOURCES= otplock.c 45 | 46 | genotpurl_SOURCES= genotpurl.c base32.c 47 | 48 | CLEANFILES= *.la *.lo *.o *.so *.slo .libs/* 49 | 50 | EXTRA_DIST= CHANGES LICENSE mod_authn_otp.c users.sample otptool.1 otplock.1 genotpurl.1 51 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | mod_authn_otp - Apache module for one-time password authentication 3 | 4 | mod_authn_otp is an Apache web server module for two-factor authentication 5 | using one-time passwords (OTP) generated via the HOTP/OATH algorithm 6 | defined in RFC 4226. This creates a simple way to protect a web site with 7 | one-time passwords, using any RFC 4226-compliant hardware or software 8 | token device. mod_authn_otp also supports the Mobile-OTP algorithm. 9 | 10 | mod_authn_otp supports both event and time based one-time passwords. It 11 | also supports "lingering" which allows the repeated re-use of a previously 12 | used one-time password up to a configurable maximum linger time. This 13 | allows one-time passwords to be used directly in HTTP authentication 14 | without forcing the user to enter a new one-time password for every 15 | page load. 16 | 17 | mod_authn_otp supports both basic and digest authentication, and will 18 | auto-synchronize with the user's token within a configurable maximum 19 | offset (auto-synchronization is not supported with digest authentication). 20 | 21 | mod_authn_otp is especially useful for setting up protected web sites 22 | that require more security than simple username/password authentication 23 | yet also don't require users to install special VPN software, and is 24 | compatible with software tokens that run on cell phones. 25 | 26 | Also included is otptool, a one-time password command line utility. 27 | 28 | For configuration information and other details please see: 29 | 30 | https://github.com/archiecobbs/mod-authn-otp 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | **mod\_authn\_otp** is an [Apache web server](http://en.wikipedia.org/wiki/Apache_HTTP_Server) module for [two-factor authentication](http://en.wikipedia.org/wiki/Two_factor_authentication) using [one-time passwords](http://en.wikipedia.org/wiki/One-time_password) (OTP) generated via the [HOTP/OATH](http://en.wikipedia.org/wiki/HOTP) algorithm defined in [RFC 4226](http://www.ietf.org/rfc/rfc4226.txt). This creates a simple way to protect a web site with one-time passwords, using any RFC 4226-compliant [token device](Tokens), including software tokens that run on cell phones such as [Google Authenticator](https://en.wikipedia.org/wiki/Google_Authenticator). **mod\_authn\_otp** also supports the obsolete [Mobile-OTP](http://motp.sourceforge.net/) algorithm. 4 | 5 | **mod\_authn\_otp** supports both event and time based one-time passwords. It also supports "lingering" which allows the repeated re-use of a previously used one-time password up to a configurable maximum linger time. This allows one-time passwords to be used directly in HTTP authentication without forcing the user to enter a new one-time password for every page load. No additional infrastructure other than the **mod\_authn\_otp** module is required to add one-time password support to any Apache web server. 6 | 7 | **mod\_authn\_otp** supports both basic and digest authentication, and will auto-synchronize with the user's token within a configurable maximum offset (auto-synchronization is not supported with digest authentication). 8 | 9 | **mod\_authn\_otp** is especially useful for setting up protected web sites that require more security than simple username/password authentication yet also don't require users to install special VPN software. 10 | 11 | Also included are **otptool**, a one-time password command line utility, and **genotpurl**. **otptool** can be used on a simple call-out basis to integrate two-factor authentication into any existing authentication solution. **genotpurl** generates `oathtoken://` URLs suitable for [Google Authenticator](https://en.wikipedia.org/wiki/Google_Authenticator) token distribution. 12 | 13 | ## Details 14 | 15 | See the wiki for a detailed description including supported Apache configuration directives. 16 | 17 | * [Configuration](https://github.com/archiecobbs/mod-authn-otp/wiki/Configuration): How to configure the Apache 2.x server 18 | * [OneTimePasswords](https://github.com/archiecobbs/mod-authn-otp/wiki/OneTimePasswords): How one-time passwords work and how they integrate with HTTP authentication 19 | * [Tokens](https://github.com/archiecobbs/mod-authn-otp/wiki/Tokens): Getting tokens for use with **mod\_authn\_otp** 20 | * [UsersFile](https://github.com/archiecobbs/mod-authn-otp/wiki/UsersFile): The users database 21 | * [DigestAuthentication](https://github.com/archiecobbs/mod-authn-otp/wiki/DigestAuthentication): Limitations of **mod\_authn\_otp** when used with HTTP digest authentication 22 | * [SecurityConsiderations](https://github.com/archiecobbs/mod-authn-otp/wiki/SecurityConsiderations): Security considerations when using one-time passwords for HTTP authentication 23 | * [OTPTool](https://github.com/archiecobbs/mod-authn-otp/wiki/OTPTool): Man page for the **otptool** command line utility 24 | * [GenOTPURL](https://github.com/archiecobbs/mod-authn-otp/wiki/GenOTPURL): Man page for the **genotpurl** command line utility 25 | -------------------------------------------------------------------------------- /Release.txt: -------------------------------------------------------------------------------- 1 | Checklist for releasing version VERSION 2 | --------------------------------------- 3 | 4 | Final check 5 | make distcheck 6 | 7 | Release tarball 8 | sh autogen.sh -C 9 | verify everything is clean 10 | verify configure.ac contains VERSION 11 | update CHANGES with today's date and next revision R=R+1 for VERSION 12 | commit -> revision R+1 13 | sh autogen.sh -c && make distcheck 14 | upload tarball to Amazon AWS 15 | 16 | Tag release 17 | git tag ... VERSION 18 | 19 | mod-authn-otp project 20 | update users file sample in UsersFile wiki page 21 | update any new configuration directives in Configuration wiki page 22 | 23 | OBS 24 | update mod-authn-otp.spec 25 | update OBS project 26 | 27 | Bump version 28 | edit configure.ac and set version to VERSION + 1 29 | commit 30 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # mod_authn_otp - Apache module for one-time password authentication 5 | # 6 | # Copyright 2009 Archie L. Cobbs 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | 21 | # 22 | # Script to regenerate all the GNU auto* gunk. 23 | # Run this from the top directory of the source tree. 24 | # 25 | 26 | set -e 27 | 28 | # Constants 29 | ACLOCAL="aclocal" 30 | AUTOHEADER="autoheader" 31 | AUTOMAKE="automake" 32 | AUTOCONF="autoconf" 33 | 34 | # Clean up autojunk 35 | echo "cleaning up" 36 | rm -rf .libs autom4te*.cache scripts aclocal.m4 configure config.log config.status .deps stamp-h1 37 | rm -f otptool otplock genotpurl *.o *.la *.lo *.slo Makefile.in Makefile 38 | rm -f mod_authn_otp-?.?.?.tar.gz 39 | rm -f config.h* 40 | rm -f TAGS tags 41 | 42 | if [ "$1" = "-C" ]; then 43 | echo "cleanup done, exiting" 44 | exit 45 | fi 46 | 47 | # Create scripts directory 48 | mkdir -p scripts 49 | 50 | echo "running aclocal" 51 | ${ACLOCAL} ${ACLOCAL_ARGS} -I scripts 52 | 53 | echo "running autoheader" 54 | ${AUTOHEADER} 55 | 56 | echo "running automake" 57 | ${AUTOMAKE} --add-missing -c --foreign 58 | 59 | echo "running autoconf" 60 | ${AUTOCONF} -f -i 61 | 62 | if [ "$1" = "-c" ]; then 63 | shift 64 | echo "running configure" 65 | ./configure ${1+"$@"} 66 | fi 67 | 68 | -------------------------------------------------------------------------------- /base32.c: -------------------------------------------------------------------------------- 1 | /** 2 | * base32 (de)coder implementation as specified by RFC4648. 3 | * 4 | * Copyright (c) 2010 Adrien Kunysz 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | **/ 24 | 25 | #include // assert() 26 | #include // CHAR_BIT 27 | 28 | #include "otpdefs.h" 29 | 30 | /** 31 | * Let this be a sequence of plain data before encoding: 32 | * 33 | * 01234567 01234567 01234567 01234567 01234567 34 | * +--------+--------+--------+--------+--------+ 35 | * |< 0 >< 1| >< 2 ><|.3 >< 4.|>< 5 ><.|6 >< 7 >| 36 | * +--------+--------+--------+--------+--------+ 37 | * 38 | * There are 5 octets of 8 bits each in each sequence. 39 | * There are 8 blocks of 5 bits each in each sequence. 40 | * 41 | * You probably want to refer to that graph when reading the algorithms in this 42 | * file. We use "octet" instead of "byte" intentionnaly as we really work with 43 | * 8 bits quantities. This implementation will probably not work properly on 44 | * systems that don't have exactly 8 bits per (unsigned) char. 45 | **/ 46 | 47 | static size_t min(size_t x, size_t y) 48 | { 49 | return x < y ? x : y; 50 | } 51 | 52 | static const unsigned char PADDING_CHAR = '='; 53 | 54 | /** 55 | * Pad the given buffer with len padding characters. 56 | */ 57 | static void pad(unsigned char *buf, int len) 58 | { 59 | int i; 60 | 61 | for (i = 0; i < len; i++) 62 | buf[i] = PADDING_CHAR; 63 | } 64 | 65 | /** 66 | * This convert a 5 bits value into a base32 character. 67 | * Only the 5 least significant bits are used. 68 | */ 69 | static unsigned char encode_char(unsigned char c) 70 | { 71 | static unsigned char base32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 72 | return base32[c & 0x1F]; // 0001 1111 73 | } 74 | 75 | /** 76 | * Decode given character into a 5 bits value. 77 | * Returns -1 iff the argument given was an invalid base32 character 78 | * or a padding character. 79 | */ 80 | static int decode_char(unsigned char c) 81 | { 82 | int retval = -1; 83 | 84 | if (c >= 'A' && c <= 'Z') 85 | retval = c - 'A'; 86 | if (c >= '2' && c <= '7') 87 | retval = c - '2' + 26; 88 | 89 | assert(retval == -1 || ((retval & 0x1F) == retval)); 90 | 91 | return retval; 92 | } 93 | 94 | /** 95 | * Given a block id between 0 and 7 inclusive, this will return the index of 96 | * the octet in which this block starts. For example, given 3 it will return 1 97 | * because block 3 starts in octet 1: 98 | * 99 | * +--------+--------+ 100 | * | ......<|.3 >....| 101 | * +--------+--------+ 102 | * octet 1 | octet 2 103 | */ 104 | static int get_octet(int block) 105 | { 106 | assert(block >= 0 && block < 8); 107 | return (block*5) / 8; 108 | } 109 | 110 | /** 111 | * Given a block id between 0 and 7 inclusive, this will return how many bits 112 | * we can drop at the end of the octet in which this block starts. 113 | * For example, given block 0 it will return 3 because there are 3 bits 114 | * we don't care about at the end: 115 | * 116 | * +--------+- 117 | * |< 0 >...| 118 | * +--------+- 119 | * 120 | * Given block 1, it will return -2 because there 121 | * are actually two bits missing to have a complete block: 122 | * 123 | * +--------+- 124 | * |.....< 1|.. 125 | * +--------+- 126 | **/ 127 | static int get_offset(int block) 128 | { 129 | assert(block >= 0 && block < 8); 130 | return (8 - 5 - (5*block) % 8); 131 | } 132 | 133 | /** 134 | * Like "b >> offset" but it will do the right thing with negative offset. 135 | * We need this as bitwise shifting by a negative offset is undefined 136 | * behavior. 137 | */ 138 | static unsigned char shift_right(unsigned char byte, int offset) 139 | { 140 | if (offset > 0) 141 | return byte >> offset; 142 | else 143 | return byte << -offset; 144 | } 145 | 146 | static unsigned char shift_left(unsigned char byte, int offset) 147 | { 148 | return shift_right(byte, - offset); 149 | } 150 | 151 | /** 152 | * Encode a sequence. A sequence is no longer than 5 octets by definition. 153 | * Thus passing a length greater than 5 to this function is an error. Encoding 154 | * sequences shorter than 5 octets is supported and padding will be added to the 155 | * output as per the specification. 156 | */ 157 | static void encode_sequence(const unsigned char *plain, int len, unsigned char *coded) 158 | { 159 | int block; 160 | 161 | assert(CHAR_BIT == 8); // not sure this would work otherwise 162 | assert(len >= 0 && len <= 5); 163 | 164 | for (block = 0; block < 8; block++) { 165 | int octet = get_octet(block); // figure out which octet this block starts in 166 | int junk = get_offset(block); // how many bits do we drop from this octet? 167 | 168 | if (octet >= len) { // we hit the end of the buffer 169 | pad(&coded[block], 8 - block); 170 | return; 171 | } 172 | 173 | unsigned char c = shift_right(plain[octet], junk); // first part 174 | 175 | if (junk < 0 // is there a second part? 176 | && octet < len - 1) // is there still something to read? 177 | { 178 | c |= shift_right(plain[octet+1], 8 + junk); 179 | } 180 | coded[block] = encode_char(c); 181 | } 182 | } 183 | 184 | void base32_encode(const unsigned char *plain, size_t len, unsigned char *coded) 185 | { 186 | size_t i; 187 | size_t j; 188 | 189 | // All the hard work is done in encode_sequence(), 190 | // here we just need to feed it the data sequence by sequence. 191 | for (i = 0, j = 0; i < len; i += 5, j += 8) { 192 | encode_sequence(&plain[i], min(len - i, 5), &coded[j]); 193 | } 194 | } 195 | 196 | static int decode_sequence(const unsigned char *coded, unsigned char *plain) 197 | { 198 | int block; 199 | 200 | assert(CHAR_BIT == 8); 201 | assert(coded && plain); 202 | 203 | plain[0] = 0; 204 | for (block = 0; block < 8; block++) { 205 | int offset = get_offset(block); 206 | int octet = get_octet(block); 207 | 208 | int c = decode_char(coded[block]); 209 | if (c < 0) // invalid char, stop here 210 | return octet; 211 | 212 | plain[octet] |= shift_left(c, offset); 213 | if (offset < 0) { // does this block overflows to next octet? 214 | assert(octet < 4); 215 | plain[octet+1] = shift_left(c, 8 + offset); 216 | } 217 | } 218 | return 5; 219 | } 220 | 221 | size_t base32_decode(const unsigned char *coded, unsigned char *plain) 222 | { 223 | size_t written = 0; 224 | size_t i; 225 | size_t j; 226 | 227 | for (i = 0, j = 0; ; i += 8, j += 5) { 228 | int n = decode_sequence(&coded[i], &plain[j]); 229 | written += n; 230 | if (n < 5) 231 | return written; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # mod_authn_otp - Apache module for one-time password authentication 4 | # 5 | # Copyright 2009 Archie L. Cobbs 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | AC_INIT([mod_authn_otp Apache2 authentication module supporting one-time passwords], [1.1.11], [https://github.com/archiecobbs/mod-authn-otp], [mod_authn_otp]) 21 | AC_CONFIG_AUX_DIR(scripts) 22 | AM_INIT_AUTOMAKE 23 | dnl AM_MAINTAINER_MODE 24 | AC_PREREQ(2.59) 25 | AC_PREFIX_DEFAULT(/usr) 26 | AC_PROG_MAKE_SET 27 | 28 | # Check for required programs 29 | AC_PROG_CC 30 | AC_PROG_INSTALL 31 | AC_PATH_PROG(STRIP, strip) 32 | AC_PATH_PROG(CP, cp) 33 | AC_PATH_PROG(APXS, apxs, "no", [/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin /usr/local/apache/bin]) 34 | AC_PATH_PROG(APXS2, apxs2, "no", [/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin /usr/local/apache/bin]) 35 | [test x"$APXS2" = "xno" -a x"$APXS" = "xno" && ] AC_MSG_ERROR([cannot find apxs or apxs2]) 36 | [test x"$APXS2" != "xno" && APXS="$APXS2"] 37 | 38 | # Add GCC flags 39 | [GCC_WARN_FLAGS="" 40 | CFLAGS="" 41 | if test x"$GCC" = "xyes"; then 42 | GCC_WARN_FLAGS="-Wall -Waggregate-return -Wcast-align -Wchar-subscripts -Wcomment -Wformat \ 43 | -Wimplicit -Wmissing-declarations -Wmissing-prototypes -Wnested-externs -Wno-long-long \ 44 | -Wparentheses -Wpointer-arith -Wredundant-decls -Wreturn-type -Wswitch -Wtrigraphs \ 45 | -Wuninitialized -Wunused -Wwrite-strings -Wshadow -Wstrict-prototypes -Wcast-qual" 46 | CFLAGS="-O3 $GCC_WARN_FLAGS" 47 | fi] 48 | AC_SUBST(GCC_WARN_FLAGS) 49 | AC_SUBST(CFLAGS) 50 | 51 | # Check for required libraries 52 | AC_CHECK_LIB(crypto, EVP_sha1,, 53 | [AC_MSG_ERROR([required library libcrypto missing])]) 54 | AC_CHECK_LIB(apr-1, apr_file_lock,, 55 | [AC_MSG_ERROR([required library libapr missing])]) 56 | 57 | # Check for optional functions 58 | AC_CHECK_FUNCS(strptime) 59 | 60 | # Check for required header files 61 | AC_CHECK_HEADERS(ctype.h errno.h openssl/evp.h openssl/hmac.h openssl/md5.h stdio.h string.h time.h unistd.h, [], 62 | [AC_MSG_ERROR([required header file '$ac_header' not found])]) 63 | AC_CHECK_HEADERS(err.h, [], []) 64 | AC_CHECK_HEADERS(apr-1/apr_file_io.h) 65 | AC_CHECK_HEADERS(apr-1.0/apr_file_io.h) 66 | 67 | # Command line flags 68 | AC_ARG_ENABLE(Werror, 69 | AS_HELP_STRING([--enable-Werror], 70 | [enable compilation with -Werror flag (default NO)]), 71 | [test x"$enableval" = "xyes" && CFLAGS="${CFLAGS} -Werror"]) 72 | 73 | # Generated files 74 | AC_CONFIG_FILES(Makefile) 75 | AC_CONFIG_HEADERS(config.h) 76 | 77 | # Go 78 | AC_OUTPUT 79 | -------------------------------------------------------------------------------- /errinc.h: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 2009 Archie L. Cobbs 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #include 19 | 20 | #ifdef HAVE_ERR_H 21 | #include 22 | #else 23 | #define err(E, FMT...) do { \ 24 | int _esave = (errno); \ 25 | fprintf(stderr, "%s: ", PROG_NAME); \ 26 | fprintf(stderr, FMT); \ 27 | fprintf(stderr, ": %s\n", strerror(_esave)); \ 28 | exit(E); \ 29 | } while (0) 30 | #define errx(E, FMT...) do { \ 31 | fprintf(stderr, "%s: ", PROG_NAME); \ 32 | fprintf(stderr, FMT); \ 33 | fprintf(stderr, "\n"); \ 34 | exit(E); \ 35 | } while (0) 36 | #define warn(FMT...) do { \ 37 | int _esave = (errno); \ 38 | fprintf(stderr, "%s: ", PROG_NAME); \ 39 | fprintf(stderr, FMT); \ 40 | fprintf(stderr, ": %s\n", strerror(_esave)); \ 41 | } while (0) 42 | #define warnx(FMT...) do { \ 43 | fprintf(stderr, "%s: ", PROG_NAME); \ 44 | fprintf(stderr, FMT); \ 45 | fprintf(stderr, "\n"); \ 46 | } while (0) 47 | #endif 48 | 49 | -------------------------------------------------------------------------------- /genotpurl.1: -------------------------------------------------------------------------------- 1 | .\" -*- nroff -*- 2 | .\" 3 | .\" genotpurl - Generate Google Authenticator URLs 4 | .\" 5 | .\" Copyright 2009 Archie L. Cobbs 6 | .\" 7 | .\" Licensed under the Apache License, Version 2.0 (the "License"); 8 | .\" you may not use this file except in compliance with the License. 9 | .\" You may obtain a copy of the License at 10 | .\" 11 | .\" http://www.apache.org/licenses/LICENSE-2.0 12 | .\" 13 | .\" Unless required by applicable law or agreed to in writing, software 14 | .\" distributed under the License is distributed on an "AS IS" BASIS, 15 | .\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | .\" See the License for the specific language governing permissions and 17 | .\" limitations under the License. 18 | .\"/ 19 | .Dd July 21, 2018 20 | .Dt GENOTPURL 1 21 | .Os 22 | .Sh NAME 23 | .Nm genotpurl 24 | .Nd Generate Google Authenticator URLs 25 | .Sh SYNOPSIS 26 | .Nm genotpurl 27 | .Bk -words 28 | .Fl I Ar issuer 29 | .Fl L Ar label 30 | .Op Fl k Ar key 31 | .Op Fl K Ar keylen 32 | .Op Fl i 33 | .Op Fl c Ar counter 34 | .Op Fl d Ar num-digits 35 | .Op Fl p Ar period 36 | .Ek 37 | .Sh DESCRIPTION 38 | .Nm 39 | is a utility for generating URLs for the Google Authenticator cell phone app, which 40 | generates one-time passwords compatible with the HOTP/OATH algorithm defined by RFC 4226. 41 | When an URL is processed by the Google Authenticator app, the corresponding secure token 42 | is automatically installed into the app. 43 | .Pp 44 | The 45 | .Ar issuer 46 | and 47 | .Ar label 48 | are required; these strings are displayed in the app along with the current token value. 49 | .Pp 50 | The 51 | .Ar key 52 | is the token's binary secret key and is specified as a hexadecimal string. 53 | If no 54 | .Ar key 55 | is given, 56 | .Nm 57 | generates a random key and prints it to standard error; 58 | this key must then be installed into the server application that verifies the one-time passwords 59 | and associated with the user who will be using the secure token. 60 | .Pp 61 | The 62 | .Fl i , 63 | .Fl c , 64 | .Fl d , 65 | and 66 | .Fl p 67 | flags allow creation of non-standard tokens, but these may not be supported by Google Authenticator. 68 | .Sh OPTIONS 69 | .Bl -tag -width Ds 70 | .It Fl c 71 | Specify the starting target counter value for the one-time password generation. 72 | This flag is incompatible with the 73 | .Fl t 74 | flag. 75 | .It Fl d 76 | Specify the number of digits in the one-time password. 77 | The default value is six. 78 | .It Fl h 79 | Print the usage message and exit successfully. 80 | .It Fl I 81 | Specify token issuer (e.g., "Example Industries, Inc.") 82 | .It Fl i 83 | Create an interval-based token instead of a time-based token. 84 | .It Fl k 85 | Specify the token binary secret key as a hexadecimal string. 86 | .Pp 87 | If no 88 | .Ar key 89 | is given, 90 | .Nm 91 | generates a random key and prints it to standard output. 92 | .It Fl K 93 | Specify generated key length. 94 | Ignored if 95 | .Fl k 96 | is also specified. 97 | .It Fl L 98 | Specify token label (e.g., "user@example.com") 99 | .It Fl p 100 | Specify the length of a single time interval in seconds. 101 | The default value is 30 seconds. 102 | Ignored if 103 | .Fl i 104 | is given. 105 | .El 106 | .Sh EXAMPLES 107 | .Bk -words 108 | To generate a new secure token and generate the corresponding QR code: 109 | .Pp 110 | genotpurl -I "Acme, Inc" -L user@acme.com | qrencode -s 6 -o qrcode.png 111 | .Ek 112 | .Sh SEE ALSO 113 | .Xr qrencode 1 114 | .Pp 115 | .Rs 116 | .%T "mod_authn_otp: Apache module for one-time password authentication" 117 | .%O "https://github.com/archiecobbs/mod-authn-otp" 118 | .Re 119 | .Rs 120 | .%T "Google Authenticator (iOS)" 121 | .%O "https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8" 122 | .Re 123 | .Rs 124 | .%T "Google Authenticator (Android)" 125 | .%O "https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US" 126 | .Re 127 | .Sh AUTHOR 128 | .An Archie L. Cobbs Aq archie.cobbs@gmail.com 129 | -------------------------------------------------------------------------------- /genotpurl.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Generates URLs for Google Authenticator. 3 | */ 4 | 5 | #include "otpdefs.h" 6 | 7 | static void urlencode(FILE *fp, const char *s); 8 | static void print_key(FILE *fp, const unsigned char *key, u_int len, int base32); 9 | static void usage(void); 10 | 11 | #define PROG_NAME "genotpurl" 12 | 13 | #define DEFAULT_COUNTER 0 14 | #define DEFAULT_KEYLEN 10 15 | #define MIN_KEYLEN 4 16 | #define DEFAULT_NUM_DIGITS 6 17 | #define DEFAULT_PERIOD 30 18 | 19 | #define RANDOM_FILE "/dev/urandom" 20 | 21 | int 22 | main(int argc, char **argv) 23 | { 24 | #ifdef DEFAULT_ISSUER 25 | const char *issuer = DEFAULT_ISSUER; 26 | #else 27 | const char *issuer = NULL; 28 | #endif 29 | #ifdef DEFAULT_LABEL 30 | const char *label = DEFAULT_LABEL; 31 | #else 32 | const char *label = NULL; 33 | #endif 34 | unsigned int period = DEFAULT_PERIOD; 35 | unsigned int counter = DEFAULT_COUNTER; 36 | unsigned int num_digits = 6; 37 | int time_based = 1; 38 | unsigned char *key = NULL; 39 | int keylen = DEFAULT_KEYLEN; 40 | FILE *fp; 41 | int i, j; 42 | unsigned int b; 43 | 44 | // Parse command line 45 | while ((i = getopt(argc, argv, "c:d:iI:k:K:L:p:")) != -1) { 46 | switch (i) { 47 | case 'c': 48 | counter = atoi(optarg); 49 | break; 50 | case 'd': 51 | num_digits = atoi(optarg); 52 | break; 53 | case 'i': 54 | time_based = 0; 55 | break; 56 | case 'I': 57 | issuer = optarg; 58 | break; 59 | case 'k': 60 | if (strlen(optarg) % 2 != 0) 61 | errx(1, "invalid hex key `%s': odd number of digits", optarg); 62 | if ((key = malloc((keylen = strlen(optarg) / 2))) == NULL) 63 | err(1, "malloc"); 64 | for (j = 0; j < keylen; j++) { 65 | if (sscanf(optarg + 2 * j, "%2x", &b) != 1) 66 | errx(1, "invalid hex key `%s': can't parse", optarg); 67 | key[j] = b & 0xff; 68 | } 69 | break; 70 | case 'K': 71 | if (key != NULL) 72 | break; 73 | keylen = atoi(optarg); 74 | if (keylen < MIN_KEYLEN) 75 | errx(1, "invalid key length `%s'", optarg); 76 | break; 77 | case 'L': 78 | label = optarg; 79 | break; 80 | case 'p': 81 | period = atoi(optarg); 82 | break; 83 | case '?': 84 | default: 85 | usage(); 86 | exit(1); 87 | } 88 | } 89 | argv += optind; 90 | argc -= optind; 91 | switch (argc) { 92 | case 0: 93 | break; 94 | default: 95 | usage(); 96 | exit(1); 97 | } 98 | 99 | // Sanity check 100 | if (time_based && counter != DEFAULT_COUNTER) 101 | errx(1, "use of `-c' flag is invalid with time-based tokens"); 102 | if (!time_based && period != DEFAULT_PERIOD) 103 | errx(1, "use of `-p' flag is invalid with time-based tokens"); 104 | if (label == NULL) 105 | errx(1, "label required; use `-L' flag"); 106 | if (issuer == NULL) 107 | errx(1, "issuer required; use `-I' flag"); 108 | if (strchr(issuer, ':') != NULL || strchr(label, ':') != NULL) 109 | errx(1, "issuer and label must not contain the colon character"); 110 | if (time_based && period != DEFAULT_PERIOD) 111 | errx(1, "google authenticator does not support time periods other than %d seconds", DEFAULT_PERIOD); 112 | if (num_digits != DEFAULT_NUM_DIGITS) 113 | errx(1, "google authenticator does not support number digits other than %d", DEFAULT_NUM_DIGITS); 114 | 115 | // Generate key (if not supplied) 116 | if (key == NULL) { 117 | if ((key = malloc((keylen))) == NULL) 118 | err(1, "malloc"); 119 | if ((fp = fopen(RANDOM_FILE, "r")) == NULL) 120 | err(1, "%s", RANDOM_FILE); 121 | if (fread(key, 1, keylen, fp) != keylen) 122 | err(1, "%s", RANDOM_FILE); 123 | fclose(fp); 124 | fprintf(stderr, "generated key (hex): "); 125 | print_key(stderr, key, keylen, 0); 126 | fprintf(stderr, "\n"); 127 | } 128 | 129 | // Output URL 130 | printf("otpauth://%s/", time_based ? "totp" : "hotp"); 131 | urlencode(stdout, issuer); 132 | printf(":"); 133 | urlencode(stdout, label); 134 | printf("?issuer="); 135 | urlencode(stdout, issuer); 136 | printf("&secret="); 137 | print_key(stdout, key, keylen, 1); 138 | if (num_digits != DEFAULT_NUM_DIGITS) 139 | printf("&digits=%u", num_digits); 140 | if (!time_based) 141 | printf("&counter=%u", counter); 142 | else if (period != DEFAULT_PERIOD) 143 | printf("&period=%u", period); 144 | printf("\n"); 145 | 146 | // Done 147 | return 0; 148 | } 149 | 150 | static void 151 | urlencode(FILE *fp, const char *s) 152 | { 153 | while (*s != '\0') { 154 | if (isalnum(*s)) 155 | fprintf(fp, "%c", *s); 156 | else 157 | fprintf(fp, "%%%02x", *s & 0xff); 158 | s++; 159 | } 160 | } 161 | 162 | static void 163 | print_key(FILE *fp, const unsigned char *key, u_int len, int base32) 164 | { 165 | unsigned char *buf; 166 | u_int buflen; 167 | int i; 168 | 169 | if (base32) { 170 | buflen = ((len + 4) / 5) * 8; 171 | if ((buf = malloc(buflen + 1)) == NULL) 172 | err(1, "malloc"); 173 | buf[buflen] = 0; 174 | base32_encode(key, len, buf); 175 | assert(buf[buflen] == 0 && (buflen == 0 || buf[buflen - 1] != 0)); 176 | for (i = 0; i < buflen; i++) 177 | fputc(buf[i], fp); 178 | free(buf); 179 | } else { 180 | for (i = 0; i < len; i++) 181 | fprintf(fp, "%02x", key[i] & 0xff); 182 | } 183 | } 184 | 185 | static void 186 | usage(void) 187 | { 188 | fprintf(stderr, "Usage: genotpurl [-i]"); 189 | #ifdef DEFAULT_ISSUER 190 | fprintf(stderr, " [-I issuer]"); 191 | #else 192 | fprintf(stderr, " -I issuer"); 193 | #endif 194 | #ifdef DEFAULT_LABEL 195 | fprintf(stderr, " [-L label]"); 196 | #else 197 | fprintf(stderr, " -L label"); 198 | #endif 199 | fprintf(stderr, " [-c counter] [-d num-digits] [-p period] [-k key | -K keylength]\n"); 200 | fprintf(stderr, "Options:\n"); 201 | fprintf(stderr, " -c\tInitial counter value (default %d)\n", DEFAULT_COUNTER); 202 | fprintf(stderr, " -d\tNumber of digits (default %d)\n", DEFAULT_NUM_DIGITS); 203 | fprintf(stderr, " -i\tInterval-based instead of time-based\n"); 204 | #ifdef DEFAULT_ISSUER 205 | fprintf(stderr, " -I\tSpecify issuer (default \"%s\")\n", DEFAULT_ISSUER); 206 | #else 207 | fprintf(stderr, " -I\tSpecify issuer (REQUIRED)\n"); 208 | #endif 209 | fprintf(stderr, " -p\tTime period in seconds (default %d)\n", DEFAULT_PERIOD); 210 | fprintf(stderr, " -k\tSpecify hex key (default auto-generate and report)\n"); 211 | fprintf(stderr, " -K\tSpecify a key length if key is to be generated (default: %d)\n", DEFAULT_KEYLEN); 212 | #ifdef DEFAULT_LABEL 213 | fprintf(stderr, " -L\tSpecify label (default \"%s\")\n", DEFAULT_LABEL); 214 | #else 215 | fprintf(stderr, " -L\tSpecify label (REQUIRED)\n"); 216 | #endif 217 | } 218 | -------------------------------------------------------------------------------- /hotp.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * otptool - HOTP/OATH one-time password utility 4 | * 5 | * Copyright 2009 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include "otpdefs.h" 21 | 22 | /* Powers of ten */ 23 | static const int powers10[] = { 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 1000000000 }; 24 | 25 | /* 26 | * Generate an OTP using the algorithm specified in RFC 4226, 27 | */ 28 | void 29 | hotp(const u_char *key, size_t keylen, uint64_t counter, int ndigits, char *buf10, char *buf16, size_t buflen) 30 | { 31 | const int max10 = sizeof(powers10) / sizeof(*powers10); 32 | const int max16 = 8; 33 | const EVP_MD *sha1_md = EVP_sha1(); 34 | u_char hash[EVP_MAX_MD_SIZE]; 35 | u_int hash_len; 36 | u_char tosign[8]; 37 | int offset; 38 | int value; 39 | int i; 40 | 41 | /* Encode counter */ 42 | for (i = sizeof(tosign) - 1; i >= 0; i--) { 43 | tosign[i] = counter & 0xff; 44 | counter >>= 8; 45 | } 46 | 47 | /* Compute HMAC */ 48 | HMAC(sha1_md, key, keylen, tosign, sizeof(tosign), hash, &hash_len); 49 | 50 | /* Extract selected bytes to get 32 bit integer value */ 51 | offset = hash[hash_len - 1] & 0x0f; 52 | value = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) 53 | | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); 54 | 55 | /* Sanity check max # digits */ 56 | if (ndigits < 1) 57 | ndigits = 1; 58 | 59 | /* Generate decimal digits */ 60 | if (buf10 != NULL) { 61 | snprintf(buf10, buflen, "%0*d", ndigits < max10 ? ndigits : max10, 62 | ndigits < max10 ? value % powers10[ndigits - 1] : value); 63 | } 64 | 65 | /* Generate hexadecimal digits */ 66 | if (buf16 != NULL) { 67 | snprintf(buf16, buflen, "%0*x", ndigits < max16 ? ndigits : max16, 68 | ndigits < max16 ? (value & ((1 << (4 * ndigits)) - 1)) : value); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /md5q.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * otptool - HOTP/OATH one-time password utility 4 | * 5 | * Copyright 2009 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include "otpdefs.h" 21 | 22 | void 23 | md5_quick(const void *data, size_t len, u_char *result) 24 | { 25 | EVP_MD_CTX *ctx; 26 | u_int md5_len; 27 | int r; 28 | 29 | ctx = EVP_MD_CTX_new(); 30 | assert(ctx != NULL); 31 | r = EVP_DigestInit_ex(ctx, EVP_md5(), NULL); 32 | assert(r != 0); 33 | r = EVP_DigestUpdate(ctx, data, len); 34 | assert(r != 0); 35 | r = EVP_DigestFinal_ex(ctx, result, &md5_len); 36 | assert(r != 0); 37 | assert(md5_len == MD5_DIGEST_LENGTH); 38 | EVP_MD_CTX_free(ctx); 39 | } 40 | -------------------------------------------------------------------------------- /mod_authn_otp.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * mod_authn_otp - Apache module for one-time password authentication 4 | * 5 | * Copyright 2009 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include "apr_lib.h" 21 | #include "ap_config.h" 22 | #include "ap_provider.h" 23 | #include "mod_auth.h" 24 | #include "apr_base64.h" 25 | 26 | // Fix libapr pollution 27 | #undef PACKAGE_BUGREPORT 28 | #undef PACKAGE_NAME 29 | #undef PACKAGE_STRING 30 | #undef PACKAGE_TARNAME 31 | #undef PACKAGE_VERSION 32 | 33 | #include "config.h" 34 | 35 | #define APR_WANT_STRFUNC 36 | #include "apr_want.h" 37 | #include "apr_strings.h" 38 | #include "apr_file_io.h" 39 | #include "apr_time.h" 40 | 41 | #include "httpd.h" 42 | #include "http_config.h" 43 | #include "http_core.h" 44 | #include "http_log.h" 45 | #include "http_protocol.h" 46 | #include "http_request.h" 47 | #include "util_md5.h" 48 | 49 | #include "otpdefs.h" 50 | 51 | /* Apache backward-compat */ 52 | #ifndef AUTHN_PROVIDER_VERSION 53 | #define AUTHN_PROVIDER_VERSION "0" 54 | #endif 55 | #if AP_MODULE_MAGIC_AT_LEAST(20111203, 0) 56 | #define USER_AGENT_IP(req) ((req)->useragent_ip) 57 | #else 58 | #define USER_AGENT_IP(req) ((req)->connection->remote_ip) 59 | #endif 60 | 61 | /* Module definition */ 62 | module AP_MODULE_DECLARE_DATA authn_otp_module; 63 | 64 | /* Our unique authentication provider name */ 65 | #define OTP_AUTHN_PROVIDER_NAME "OTP" 66 | 67 | /* Definitions related to users file */ 68 | #define WHITESPACE " \t\r\n\v" 69 | #define NEWFILE_SUFFIX ".new" 70 | #define LOCKFILE_SUFFIX ".lock" 71 | #define PIN_EXTERNAL "+" 72 | #define PIN_NONE "-" 73 | 74 | /* Formatting of time values */ 75 | #if HAVE_STRPTIME 76 | #define TIME_FORMAT "%Y-%m-%dT%H:%M:%SL" 77 | #endif 78 | 79 | /* OTP counter algorithms */ 80 | #define OTP_ALGORITHM_HOTP 1 81 | #define OTP_ALGORITHM_MOTP 2 82 | 83 | /* Default configuration settings */ 84 | #define DEFAULT_NUM_DIGITS 6 85 | #define DEFAULT_MAX_OFFSET 4 86 | #define DEFAULT_MAX_LINGER (10 * 60) /* 10 minutes */ 87 | #define DEFAULT_LOGOUT_IP_CHANGE 0 88 | #define DEFAULT_ALLOW_FALLTHROUGH 0 89 | #define DEFAULT_PIN_FAKE_BASIC_AUTH 0 90 | 91 | /* PIN configuration */ 92 | #define PIN_CONFIG_LITERAL 0 93 | #define PIN_CONFIG_NONE 1 /* User has no PIN */ 94 | #define PIN_CONFIG_EXTERNAL 2 /* PIN must be gotten from OTPAuthPINAuthProvider */ 95 | 96 | /* MobileOTP defaults */ 97 | #define MOTP_TIME_INTERVAL 10 98 | 99 | /* Buffer size for OTPs */ 100 | #define OTP_BUF_SIZE 16 101 | 102 | /* Other buffer sizes */ 103 | #define MAX_USERNAME 128 104 | #define MAX_PIN 128 105 | #define MAX_KEY 256 106 | #define MAX_OTP 128 107 | #define MAX_IP 128 108 | #define MAX_TOKEN 128 109 | 110 | /* Per-directory configuration */ 111 | struct otp_config { 112 | char *users_file; /* Name of the users file */ 113 | int max_offset; /* Maximum allowed counter offset from expected value */ 114 | int max_linger; /* Maximum time for which the same OTP can be used repeatedly */ 115 | u_int max_otp_failures; /* Maximum wrong OTP values before account becomes locked, or zero for no limit */ 116 | int logout_ip_change; /* Auto-logout user if IP address changes */ 117 | int allow_fallthrough; /* Allow fall-through if OTP auth fails */ 118 | authn_provider_list *provlist; /* Authorization providers for checking PINs */ 119 | int pin_fake_basic_auth; /* Emulate basic authentication with username and PIN */ 120 | }; 121 | 122 | /* User info structure */ 123 | struct otp_user { 124 | int algorithm; /* one of OTP_ALGORITHM_* */ 125 | int time_interval; /* in seconds, or zero for event-based tokens */ 126 | int num_digits; 127 | char username[MAX_USERNAME]; 128 | u_char key[MAX_KEY]; 129 | int keylen; 130 | int pincfg; /* one of PIN_CONFIG_* */ 131 | char pin[MAX_PIN]; 132 | long offset; /* if event: next expected count; if time: time slew */ 133 | char last_otp[MAX_OTP]; 134 | time_t last_auth; 135 | char last_ip[MAX_IP]; 136 | u_int num_otp_failures; 137 | }; 138 | 139 | /* Internal functions */ 140 | static authn_status find_update_user(request_rec *r, const char *usersfile, struct otp_user *const user, int update); 141 | static int parse_token_type(const char *type, struct otp_user *tokinfo); 142 | static apr_status_t print_user(apr_file_t *file, const struct otp_user *user); 143 | static void pin_fake_basic_auth(request_rec *r, const char *user, const char *pin); 144 | static authn_status authn_otp_check_pin(request_rec *r, struct otp_config *const conf, struct otp_user *const user, const char *pin); 145 | static authn_status authn_otp_check_pin_external(request_rec *r, struct otp_config *const conf, const char *user, const char *pin); 146 | static authn_status authn_otp_check_password(request_rec *r, const char *username, const char *password); 147 | static authn_status authn_otp_get_realm_hash(request_rec *r, const char *username, const char *realm, char **rethash); 148 | static void *create_authn_otp_dir_config(apr_pool_t *p, char *d); 149 | static void *merge_authn_otp_dir_config(apr_pool_t *p, void *base_conf, void *new_conf); 150 | static const char *add_authn_provider(cmd_parms *cmd, void *config, const char *provider_name); 151 | static void copy_provider_list(apr_pool_t *p, authn_provider_list **dstp, authn_provider_list *src); 152 | static struct otp_config *get_config(request_rec *r); 153 | static void register_hooks(apr_pool_t *p); 154 | 155 | /* Powers of ten */ 156 | static const int powers10[] = { 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 1000000000 }; 157 | 158 | /* Mutex to augment file locking for multi-threaded processes */ 159 | static apr_thread_mutex_t *mutex; 160 | 161 | /* 162 | * Find/update a user in the users file. 163 | * 164 | * Note: finding, the "user" structure must be initialized with zeroes. 165 | */ 166 | static authn_status 167 | find_update_user(request_rec *r, const char *usersfile, struct otp_user *const user, const int update) 168 | { 169 | char invalid_reason[128]; 170 | char newusersfile[APR_PATH_MAX]; 171 | char lockusersfile[APR_PATH_MAX]; 172 | char linebuf[1024]; 173 | apr_file_t *file = NULL; 174 | apr_file_t *newfile = NULL; 175 | apr_file_t *lockfile = NULL; 176 | apr_status_t status; 177 | int got_mutex = 0; 178 | char errbuf[64]; 179 | int found = 0; 180 | int linenum; 181 | 182 | /* If updating, open and lock lockfile and grab mutex */ 183 | if (update) { 184 | apr_snprintf(lockusersfile, sizeof(lockusersfile), "%s%s", usersfile, LOCKFILE_SUFFIX); 185 | if ((status = apr_file_open(&lockfile, lockusersfile, 186 | APR_WRITE|APR_CREATE|APR_TRUNCATE, APR_UREAD|APR_UWRITE, r->pool)) != 0) { 187 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't open OTP users lock file \"%s\": %s", 188 | lockusersfile, apr_strerror(status, errbuf, sizeof(errbuf))); 189 | goto fail; 190 | } 191 | if ((status = apr_file_lock(lockfile, APR_FLOCK_EXCLUSIVE)) != 0) { 192 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't lock OTP users lock file \"%s\": %s", 193 | lockusersfile, apr_strerror(status, errbuf, sizeof(errbuf))); 194 | goto fail; 195 | } 196 | if (mutex == NULL) { 197 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't acquire OTP mutex: %s", "no mutex exists"); 198 | goto fail; 199 | } 200 | if ((status = apr_thread_mutex_lock(mutex)) != 0) { 201 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't acquire OTP mutex: %s", apr_strerror(status, errbuf, sizeof(errbuf))); 202 | goto fail; 203 | } 204 | got_mutex = 1; 205 | } 206 | 207 | /* Open existing users file */ 208 | if ((status = apr_file_open(&file, usersfile, APR_READ, 0, r->pool)) != 0) { 209 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't open OTP users file \"%s\": %s", 210 | usersfile, apr_strerror(status, errbuf, sizeof(errbuf))); 211 | goto fail; 212 | } 213 | 214 | /* Open new users file if updating */ 215 | if (update) { 216 | apr_snprintf(newusersfile, sizeof(newusersfile), "%s%s", usersfile, NEWFILE_SUFFIX); 217 | if ((status = apr_file_open(&newfile, newusersfile, 218 | APR_WRITE|APR_CREATE|APR_TRUNCATE, APR_UREAD|APR_UWRITE, r->pool)) != 0) { 219 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't new open OTP users file \"%s\": %s", newusersfile, 220 | apr_strerror(status, errbuf, sizeof(errbuf))); 221 | goto fail; 222 | } 223 | } 224 | 225 | /* Scan entries */ 226 | for (linenum = 1; apr_file_gets(linebuf, sizeof(linebuf), file) == 0; linenum++) { 227 | struct otp_user tokinfo; 228 | int nibs[2]; 229 | char linecopy[1024]; 230 | char *fields[4]; 231 | int field_count; 232 | char *fail_count; 233 | char *timestamp; 234 | char *last_otp; 235 | char *last_ip; 236 | char *last; 237 | char *s; 238 | int i; 239 | 240 | /* Save a copy of the line */ 241 | apr_snprintf(linecopy, sizeof(linecopy), "%s", linebuf); 242 | 243 | /* Ignore lines starting with '#' and empty lines */ 244 | if (*linebuf == '#') 245 | goto copy; 246 | if ((s = apr_strtok(linebuf, WHITESPACE, &last)) == NULL) 247 | goto copy; 248 | 249 | /* Parse token type */ 250 | if (parse_token_type(s, &tokinfo) != 0) { 251 | apr_snprintf(invalid_reason, sizeof(invalid_reason), "invalid token type \"%s\"", s); 252 | goto invalid; 253 | } 254 | 255 | /* Get username */ 256 | if ((s = apr_strtok(NULL, WHITESPACE, &last)) == NULL) { 257 | apr_snprintf(invalid_reason, sizeof(invalid_reason), "missing username field"); 258 | goto invalid; 259 | } 260 | 261 | /* Is this the user we're interested in? */ 262 | if (strcmp(s, user->username) != 0) 263 | goto copy; 264 | found = 1; 265 | 266 | /* If we're updating, print out updated user info to new file */ 267 | if (update) { 268 | if ((status = print_user(newfile, user)) != 0) 269 | goto write_error; 270 | continue; 271 | } 272 | 273 | /* Initialize user record */ 274 | memset(user, 0, sizeof(*user)); 275 | apr_snprintf(user->username, sizeof(user->username), "%s", s); 276 | user->algorithm = tokinfo.algorithm; 277 | user->time_interval = tokinfo.time_interval; 278 | user->num_digits = tokinfo.num_digits; 279 | 280 | /* Read PIN and decode special values */ 281 | if ((s = apr_strtok(NULL, WHITESPACE, &last)) == NULL) { 282 | apr_snprintf(invalid_reason, sizeof(invalid_reason), "missing PIN field"); 283 | goto invalid; 284 | } 285 | if (strcmp(s, PIN_NONE) == 0) { 286 | *s = '\0'; 287 | user->pincfg = PIN_CONFIG_NONE; 288 | } else if (strcmp(s, PIN_EXTERNAL) == 0) { 289 | *s = '\0'; 290 | user->pincfg = PIN_CONFIG_EXTERNAL; 291 | } else 292 | user->pincfg = PIN_CONFIG_LITERAL; 293 | apr_snprintf(user->pin, sizeof(user->pin), "%s", s); 294 | 295 | /* Read key */ 296 | if ((s = apr_strtok(NULL, WHITESPACE, &last)) == NULL) { 297 | apr_snprintf(invalid_reason, sizeof(invalid_reason), "missing token key field"); 298 | goto invalid; 299 | } 300 | for (user->keylen = 0; user->keylen < sizeof(user->key) && *s != '\0'; user->keylen++) { 301 | for (i = 0; i < 2; i++) { 302 | if (apr_isdigit(*s)) 303 | nibs[i] = *s - '0'; 304 | else if (apr_isxdigit(*s)) 305 | nibs[i] = apr_tolower(*s) - 'a' + 10; 306 | else { 307 | apr_snprintf(invalid_reason, sizeof(invalid_reason), "invalid key starting with \"%s\"", s); 308 | goto invalid; 309 | } 310 | s++; 311 | } 312 | user->key[user->keylen] = (nibs[0] << 4) | nibs[1]; 313 | } 314 | 315 | /* Read offset (optional) */ 316 | if ((s = apr_strtok(NULL, WHITESPACE, &last)) == NULL) 317 | goto found; 318 | user->offset = atol(s); 319 | 320 | /* 321 | * At this point, we will read one of the following remaining field combinations. The reason 322 | * for these cases is because of backward compatibility with older versions of the users file. 323 | * 324 | * 0. No more fields 325 | * 1. Fail count 326 | * 2. Fail count, Last OTP, Timestamp, IP Address 327 | * 3. Last OTP, Timestamp 328 | * 4. Last OTP, Timestamp, IP Address 329 | * 330 | * Note that in each case, a different number of fields is found, so we can use the field count 331 | * to determine which case we're in. 332 | */ 333 | for (i = field_count = 0; i < 4; i++) { 334 | if ((fields[i] = apr_strtok(NULL, WHITESPACE, &last)) != NULL) 335 | field_count++; 336 | } 337 | 338 | /* Interpret fields based on cases 0..4 */ 339 | i = 0; 340 | fail_count = (field_count < 2 || field_count == 4) ? fields[i++] : NULL; 341 | last_otp = fields[i++]; 342 | timestamp = fields[i++]; 343 | last_ip = fields[i++]; 344 | 345 | /* Parse OTP failure count (if any) */ 346 | if (fail_count != NULL) 347 | user->num_otp_failures = atoi(fail_count); 348 | 349 | /* Parse last used OTP and parse last successful authentication timestamp (if any) */ 350 | if (last_otp != NULL && timestamp != NULL) { 351 | #if HAVE_STRPTIME 352 | struct tm tm; 353 | #else 354 | char *eptr; 355 | u_long secs; 356 | #endif 357 | 358 | /* Copy last used OTP */ 359 | apr_snprintf(user->last_otp, sizeof(user->last_otp), "%s", last_otp); 360 | 361 | /* Parse last successful authentication timestamp */ 362 | #if HAVE_STRPTIME 363 | if ((s = strptime(timestamp, TIME_FORMAT, &tm)) == NULL || *s != '\0') { 364 | apr_snprintf(invalid_reason, sizeof(invalid_reason), "invalid auth timestamp \"%s\"", timestamp); 365 | goto invalid; 366 | } 367 | tm.tm_isdst = -1; 368 | user->last_auth = mktime(&tm); 369 | #else 370 | secs = strtol(timestamp, &eptr, 10); 371 | if (secs == LONG_MIN || secs == LONG_MAX || *eptr != '\0') { 372 | apr_snprintf(invalid_reason, sizeof(invalid_reason), "invalid auth timestamp \"%s\"", timestamp); 373 | goto invalid; 374 | } 375 | user->last_auth = (time_t)secs; 376 | #endif 377 | } 378 | 379 | /* Copy last used IP address (if any) */ 380 | if (last_ip != NULL) 381 | apr_snprintf(user->last_ip, sizeof(user->last_ip), "%s", last_ip); 382 | 383 | found: 384 | /* We are not updating; return the user we found */ 385 | AP_DEBUG_ASSERT(!update); 386 | AP_DEBUG_ASSERT(newfile == NULL); 387 | AP_DEBUG_ASSERT(lockfile == NULL); 388 | AP_DEBUG_ASSERT(!got_mutex); 389 | apr_file_close(file); 390 | return AUTH_USER_FOUND; 391 | 392 | invalid: 393 | /* Report invalid entry (but copy it anyway) */ 394 | ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "ignoring invalid entry in OTP users file \"%s\" on line %d: %s", 395 | usersfile, linenum, invalid_reason); 396 | 397 | copy: 398 | /* Copy line to new file */ 399 | if (newfile != NULL) { 400 | if ((status = apr_file_puts(linecopy, newfile)) != 0) 401 | goto write_error; 402 | } 403 | } 404 | 405 | /* Close original file */ 406 | apr_file_close(file); 407 | file = NULL; 408 | 409 | /* If we're not updating and we get here, then the user was not found */ 410 | if (!update) { 411 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "user \"%s\" not found in OTP users file \"%s\"", user->username, usersfile); 412 | return AUTH_USER_NOT_FOUND; 413 | } 414 | 415 | /* Close temporary file */ 416 | if ((status = apr_file_close(newfile)) != 0) { 417 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "error closing new OTP users file \"%s\": %s", 418 | newusersfile, apr_strerror(status, errbuf, sizeof(errbuf))); 419 | goto fail; 420 | } 421 | newfile = NULL; 422 | 423 | /* Replace old file with new one */ 424 | if ((status = apr_file_rename(newusersfile, usersfile, r->pool)) != 0) { 425 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "error renaming new OTP users file \"%s\" to \"%s\": %s", 426 | newusersfile, usersfile, apr_strerror(status, errbuf, sizeof(errbuf))); 427 | goto fail; 428 | } 429 | 430 | /* Close (and implicitly unlock) lock file and release mutex */ 431 | apr_file_close(lockfile); 432 | lockfile = NULL; 433 | AP_DEBUG_ASSERT(got_mutex); 434 | apr_thread_mutex_unlock(mutex); 435 | 436 | /* Done updating */ 437 | return found ? AUTH_USER_FOUND : AUTH_USER_NOT_FOUND; 438 | 439 | write_error: 440 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "error writing to new OTP users file \"%s\": %s", 441 | newusersfile, apr_strerror(status, errbuf, sizeof(errbuf))); 442 | 443 | fail: 444 | if (file != NULL) 445 | apr_file_close(file); 446 | if (newfile != NULL) { 447 | apr_file_close(newfile); 448 | (void)apr_file_remove(newusersfile, r->pool); 449 | } 450 | if (lockfile != NULL) 451 | apr_file_close(lockfile); 452 | if (got_mutex) 453 | apr_thread_mutex_unlock(mutex); 454 | return AUTH_GENERAL_ERROR; 455 | } 456 | 457 | /* 458 | * Parse a token type string such as "HOTP/T30/6". 459 | * Returns 0 if successful, else -1 on parse error. 460 | */ 461 | static int 462 | parse_token_type(const char *type, struct otp_user *tokinfo) 463 | { 464 | char tokbuf[MAX_TOKEN]; 465 | char *last; 466 | char *eptr; 467 | char *t; 468 | 469 | /* Backwards compatibility hack */ 470 | if (strcmp(type, "E") == 0) 471 | type = "HOTP/E"; 472 | else if (strcmp(type, "T") == 0) 473 | type = "HOTP/T30"; 474 | 475 | /* Initialize */ 476 | memset(tokinfo, 0, sizeof(*tokinfo)); 477 | apr_snprintf(tokbuf, sizeof(tokbuf), "%s", type); 478 | 479 | /* Parse algorithm */ 480 | if ((t = apr_strtok(tokbuf, "/", &last)) == NULL) 481 | return -1; 482 | 483 | /* Apply per-algorithm defaults */ 484 | if (strcasecmp(t, "HOTP") == 0) { 485 | tokinfo->algorithm = OTP_ALGORITHM_HOTP; 486 | tokinfo->time_interval = 0; 487 | tokinfo->num_digits = DEFAULT_NUM_DIGITS; 488 | } else if (strcasecmp(t, "MOTP") == 0) { 489 | tokinfo->algorithm = OTP_ALGORITHM_MOTP; 490 | tokinfo->time_interval = MOTP_TIME_INTERVAL; 491 | tokinfo->num_digits = DEFAULT_NUM_DIGITS; 492 | } else 493 | return -1; 494 | 495 | /* Parse token type: event or time-based */ 496 | if ((t = apr_strtok(NULL, "/", &last)) == NULL) 497 | return 0; 498 | if (strcmp(t, "E") == 0) 499 | tokinfo->time_interval = 0; 500 | else if (*t == 'T') { 501 | if (!isdigit(*++t)) 502 | return -1; 503 | tokinfo->time_interval = strtol(t, &eptr, 10); 504 | if (tokinfo->time_interval <= 0 || *eptr != '\0') 505 | return -1; 506 | } else 507 | return -1; 508 | 509 | /* Parse #digits */ 510 | if ((t = apr_strtok(NULL, "/", &last)) == NULL) 511 | return 0; 512 | if (!isdigit(*t)) 513 | return -1; 514 | tokinfo->num_digits = strtol(t, &eptr, 10); 515 | if (tokinfo->num_digits <= 0 || *eptr != '\0' || tokinfo->num_digits > sizeof(powers10) / sizeof(*powers10)) 516 | return -1; 517 | 518 | /* Done */ 519 | return 0; 520 | } 521 | 522 | static apr_status_t 523 | print_user(apr_file_t *file, const struct otp_user *user) 524 | { 525 | const char *pinstr = NULL; 526 | const char *alg; 527 | char cbuf[64]; 528 | char nbuf[64]; 529 | char tbuf[MAX_TOKEN]; 530 | int i; 531 | 532 | /* Format token type sub-fields */ 533 | switch (user->algorithm) { 534 | case OTP_ALGORITHM_HOTP: 535 | alg = "HOTP"; 536 | break; 537 | case OTP_ALGORITHM_MOTP: 538 | alg = "MOTP"; 539 | break; 540 | default: 541 | abort(); 542 | } 543 | if (user->time_interval == 0) 544 | apr_snprintf(cbuf, sizeof(cbuf), "/E"); 545 | else 546 | apr_snprintf(cbuf, sizeof(cbuf), "/T%d", user->time_interval); 547 | apr_snprintf(nbuf, sizeof(nbuf), "/%d", user->num_digits); 548 | 549 | /* Abbreviate when default values apply */ 550 | if (user->num_digits == DEFAULT_NUM_DIGITS) { 551 | *nbuf = '\0'; 552 | if (user->algorithm == OTP_ALGORITHM_HOTP && user->time_interval == 0) 553 | *cbuf = '\0'; 554 | else if (user->algorithm == OTP_ALGORITHM_MOTP && user->time_interval == 10) 555 | *cbuf = '\0'; 556 | } 557 | apr_snprintf(tbuf, sizeof(tbuf), "%s%s%s", alg, cbuf, nbuf); 558 | 559 | /* Get PIN representation */ 560 | switch (user->pincfg) { 561 | case PIN_CONFIG_LITERAL: 562 | pinstr = user->pin; 563 | break; 564 | case PIN_CONFIG_NONE: 565 | pinstr = PIN_NONE; 566 | break; 567 | case PIN_CONFIG_EXTERNAL: 568 | pinstr = PIN_EXTERNAL; 569 | break; 570 | default: 571 | abort(); 572 | } 573 | 574 | /* Print line in users file */ 575 | apr_file_printf(file, "%-7s %-13s %-7s ", tbuf, user->username, pinstr); 576 | for (i = 0; i < user->keylen; i++) 577 | apr_file_printf(file, "%02x", user->key[i]); 578 | apr_file_printf(file, " %-3ld %-2u", user->offset, user->num_otp_failures); 579 | if (*user->last_otp != '\0') { 580 | #if HAVE_STRPTIME 581 | strftime(tbuf, sizeof(tbuf), TIME_FORMAT, localtime(&user->last_auth)); 582 | #else 583 | apr_snprintf(tbuf, sizeof(tbuf), "%lu", (u_long)user->last_auth); 584 | #endif 585 | apr_file_printf(file, " %-7s %s %s", user->last_otp, tbuf, user->last_ip); 586 | } 587 | return apr_file_putc('\n', file); 588 | } 589 | 590 | /* 591 | * Insert fake Basic Authentication Header for PIN 592 | */ 593 | static void 594 | pin_fake_basic_auth(request_rec *r, const char *user, const char *pin){ 595 | char *basic = apr_pstrcat(r->pool, user, ":", pin, NULL); 596 | apr_size_t size = (apr_size_t) strlen(basic); 597 | char *base64 = apr_palloc(r->pool, 598 | apr_base64_encode_len(size + 1) * sizeof(char)); 599 | apr_base64_encode(base64, basic, size); 600 | apr_table_setn(r->headers_in, "Authorization", 601 | apr_pstrcat(r->pool, "Basic ", base64, NULL)); 602 | } 603 | 604 | /* 605 | * Verify PIN using an external authn provider configured via "OTPAuthPINAuthProvider". 606 | */ 607 | static authn_status 608 | authn_otp_check_pin_external(request_rec *r, struct otp_config *const conf, const char *username, const char *pin) 609 | { 610 | authn_provider_list *pentry; 611 | authn_status status; 612 | 613 | /* Verify that at least one authn provider is configured */ 614 | if ((pentry = conf->provlist) == NULL) { 615 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, 616 | "user \"%s\" PIN to be verified externally but no \"OTPAuthPINAuthProvider\" was configured", username); 617 | return AUTH_DENIED; 618 | } 619 | 620 | /* Try each configured authn provider until we find one that recognizes this user */ 621 | do { 622 | apr_table_setn(r->notes, AUTHN_PROVIDER_NAME_NOTE, pentry->provider_name); 623 | status = pentry->provider->check_password(r, username, pin); 624 | apr_table_unset(r->notes, AUTHN_PROVIDER_NAME_NOTE); 625 | if (status != AUTH_USER_NOT_FOUND) 626 | break; 627 | pentry = pentry->next; 628 | } while (pentry != NULL); 629 | 630 | /* Check result */ 631 | switch (status) { 632 | case AUTH_GRANTED: 633 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "user \"%s\" PIN successfully validated by auth provider \"%s\"", 634 | username, pentry->provider_name); 635 | break; 636 | case AUTH_DENIED: 637 | ap_log_rerror(APLOG_MARK, conf->allow_fallthrough ? APLOG_INFO : APLOG_NOTICE, 0, r, 638 | "user \"%s\" gave incorrect PIN according to PIN auth provider \"%s\"", 639 | username, pentry->provider_name); 640 | break; 641 | case AUTH_USER_NOT_FOUND: 642 | ap_log_rerror(APLOG_MARK, conf->allow_fallthrough ? APLOG_INFO : APLOG_NOTICE, 0, r, 643 | "user \"%s\" is not known by any configured PIN auth provider", username); 644 | break; 645 | case AUTH_GENERAL_ERROR: /* assume the auth provider logged something interesting */ 646 | break; 647 | case AUTH_USER_FOUND: 648 | default: 649 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, 650 | "PIN auth provider \"%s\" returned unexpected value %d for user \"%s\"; treating as AUTH_DENIED", 651 | pentry->provider_name, status, username); 652 | status = AUTH_DENIED; 653 | break; 654 | } 655 | 656 | /* Done */ 657 | return status; 658 | } 659 | 660 | /* 661 | * Verify PIN. 662 | */ 663 | static authn_status 664 | authn_otp_check_pin(request_rec *r, struct otp_config *const conf, struct otp_user *const user, const char *pin) 665 | { 666 | switch (user->pincfg) { 667 | case PIN_CONFIG_NONE: /* User has no PIN, so provided PIN must be the empty string */ 668 | if (*pin != '\0') { 669 | ap_log_rerror(APLOG_MARK, conf->allow_fallthrough ? APLOG_INFO : APLOG_NOTICE, 0, r, 670 | "user \"%s\" supplied a PIN but none is configured in the users file", user->username); 671 | return AUTH_DENIED; 672 | } 673 | return AUTH_GRANTED; 674 | case PIN_CONFIG_EXTERNAL: /* User's PIN must be verified externally via an OTPAuthPINAuthProvider */ 675 | return authn_otp_check_pin_external(r, conf, user->username, pin); 676 | case PIN_CONFIG_LITERAL: /* User's PIN was given explicitly in the users file */ 677 | if (strcmp(pin, user->pin) != 0) { 678 | ap_log_rerror(APLOG_MARK, conf->allow_fallthrough ? APLOG_INFO : APLOG_NOTICE, 0, r, 679 | "user \"%s\" PIN does not match value in users file", user->username); 680 | return AUTH_DENIED; 681 | } 682 | return AUTH_GRANTED; 683 | default: /* This should never happen */ 684 | abort(); 685 | return AUTH_DENIED; 686 | } 687 | } 688 | 689 | /* 690 | * HTTP basic authentication 691 | */ 692 | static authn_status 693 | authn_otp_check_password(request_rec *r, const char *username, const char *otp_given) 694 | { 695 | struct otp_config *const conf = get_config(r); 696 | struct otp_user userbuf; 697 | struct otp_user *const user = &userbuf; 698 | authn_status status; 699 | int window_start; 700 | int window_stop; 701 | char otpbuf10[32]; 702 | char otpbuf16[32]; 703 | int counter; 704 | int offset; 705 | time_t now; 706 | 707 | /* Is the users file defined? */ 708 | if (conf->users_file == NULL) { 709 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No OTPAuthUsersFile has been configured"); 710 | return AUTH_GENERAL_ERROR; 711 | } 712 | 713 | /* Lookup user in the users file */ 714 | memset(user, 0, sizeof(*user)); 715 | apr_snprintf(user->username, sizeof(user->username), "%s", username); 716 | if ((status = find_update_user(r, conf->users_file, user, 0)) != AUTH_USER_FOUND) 717 | return status; 718 | 719 | /* Check for max failures */ 720 | if (conf->max_otp_failures != 0 && user->num_otp_failures >= conf->max_otp_failures) { 721 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "user \"%s\" has reached the maximum wrong OTP limit of %u", 722 | user->username, conf->max_otp_failures); 723 | return conf->allow_fallthrough ? AUTH_USER_NOT_FOUND : AUTH_DENIED; 724 | } 725 | 726 | /* Check for a "logout" via empty password */ 727 | if (*otp_given == '\0' && *user->last_otp != '\0' && *user->last_ip != '\0' && strcmp(user->last_ip, USER_AGENT_IP(r)) == 0) { 728 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "logout for user \"%s\" via empty password", user->username); 729 | 730 | /* Forget previous OTP */ 731 | *user->last_otp = '\0'; 732 | find_update_user(r, conf->users_file, user, 1); 733 | return conf->allow_fallthrough ? AUTH_USER_NOT_FOUND : AUTH_DENIED; 734 | } 735 | 736 | /* Check PIN prefix (if appropriate) */ 737 | if (user->algorithm != OTP_ALGORITHM_MOTP) { 738 | char pinbuf[MAX_PIN]; 739 | int pinlen; 740 | 741 | /* Determine the length of the PIN that the user supplied */ 742 | pinlen = strlen(otp_given) - user->num_digits; 743 | if (pinlen < 0) { 744 | ap_log_rerror(APLOG_MARK, conf->allow_fallthrough ? APLOG_INFO : APLOG_NOTICE, 0, r, "user \"%s\" provided a too-short OTP", user->username); 745 | return conf->allow_fallthrough ? AUTH_USER_NOT_FOUND : AUTH_DENIED; 746 | } 747 | 748 | /* Extract the PIN from the password given */ 749 | apr_snprintf(pinbuf, sizeof(pinbuf), "%.*s", pinlen, otp_given); 750 | otp_given += pinlen; 751 | 752 | /* Check the PIN */ 753 | if ((status = authn_otp_check_pin(r, conf, user, pinbuf)) != AUTH_GRANTED) { 754 | if (status == AUTH_DENIED && conf->allow_fallthrough) 755 | status = AUTH_USER_NOT_FOUND; 756 | return status; 757 | } 758 | 759 | /* Insert Basic Authentication header if PINFakeBasicAuth is set */ 760 | if (conf->pin_fake_basic_auth) 761 | pin_fake_basic_auth(r, user->username, pinbuf); 762 | } 763 | 764 | /* Check OTP length */ 765 | if (strlen(otp_given) != user->num_digits) { 766 | ap_log_rerror(APLOG_MARK, conf->allow_fallthrough ? APLOG_INFO : APLOG_NOTICE, 0, r, "user \"%s\" OTP has the wrong length %d != %d", 767 | user->username, (int)strlen(otp_given), user->num_digits); 768 | return conf->allow_fallthrough ? AUTH_USER_NOT_FOUND : AUTH_DENIED; 769 | } 770 | 771 | /* Check for reuse of previous OTP */ 772 | now = time(NULL); 773 | if (strcmp(otp_given, user->last_otp) == 0) { 774 | 775 | /* Did user's IP address change? */ 776 | if (conf->logout_ip_change && *user->last_ip != '\0' && strcmp(user->last_ip, USER_AGENT_IP(r)) != 0) { 777 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "user \"%s\" provided the previous OTP" 778 | " but from a different IP address (was %s, now %s)", user->username, user->last_ip, USER_AGENT_IP(r)); 779 | goto fail; 780 | } 781 | 782 | /* Is it within the configured linger time? */ 783 | if (now >= user->last_auth && now < user->last_auth + conf->max_linger) { 784 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "accepting reuse of OTP for \"%s\" within %d sec. linger time", 785 | user->username, conf->max_linger); 786 | return AUTH_GRANTED; 787 | } 788 | 789 | /* Report failure to the log */ 790 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "user \"%s\" provided the previous OTP" 791 | " but it has expired (max linger is %d sec.)", user->username, conf->max_linger); 792 | goto fail; 793 | } 794 | 795 | /* Get expected counter value and offset window */ 796 | if (user->time_interval == 0) { 797 | counter = user->offset; 798 | window_start = 1; 799 | window_stop = conf->max_offset; 800 | } else { 801 | counter = (int)(now / user->time_interval) + user->offset; 802 | window_start = -conf->max_offset; 803 | window_stop = conf->max_offset; 804 | 805 | /* Expand upper bound of window to ensure an absolute offset of zero is included in the search (issue #14) */ 806 | if (window_stop < -user->offset) 807 | window_stop = -user->offset; 808 | } 809 | 810 | /* Test OTP using expected counter first */ 811 | *otpbuf10 = '\0'; 812 | if (user->algorithm == OTP_ALGORITHM_MOTP) 813 | motp(user->key, user->keylen, user->pin, counter, user->num_digits, otpbuf16, OTP_BUF_SIZE); 814 | else 815 | hotp(user->key, user->keylen, counter, user->num_digits, otpbuf10, otpbuf16, OTP_BUF_SIZE); 816 | if (strcmp(otp_given, otpbuf10) == 0 || strcasecmp(otp_given, otpbuf16) == 0) { 817 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "accepting OTP for \"%s\" at counter %d", user->username, counter); 818 | offset = 0; 819 | goto success; 820 | } 821 | 822 | /* Try other OTP counter values within the maximum allowed offset */ 823 | for (offset = window_start; offset <= window_stop; offset++) { 824 | if (offset == 0) /* already tried it */ 825 | continue; 826 | if (user->algorithm == OTP_ALGORITHM_MOTP) 827 | motp(user->key, user->keylen, user->pin, counter + offset, user->num_digits, otpbuf16, OTP_BUF_SIZE); 828 | else 829 | hotp(user->key, user->keylen, counter + offset, user->num_digits, otpbuf10, otpbuf16, OTP_BUF_SIZE); 830 | if (strcmp(otp_given, otpbuf10) == 0 || strcasecmp(otp_given, otpbuf16) == 0) { 831 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "accepting OTP for \"%s\" at counter %d (offset adjust %d)", 832 | user->username, counter + offset, offset); 833 | goto success; 834 | } 835 | } 836 | 837 | /* Report failure to the log */ 838 | if (conf->max_otp_failures != 0) { 839 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "user \"%s\" provided the wrong OTP (%d/%d consecutive)", 840 | user->username, user->num_otp_failures + 1, conf->max_otp_failures); 841 | } else { 842 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "user \"%s\" provided the wrong OTP (%d consecutive)", 843 | user->username, user->num_otp_failures + 1); 844 | } 845 | goto fail; 846 | 847 | success: 848 | /* Update user's last auth information and next expected offset */ 849 | user->offset = user->time_interval == 0 ? counter + offset + 1 : user->offset + offset; 850 | user->num_otp_failures = 0; 851 | apr_snprintf(user->last_otp, sizeof(user->last_otp), "%s", otp_given); 852 | user->last_auth = now; 853 | apr_snprintf(user->last_ip, sizeof(user->last_ip), "%s", USER_AGENT_IP(r)); 854 | 855 | /* Update user's record */ 856 | find_update_user(r, conf->users_file, user, 1); 857 | 858 | /* Done */ 859 | return AUTH_GRANTED; 860 | 861 | fail: 862 | /* Update user's failure count */ 863 | if (user->num_otp_failures < UINT_MAX) { 864 | user->num_otp_failures++; 865 | find_update_user(r, conf->users_file, user, 1); 866 | } 867 | return AUTH_DENIED; 868 | } 869 | 870 | /* 871 | * HTTP digest authentication 872 | * 873 | * NOTE: OTPAuthMaxOTPFailure does not count digest authentication failures! 874 | */ 875 | static authn_status 876 | authn_otp_get_realm_hash(request_rec *r, const char *username, const char *realm, char **rethash) 877 | { 878 | struct otp_config *const conf = get_config(r); 879 | struct otp_user userbuf; 880 | struct otp_user *const user = &userbuf; 881 | authn_status status; 882 | char hashbuf[256]; 883 | char otpbuf[32]; 884 | int counter = 0; 885 | int linger; 886 | time_t now; 887 | 888 | /* Is the users file configured? */ 889 | if (conf->users_file == NULL) { 890 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No OTPAuthUsersFile has been configured"); 891 | return AUTH_GENERAL_ERROR; 892 | } 893 | 894 | /* Lookup the user in the users file */ 895 | memset(user, 0, sizeof(*user)); 896 | apr_snprintf(user->username, sizeof(user->username), "%s", username); 897 | if ((status = find_update_user(r, conf->users_file, user, 0)) != AUTH_USER_FOUND) 898 | return status; 899 | 900 | /* Check for max failures */ 901 | if (conf->max_otp_failures != 0 && user->num_otp_failures >= conf->max_otp_failures) { 902 | ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "user \"%s\" has reached the maximum wrong OTP limit of %u", 903 | user->username, conf->max_otp_failures); 904 | return conf->allow_fallthrough ? AUTH_USER_NOT_FOUND : AUTH_DENIED; 905 | } 906 | 907 | /* The user's PIN must be known to us */ 908 | switch (user->pincfg) { 909 | case PIN_CONFIG_NONE: 910 | case PIN_CONFIG_LITERAL: 911 | break; 912 | default: 913 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 914 | "user \"%s\" has an externally verified PIN which is not compatible with digest authentication", user->username); 915 | return AUTH_USER_NOT_FOUND; 916 | } 917 | 918 | /* Determine the expected OTP, assuming OTP reuse if we are within the linger time and IP has not changed */ 919 | now = time(NULL); 920 | if (now >= user->last_auth 921 | && now < user->last_auth + conf->max_linger 922 | && (!conf->logout_ip_change || *user->last_ip == '\0' || strcmp(user->last_ip, USER_AGENT_IP(r)) == 0)) { 923 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, 924 | "generating digest hash for \"%s\" assuming reuse of OTP within %d sec. linger time", 925 | user->username, conf->max_linger); 926 | apr_snprintf(otpbuf, sizeof(otpbuf), "%s", user->last_otp); 927 | linger = 1; 928 | } else { 929 | 930 | /* Log note if previous OTP has expired */ 931 | if (user->last_auth != 0 && *user->last_otp != '\0') { 932 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "not using previous expired OTP for user \"%s\" (max linger is %d sec.)", 933 | user->username, conf->max_linger); 934 | } 935 | 936 | /* Get expected counter value */ 937 | counter = user->time_interval == 0 ? user->offset : (int)(now / user->time_interval) + user->offset; 938 | 939 | /* Generate OTP using expected counter */ 940 | ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "generating digest hash for \"%s\" assuming OTP counter %d", 941 | user->username, counter); 942 | if (user->algorithm == OTP_ALGORITHM_MOTP) 943 | motp(user->key, user->keylen, user->pin, counter, user->num_digits, otpbuf, OTP_BUF_SIZE); 944 | else 945 | hotp(user->key, user->keylen, counter, user->num_digits, otpbuf, NULL, OTP_BUF_SIZE); /* assume decimal! */ 946 | linger = 0; 947 | } 948 | 949 | /* Generate digest hash */ 950 | apr_snprintf(hashbuf, sizeof(hashbuf), "%s:%s:%s%s", user->username, realm, 951 | user->algorithm == OTP_ALGORITHM_MOTP ? "" : user->pin, otpbuf); 952 | *rethash = ap_md5(r->pool, (void *)hashbuf); 953 | 954 | #if 0 955 | ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "OTP=\"%s\" counter=%d user=\"%s\" realm=\"%s\" pin=\"%s\" digest=\"%s\"", 956 | otpbuf10, counter, user->username, realm, user->pin, *rethash); 957 | #endif 958 | 959 | /* If we are past the previous linger time, assume counter advance and update user's info */ 960 | if (!linger) { 961 | if (user->time_interval == 0) 962 | user->offset = counter + 1; 963 | apr_snprintf(user->last_otp, sizeof(user->last_otp), "%s", otpbuf); 964 | user->last_auth = now; 965 | find_update_user(r, conf->users_file, user, 1); 966 | } 967 | 968 | /* Done */ 969 | return AUTH_USER_FOUND; 970 | } 971 | 972 | /* 973 | * Get configuration 974 | */ 975 | static struct otp_config * 976 | get_config(request_rec *r) 977 | { 978 | struct otp_config *dir_conf; 979 | struct otp_config *conf; 980 | 981 | /* I don't understand this bug: sometimes r->per_dir_config == NULL. Some weird linking problem. */ 982 | if (r->per_dir_config == NULL) { 983 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Oops, bug detected in mod_authn_otp: r->per_dir_config == NULL?"); 984 | dir_conf = create_authn_otp_dir_config(r->pool, NULL); 985 | } else 986 | dir_conf = ap_get_module_config(r->per_dir_config, &authn_otp_module); 987 | 988 | /* Make a copy of the current per-directory config */ 989 | conf = apr_pcalloc(r->pool, sizeof(*conf)); 990 | if (dir_conf->users_file != NULL) 991 | conf->users_file = apr_pstrdup(r->pool, dir_conf->users_file); 992 | conf->max_offset = dir_conf->max_offset; 993 | conf->max_linger = dir_conf->max_linger; 994 | conf->max_otp_failures = dir_conf->max_otp_failures; 995 | conf->logout_ip_change = dir_conf->logout_ip_change; 996 | conf->allow_fallthrough = dir_conf->allow_fallthrough; 997 | copy_provider_list(r->pool, &conf->provlist, dir_conf->provlist); 998 | conf->pin_fake_basic_auth = dir_conf->pin_fake_basic_auth; 999 | 1000 | /* Apply defaults for any unset values */ 1001 | if (conf->max_offset == -1) 1002 | conf->max_offset = DEFAULT_MAX_OFFSET; 1003 | if (conf->max_linger == -1) 1004 | conf->max_linger = DEFAULT_MAX_LINGER; 1005 | if (conf->logout_ip_change == -1) 1006 | conf->logout_ip_change = DEFAULT_LOGOUT_IP_CHANGE; 1007 | if (conf->allow_fallthrough == -1) 1008 | conf->allow_fallthrough = DEFAULT_ALLOW_FALLTHROUGH; 1009 | if (conf->pin_fake_basic_auth == -1) 1010 | conf->pin_fake_basic_auth = DEFAULT_PIN_FAKE_BASIC_AUTH; 1011 | 1012 | /* Done */ 1013 | return conf; 1014 | } 1015 | 1016 | /* 1017 | * Constructor for per-directory configuration 1018 | */ 1019 | static void * 1020 | create_authn_otp_dir_config(apr_pool_t *p, char *d) 1021 | { 1022 | struct otp_config *conf = apr_pcalloc(p, sizeof(struct otp_config)); 1023 | 1024 | conf->users_file = NULL; 1025 | conf->max_offset = -1; 1026 | conf->max_linger = -1; 1027 | conf->max_otp_failures = 0; 1028 | conf->logout_ip_change = -1; 1029 | conf->allow_fallthrough = -1; 1030 | conf->provlist = NULL; 1031 | conf->pin_fake_basic_auth = -1; 1032 | return conf; 1033 | } 1034 | 1035 | static void * 1036 | merge_authn_otp_dir_config(apr_pool_t *p, void *base_conf, void *new_conf) 1037 | { 1038 | struct otp_config *const conf1 = base_conf; 1039 | struct otp_config *const conf2 = new_conf; 1040 | struct otp_config *conf = apr_pcalloc(p, sizeof(struct otp_config)); 1041 | 1042 | if (conf2->users_file != NULL) 1043 | conf->users_file = apr_pstrdup(p, conf2->users_file); 1044 | else if (conf1->users_file != NULL) 1045 | conf->users_file = apr_pstrdup(p, conf1->users_file); 1046 | conf->max_offset = conf2->max_offset != -1 ? conf2->max_offset : conf1->max_offset; 1047 | conf->max_linger = conf2->max_linger != -1 ? conf2->max_linger : conf1->max_linger; 1048 | conf->max_otp_failures = conf2->max_otp_failures != 0 ? conf2->max_otp_failures : conf1->max_otp_failures; 1049 | conf->logout_ip_change = conf2->logout_ip_change != -1 ? conf2->logout_ip_change : conf1->logout_ip_change; 1050 | conf->allow_fallthrough = conf2->allow_fallthrough != -1 ? conf2->allow_fallthrough : conf1->allow_fallthrough; 1051 | copy_provider_list(p, &conf->provlist, conf2->provlist != NULL ? conf2->provlist : conf1->provlist); 1052 | conf->pin_fake_basic_auth = conf2->pin_fake_basic_auth != -1 ? conf2->pin_fake_basic_auth : conf1->pin_fake_basic_auth; 1053 | return conf; 1054 | } 1055 | 1056 | /* 1057 | * This code is more-or-less copied from mod_auth_basic.c 1058 | */ 1059 | static const char * 1060 | add_authn_provider(cmd_parms *cmd, void *config, const char *provider_name) 1061 | { 1062 | struct otp_config *const conf = (struct otp_config *)config; 1063 | authn_provider_list *pentry; 1064 | authn_provider_list *last; 1065 | 1066 | /* Sanity check */ 1067 | if (strcmp(provider_name, OTP_AUTHN_PROVIDER_NAME) == 0) 1068 | return apr_psprintf(cmd->pool, "Invalid recursive Authn provider: %s", provider_name); 1069 | 1070 | /* Create new provider list entry */ 1071 | pentry = apr_pcalloc(cmd->pool, sizeof(*pentry)); 1072 | pentry->provider_name = apr_pstrdup(cmd->pool, provider_name); 1073 | 1074 | /* Lookup and cache the actual provider now */ 1075 | pentry->provider = ap_lookup_provider(AUTHN_PROVIDER_GROUP, pentry->provider_name, AUTHN_PROVIDER_VERSION); 1076 | if (pentry->provider == NULL) 1077 | return apr_psprintf(cmd->pool, "Unknown Authn provider: %s", pentry->provider_name); 1078 | 1079 | /* Verify this authentication provider can check plain passwords */ 1080 | if (pentry->provider->check_password == NULL) { 1081 | return apr_psprintf(cmd->pool, 1082 | "The '%s' Authn provider doesn't support plaintext password checks", pentry->provider_name); 1083 | } 1084 | 1085 | /* Add it to the end of the list */ 1086 | if (conf->provlist == NULL) 1087 | conf->provlist = pentry; 1088 | else { 1089 | for (last = conf->provlist; last->next != NULL; last = last->next) 1090 | ; 1091 | last->next = pentry; 1092 | } 1093 | 1094 | /* Done */ 1095 | return NULL; 1096 | } 1097 | 1098 | static void 1099 | copy_provider_list(apr_pool_t *p, authn_provider_list **dstp, authn_provider_list *src) 1100 | { 1101 | while (src != NULL) { 1102 | *dstp = apr_pcalloc(p, sizeof(**dstp)); 1103 | (*dstp)->provider_name = apr_pstrdup(p, src->provider_name); 1104 | (*dstp)->provider = src->provider; 1105 | dstp = &(*dstp)->next; 1106 | src = src->next; 1107 | } 1108 | } 1109 | 1110 | /* Authorization provider information */ 1111 | static const authn_provider authn_otp_provider = 1112 | { 1113 | &authn_otp_check_password, 1114 | &authn_otp_get_realm_hash 1115 | }; 1116 | 1117 | static void 1118 | register_hooks(apr_pool_t *p) 1119 | { 1120 | ap_register_provider(p, AUTHN_PROVIDER_GROUP, OTP_AUTHN_PROVIDER_NAME, AUTHN_PROVIDER_VERSION, &authn_otp_provider); 1121 | apr_status_t status; 1122 | char errbuf[64]; 1123 | 1124 | /* Initialize mutex */ 1125 | if (mutex == NULL && (status = apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, p)) != 0) { 1126 | ap_log_perror(APLOG_MARK, APLOG_ERR, 0, p, "can't create OTP mutex: %s", apr_strerror(status, errbuf, sizeof(errbuf))); 1127 | return; 1128 | } 1129 | } 1130 | 1131 | /* Configuration directives */ 1132 | static const command_rec authn_otp_cmds[] = 1133 | { 1134 | AP_INIT_TAKE1("OTPAuthUsersFile", 1135 | ap_set_file_slot, 1136 | (void *)APR_OFFSETOF(struct otp_config, users_file), 1137 | OR_AUTHCFG, 1138 | "pathname of the one-time password users file"), 1139 | AP_INIT_TAKE1("OTPAuthMaxOffset", 1140 | ap_set_int_slot, 1141 | (void *)APR_OFFSETOF(struct otp_config, max_offset), 1142 | OR_AUTHCFG, 1143 | "maximum allowed offset from expected event or time counter value"), 1144 | AP_INIT_TAKE1("OTPAuthMaxLinger", 1145 | ap_set_int_slot, 1146 | (void *)APR_OFFSETOF(struct otp_config, max_linger), 1147 | OR_AUTHCFG, 1148 | "maximum time (in seconds) for which a one-time password can be repeatedly used"), 1149 | AP_INIT_TAKE1("OTPAuthMaxOTPFailure", 1150 | ap_set_int_slot, 1151 | (void *)APR_OFFSETOF(struct otp_config, max_otp_failures), 1152 | OR_AUTHCFG, 1153 | "maximum number of consecutive wrong OTP values before account becomes locked"), 1154 | AP_INIT_FLAG("OTPAuthLogoutOnIPChange", 1155 | ap_set_flag_slot, 1156 | (void *)APR_OFFSETOF(struct otp_config, logout_ip_change), 1157 | OR_AUTHCFG, 1158 | "enable automatic logout of user if the user's IP address changes"), 1159 | AP_INIT_ITERATE("OTPAuthPINAuthProvider", 1160 | add_authn_provider, 1161 | NULL, 1162 | OR_AUTHCFG, 1163 | "specify auth provider(s) to be used for PIN verification for a directory or location"), 1164 | AP_INIT_FLAG("OTPAuthFallThrough", 1165 | ap_set_flag_slot, 1166 | (void *)APR_OFFSETOF(struct otp_config, allow_fallthrough), 1167 | OR_AUTHCFG, 1168 | "allow failed auth attempts to fall through to the next auth provider (if any)"), 1169 | AP_INIT_FLAG("PINFakeBasicAuth", 1170 | ap_set_flag_slot, 1171 | (void *)APR_OFFSETOF(struct otp_config, pin_fake_basic_auth), 1172 | OR_AUTHCFG, 1173 | "pass through authentication to the rest of the server as a basic authentication header"), 1174 | { NULL } 1175 | }; 1176 | 1177 | /* Module declaration */ 1178 | module AP_MODULE_DECLARE_DATA authn_otp_module = { 1179 | STANDARD20_MODULE_STUFF, 1180 | create_authn_otp_dir_config, /* create per-dir config */ 1181 | merge_authn_otp_dir_config, /* merge per-dir config */ 1182 | NULL, /* create per-server config */ 1183 | NULL, /* merge per-server config */ 1184 | authn_otp_cmds, /* command apr_table_t */ 1185 | register_hooks /* register hooks */ 1186 | }; 1187 | 1188 | -------------------------------------------------------------------------------- /motp.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * otptool - HOTP/OATH one-time password utility 4 | * 5 | * Copyright 2009 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include "otpdefs.h" 21 | 22 | /* Definitions */ 23 | #define MOTP_NUM_BYTES 3 24 | 25 | /* 26 | * Generate an OTP using the mOTP algorithm defined by http://motp.sourceforge.net/ 27 | */ 28 | void 29 | motp(const u_char *key, size_t keylen, const char *pin, u_long counter, int ndigits, char *buf, size_t buflen) 30 | { 31 | u_char hash[MD5_DIGEST_LENGTH]; 32 | char keybuf[keylen * 2 + 1]; 33 | char hashbuf[64 + (keylen * 2) + strlen(pin)]; 34 | 35 | printhex(keybuf, sizeof(keybuf), key, keylen, keylen * 2); 36 | snprintf(hashbuf, sizeof(hashbuf), "%lu%s%s", counter, keybuf, pin); 37 | md5_quick(hashbuf, strlen(hashbuf), hash); 38 | printhex(buf, buflen, hash, sizeof(hash), ndigits); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /otpdefs.h: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * otptool - HOTP/OATH one-time password utility 4 | * 5 | * Copyright 2009 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include 33 | #include 34 | #include 35 | 36 | #include "errinc.h" 37 | 38 | /* Error exit values */ 39 | #define EXIT_USAGE_ERROR 1 /* Incorrect command line usage */ 40 | #define EXIT_NOT_MATCHED 2 /* OTP failed to match */ 41 | #define EXIT_SYSTEM_ERROR 3 /* Could not open file, etc. */ 42 | 43 | /* Default settings */ 44 | #define DEFAULT_NUM_DIGITS 6 45 | #define DEFAULT_TIME_INTERVAL 30 46 | #define DEFAULT_WINDOW 0 47 | 48 | /* hotp.c */ 49 | extern void hotp(const u_char *key, size_t keylen, uint64_t counter, int ndigits, char *buf10, char *buf16, size_t buflen); 50 | 51 | /* motp.c */ 52 | extern void motp(const u_char *key, size_t keylen, const char *pin, u_long counter, int ndigits, char *buf, size_t buflen); 53 | 54 | /* phex.c */ 55 | extern void printhex(char *buf, size_t buflen, const u_char *data, size_t dlen, int max_digits); 56 | 57 | /* md5q.c */ 58 | extern void md5_quick(const void *data, size_t len, u_char *result); 59 | 60 | /* base32.c */ 61 | extern void base32_encode(const unsigned char *plain, size_t len, unsigned char *coded); 62 | extern size_t base32_decode(const unsigned char *coded, unsigned char *plain); 63 | -------------------------------------------------------------------------------- /otplock.1: -------------------------------------------------------------------------------- 1 | .\" -*- nroff -*- 2 | .\" 3 | .\" otplock - Apache mod_authn_otp one-time users file locker 4 | .\" 5 | .\" Copyright 2023 Archie L. Cobbs 6 | .\" 7 | .\" Licensed under the Apache License, Version 2.0 (the "License"); 8 | .\" you may not use this file except in compliance with the License. 9 | .\" You may obtain a copy of the License at 10 | .\" 11 | .\" http://www.apache.org/licenses/LICENSE-2.0 12 | .\" 13 | .\" Unless required by applicable law or agreed to in writing, software 14 | .\" distributed under the License is distributed on an "AS IS" BASIS, 15 | .\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | .\" See the License for the specific language governing permissions and 17 | .\" limitations under the License. 18 | .\"/ 19 | .Dd January 7, 2023 20 | .Dt OTPLOCK 1 21 | .Os 22 | .Sh NAME 23 | .Nm otplock 24 | .Nd Apache mod_authn_otp one-time users file locker 25 | .Sh SYNOPSIS 26 | .Nm otplock 27 | .Bk -words 28 | .Ar usersfile 29 | .Op command ... 30 | .Ek 31 | .Pp 32 | .Nm otplock 33 | .Fl e 34 | .Ar usersfile 35 | .Pp 36 | .Nm otplock 37 | .Fl h 38 | .Sh DESCRIPTION 39 | .Nm 40 | is a utility for safely accessing the 41 | .Ar mod_authn_otp 42 | users file while the Apache server is running. 43 | .Pp 44 | Because the users file is dynamically read and updated during normal server operation, 45 | it's not safe to simultaneously view or edit the file in a separate process without locking it first. 46 | .Pp 47 | This utility implements the same locking protocol as the 48 | .Ar mod_authn_otp 49 | module. 50 | It holds the exclusive lock while the given 51 | .Ar command 52 | executes. 53 | This means that long as 54 | .Ar command 55 | executes, all server requests that require 56 | .Ar mod_authn_otp 57 | for authentication will be temporarily suspended, so command execution should be as brief as possible. 58 | .Pp 59 | If no 60 | .Ar command 61 | is given, 62 | .Nm 63 | simply waits until a lock can be obtained and then exits. 64 | .Sh OPTIONS 65 | .Bl -tag -width Ds 66 | .It Fl e 67 | Invoke 68 | .Ar $EDITOR 69 | with the given 70 | .Ar usersfile . 71 | .Pp 72 | If no 73 | .Ar $EDITOR 74 | environment variable is defined, 75 | .Xr vim 1 76 | is used. 77 | .It Fl h 78 | Print the usage message and exit successfully. 79 | .El 80 | .Sh RETURN VALUE 81 | .Nm 82 | exits with one of the following return values: 83 | .Bl -tag -width xxx 84 | .It 0 85 | The users file was successfully locked, and either no 86 | .Ar command 87 | was given, or the command exited normally. 88 | .It N 89 | The given 90 | .Ar command 91 | executed but exited with non-zero exit value 92 | .Ar N . 93 | .It 85 94 | .Nm 95 | was invoked with invalid command line flags or parameters. 96 | .It 86 97 | A system error occurred while either locking the file or launching 98 | .Ar command . 99 | .It 87 100 | The given 101 | .Ar command 102 | executed but terminated by catching a signal. 103 | .El 104 | .Sh SEE ALSO 105 | .Rs 106 | .%T "mod_authn_otp: Apache module for one-time password authentication" 107 | .%O "https://github.com/archiecobbs/mod-authn-otp" 108 | .Re 109 | .Sh AUTHOR 110 | .An Archie L. Cobbs Aq archie.cobbs@gmail.com 111 | -------------------------------------------------------------------------------- /otplock.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * otplock - Apache mod_authn_otp one-time users file locker 4 | * 5 | * Copyright 2023 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include "otpdefs.h" 21 | #include "config.h" 22 | 23 | #if HAVE_APR_1_APR_FILE_IO_H 24 | #include 25 | #include 26 | #include 27 | #include 28 | #elif HAVE_APR_1_0_APR_FILE_IO_H 29 | #include 30 | #include 31 | #include 32 | #include 33 | #else 34 | #error "libapr header files not found" 35 | #endif 36 | 37 | /* Program name */ 38 | #define PROG_NAME "otplock" 39 | 40 | #define LOCKFILE_SUFFIX ".lock" 41 | #define DEFAULT_EDITOR "vim" 42 | 43 | /* Error exit values */ 44 | #undef EXIT_USAGE_ERROR 45 | #undef EXIT_SYSTEM_ERROR 46 | 47 | #define EXIT_USAGE_ERROR 85 /* Incorrect command line usage */ 48 | #define EXIT_SYSTEM_ERROR 86 /* Could not open file, etc. */ 49 | #define EXIT_CAUGHT_SIGNAL 87 50 | 51 | extern const char *const *environ; 52 | 53 | static void usage(void); 54 | 55 | int 56 | main(int argc, const char *const *argv) 57 | { 58 | char lockfile[APR_PATH_MAX]; 59 | const char *usersfile; 60 | const char *editcmd[3]; 61 | char errbuf[256]; 62 | apr_pool_t *pool; 63 | apr_file_t *handle = NULL; 64 | apr_status_t status; 65 | int edit = 0; 66 | int ch; 67 | int r; 68 | 69 | // Initialize APR 70 | if ((status = apr_app_initialize(&argc, &argv, &environ)) != APR_SUCCESS) { 71 | warnx("%s: %s", "apr_app_initialize", apr_strerror(status, errbuf, sizeof(errbuf))); 72 | r = EXIT_SYSTEM_ERROR; 73 | goto out0; 74 | } 75 | if ((status = apr_pool_create(&pool, NULL)) != APR_SUCCESS) { 76 | warnx("%s: %s", "apr_pool_create", apr_strerror(status, errbuf, sizeof(errbuf))); 77 | r = EXIT_SYSTEM_ERROR; 78 | goto out1; 79 | } 80 | 81 | // Parse command line 82 | while ((ch = getopt(argc, (char **)(intptr_t)argv, "eh")) != -1) { 83 | switch (ch) { 84 | case 'e': 85 | edit = 1; 86 | break; 87 | case 'h': 88 | usage(); 89 | r = 0; 90 | goto out2; 91 | default: 92 | usage(); 93 | r = EXIT_USAGE_ERROR; 94 | goto out2; 95 | } 96 | } 97 | argc -= optind; 98 | argv += optind; 99 | switch (argc) { 100 | case 0: 101 | usage(); 102 | r = EXIT_USAGE_ERROR; 103 | goto out2; 104 | default: 105 | argc--; 106 | usersfile = *argv++; 107 | break; 108 | } 109 | 110 | // Handle "-e" flag 111 | if (edit) { 112 | const char *const *ev; 113 | 114 | if (argc > 0) { 115 | r = EXIT_USAGE_ERROR; 116 | goto out2; 117 | } 118 | editcmd[0] = DEFAULT_EDITOR; 119 | for (ev = environ; *ev != NULL; ev++) { 120 | if (strncmp(*ev, "EDITOR=", 7) == 0) { 121 | editcmd[0] = *ev + 7; 122 | break; 123 | } 124 | } 125 | editcmd[1] = usersfile; 126 | editcmd[2] = NULL; 127 | argv = editcmd; 128 | argc = 2; 129 | } 130 | 131 | // Open the lock file 132 | apr_snprintf(lockfile, sizeof(lockfile), "%s%s", usersfile, LOCKFILE_SUFFIX); 133 | if ((status = apr_file_open(&handle, lockfile, APR_WRITE|APR_CREATE|APR_TRUNCATE, APR_UREAD|APR_UWRITE, pool)) != APR_SUCCESS) { 134 | warnx("can't open \"%s\": %s", lockfile, apr_strerror(status, errbuf, sizeof(errbuf))); 135 | r = EXIT_SYSTEM_ERROR; 136 | goto out2; 137 | } 138 | 139 | // Lock the lock file 140 | if ((status = apr_file_lock(handle, APR_FLOCK_EXCLUSIVE)) != APR_SUCCESS) { 141 | warnx("can't lock \"%s\": %s", lockfile, apr_strerror(status, errbuf, sizeof(errbuf))); 142 | r = EXIT_SYSTEM_ERROR; 143 | goto out3; 144 | } 145 | 146 | // Execute command, if any 147 | if (*argv != NULL) { 148 | apr_procattr_t *pattr; 149 | apr_exit_why_e why; 150 | apr_proc_t proc; 151 | int rval; 152 | 153 | if ((status = apr_procattr_create(&pattr, pool)) != APR_SUCCESS) { 154 | warnx("%s: %s", "apr_procattr_create", apr_strerror(status, errbuf, sizeof(errbuf))); 155 | r = EXIT_SYSTEM_ERROR; 156 | goto out4; 157 | } 158 | if ((status = apr_procattr_cmdtype_set(pattr, APR_SHELLCMD_ENV)) != APR_SUCCESS) { 159 | warnx("%s: %s", "apr_procattr_cmdtype_set", apr_strerror(status, errbuf, sizeof(errbuf))); 160 | r = EXIT_SYSTEM_ERROR; 161 | goto out4; 162 | } 163 | if ((status = apr_proc_create(&proc, *argv, argv, environ, pattr, pool)) != APR_SUCCESS) { 164 | warnx("%s: %s", "apr_proc_create", apr_strerror(status, errbuf, sizeof(errbuf))); 165 | r = EXIT_SYSTEM_ERROR; 166 | goto out4; 167 | } 168 | if ((status = apr_proc_wait(&proc, &rval, &why, APR_WAIT)) != APR_CHILD_DONE) { 169 | warnx("%s: %s", "apr_proc_wait", apr_strerror(status, errbuf, sizeof(errbuf))); 170 | r = EXIT_SYSTEM_ERROR; 171 | goto out4; 172 | } 173 | switch (why) { 174 | case APR_PROC_EXIT: 175 | if ((r = rval) == APR_ENOTIMPL) 176 | r = 0; 177 | break; 178 | case APR_PROC_SIGNAL: 179 | case APR_PROC_SIGNAL_CORE: 180 | r = EXIT_CAUGHT_SIGNAL; 181 | break; 182 | default: 183 | warnx("%s: %s", "apr_proc_wait", "unknown 'why' code"); 184 | r = EXIT_SYSTEM_ERROR; 185 | break; 186 | } 187 | } else 188 | r = 0; 189 | 190 | // Clean up and exit 191 | out4: 192 | apr_file_unlock(handle); 193 | out3: 194 | apr_file_close(handle); 195 | out2: 196 | apr_pool_destroy(pool); 197 | out1: 198 | apr_terminate(); 199 | out0: 200 | return r; 201 | } 202 | 203 | static void 204 | usage() 205 | { 206 | fprintf(stderr, "Usage:\n"); 207 | fprintf(stderr, " %s usersfile [ command ... ]\n", PROG_NAME); 208 | fprintf(stderr, " %s -e usersfile\n", PROG_NAME); 209 | fprintf(stderr, " %s -h\n", PROG_NAME); 210 | fprintf(stderr, "Options:\n"); 211 | fprintf(stderr, " -e\tInvoke $EDITOR with usersfile\n"); 212 | fprintf(stderr, " -h\tDisplay this usage message\n"); 213 | } 214 | -------------------------------------------------------------------------------- /otptool.1: -------------------------------------------------------------------------------- 1 | .\" -*- nroff -*- 2 | .\" 3 | .\" otptool - HOTP/OATH one-time password utility 4 | .\" 5 | .\" Copyright 2009 Archie L. Cobbs 6 | .\" 7 | .\" Licensed under the Apache License, Version 2.0 (the "License"); 8 | .\" you may not use this file except in compliance with the License. 9 | .\" You may obtain a copy of the License at 10 | .\" 11 | .\" http://www.apache.org/licenses/LICENSE-2.0 12 | .\" 13 | .\" Unless required by applicable law or agreed to in writing, software 14 | .\" distributed under the License is distributed on an "AS IS" BASIS, 15 | .\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | .\" See the License for the specific language governing permissions and 17 | .\" limitations under the License. 18 | .\"/ 19 | .Dd June 21, 2008 20 | .Dt OTPTOOL 1 21 | .Os 22 | .Sh NAME 23 | .Nm otptool 24 | .Nd HOTP/OATH one-time password utility 25 | .Sh SYNOPSIS 26 | .Nm otptool 27 | .Bk -words 28 | .Op Fl Ffht 29 | .Op Fl c Ar counter 30 | .Op Fl d Ar #digits 31 | .Op Fl i Ar secs 32 | .Op Fl m Ar PIN 33 | .Op Fl w Ar num 34 | .Ar key 35 | .Op Ar password 36 | .Ek 37 | .Sh DESCRIPTION 38 | .Nm 39 | is a utility for generating, verifying, and synchronizing one-time passwords 40 | created using the HOTP/OATH algorithm defined by RFC 4226. 41 | .Pp 42 | The 43 | .Ar key 44 | is the token's binary secret key and may be specified on the command line as a hexadecimal string, 45 | or read from a file using the 46 | .Fl F 47 | or 48 | .Fl f 49 | flag. 50 | .Pp 51 | If no 52 | .Ar password 53 | is given, 54 | .Nm 55 | generates the one-time password corresponding to the given key and target counter value 56 | and prints to standard output the counter followed by the decimal and hexadecimal one-time passwords. 57 | If 58 | .Ar password 59 | is given, then 60 | .Nm 61 | verifies that 62 | .Ar password 63 | is the correct one-time password for the given 64 | .Ar key 65 | and counter value. 66 | If so, it outputs the counter value. 67 | .Ar password 68 | may be either the decimal or hexadecimal one-time password. 69 | .Pp 70 | The target counter value is determined as follows: if the 71 | .Fl t 72 | flag is given, use the current time in seconds since the UNIX epoch divided by the configured time interval (default 30 seconds); 73 | otherwise, if the 74 | .Fl c 75 | flag is given, use the given 76 | .Ar counter ; 77 | otherwise, use the value zero. 78 | .Pp 79 | In both cases, a range of target counter values may be specified using the 80 | .Fl w 81 | flag. 82 | When both 83 | .Fl w 84 | and 85 | .Ar password 86 | are given, 87 | .Nm 88 | will search the entire range for a matching counter value, 89 | starting with the target counter value and working away from it. 90 | This mode can be used to resynchronize an unsychronized counter. 91 | .Sh OPTIONS 92 | .Bl -tag -width Ds 93 | .It Fl c 94 | Specify the starting target counter value for the one-time password generation or search. 95 | This flag is incompatible with the 96 | .Fl t 97 | flag; 98 | if neither flag is given, the default value is zero. 99 | .It Fl d 100 | Specify the required number of digits in the one-time password. 101 | Giving a 102 | .Ar password 103 | argument and specifying a different length here will result in no match being found (no search is performed). 104 | Otherwise, the default value is the length of 105 | .Ar password , 106 | if given, or else six if not. 107 | .It Fl F 108 | Read the key as a hexadecimal string from the file named 109 | .Ar key . 110 | .It Fl f 111 | Read the key in raw, binary format from the file named 112 | .Ar key . 113 | .It Fl h 114 | Print the usage message and exit successfully. 115 | .It Fl i 116 | Specify the length of a single time interval in seconds. 117 | The default value is 30 seconds. 118 | This flag is ignored unless the 119 | .Fl t 120 | flag is also given. 121 | .It Fl m 122 | Use the Mobile-OTP algorithm with the given PIN instead of the HOTP/OATH algorithm. 123 | This flag imples 124 | .Fl i Ar 10 125 | and 126 | .Fl d Ar 6 . 127 | Normally you also want to specify 128 | .Fl t . 129 | .It Fl t 130 | Use the current time as the basis for the target counter value. 131 | This flag is incompatible with the 132 | .Fl c 133 | flag. 134 | .It Fl w 135 | Specify the width of a window of counter values within which to iterate when 136 | generating or searching for one-time passwords. 137 | When 138 | .Fl t 139 | is used, the window extends the given distance both before and after the target counter value; 140 | otherwise, the window extends forward of the target counter value. 141 | When both 142 | .Ar password 143 | and 144 | .Fl t 145 | are given, the search starts with the initial target counter and works away from it 146 | in both directions. 147 | .El 148 | .Sh RETURN VALUE 149 | .Nm 150 | exits with one of the following return values: 151 | .Bl -tag -width xxx 152 | .It 0 153 | The one-time password(s) was/were successfully generated, or 154 | .Ar password 155 | correctly matched the password generated using (one of) the target counter value(s). 156 | .It 1 157 | .Nm 158 | was invoked with invalid command line flags or parameters. 159 | .It 2 160 | The given 161 | .Ar password 162 | did not match any counter value(s) in the search window. 163 | .It 3 164 | A system error occurred. 165 | .El 166 | .Sh SEE ALSO 167 | .Rs 168 | .%T "HOTP: An HMAC-Based One-Time Password Algorithm" 169 | .%O "http://www.ietf.org/rfc/rfc4226.txt" 170 | .Re 171 | .Rs 172 | .%T "mod_authn_otp: Apache module for one-time password authentication" 173 | .%O "https://github.com/archiecobbs/mod-authn-otp" 174 | .Re 175 | .Rs 176 | .%T "Mobile-OTP: Mobile One Time Passwords" 177 | .%O "http://motp.sourceforge.net/" 178 | .Re 179 | .Sh AUTHOR 180 | .An Archie L. Cobbs Aq archie.cobbs@gmail.com 181 | -------------------------------------------------------------------------------- /otptool.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * otptool - HOTP/OATH one-time password utility 4 | * 5 | * Copyright 2009 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include "otpdefs.h" 21 | 22 | /* Program name */ 23 | #define PROG_NAME "otptool" 24 | 25 | /* Definitions */ 26 | #define OTP_BUF_SIZE 16 27 | 28 | /* Internal functions */ 29 | static void usage(void); 30 | 31 | int 32 | main(int argc, char **argv) 33 | { 34 | const char *otp = NULL; 35 | const char *key = NULL; 36 | const char *motp_pin = NULL; 37 | unsigned char keybuf[128]; 38 | unsigned char filebuf[520]; 39 | char otpbuf10[OTP_BUF_SIZE]; 40 | char otpbuf16[OTP_BUF_SIZE]; 41 | size_t keylen = -1; 42 | int time_interval = DEFAULT_TIME_INTERVAL; 43 | int ndigits = -1; 44 | int counter_start; 45 | int counter_stop; 46 | int read_from_file = 0; 47 | int parse_ascii_key = 1; 48 | int counter = -1; 49 | int use_time = 0; 50 | int window = 0; 51 | int ch; 52 | int i; 53 | 54 | /* Parse command line */ 55 | while ((ch = getopt(argc, argv, "c:d:fFhi:m:tvw:")) != -1) { 56 | switch (ch) { 57 | case 'c': 58 | if (use_time) 59 | errx(EXIT_USAGE_ERROR, "only one of `-c' or `-t' should be specified"); 60 | counter = atoi(optarg); 61 | if (counter < 0) 62 | errx(EXIT_USAGE_ERROR, "invalid counter value `%s'", optarg); 63 | break; 64 | case 'd': 65 | ndigits = atoi(optarg); 66 | if (ndigits < 1) 67 | errx(EXIT_USAGE_ERROR, "invalid digit count `%s'", optarg); 68 | break; 69 | case 'F': 70 | read_from_file = 1; 71 | parse_ascii_key = 1; 72 | break; 73 | case 'f': 74 | read_from_file = 1; 75 | parse_ascii_key = 0; 76 | break; 77 | case 'h': 78 | usage(); 79 | return 0; 80 | case 'i': 81 | time_interval = atoi(optarg); 82 | break; 83 | case 'm': 84 | ndigits = 6; 85 | time_interval = 10; 86 | motp_pin = optarg; 87 | *otpbuf10 = '\0'; 88 | break; 89 | case 't': 90 | if (counter != -1) 91 | errx(EXIT_USAGE_ERROR, "only one of `-c' or `-t' should be specified"); 92 | use_time = 1; 93 | break; 94 | case 'w': 95 | window = atoi(optarg); 96 | if (window < 0) 97 | errx(EXIT_USAGE_ERROR, "invalid counter window `%s'", optarg); 98 | break; 99 | default: 100 | usage(); 101 | return EXIT_USAGE_ERROR; 102 | } 103 | } 104 | 105 | /* Parse command line arguments */ 106 | switch (argc - optind) { 107 | case 2: 108 | otp = argv[optind + 1]; 109 | if (ndigits == -1) 110 | ndigits = strlen(otp); 111 | if (strlen(otp) != ndigits) 112 | errx(EXIT_NOT_MATCHED, "the given OTP `%s' has the wrong length %d != %d", otp, (int)strlen(otp), ndigits); 113 | // FALLTHROUGH 114 | case 1: 115 | key = argv[optind]; 116 | break; 117 | default: 118 | warnx("wrong number of command line arguments"); 119 | usage(); 120 | return EXIT_USAGE_ERROR; 121 | } 122 | 123 | /* Set default #digits */ 124 | if (ndigits == -1) 125 | ndigits = DEFAULT_NUM_DIGITS; 126 | 127 | /* Read from file if needed */ 128 | if (read_from_file) { 129 | FILE *fp; 130 | size_t len; 131 | 132 | /* Read data */ 133 | if ((fp = fopen(key, "rb")) == NULL) 134 | err(EXIT_SYSTEM_ERROR, "error reading `%s'", key); 135 | len = fread(filebuf, 1, sizeof(filebuf) - 1, fp); 136 | if (ferror(fp)) 137 | err(EXIT_SYSTEM_ERROR, "error reading `%s'", key); 138 | if (!feof(fp)) 139 | errx(EXIT_SYSTEM_ERROR, "error reading `%s': %s", key, "key is too long"); 140 | fclose(fp); 141 | 142 | /* Optionally parse file content as ASCII */ 143 | if (parse_ascii_key) { 144 | filebuf[len] = 0; 145 | while (len > 0 && isspace(filebuf[len - 1])) /* trim whitespace */ 146 | filebuf[--len] = 0; 147 | key = (char *)filebuf; 148 | } else { 149 | if (len > sizeof(keybuf)) 150 | errx(EXIT_SYSTEM_ERROR, "error reading `%s': %s", key, "key is too long"); 151 | memcpy(keybuf, filebuf, len); 152 | keylen = len; 153 | } 154 | } 155 | 156 | /* Parse key */ 157 | if (parse_ascii_key) { 158 | for (keylen = 0; keylen < sizeof(keybuf) && key[keylen * 2] != '\0'; keylen++) { 159 | const char *s = &key[keylen * 2]; 160 | int nibs[2]; 161 | 162 | for (i = 0; i < 2; i++) { 163 | if (isdigit(s[i])) 164 | nibs[i] = s[i] - '0'; 165 | else if (isxdigit(s[i])) 166 | nibs[i] = tolower(s[i]) - 'a' + 10; 167 | else 168 | errx(EXIT_USAGE_ERROR, "invalid key `%s'", key); 169 | } 170 | keybuf[keylen] = (nibs[0] << 4) | nibs[1]; 171 | } 172 | } 173 | 174 | /* Determine target counter */ 175 | if (use_time) 176 | counter = time(NULL) / time_interval; 177 | else if (counter < 0) 178 | counter = 0; 179 | 180 | /* Search or generate */ 181 | if (otp == NULL) { 182 | if (use_time) { 183 | counter_start = counter - window; 184 | counter_stop = counter + window; 185 | } else { 186 | counter_start = counter; 187 | counter_stop = counter + window; 188 | } 189 | for (counter = counter_start; counter <= counter_stop; counter++) { 190 | if (motp_pin != NULL) 191 | motp(keybuf, keylen, motp_pin, counter, ndigits, otpbuf16, OTP_BUF_SIZE); 192 | else 193 | hotp(keybuf, keylen, counter, ndigits, otpbuf10, otpbuf16, OTP_BUF_SIZE); 194 | printf("%d: %s%s%s\n", counter, otpbuf10, *otpbuf10 != '\0' && *otpbuf16 != '\0' ? " " : "", otpbuf16); 195 | } 196 | return 0; 197 | } else { 198 | for (i = 0; i <= window; i++) { 199 | int try; 200 | 201 | try = counter + i; 202 | if (motp_pin != NULL) 203 | motp(keybuf, keylen, motp_pin, try, ndigits, otpbuf16, OTP_BUF_SIZE); 204 | else 205 | hotp(keybuf, keylen, try, ndigits, otpbuf10, otpbuf16, OTP_BUF_SIZE); 206 | if (strcasecmp(otp, otpbuf10) == 0 || strcasecmp(otp, otpbuf16) == 0) 207 | goto match; 208 | if (use_time && i != 0) { 209 | try = counter - i; 210 | if (motp_pin != NULL) 211 | motp(keybuf, keylen, motp_pin, try, ndigits, otpbuf16, OTP_BUF_SIZE); 212 | else 213 | hotp(keybuf, keylen, try, ndigits, otpbuf10, otpbuf16, OTP_BUF_SIZE); 214 | if (strcasecmp(otp, otpbuf10) == 0 || strcasecmp(otp, otpbuf16) == 0) 215 | goto match; 216 | } 217 | continue; 218 | match: 219 | printf("%d\n", try); 220 | return 0; 221 | } 222 | } 223 | 224 | /* Not found */ 225 | fprintf(stderr, "one-time password \"%s\" was not found within the counter range %d ... %d\n", otp, 226 | use_time ? counter - window : counter, counter + window); 227 | return EXIT_NOT_MATCHED; 228 | } 229 | 230 | static void 231 | usage() 232 | { 233 | fprintf(stderr, "Usage: %s [-Ffht] [-c counter] [-d digits] [-i interval] [-m PIN] [-w window] key [otp]\n", PROG_NAME); 234 | fprintf(stderr, "Options:\n"); 235 | fprintf(stderr, " -c\tSpecify the initial counter value (conflicts with `-t')\n"); 236 | fprintf(stderr, " -F\t`key' refers to the file containing the key in hexadecimal format\n"); 237 | fprintf(stderr, " -f\t`key' refers to the file containing the key in binary format\n"); 238 | fprintf(stderr, " -h\tDisplay this usage message\n"); 239 | fprintf(stderr, " -i\tSpecify time interval in seconds (default %d)\n", DEFAULT_TIME_INTERVAL); 240 | fprintf(stderr, " -m\tUse mOTP algorithm with given PIN; also implies `-d 6' and `-i 10'\n"); 241 | fprintf(stderr, " -t\tDerive initial counter value from the current time (conflicts with `-c')\n"); 242 | fprintf(stderr, " -d\tSpecify number of digits in the generated OTP(s) (default %d)\n", DEFAULT_NUM_DIGITS); 243 | fprintf(stderr, " -w\tSpecify size of window for additional counter values (default %d)\n", DEFAULT_WINDOW); 244 | } 245 | -------------------------------------------------------------------------------- /phex.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * otptool - HOTP/OATH one-time password utility 4 | * 5 | * Copyright 2009 Archie L. Cobbs 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include "otpdefs.h" 21 | 22 | void 23 | printhex(char *buf, size_t buflen, const u_char *data, size_t dlen, int max_digits) 24 | { 25 | const char *hexdig = "0123456789abcdef"; 26 | int i; 27 | 28 | if (buflen > 0) 29 | *buf = '\0'; 30 | for (i = 0; i / 2 < (int)dlen && i < max_digits && i < (int)buflen - 1; i++) { 31 | u_int val = data[i / 2]; 32 | if ((i & 1) == 0) 33 | val >>= 4; 34 | val &= 0x0f; 35 | *buf++ = hexdig[val]; 36 | *buf = '\0'; 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /users.sample: -------------------------------------------------------------------------------- 1 | # 2 | # Example users file for mod_authn_otp 3 | # 4 | # Blank lines and lines starting with '#' are ignored. Fields are whitespace-separated. 5 | # 6 | # Fields: 7 | # 8 | # 1. Token Type See below 9 | # 2. Username User's username 10 | # 3. PIN User's PIN, or "-" if user has no PIN, or "+" to verify PIN via "OTPAuthPINAuthProvider" 11 | # 4. Token Key Secret key for the token algorithm (see RFC 4226) 12 | # 5. Counter/Offset Next expected counter value (event tokens) or counter offset (time tokens) 13 | # 6. Failure counter Number of consecutive wrong OTP's provided by this users (for "OTPAuthMaxOTPFailure") 14 | # 7. Last OTP The previous successfully used one-time password 15 | # 8. Time of Last OTP Local timestamp when the last OTP was generated (in the form 2009-06-12T17:52:32L) 16 | # 9. Last IP address IP address used during the most recent successful attempt 17 | # 18 | # Fields 5 and beyond are optional. Fields 6 and beyond should be omitted for new users. 19 | # 20 | # Token Type Field: 21 | # 22 | # This field contains a string in the format: ALGORITHM [ / COUNTERINFO [ / DIGITS ] ] 23 | # 24 | # The ALGORITHM is either "HOTP" (RFC 4226) or "MOTP" (http://motp.sourceforge.net/). 25 | # 26 | # The COUNTERINFO is either "E" for an event-based token, or "TNN" for a time based token 27 | # where "NN" is the number of seconds in one time interval. For HOTP, the default is "E"; 28 | # for MOTP, the default is "T10". 29 | # 30 | # The DIGITS is the number of digits in the one-time password; the default is six. 31 | # 32 | # Examples: 33 | # 34 | # HOTP - HOTP event-based token with six digit OTP 35 | # HOTP/E - HOTP event-based token with six digit OTP 36 | # HOTP/E/8 - HOTP event-based token with eight digit OTP 37 | # HOTP/T30 - HOTP time-based token with 30 second interval and six digit OTP 38 | # HOTP/T60 - HOTP time-based token with 60 second interval and six digit OTP 39 | # HOTP/T60/5 - HOTP time-based token with 60 second interval and five digit OTP 40 | # MOTP - Mobile-OTP time-based token 10 second interval and six digit OTP 41 | # MOTP/E - Mobile-OTP event-based token with six digit OTP 42 | # 43 | # For more info see: https://github.com/archiecobbs/mod-authn-otp/wiki/UsersFile 44 | # 45 | 46 | # Some users who have logged in at least once. 47 | 48 | HOTP barney 1234 8a2d55707a9084982649dadc04b426a06df19ab2 21 0 820658 2009-06-12T17:52:32L 192.168.1.1 49 | HOTP fred 5678 acbd18db4cc2f85cedef654fccc4a4d8bd537891 78 0 617363 2009-06-04T21:17:03L 192.168.1.2 50 | HOTP/T joe 999999 ef654fccdef654fccc4a4d8acbd18db4cc2f85ce -2 2 883913 2009-06-04T21:17:03L 10.1.1.153 51 | 52 | # Wilma and Betty are new users. Note betty does not have a PIN so "-" is used instead as a placeholder 53 | 54 | HOTP wilma 5678 a4d8acbddef654fccc418db4cc2f85cea6339f00 55 | HOTP betty - 54fccc418a4d8acbddef6db4cc2f85ce99321d64 56 | 57 | # Here is a user who's PIN is verified externally using whatever "OTPAuthPINAuthProvider" list you have configured. 58 | # E.g. to use an htpasswd type file, specify "OTPAuthPINAuthProvider file" and then "AuthUserFile /some/file". 59 | HOTP bambam + d8acbddef6db4cc254fccc418a4f85ce99321d64 60 | 61 | --------------------------------------------------------------------------------