Skip to content

Commit

Permalink
Merge pull request #152 from tdviet/devel
Browse files Browse the repository at this point in the history
Add encryption and value from file to fedcloud.secret
  • Loading branch information
tdviet committed Jun 21, 2022
2 parents 6a9deb9 + fffb194 commit bb99ca5
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 16 deletions.
135 changes: 130 additions & 5 deletions fedcloudclient/secret.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
"""
Implementation of "fedcloud secret" commands for accessing secret management service
"""
import base64
import json

import click
import hvac
import yaml

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from tabulate import tabulate

from fedcloudclient.checkin import get_checkin_id
Expand All @@ -13,6 +19,7 @@
VAULT_ADDR = "https://vault.services.fedcloud.eu:8200"
VAULT_ROLE = "demo"
VAULT_MOUNT_POINT = "/secrets"
VAULT_SALT = b"e8d3af638e26ede70afc3b3755e7c093"


def secret_client(access_token, command, path, data):
Expand All @@ -34,7 +41,7 @@ def secret_client(access_token, command, path, data):
"read_secret": client.secrets.kv.v1.read_secret,
"delete_secret": client.secrets.kv.v1.read_secret,
}
if command == "set":
if command == "put":
response = client.secrets.kv.v1.create_or_update_secret(
path=full_path,
mount_point=VAULT_MOUNT_POINT,
Expand All @@ -45,6 +52,80 @@ def secret_client(access_token, command, path, data):
return response


def secret_params_to_dict(params):
"""
Convert secret params "key=value" to dict {"key":"value"}
:param params: input string in format "key=value"
:return: dict {"key":"value"}
"""

result = {}

if len(params) == 0:
raise click.UsageError(
"Expecting 'key=value' arguments for secrets, None provided."
)

for param in params:
try:
key, value = param.split("=", 1)
except ValueError:
raise click.UsageError(
f"Expecting 'key=value' arguments for secrets. '{param}' provided."
)
if value.startswith("@"):
with open(value[1:]) as f:
value = f.read()
result[key] = value
return result


def generate_derived_key(passphrase):
"""
Generate derived encryption/decryption key from passphrase
:param passphrase:
:return: derived key
"""

kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=VAULT_SALT,
iterations=390000,
)
return base64.urlsafe_b64encode(kdf.derive(passphrase.encode()))


def encrypt_data(encrypt_key, secrets):
"""
Encrypt values in secrets using key
:param encrypt_key: encryption key
:param secrets: dict containing secrets
:return: dict with encrypted values
"""

derived_key = generate_derived_key(encrypt_key)
fernet = Fernet(derived_key)
for key in secrets:
secrets[key] = fernet.encrypt(secrets[key].encode())
return secrets


def decrypt_data(decrypt_key, secrets):
"""
Decrypt values in secrets using key
:param decrypt_key: decryption key
:param secrets: dict containing encrypted secrets
:return: dict with decrypted values
"""

derived_key = generate_derived_key(decrypt_key)
fernet = Fernet(derived_key)
for key in secrets:
secrets[key] = fernet.decrypt(secrets[key].encode()).decode()
return secrets


@click.group()
def secret():
"""
Expand All @@ -54,22 +135,46 @@ def secret():

@secret.command()
@oidc_params
@click.argument("short_path")
@click.option(
"--output-format",
"-f",
required=False,
type=click.Choice(["text", "YAML", "JSON"], case_sensitive=False),
)
@click.option("--decrypt-key", required=False)
@click.argument("short_path", metavar="[secret path]")
@click.argument("key", metavar="[key]", required=False)
def get(
access_token,
short_path,
key,
output_format,
decrypt_key,
):
"""
Get a secret from the path
Get a secret from the path. If a key is given, print only the value of the key
"""

data = secret_client(access_token, "read_secret", short_path, None)
print(tabulate(data["data"].items(), headers=["key", "value"]))
if decrypt_key:
data["data"] = decrypt_data(decrypt_key, data["data"])
if not key:
if output_format == "JSON":
print(json.dumps(data["data"], indent=4))
elif output_format == "YAML":
print(yaml.dump(data["data"], sort_keys=False))
else:
print(tabulate(data["data"].items(), headers=["key", "value"]))
else:
if key in data["data"]:
print(data["data"][key])
else:
raise SystemExit(f"Error: {key} not found in {short_path}")


@secret.command("list")
@oidc_params
@click.argument("short_path", required=False, default="")
@click.argument("short_path", metavar="[secret path]", required=False, default="")
def list_(
access_token,
short_path,
Expand All @@ -80,3 +185,23 @@ def list_(

data = secret_client(access_token, "list_secrets", short_path, None)
print("\n".join(map(str, data["data"]["keys"])))


@secret.command()
@oidc_params
@click.argument("short_path", metavar="[secret path]")
@click.argument("secrets", nargs=-1, metavar="[key=value...]")
@click.option("--encrypt-key", required=False)
def put(
access_token,
short_path,
secrets,
encrypt_key,
):
"""
Put secrets to the path. Secrets are provided in form key=value
"""
secret_dict = secret_params_to_dict(secrets)
if encrypt_key:
secret_dict = encrypt_data(encrypt_key, secret_dict)
secret_client(access_token, "put", short_path, secret_dict)
23 changes: 12 additions & 11 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
click==8.1.3
click~=8.1.3
click_option_group>=0.5.3
tabulate~=0.8.9
requests==2.28.0
defusedxml==0.7.1
pyjwt==2.4.0
tabulate==0.8.10
requests~=2.28.0
defusedxml~=0.7.1
pyjwt~=2.4.0
python-openstackclient==5.8.0
liboidcagent==0.4.0
liboidcagent~=0.4.0
jsonpath-ng==1.5.3
PyYAML==6.0
setuptools==62.6.0
jsonschema==4.6.0
psutil==5.9.1
hvac~=0.11.2
PyYAML~=6.0
setuptools~=62.6.0
jsonschema~=4.6.0
psutil~=5.9.1
hvac~=0.11.2
cryptography==37.0.3

0 comments on commit bb99ca5

Please sign in to comment.