Skip to content

Commit

Permalink
202001.2
Browse files Browse the repository at this point in the history
  • Loading branch information
lechgu committed Jan 18, 2020
1 parent dab6ac7 commit cf03620
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 21 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Hush does not require a master password as other password managers do, instead i

#### What's new?

- Version 202001.2 (Jan 17, 2020)

- Added command to initialize the default config

- Version 202001.1 (Jan 16, 2020)

- Python 3.8 compatibility
Expand Down Expand Up @@ -62,8 +66,4 @@ push decrypt password.txt | clip

### configuration

hush requires to know where to find the private and public key files, these locations can be provided either as parameters or set as environment variables. For more options, run

```
hush --help
```
You can pass the options through the command line parameters or use the configuration. `hush init` creates the default configuration, you will still need to provide the private and public RSA keys though. If you don't have them, you can run `hush keygen` to generate a pair.
70 changes: 61 additions & 9 deletions hush/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

from . import keypairs, passwords, secrets

DEFAULT_CONFIG_FILE = "~/.hush"
DEFAULT_PASSWORD_LENGTH = 8
DEFAULT_CHARACTER_CLASSES = "aA8#"


class Context:
def __init__(self):
Expand All @@ -28,32 +32,45 @@ def configuration(config_file):


def config_callback(ctx, param, value):
null_config = {
"generate.length": DEFAULT_PASSWORD_LENGTH,
"generate.character_classes": DEFAULT_CHARACTER_CLASSES,
}
section = ctx.command.name
key = param.name
config_key = f"{section}.{key}"
if not value:
with configuration(ctx.obj.config_file) as conf:
if param.type.name == "integer":
value = conf.getint(section, key)
value = (
conf.getint(section, key)
if conf.has_option(section, key)
else None
)
else:
value = conf.get(section, key)
value = (
conf.get(section, key)
if conf.has_option(section, key)
else None
)
if not value:
value = param.default
value = null_config.get(config_key, None)
if not value:
raise click.UsageError(
f"{section}.{key} is missing. "
f"Either set it in the config or pass as an option"
f"{config_key} is missing. "
f"Either set it in the config or supply as an option"
)
return value


@click.group()
@click.version_option("202001.1")
@click.version_option("202001.2")
@click.option(
"-c",
"--config-file",
type=str,
default="~/.hush",
help="Config file name, default '~/.hush' ",
help=f"Config file name [default: {DEFAULT_CONFIG_FILE}] ",
)
@pass_context
def cli(ctx, config_file):
Expand Down Expand Up @@ -123,15 +140,15 @@ def decrypt(ctx, private_key_file, ask_passphrase, passphrase, file):
"-l",
"--length",
type=int,
help="Password Length",
help=f"Password Length [default: {DEFAULT_PASSWORD_LENGTH}]",
callback=config_callback,
)
@click.option(
"--character-classes",
"-c",
type=str,
callback=config_callback,
help="Character classes",
help=f"Character classes [default: {DEFAULT_CHARACTER_CLASSES}]",
)
@pass_context
def generate(ctx, length, character_classes):
Expand Down Expand Up @@ -296,3 +313,38 @@ def config(ctx, list, set, val):
section = parts[0]
key = parts[1]
click.echo(config.get(section, key))


@cli.command(help="Init the configuration")
@pass_context
@click.option(
"-r",
"--private-key-file",
type=str,
required=True,
help="Private key file",
)
@click.option(
"-p", "--public-key-file", type=str, required=True, help="Public key file",
)
@click.option(
"--yes", type=bool, help="Overwrite the existing config file if exists.",
)
def init(ctx, private_key_file, public_key_file, yes):
if not yes and os.path.exists(ctx.config_file):
yn = click.prompt(f"{ctx.config_file} exists, overwrite? [y/n]")
yes = yn.strip() and yn[0].lower() == "y"
if yes:
with configuration(ctx.config_file) as config:
values = [
('generate', 'length', str(DEFAULT_PASSWORD_LENGTH)),
('generate', 'character_clsses', DEFAULT_CHARACTER_CLASSES),
('decrypt', 'private_key_file', private_key_file),
('encrypt', 'public_key_file', public_key_file),
]
for (section, key, v) in values:
if section not in config.sections():
config.add_section(section)
config[section][key] = v

pass
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ push = false

[tool.poetry]
name = "hush"
version = "202001.1"
version = "202001.2"
description = "Minimalistic command line secret management"
license = "BSD-3-Clause"
authors = ["Lech Gudalewicz <[email protected]>"]
Expand Down
52 changes: 46 additions & 6 deletions tests/test_cmd.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import os
import string
from contextlib import contextmanager
from tempfile import mktemp

from click.testing import CliRunner

from hush import cli
from hush.console import DEFAULT_PASSWORD_LENGTH


@contextmanager
Expand All @@ -21,25 +23,34 @@ def keypair(name="rsa", passphrase=None):
os.remove(f"{name}.pri")


@contextmanager
def config_file(lines):
file_name = mktemp()
with open(file_name, "w") as f:
f.writelines(lines)
yield file_name
os.remove(file_name)


def test_version():
runner = CliRunner()
output = runner.invoke(cli, "--version").output.strip()

assert "202001.1" in output
assert "202001.2" in output


def test_generate_default():
runner = CliRunner()
output = runner.invoke(cli, "generate").output.strip()
assert len(output) == 16
assert len(output) == DEFAULT_PASSWORD_LENGTH


def test_generate_lowercase():
runner = CliRunner()
result = runner.invoke(cli, ["generate", "-c", "a"])
assert result.exit_code == 0
output = result.output.strip()
assert len(output) == 16
assert len(output) == DEFAULT_PASSWORD_LENGTH
assert all([x in string.ascii_lowercase for x in output])


Expand All @@ -48,7 +59,7 @@ def test_generate_uppercase():
result = runner.invoke(cli, ["generate", "-c", "A"])
assert result.exit_code == 0
output = result.output.strip()
assert len(output) == 16
assert len(output) == DEFAULT_PASSWORD_LENGTH
assert all([x in string.ascii_uppercase for x in output])


Expand All @@ -57,7 +68,7 @@ def test_generate_digits():
result = runner.invoke(cli, ["generate", "-c", "8"])
assert result.exit_code == 0
output = result.output.strip()
assert len(output) == 16
assert len(output) == DEFAULT_PASSWORD_LENGTH
assert all([x in string.digits for x in output])


Expand All @@ -67,7 +78,7 @@ def test_generate_nonalphanumeric():
assert result.exit_code == 0
output = result.output.strip()
nonalphanumeric = r"~!@#$%^&*_-+=|\(){}[]:;<>,.?/"
assert len(output) == 16
assert len(output) == 8
assert all([x in nonalphanumeric for x in output])


Expand Down Expand Up @@ -216,3 +227,32 @@ def test_change_passphrase_empty():
result = runner.invoke(cli, ["decrypt", "-r", "rsa.pri"], input=output)
assert result.exit_code == 0
assert result.output.strip() == "secret"


def test_alterantive_config():
lines = """
[generate]
length = 40
character_classes = a
[decrypt]
private_key_file = foo.pri
[encrypt]
public_key_file = foo.pub
"""
with keypair("foo"):
with config_file(lines) as alternative_config:
runner = CliRunner()
result = runner.invoke(cli, ["-c", alternative_config, "generate"])
assert result.exit_code == 0
output = result.output.strip()
assert len(output) == 40
assert all([x in string.ascii_lowercase for x in output])
result = runner.invoke(
cli, ["encrypt", "-p", "foo.pub"], input="secret"
)
assert result.exit_code == 0
output = result.output
result = runner.invoke(cli, ["decrypt", "-r", "foo.pri"], input=output)
assert result.exit_code == 0
assert result.output.strip() == "secret"

0 comments on commit cf03620

Please sign in to comment.