├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── mina ├── __init__.py └── mina.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # custom 104 | .mina.json 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 MinaOTP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MinaOTP-Shell 2 | 3 | MinaOTP-Shell is a two-factor authentication tool that runs in a terminal as a command-line tool. 4 | 5 | This command-line tool will generate secure dynamic 2FA tokens for you, the `add`, `remove`, `list`, `show` and `import` will be pretty convenient in the terminal. 6 | 7 | ### Installation 8 | 9 | ```shell 10 | pip install minaotp 11 | ``` 12 | 13 | ### Usage 14 | 15 | * Help information 16 | 17 | ```shell 18 | $ mina -h 19 | usage: mina [-h] {list,add,remove,show,import} ... 20 | 21 | MinaOTP is a two-factor authentication tool that runs in the terminal 22 | 23 | positional arguments: 24 | {list,add,remove,show,import} 25 | Available commands 26 | list List all tokens. 27 | add Add a new token. 28 | remove Remove a token. 29 | show Show a token on-time 30 | import Import tokens from a local json file 31 | 32 | optional arguments: 33 | -h, --help show this help message and exit 34 | ``` 35 | 36 | * List all tokens 37 | 38 | ```shell 39 | $ mina list 40 | =OID== =====ISSUER===== =====REMARK===== ======OTP======= 41 | 0 test test 904540 42 | 1 hello world 344962 43 | 2 mina otp 032218 44 | ``` 45 | 46 | * Add a new token 47 | 48 | ```shell 49 | $ mina add -h 50 | usage: mina add [-h] --secret SECRET --issuer ISSUER --remark REMARK 51 | 52 | optional arguments: 53 | -h, --help show this help message and exit 54 | --secret SECRET Secret info to generate otp object. 55 | --issuer ISSUER Issuer info about new otp object. 56 | --remark REMARK Remark info about new otp object. 57 | ``` 58 | 59 | * Remove a token 60 | 61 | ```shell 62 | $ mina remove -h 63 | usage: mina remove [-h] oid 64 | 65 | positional arguments: 66 | oid oid of the token 67 | 68 | optional arguments: 69 | -h, --help show this help message and exit 70 | ``` 71 | 72 | * Show a token on-time 73 | 74 | ```shell 75 | $ mina show 2 76 | =OID== =====ISSUER===== =====REMARK===== ======OTP======= 77 | 2 mina otp 983418 78 | ``` 79 | 80 | * Import tokens from a local json file 81 | 82 | ```shell 83 | $ mina import -h 84 | usage: mina import [-h] file_path 85 | 86 | positional arguments: 87 | file_path path of the local json file 88 | 89 | optional arguments: 90 | -h, --help show this help message and exit 91 | ``` 92 | 93 | ### Todos 94 | 95 | - [x] import tokens from a local json file 96 | - [ ] export all tokens to a local json file -------------------------------------------------------------------------------- /mina/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinaOTP/MinaOTP-Shell/b0c6685a52cf2c6a1064dd8385840cefffbf8ad2/mina/__init__.py -------------------------------------------------------------------------------- /mina/mina.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Date : since 2018-02-06 02:26 4 | # @Author : Gin (gin.lance.inside@hotmail.com) 5 | # @Link : 6 | # @Disc : add remove show and list totp tokens in the terminal 7 | 8 | from __future__ import print_function 9 | import json 10 | import logging 11 | import argparse 12 | import pyotp 13 | import os 14 | import os.path 15 | import time 16 | import datetime 17 | import sys 18 | 19 | # global variable 20 | OID_LEN = 6 21 | ISSUER_LEN = 16 22 | REMARK_LEN = 16 23 | OTP_LEN = 16 24 | JSON_URL = os.path.expanduser("~") + os.sep + '.mina.json' 25 | 26 | # dev json url 27 | # JSON_URL = './.mina.json' 28 | 29 | # configure the basic logging level 30 | logging.basicConfig( 31 | level=logging.INFO, 32 | format="[%(asctime)s] %(name)s:%(levelname)s: %(message)s" 33 | ) 34 | 35 | # load tokens from json file 36 | def load_json(json_url): 37 | with open(json_url, "r") as f: 38 | if os.path.getsize(json_url): 39 | return json.load(f) 40 | else: 41 | raise Warning 42 | 43 | # update token to json file 44 | def upd_json(data, json_url): 45 | with open(json_url, "w") as f: 46 | json.dump(data, f, sort_keys=True, indent=4, separators=(',', ':')) 47 | 48 | # show time process 49 | def time_process(): 50 | try: 51 | sec = datetime.datetime.now().second 52 | sharp_c = sec % 30 53 | b = '#' * sharp_c + '' 54 | while sharp_c <= 30: 55 | expired_time = 30 - sharp_c 56 | sys.stdout.write("expired after: [" + b.ljust(30) + "] " + str(expired_time).rjust(2) + "s\r") 57 | sys.stdout.flush() 58 | time.sleep(1) 59 | b = "#" + b 60 | sharp_c += 1 61 | print("tokens have been expired, try to list or show again..") 62 | except KeyboardInterrupt: 63 | print("\nthe script has been quited manually.") 64 | 65 | # list all tokens 66 | def list(): 67 | try: 68 | tokens = load_json(JSON_URL) 69 | except IOError: 70 | print("ERROR: there is no .mina.json file") 71 | except Warning: 72 | print("WARNING: there is no any otp tokens in the .mina.json file.") 73 | else: 74 | print("OID".center(OID_LEN, "="), "ISSUER".center(ISSUER_LEN, "="), "REMARK".center(REMARK_LEN, "="), "OTP".center(OTP_LEN, "="), sep=' ') 75 | for oid, token in enumerate(tokens): 76 | # generate tmp TOTO object and calculate the token 77 | secret = token["secret"] 78 | remark = token["remark"] 79 | issuer = token["issuer"] 80 | totp_tmp = pyotp.TOTP(secret) 81 | current_otp = totp_tmp.now() 82 | print(str(oid).center(OID_LEN), issuer.center(ISSUER_LEN), remark.center(REMARK_LEN), current_otp.center(OTP_LEN), sep=' ') 83 | 84 | time_process() 85 | 86 | # show a token on-time 87 | def show(oid): 88 | try: 89 | tokens = load_json(JSON_URL) 90 | except IOError: 91 | print("ERROR: there is no .mina.json file") 92 | except Warning: 93 | print("WARNING: there is no any otp tokens in the .mina.json file.") 94 | else: 95 | print("OID".center(OID_LEN, "="), "ISSUER".center(ISSUER_LEN, "="), "REMARK".center(REMARK_LEN, "="), "OTP".center(OTP_LEN, "="), sep=' ') 96 | token = tokens[int(oid)] 97 | issuer = token["issuer"] 98 | secret = token["secret"] 99 | remark = token["remark"] 100 | # generate tmp TOTO object and calculate the token 101 | totp_tmp = pyotp.TOTP(secret) 102 | current_otp = totp_tmp.now() 103 | print(oid.center(OID_LEN), issuer.center(ISSUER_LEN), remark.center(REMARK_LEN), current_otp.center(OTP_LEN), sep=' ') 104 | 105 | time_process() 106 | 107 | # add a new token 108 | def add(otp): 109 | try: 110 | tokens = load_json(JSON_URL) 111 | except IOError: 112 | print("ERROR: there is no .mina.json file") 113 | except Warning: 114 | tokens = [] 115 | tokens.append(otp) 116 | upd_json(tokens, JSON_URL) 117 | else: 118 | tokens.append(otp) 119 | upd_json(tokens, JSON_URL) 120 | 121 | # remove a token 122 | def remove(oid): 123 | try: 124 | tokens = load_json(JSON_URL) 125 | except IOError: 126 | print("ERROR: there is no .mina.json file") 127 | except Warning: 128 | print("WARNING: there is no any otp tokens in the .mina.json file.") 129 | else: 130 | tokens.pop(int(oid)) 131 | upd_json(tokens, JSON_URL) 132 | 133 | # import from a local json file 134 | def import_from(file_path): 135 | try: 136 | tokens = load_json(JSON_URL) 137 | except IOError: 138 | print("ERROR: there is no .mina.json file") 139 | except Warning: 140 | print("WARNING: there is no any otp tokens in the .mina.json file.") 141 | else: 142 | try: 143 | append_tokens = load_json(file_path) 144 | except IOError: 145 | print("ERROR: " + file_path + " is not a file!") 146 | except Warning: 147 | print("WARNING: there is no any otp tokens in the file!") 148 | else: 149 | tokens = tokens + append_tokens 150 | upd_json(tokens, JSON_URL) 151 | 152 | # the main function to control the script 153 | def main(): 154 | # Define the basic_parser and subparsers 155 | logging.debug('Initial basic_parser') 156 | 157 | _desc = 'MinaOTP is a two-factor authentication tool that runs in the terminal' 158 | basic_parser = argparse.ArgumentParser(description=_desc) 159 | subparsers = basic_parser.add_subparsers( 160 | dest="command", 161 | help="Available commands" 162 | ) 163 | 164 | # Subparser for the list command 165 | logging.debug("Initial list subparser") 166 | 167 | list_parser = subparsers.add_parser( 168 | "list", 169 | help="List all tokens." 170 | ) 171 | 172 | # Subparser for the add command 173 | logging.debug("Initial add subparser") 174 | 175 | add_parser = subparsers.add_parser( 176 | "add", 177 | help="Add a new token." 178 | ) 179 | # OTP optional arguments 180 | add_parser.add_argument( 181 | "--secret", 182 | required=True, 183 | help="Secret info to generate otp object." 184 | ) 185 | add_parser.add_argument( 186 | "--issuer", 187 | required=True, 188 | help="Issuer info about new otp object." 189 | ) 190 | add_parser.add_argument( 191 | "--remark", 192 | required=True, 193 | help="Remark info about new otp object." 194 | ) 195 | 196 | # Subparser for the remove command 197 | logging.debug("Initial remove subparser") 198 | 199 | remove_parser = subparsers.add_parser( 200 | "remove", 201 | help="Remove a token." 202 | ) 203 | remove_parser.add_argument( 204 | "oid", 205 | help="oid of the token" 206 | ) 207 | 208 | # Subparser for the show command 209 | logging.debug("Initial show subparser") 210 | 211 | show_parser = subparsers.add_parser( 212 | "show", 213 | help="Show a token on-time" 214 | ) 215 | show_parser.add_argument( 216 | "oid", 217 | help="oid of the token" 218 | ) 219 | 220 | # Subparser for the import command 221 | logging.debug("Initial import subparser") 222 | 223 | import_parser = subparsers.add_parser( 224 | "import", 225 | help="Import tokens from a local json file" 226 | ) 227 | import_parser.add_argument( 228 | "file_path", 229 | help="path of the local json file" 230 | ) 231 | 232 | # handle the args input by user 233 | args = basic_parser.parse_args() 234 | # convert arguments to dict 235 | arguments = vars(args) 236 | command = arguments.pop("command") 237 | 238 | if command == "list": 239 | list() 240 | if command == "add": 241 | otp = { 242 | "secret": args.secret, 243 | "issuer": args.issuer, 244 | "remark": args.remark 245 | } 246 | add(otp) 247 | if command == "remove": 248 | target_oid = args.oid 249 | remove(target_oid) 250 | if command == "show": 251 | target_oid = args.oid 252 | show(target_oid) 253 | if command == "import": 254 | file_path = args.file_path 255 | import_from(file_path) 256 | 257 | 258 | if __name__ == '__main__': 259 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyotp -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | """ A setuptools based setup module. 3 | https://github.com/MinaOTP/MinaOTP-Shell 4 | """ 5 | 6 | # Always prefer setuptools over distutils 7 | from setuptools import setup, find_packages 8 | # To use a consistent encoding 9 | from codecs import open 10 | import os 11 | from os import path 12 | 13 | # here = path.abspath(path.dirname(__file__)) 14 | 15 | # Get the long description from the README file 16 | # with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 17 | # long_description = f.read() 18 | 19 | # touch a basic .mina.json file if NOT EXIST 20 | default = os.path.expanduser("~") + os.sep + '.mina.json' 21 | if not os.path.isfile(default): 22 | cmd = 'touch ' + default 23 | os.system(cmd) 24 | 25 | setup( 26 | name='minaotp', 27 | 28 | # Versions should comply with PEP440. For a discussion on single-sourcing 29 | # the version across setup.py and the project code, see 30 | # https://packaging.python.org/en/latest/single_source_version.html 31 | version='1.0.8', 32 | 33 | description='TOTP authenticator implement as a terminal tool', 34 | long_description='TOTP authenticator implement as a terminal tool, and this project is developed in Python', 35 | 36 | # The project's main homepage. 37 | url='https://github.com/MinaOTP/MinaOTP-Shell', 38 | 39 | # Author details 40 | author='lancegin', 41 | author_email='gin.lance.inside@hotmail.com', 42 | 43 | # Choose your license 44 | license='MIT', 45 | 46 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 47 | classifiers=[ 48 | # How mature is this project? Common values are 49 | # 3 - Alpha 50 | # 4 - Beta 51 | # 5 - Production/Stable 52 | 'Development Status :: 5 - Production/Stable', 53 | 54 | # Indicate who your project is intended for 55 | 'Intended Audience :: Developers', 56 | 'Topic :: Software Development :: Libraries :: Python Modules', 57 | 58 | # Pick your license as you wish (should match "license" above) 59 | 'License :: OSI Approved :: MIT License', 60 | 61 | # Specify the Python versions you support here. In particular, ensure 62 | # that you indicate whether you support Python 2, Python 3 or both. 63 | 'Programming Language :: Python :: 3 :: Only', 64 | 65 | ], 66 | 67 | # What does your project relate to? 68 | keywords='authenticator, totp, rfc-6238, command-line-tool', 69 | 70 | # You can just specify the packages manually here if your project is 71 | # simple. Or you can use find_packages(). 72 | packages=find_packages(), 73 | 74 | # Alternatively, if you want to distribute just a my_module.py, uncomment 75 | # this: 76 | # py_modules=["my_module"], 77 | 78 | # List run-time dependencies here. These will be installed by pip when 79 | # your project is installed. For an analysis of "install_requires" vs pip's 80 | # requirements files see: 81 | # https://packaging.python.org/en/latest/requirements.html 82 | install_requires=['pyotp'], 83 | 84 | # List additional groups of dependencies here (e.g. development 85 | # dependencies). You can install these using the following syntax, 86 | # for example: 87 | # $ pip install -e .[dev,test] 88 | # extras_require={ 89 | # 'dev': ['check-manifest'], 90 | # 'test': ['coverage'], 91 | # }, 92 | 93 | # If there are data files included in your packages that need to be 94 | # installed, specify them here. If using Python 2.6 or less, then these 95 | # have to be included in MANIFEST.in as well. 96 | # package_data={ 97 | # 'sample': ['package_data.dat'], 98 | # }, 99 | 100 | # Although 'package_data' is the preferred approach, in some case you may 101 | # need to place data files outside of your packages. See: 102 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 103 | # In this case, 'data_file' will be installed into '/my_data' 104 | # data_files=[('my_data', ['data/data_file'])], 105 | 106 | # To provide executable scripts, use entry points in preference to the 107 | # "scripts" keyword. Entry points provide cross-platform support and allow 108 | # pip to create the appropriate form of executable for the target platform. 109 | entry_points={ 110 | 'console_scripts': [ 111 | 'mina=mina.mina:main', 112 | ], 113 | }, 114 | ) --------------------------------------------------------------------------------