Skip to content

Commit

Permalink
Password Handling works so far
Browse files Browse the repository at this point in the history
  • Loading branch information
cimnine committed Jan 17, 2021
0 parents commit 1860288
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 2
indent_style = space
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.venv
*.egg-info/
build/
dist/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Christian Mäder

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# yk-totp

`yk-totp` is a little CLI util for YubiKeys,
that will generate TOTP codes upon request.

The added benefit compared to [the official `ykman`][ykman] is that it offers
to store the password for unlocking your YubiKey in your system's keyring,
whereas `ykman` stores your password in it's config file.
(While the password is stored as `PBKDF2HMAC`-hash and not in plain-text,
this hash is all that is required to get to your 2FA
but this hash is not protected in anyway.)

This allows `yk-totp` to be used in other tools (like in an [Alfred Worflow][alred-wf])
which don't offer facilities to store or enter a password,
or where it's inconvenient to repeatedly enter the password.

## Requirements

This tool requires [_Python 3_][python] and an operating system that is supported by [the `keyring` Python module][keyring].

## Installation

For now, the way to install `yk-totp` is via PIP:

```bash
pip install yk-totp
```

## Licensing and Copyright

This code is copyrighted.
But it can be used under the terms of the [MIT license](./License) for your own purposes.
It builds upon the following third party modules:

- [keyring][keyring] for the interaction with the operating system's keyring, which is MIT licensed.
- [yubikey-manager][ykman] for communicating with the YubiKey, which is licensed under a BSD-2-Clause License.
- [click][click] for the CLI interface, which is licensed under a BSD-3-Clause License.

Open source software rocks 🎸!

[ykman]: https://github.com/Yubico/yubikey-manager#readme
[alfred-wf]: https://www.alfredapp.com/help/workflows/
[python]: https://www.python.org
[keyring]: https://github.com/jaraco/keyring#readme
[click]: https://github.com/pallets/click#readme
12 changes: 12 additions & 0 deletions development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Development

```bash
# Load venv
source .venv/bin/active

# Install locally so that you can edit the code
pip install --editable .

# Test it
.venv/bin/yk-totp version
```
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
click
keyring
yubikey-manager
setuptools
41 changes: 41 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[metadata]
name = yk-totp
version = 0.1.0

description = A CLI tool to generate TOTP values from a password protected YubiKey by storing the password in the system-protected keyring.

license = BSD 3-Clause License
license_file = LICENSE

author = Christian Mäder
author_email = yktotp@cimnine.ch
url = https://github.com/cimnine/yktotp

classifiers =
Development Status :: 3 - Alpha
Environment :: Console
Intended Audience :: Developers
Intended Audience :: System Administrators
License :: OSI Approved :: MIT License
Natural Language :: English
Operating System :: MacOS :: MacOS X
Operating System :: Microsoft :: Windows
Operating System :: POSIX
Programming Language :: Python :: 3
Topic :: Security
Topic :: Software Development
Topic :: System :: Operating System
Topic :: Utilities

[options]
packages = yktotp

install_requires =
click >= 7.1.2
keyring >= 21.8.0
yubikey-manager >= 3.1.1
python_requires = >=3.6

[options.entry_points]
console_scripts =
yk-totp = yktotp.__main__:cli
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from setuptools import setup
setup()
Empty file added yktotp/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions yktotp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import click

from click import echo
from ykman import __version__ as YKMAN_VERSION

from .tool import TOOL_NAME, TOOL_VERSION
from .password import password_group


@click.group()
def cli():
pass

SUBGROUPS=[password_group]

for SUBGROUP in SUBGROUPS:
cli.add_command(SUBGROUP)

@cli.command()
def version():
"""
Shows version information.
"""
echo("%s Version: %s" % (TOOL_NAME, TOOL_VERSION))
echo("YubiKey Manager Version: %s" % YKMAN_VERSION)

if __name__ == '__main__':
cli()
2 changes: 2 additions & 0 deletions yktotp/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class PasswordError(Exception):
pass
75 changes: 75 additions & 0 deletions yktotp/password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import click
import keyring

from click import echo
from click.exceptions import Abort

from .error import PasswordError
from .tool import TOOL_NAME
from .yk import getDevice, getController, validate

@click.group(name="password")
@click.pass_context
def password_group(ctx):
"""
Provides commands for storing your password.
This can be useful if you would like to not enter your
password repeatedly.
"""
ctx.ensure_object(dict)
ctx.obj['device'] = getDevice()


@password_group.command()
@click.pass_context
def remember(ctx):
"""
Asks for a password and remembers it.
If multiple YubiKeys are connected, then it will ask to select
a YubiKey first.
Then you will need to enter the corresponding password.
If the password is correct, it is stored to the system's keyring.
"""
device = ctx.obj['device']
controller = getController(device)

yk_serial = device.serial

if not controller.locked:
echo("The YubiKey '%s' is not password protected." % yk_serial)
exit(1)

while True:
try:
password = click.prompt("Password for YubiKey '%s'" % yk_serial, hide_input=True, err=True)
validate(password, controller)
break
except Abort:
exit(1)
except PasswordError:
echo("Could not validate password. Try again.")
continue

keyring.set_password(TOOL_NAME, str(yk_serial), password)
echo("The password was stored in %s." % keyring.get_keyring().name)


@password_group.command()
@click.pass_context
def forget(ctx):
"""
Forgets the stored password.
If there is one YubiKey connected, it will forget the password,
that is stored for this YubiKey.
If multiple YubiKeys are connected, then it will ask to select
a YubiKey first.
"""

yk_serial = ctx.obj['device'].serial
try:
keyring.delete_password(TOOL_NAME, str(yk_serial))
except keyring.errors.PasswordDeleteError:
pass
2 changes: 2 additions & 0 deletions yktotp/tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TOOL_NAME = "yk-totp"
TOOL_VERSION = '0.1.0'
49 changes: 49 additions & 0 deletions yktotp/yk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from click import echo, prompt
from click.exceptions import Abort

from ykman.descriptor import list_devices
from ykman.util import TRANSPORT
from ykman.oath import OathController

from .error import PasswordError

def getDevice():
devices = list(list_devices(transports=TRANSPORT.CCID))

if len(devices) == 0:
echo("No YubiKey discovered.")
exit(1)

selectedDeviceIndex = 0
if len(devices) > 1:
for deviceIndex in range(len(devices)):
echo("%d %s" % (deviceIndex+1, devices[deviceIndex]))

while True:
try:
selectedDeviceIndex = -1 + int(prompt("Select device: ", default=1, type=int))
if selectedDeviceIndex >= 0 and selectedDeviceIndex < len(devices):
break
except Abort:
exit(1)
except ValueError:
continue

return devices[selectedDeviceIndex]

def getController(device=None):
if device is None:
device = getDevice()

return OathController(device.driver)

def validate(password, controller=None, device=None):
if controller is None:
controller = getController(device)

key = controller.derive_key(password)
try:
controller.validate(key)
return
except Exception:
raise PasswordError

0 comments on commit 1860288

Please sign in to comment.