-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fbdaa28
commit 447d9f1
Showing
7 changed files
with
302 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"""Command line interface for the ai4-api-keys package.""" | ||
|
||
import typer | ||
|
||
import ai4_api_keys | ||
import ai4_api_keys.keys | ||
import ai4_api_keys.fernet | ||
|
||
app = typer.Typer(help="AI4 API Keys management CLI.") | ||
app.add_typer(ai4_api_keys.fernet.app, name="fernet") | ||
app.add_typer(ai4_api_keys.keys.app, name="keys") | ||
|
||
|
||
def version_callback(value: bool): | ||
"""Return the version for the --version option.""" | ||
if value: | ||
typer.echo(ai4_api_keys.extract_version()) | ||
raise typer.Exit() | ||
|
||
|
||
@app.callback() | ||
def version( | ||
version: bool = typer.Option( | ||
None, | ||
"--version", | ||
"-v", | ||
callback=version_callback, | ||
is_eager=True, | ||
help="Print the version and exit", | ||
) | ||
): | ||
"""Show version and exit.""" | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
"""Manage AI4 Fernet signing keys.""" | ||
|
||
from typing_extensions import Annotated | ||
from typing import Optional | ||
|
||
import cryptography.fernet | ||
import typer | ||
|
||
app = typer.Typer(help="AI4 Fernet keys management CLI.") | ||
|
||
|
||
@app.command(name="generate") | ||
def generate_cli( | ||
output: Annotated[ | ||
Optional[str], | ||
typer.Option("--output", "-o", help="Output file for the generated key."), | ||
] = None, | ||
) -> None: | ||
"""Generate a new Fernet key.""" | ||
key = generate() | ||
if output is None: | ||
typer.echo(key.decode()) | ||
else: | ||
with open(output, "wb") as key_file: | ||
key_file.write(key) | ||
|
||
|
||
def generate() -> bytes: | ||
"""Generate a new Fernet key.""" | ||
key = cryptography.fernet.Fernet.generate_key() | ||
return key | ||
|
||
|
||
@app.command(name="encrypt") | ||
def encrypt_cli( | ||
key: str = typer.Argument(..., help="The Fernet key to use."), | ||
data: str = typer.Argument(..., help="The data to encrypt."), | ||
) -> None: | ||
"""Encrypt data using a Fernet key (CLI).""" | ||
typer.echo(encrypt(key, data)) | ||
|
||
|
||
def encrypt(key: str, data: str) -> str: | ||
"""Encrypt data using a Fernet key.""" | ||
fernet = cryptography.fernet.Fernet(key.encode()) | ||
encrypted_data = fernet.encrypt(data.encode()) | ||
return encrypted_data.decode() | ||
|
||
|
||
@app.command(name="decrypt") | ||
def decrypt_cli( | ||
key: str = typer.Argument(..., help="The Fernet key to use."), | ||
data: str = typer.Argument(..., help="The data to decrypt."), | ||
) -> None: | ||
"""Decrypt data using a Fernet key (CLI).""" | ||
typer.echo(decrypt(key, data)) | ||
|
||
|
||
def decrypt(key: str, data: str) -> str: | ||
"""Decrypt data using a Fernet key.""" | ||
fernet = cryptography.fernet.Fernet(key.encode()) | ||
decrypted_data = fernet.decrypt(data.encode()) | ||
return decrypted_data.decode() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
"""Module to generate API keys.""" | ||
|
||
import enum | ||
import json | ||
import pathlib | ||
import secrets | ||
from typing_extensions import Annotated | ||
from typing import Optional | ||
|
||
import typer | ||
|
||
from ai4_api_keys import fernet | ||
|
||
|
||
app = typer.Typer(help="AI4 API Keys management CLI.") | ||
|
||
|
||
class APILevels(str, enum.Enum): | ||
"""Levels of API keys.""" | ||
|
||
GOLD = "gold" | ||
SILVER = "silver" | ||
BRONZE = "bronze" | ||
PLATINUM = "platinum" | ||
|
||
|
||
@app.command(name="create") | ||
def create_cli( | ||
key_file: Annotated[ | ||
Optional[pathlib.Path], | ||
typer.Option("--key-file", "-k", help="Read fernet key from a file."), | ||
] = None, | ||
key: Annotated[ | ||
Optional[str], typer.Option("--key", "-K", help="Use a specific fernet key.") | ||
] = None, | ||
level: Annotated[ | ||
APILevels, | ||
typer.Option("--level", "-l", help="The level of the API key."), | ||
] = APILevels.BRONZE, | ||
scope: Annotated[ | ||
str, typer.Option("--scope", "-s", help="The scope of the API key.") | ||
] = "ai4eosc", | ||
) -> None: | ||
"""Create a new API key (CLI).""" | ||
if key_file and key: | ||
raise typer.BadParameter("Cannot use both --key-file and --key.") | ||
|
||
if key_file is not None: | ||
with open(key_file, "r") as f: | ||
key = f.read().strip() | ||
|
||
if key is None: | ||
raise typer.BadParameter("Either --key-file or --key must be provided.") | ||
|
||
typer.echo(create(key, scope, level)) | ||
|
||
|
||
def create(key: str, scope: str, level: APILevels) -> str: | ||
"""Create a new API key. | ||
:param key: The Fernet key to use. | ||
:param scope: The scope of the API key. | ||
:param level: The level of the API key. | ||
:return: The new API key. | ||
""" | ||
|
||
message = { | ||
"nonce": secrets.token_hex(8), | ||
"scope": scope, | ||
"level": level.value, | ||
} | ||
|
||
return fernet.encrypt(key, json.dumps(message)) | ||
|
||
|
||
@app.command(name="validate") | ||
def validate_cli( | ||
key_file: Annotated[ | ||
Optional[pathlib.Path], | ||
typer.Option("--key-file", "-k", help="Read fernet key from a file."), | ||
] = None, | ||
key: Annotated[ | ||
Optional[str], typer.Option("--key", "-K", help="Use a specific fernet key.") | ||
] = None, | ||
quiet: Annotated[ | ||
bool, typer.Option("--quiet", "-q", help="Do not print the result.") | ||
] = False, | ||
scope: str = typer.Argument("ai4eosc", help="The scope of the API key."), | ||
api_key: str = typer.Argument(..., help="The API key to validate."), | ||
) -> None: | ||
"""validate an API key (CLI).""" | ||
if key_file and key: | ||
raise typer.BadParameter("Cannot use both --key-file and --key.") | ||
|
||
if key_file is not None: | ||
with open(key_file, "r") as f: | ||
key = f.read().strip() | ||
|
||
if key is None: | ||
raise typer.BadParameter("Either --key-file or --key must be provided.") | ||
|
||
valid = validate(key, api_key, scope) | ||
|
||
if valid: | ||
if not quiet: | ||
typer.echo("API key is valid.") | ||
else: | ||
if not quiet: | ||
typer.echo("API key is invalid.") | ||
raise typer.Exit(code=1) | ||
|
||
|
||
def validate(key: str, api_key: str, scope: str) -> bool: | ||
"""validate an API key. | ||
:param key: The Fernet key to use. | ||
:param api_key: The API key to validate. | ||
:param scope: The scope of the API key. | ||
:return: Whether the API key is valid. | ||
""" | ||
|
||
try: | ||
decrypted = fernet.decrypt(key, api_key) | ||
except Exception: | ||
return False | ||
|
||
message = json.loads(decrypted) | ||
if message["scope"] != scope: | ||
return False | ||
return True |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import pytest | ||
|
||
from ai4_api_keys import fernet | ||
|
||
|
||
@pytest.fixture | ||
def key(): | ||
"""Return a Fernet key.""" | ||
return "D5muj8QWVvkzDLRA0iPITxjGGYF22U2AA6eopxIWX2M=" | ||
|
||
|
||
def test_generate_key(): | ||
"""Test the fernet.generate function.""" | ||
key = fernet.generate() | ||
assert key | ||
assert len(key) == 44 | ||
|
||
|
||
def test_encrypt_decrypt(key): | ||
"""Test the fernet.encrypt and fernet.decrypt functions.""" | ||
|
||
data = "Hello, World!" | ||
encrypted_data = fernet.encrypt(key, data) | ||
assert encrypted_data | ||
|
||
decrypted_data = fernet.decrypt(key, encrypted_data) | ||
assert decrypted_data == data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import pytest | ||
|
||
from ai4_api_keys import keys | ||
|
||
|
||
@pytest.fixture | ||
def key(): | ||
"""Return a Fernet key.""" | ||
return "D5muj8QWVvkzDLRA0iPITxjGGYF22U2AA6eopxIWX2M=" | ||
|
||
|
||
def test_create_key(key): | ||
"""Test the fernet.create function.""" | ||
|
||
scope = "ai4eosc" | ||
level = keys.APILevels.BRONZE | ||
|
||
new_key = keys.create(key, scope, level) | ||
assert new_key | ||
|
||
assert keys.validate(key, new_key, scope) | ||
|
||
|
||
@pytest.fixture() | ||
def valid_api_key(): | ||
"""Return a valid API key.""" | ||
return ("gAAAAABnDh1srDPSh7F3R8f3dhNVR1_t8pGX_pOc8RZRq0j_0UWIluMjxttgieXdfihMUChb" | ||
"smz5ByfRw4K3t_N8Nhp_pbsi7KbBFw9H-AK7qqMRAZvef527SEkHP-j0S8TLYoE93WD_PkqQI" | ||
"Oe4N0ShUnd8wHrSrI1QzOBlsWnzmPv3lkUV0uI=") | ||
|
||
|
||
def test_validate_key(key, valid_api_key): | ||
scope = "ai4eosc" | ||
|
||
assert keys.validate(key, valid_api_key, scope) | ||
|
||
|
||
def test_validate_key_invalid(key): | ||
scope = "ai4eosc" | ||
|
||
assert not keys.validate(key, "invalid_key", scope) | ||
|
||
|
||
def test_validate_key_invalid_scope(key, valid_api_key): | ||
scope = "invalid_scope" | ||
|
||
assert not keys.validate(key, valid_api_key, scope) |