sovereign 0.24.6__tar.gz → 0.25.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sovereign might be problematic. Click here for more details.
- {sovereign-0.24.6 → sovereign-0.25.0}/PKG-INFO +2 -1
- {sovereign-0.24.6 → sovereign-0.25.0}/pyproject.toml +6 -4
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/__init__.py +14 -22
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/configuration.py +10 -18
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/context.py +8 -6
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/schemas.py +39 -10
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/auth.py +7 -8
- sovereign-0.25.0/src/sovereign/utils/crypto/crypto.py +133 -0
- sovereign-0.25.0/src/sovereign/utils/crypto/suites/__init__.py +21 -0
- sovereign-0.25.0/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
- sovereign-0.25.0/src/sovereign/utils/crypto/suites/base_cipher.py +26 -0
- sovereign-0.25.0/src/sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
- sovereign-0.25.0/src/sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
- sovereign-0.25.0/src/sovereign/views/__init__.py +0 -0
- sovereign-0.25.0/src/sovereign/views/crypto.py +99 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/views/discovery.py +4 -8
- sovereign-0.24.6/src/sovereign/utils/crypto.py +0 -103
- sovereign-0.24.6/src/sovereign/views/crypto.py +0 -69
- {sovereign-0.24.6 → sovereign-0.25.0}/LICENSE.txt +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/README.md +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/app.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/config_loader.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/constants.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/discovery.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/error_info.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/logging/access_logger.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/logging/application_logger.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/logging/base_logger.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/logging/bootstrapper.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/logging/types.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/middlewares.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/modifiers/__init__.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/modifiers/lib.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/modifiers/test.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/response_class.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/server.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/sources/__init__.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/sources/file.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/sources/inline.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/sources/lib.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/sources/poller.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/static/sass/style.scss +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/static/style.css +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/statistics.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/templates/base.html +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/templates/err.html +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/templates/resources.html +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/templates/ul_filter.html +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/__init__.py +0 -0
- {sovereign-0.24.6/src/sovereign/views → sovereign-0.25.0/src/sovereign/utils/crypto}/__init__.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/dictupdate.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/eds.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/entry_point_loader.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/mock.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/resources.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/templates.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/timer.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/version_info.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/utils/weighted_clusters.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/views/admin.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/views/healthchecks.py +0 -0
- {sovereign-0.24.6 → sovereign-0.25.0}/src/sovereign/views/interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sovereign
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: Envoy Proxy control-plane written in Python
|
|
5
5
|
Home-page: https://pypi.org/project/sovereign/
|
|
6
6
|
License: Apache-2.0
|
|
@@ -34,6 +34,7 @@ Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
|
|
|
34
34
|
Requires-Dist: aiofiles (>=23.2.1,<24.0.0)
|
|
35
35
|
Requires-Dist: boto3 (>=1.28.62,<2.0.0) ; extra == "boto"
|
|
36
36
|
Requires-Dist: cachelib (>=0.10.2,<0.11.0)
|
|
37
|
+
Requires-Dist: cachetools (>=5.3.2,<6.0.0)
|
|
37
38
|
Requires-Dist: cashews[redis] (>=6.3.0,<7.0.0) ; extra == "caching"
|
|
38
39
|
Requires-Dist: croniter (>=1.4.1,<2.0.0)
|
|
39
40
|
Requires-Dist: cryptography (>=41.0.4,<42.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "sovereign"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.0"
|
|
4
4
|
description = "Envoy Proxy control-plane written in Python"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
packages = [
|
|
@@ -62,6 +62,7 @@ croniter = "^1.4.1"
|
|
|
62
62
|
cashews = {extras = ["redis"], version = "^6.3.0", optional = true}
|
|
63
63
|
redis = {version = "<= 5.0.0", optional = true}
|
|
64
64
|
httptools = {version = "^0.6.0", optional = true}
|
|
65
|
+
cachetools = "^5.3.2"
|
|
65
66
|
|
|
66
67
|
[tool.poetry.extras]
|
|
67
68
|
sentry = ["sentry-sdk"]
|
|
@@ -73,7 +74,7 @@ caching = ["cashews"]
|
|
|
73
74
|
httptools = ["httptools"]
|
|
74
75
|
|
|
75
76
|
[tool.poetry.group.dev.dependencies]
|
|
76
|
-
pytest = "^
|
|
77
|
+
pytest = "^7.0.0"
|
|
77
78
|
"ruamel.yaml" = "^0.17.32"
|
|
78
79
|
pytest-asyncio = "^0.20.0"
|
|
79
80
|
pytest-mock = "^3.10.0"
|
|
@@ -82,10 +83,10 @@ pytest-timeout = "^2.1.0"
|
|
|
82
83
|
coverage = "^7.3.1"
|
|
83
84
|
invoke = "^2.2.0"
|
|
84
85
|
pylint = "^2.17.6"
|
|
85
|
-
tavern = "^
|
|
86
|
+
tavern = "^2.9.2"
|
|
86
87
|
twine = "^4.0.2"
|
|
87
88
|
poethepoet = "^0.23.0"
|
|
88
|
-
mypy = "^1.
|
|
89
|
+
mypy = "^1.8.0"
|
|
89
90
|
black = "^23.9.1"
|
|
90
91
|
freezegun = "^1.2.2"
|
|
91
92
|
moto = "^4.2.4"
|
|
@@ -98,6 +99,7 @@ types-PyYAML = "^6.0.12.12"
|
|
|
98
99
|
pylama = "^8.4.1"
|
|
99
100
|
prospector = "^1.10.2"
|
|
100
101
|
toml = "^0.10.2"
|
|
102
|
+
types-cachetools = "^5.3.0.7"
|
|
101
103
|
|
|
102
104
|
[tool.poe.tasks]
|
|
103
105
|
types = { cmd = "mypy src/sovereign --ignore-missing-imports", help = "Check types with mypy" }
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from contextvars import ContextVar
|
|
3
|
-
from typing import Type, Any, Mapping
|
|
4
3
|
from importlib.metadata import version
|
|
4
|
+
from typing import Any, Mapping, Type
|
|
5
5
|
|
|
6
6
|
from fastapi.responses import JSONResponse
|
|
7
|
-
from starlette.templating import Jinja2Templates
|
|
8
7
|
from pydantic.error_wrappers import ValidationError
|
|
8
|
+
from starlette.templating import Jinja2Templates
|
|
9
9
|
|
|
10
|
-
from sovereign.schemas import (
|
|
11
|
-
SovereignAsgiConfig,
|
|
12
|
-
SovereignConfig,
|
|
13
|
-
SovereignConfigv2,
|
|
14
|
-
)
|
|
15
10
|
from sovereign import config_loader
|
|
11
|
+
from sovereign.context import TemplateContext
|
|
16
12
|
from sovereign.logging.bootstrapper import LoggerBootstrapper
|
|
13
|
+
from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
|
|
14
|
+
from sovereign.sources import SourcePoller
|
|
17
15
|
from sovereign.statistics import configure_statsd
|
|
16
|
+
from sovereign.utils.crypto.crypto import CipherContainer
|
|
18
17
|
from sovereign.utils.dictupdate import merge # type: ignore
|
|
19
|
-
from sovereign.sources import SourcePoller
|
|
20
|
-
from sovereign.context import TemplateContext
|
|
21
|
-
from sovereign.utils.crypto import CipherContainer, create_cipher_suite
|
|
22
18
|
from sovereign.utils.resources import get_package_file
|
|
23
19
|
|
|
24
|
-
|
|
25
20
|
json_response_class: Type[JSONResponse] = JSONResponse
|
|
26
21
|
try:
|
|
27
22
|
import orjson
|
|
@@ -69,6 +64,8 @@ asgi_config = SovereignAsgiConfig()
|
|
|
69
64
|
XDS_TEMPLATES = config.xds_templates()
|
|
70
65
|
|
|
71
66
|
logs = LoggerBootstrapper(config)
|
|
67
|
+
application_logger = logs.application_logger.logger
|
|
68
|
+
|
|
72
69
|
stats = configure_statsd(config=config.statsd)
|
|
73
70
|
poller = SourcePoller(
|
|
74
71
|
sources=config.sources,
|
|
@@ -76,17 +73,13 @@ poller = SourcePoller(
|
|
|
76
73
|
node_match_key=config.matching.node_key,
|
|
77
74
|
source_match_key=config.matching.source_key,
|
|
78
75
|
source_refresh_rate=config.source_config.refresh_rate,
|
|
79
|
-
logger=
|
|
76
|
+
logger=application_logger,
|
|
80
77
|
stats=stats,
|
|
81
78
|
)
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
[
|
|
87
|
-
create_cipher_suite(key=key, logger=logs.application_logger.logger)
|
|
88
|
-
for key in encryption_keys
|
|
89
|
-
]
|
|
80
|
+
encryption_configs = config.authentication.encryption_configs
|
|
81
|
+
server_cipher_container = CipherContainer.from_encryption_configs(
|
|
82
|
+
encryption_configs, logger=application_logger
|
|
90
83
|
)
|
|
91
84
|
|
|
92
85
|
template_context = TemplateContext(
|
|
@@ -96,9 +89,8 @@ template_context = TemplateContext(
|
|
|
96
89
|
refresh_retry_interval_secs=config.template_context.refresh_retry_interval_secs,
|
|
97
90
|
configured_context=config.template_context.context,
|
|
98
91
|
poller=poller,
|
|
99
|
-
encryption_suite=
|
|
100
|
-
|
|
101
|
-
logger=logs.application_logger.logger,
|
|
92
|
+
encryption_suite=server_cipher_container,
|
|
93
|
+
logger=application_logger,
|
|
102
94
|
stats=stats,
|
|
103
95
|
)
|
|
104
96
|
poller.lazy_load_modifiers(config.modifiers)
|
|
@@ -3,17 +3,13 @@ from typing import Any, Mapping
|
|
|
3
3
|
|
|
4
4
|
from pydantic.error_wrappers import ValidationError
|
|
5
5
|
|
|
6
|
-
from sovereign
|
|
7
|
-
|
|
8
|
-
SovereignConfig,
|
|
9
|
-
SovereignConfigv2,
|
|
10
|
-
)
|
|
6
|
+
from sovereign import config_loader
|
|
7
|
+
from sovereign.context import TemplateContext
|
|
11
8
|
from sovereign.logging.bootstrapper import LoggerBootstrapper
|
|
12
|
-
from sovereign.
|
|
9
|
+
from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
|
|
13
10
|
from sovereign.sources import SourcePoller
|
|
14
|
-
from sovereign.
|
|
15
|
-
from sovereign.utils.crypto import CipherContainer
|
|
16
|
-
from sovereign import config_loader
|
|
11
|
+
from sovereign.statistics import configure_statsd
|
|
12
|
+
from sovereign.utils.crypto.crypto import CipherContainer
|
|
17
13
|
from sovereign.utils.dictupdate import merge # type: ignore
|
|
18
14
|
|
|
19
15
|
|
|
@@ -40,13 +36,10 @@ XDS_TEMPLATES = CONFIG.xds_templates()
|
|
|
40
36
|
|
|
41
37
|
LOGS = LoggerBootstrapper(CONFIG)
|
|
42
38
|
STATS = configure_statsd(config=CONFIG.statsd)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
create_cipher_suite(key=key, logger=LOGS.application_logger.logger)
|
|
48
|
-
for key in ENCRYPTION_KEYS
|
|
49
|
-
]
|
|
39
|
+
ENCRYPTION_CONFIGS = CONFIG.authentication.encryption_configs
|
|
40
|
+
CIPHER_CONTAINER = CipherContainer.from_encryption_configs(
|
|
41
|
+
encryption_configs=ENCRYPTION_CONFIGS,
|
|
42
|
+
logger=LOGS.application_logger.logger,
|
|
50
43
|
)
|
|
51
44
|
|
|
52
45
|
POLLER = SourcePoller(
|
|
@@ -65,8 +58,7 @@ TEMPLATE_CONTEXT = TemplateContext(
|
|
|
65
58
|
refresh_retry_interval_secs=CONFIG.template_context.refresh_retry_interval_secs,
|
|
66
59
|
configured_context=CONFIG.template_context.context,
|
|
67
60
|
poller=POLLER,
|
|
68
|
-
encryption_suite=
|
|
69
|
-
disabled_suite=create_cipher_suite(b"", LOGS.application_logger.logger),
|
|
61
|
+
encryption_suite=CIPHER_CONTAINER,
|
|
70
62
|
logger=LOGS.application_logger.logger,
|
|
71
63
|
stats=STATS,
|
|
72
64
|
)
|
|
@@ -16,9 +16,10 @@ from fastapi import HTTPException
|
|
|
16
16
|
from structlog.stdlib import BoundLogger
|
|
17
17
|
|
|
18
18
|
from sovereign.config_loader import Loadable
|
|
19
|
-
from sovereign.schemas import DiscoveryRequest, XdsTemplate
|
|
19
|
+
from sovereign.schemas import DiscoveryRequest, EncryptionConfig, XdsTemplate
|
|
20
20
|
from sovereign.sources import SourcePoller
|
|
21
|
-
from sovereign.utils.crypto import CipherContainer
|
|
21
|
+
from sovereign.utils.crypto.crypto import CipherContainer
|
|
22
|
+
from sovereign.utils.crypto.suites import EncryptionType
|
|
22
23
|
from sovereign.utils.timer import poll_forever, poll_forever_cron
|
|
23
24
|
|
|
24
25
|
|
|
@@ -38,7 +39,6 @@ class TemplateContext:
|
|
|
38
39
|
configured_context: Dict[str, Loadable],
|
|
39
40
|
poller: SourcePoller,
|
|
40
41
|
encryption_suite: Optional[CipherContainer],
|
|
41
|
-
disabled_suite: CipherSuite,
|
|
42
42
|
logger: BoundLogger,
|
|
43
43
|
stats: Any,
|
|
44
44
|
) -> None:
|
|
@@ -48,8 +48,7 @@ class TemplateContext:
|
|
|
48
48
|
self.refresh_num_retries = refresh_num_retries
|
|
49
49
|
self.refresh_retry_interval_secs = refresh_retry_interval_secs
|
|
50
50
|
self.configured_context = configured_context
|
|
51
|
-
self.crypto = encryption_suite
|
|
52
|
-
self.disabled_suite = disabled_suite
|
|
51
|
+
self.crypto: CipherContainer | None = encryption_suite
|
|
53
52
|
self.logger = logger
|
|
54
53
|
self.stats = stats
|
|
55
54
|
# initial load
|
|
@@ -164,7 +163,10 @@ class TemplateContext:
|
|
|
164
163
|
node_value=self.poller.extract_node_key(request.node),
|
|
165
164
|
)
|
|
166
165
|
if request.hide_private_keys:
|
|
167
|
-
ret["crypto"] =
|
|
166
|
+
ret["crypto"] = CipherContainer.from_encryption_configs(
|
|
167
|
+
encryption_configs=[EncryptionConfig("", EncryptionType.DISABLED)],
|
|
168
|
+
logger=self.logger,
|
|
169
|
+
)
|
|
168
170
|
if not template.is_python_source:
|
|
169
171
|
keys_to_remove = self.unused_variables(list(ret), template.jinja_variables)
|
|
170
172
|
for key in keys_to_remove:
|
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
from os import getenv
|
|
2
|
-
import warnings
|
|
3
1
|
import multiprocessing
|
|
2
|
+
import warnings
|
|
4
3
|
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
from enum import Enum
|
|
6
|
+
from os import getenv
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
9
|
+
|
|
10
|
+
from croniter import CroniterBadCronError, croniter
|
|
11
|
+
from fastapi.responses import JSONResponse
|
|
12
|
+
from jinja2 import Template, meta
|
|
6
13
|
from pydantic import (
|
|
7
14
|
BaseModel,
|
|
8
|
-
Field,
|
|
9
15
|
BaseSettings,
|
|
16
|
+
Field,
|
|
10
17
|
SecretStr,
|
|
11
|
-
validator,
|
|
12
18
|
root_validator,
|
|
19
|
+
validator,
|
|
13
20
|
)
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from fastapi.responses import JSONResponse
|
|
18
|
-
from sovereign.config_loader import jinja_env, Serialization, Protocol, Loadable
|
|
21
|
+
|
|
22
|
+
from sovereign.config_loader import Loadable, Protocol, Serialization, jinja_env
|
|
23
|
+
from sovereign.utils.crypto.suites import EncryptionType
|
|
19
24
|
from sovereign.utils.version_info import compute_hash
|
|
20
|
-
from croniter import croniter, CroniterBadCronError
|
|
21
25
|
|
|
22
26
|
missing_arguments = {"missing", "positional", "arguments:"}
|
|
23
27
|
|
|
@@ -494,11 +498,36 @@ class NodeMatching(BaseSettings):
|
|
|
494
498
|
}
|
|
495
499
|
|
|
496
500
|
|
|
501
|
+
@dataclass
|
|
502
|
+
class EncryptionConfig:
|
|
503
|
+
encryption_key: str
|
|
504
|
+
encryption_type: EncryptionType
|
|
505
|
+
|
|
506
|
+
|
|
497
507
|
class AuthConfiguration(BaseSettings):
|
|
498
508
|
enabled: bool = False
|
|
499
509
|
auth_passwords: SecretStr = SecretStr("")
|
|
500
510
|
encryption_key: SecretStr = SecretStr("")
|
|
501
511
|
|
|
512
|
+
@staticmethod
|
|
513
|
+
def _create_encryption_config(encryption_key_setting: str) -> EncryptionConfig:
|
|
514
|
+
encryption_key, _, encryption_type_raw = encryption_key_setting.partition(":")
|
|
515
|
+
if encryption_type_raw:
|
|
516
|
+
encryption_type = EncryptionType(encryption_type_raw)
|
|
517
|
+
else:
|
|
518
|
+
encryption_type = EncryptionType.FERNET
|
|
519
|
+
return EncryptionConfig(encryption_key, encryption_type)
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def encryption_configs(self) -> tuple[EncryptionConfig, ...]:
|
|
523
|
+
secret_values = self.encryption_key.get_secret_value().split()
|
|
524
|
+
|
|
525
|
+
configs = tuple(
|
|
526
|
+
self._create_encryption_config(encryption_key_setting)
|
|
527
|
+
for encryption_key_setting in secret_values
|
|
528
|
+
)
|
|
529
|
+
return configs
|
|
530
|
+
|
|
502
531
|
class Config:
|
|
503
532
|
fields = {
|
|
504
533
|
"enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from fastapi.exceptions import HTTPException
|
|
2
1
|
from cryptography.fernet import InvalidToken
|
|
3
|
-
from
|
|
2
|
+
from fastapi.exceptions import HTTPException
|
|
3
|
+
|
|
4
|
+
from sovereign import config, server_cipher_container, stats
|
|
4
5
|
from sovereign.schemas import DiscoveryRequest
|
|
5
6
|
|
|
6
7
|
AUTH_ENABLED = config.authentication.enabled
|
|
@@ -8,7 +9,7 @@ AUTH_ENABLED = config.authentication.enabled
|
|
|
8
9
|
|
|
9
10
|
def validate_authentication_string(s: str) -> bool:
|
|
10
11
|
try:
|
|
11
|
-
password =
|
|
12
|
+
password = server_cipher_container.decrypt(s)
|
|
12
13
|
except Exception:
|
|
13
14
|
stats.increment("discovery.auth.failed")
|
|
14
15
|
raise
|
|
@@ -23,12 +24,10 @@ def validate_authentication_string(s: str) -> bool:
|
|
|
23
24
|
def authenticate(request: DiscoveryRequest) -> None:
|
|
24
25
|
if not AUTH_ENABLED:
|
|
25
26
|
return
|
|
26
|
-
if not
|
|
27
|
+
if not server_cipher_container.key_available:
|
|
27
28
|
raise RuntimeError(
|
|
28
|
-
"No
|
|
29
|
-
"
|
|
30
|
-
"See https://vsyrakis.bitbucket.io/sovereign/docs/html/guides/encryption.html "
|
|
31
|
-
"for more details"
|
|
29
|
+
"No encryption key loaded, and auth is enabled. "
|
|
30
|
+
"An encryption key must be provided via SOVEREIGN_ENCRYPTION_KEY. "
|
|
32
31
|
)
|
|
33
32
|
try:
|
|
34
33
|
encrypted_auth = request.node.metadata["auth"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Literal, Self, Sequence
|
|
2
|
+
|
|
3
|
+
from cachetools import TTLCache, cached
|
|
4
|
+
from fastapi.exceptions import HTTPException
|
|
5
|
+
from structlog.stdlib import BoundLogger
|
|
6
|
+
from typing_extensions import TypedDict
|
|
7
|
+
|
|
8
|
+
from sovereign.schemas import EncryptionConfig
|
|
9
|
+
from sovereign.utils.crypto.suites import (
|
|
10
|
+
AESGCMCipher,
|
|
11
|
+
CipherSuite,
|
|
12
|
+
DisabledCipher,
|
|
13
|
+
EncryptionType,
|
|
14
|
+
FernetCipher,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EncryptOutput(TypedDict):
|
|
19
|
+
encrypted_data: str
|
|
20
|
+
encryption_type: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DecryptOutput(TypedDict):
|
|
24
|
+
decrypted_data: str
|
|
25
|
+
encryption_type: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CipherContainer:
|
|
29
|
+
"""
|
|
30
|
+
Object which intercepts encrypt/decryptions
|
|
31
|
+
Tries to decrypt data using the ciphers provided in order
|
|
32
|
+
Encrypts with the first suite available.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, suites: Sequence[CipherSuite], logger: BoundLogger) -> None:
|
|
36
|
+
self.suites: Sequence[CipherSuite] = suites
|
|
37
|
+
self.logger = logger
|
|
38
|
+
|
|
39
|
+
def encrypt(self, data: str) -> EncryptOutput:
|
|
40
|
+
try:
|
|
41
|
+
# Use the first cipher suite to encrypt the data
|
|
42
|
+
encrypted_data = self.suites[0].encrypt(data)
|
|
43
|
+
return {
|
|
44
|
+
"encrypted_data": encrypted_data,
|
|
45
|
+
"encryption_type": str(self.suites[0]),
|
|
46
|
+
}
|
|
47
|
+
# pylint: disable=broad-except
|
|
48
|
+
except Exception as e:
|
|
49
|
+
self.logger.exception(str(e))
|
|
50
|
+
# TODO: defer this http error to later, return a normal error here
|
|
51
|
+
raise HTTPException(status_code=400, detail="Encryption failed")
|
|
52
|
+
|
|
53
|
+
def decrypt_with_type(self, data: str) -> DecryptOutput:
|
|
54
|
+
return self._decrypt(data)
|
|
55
|
+
|
|
56
|
+
def decrypt(self, data: str) -> str:
|
|
57
|
+
return self._decrypt(data)["decrypted_data"]
|
|
58
|
+
|
|
59
|
+
@cached(cache=TTLCache(maxsize=128, ttl=600))
|
|
60
|
+
def _decrypt(self, data: str) -> DecryptOutput:
|
|
61
|
+
try:
|
|
62
|
+
for suite in self.suites:
|
|
63
|
+
try:
|
|
64
|
+
decrypted_data = suite.decrypt(data)
|
|
65
|
+
return {
|
|
66
|
+
"decrypted_data": decrypted_data,
|
|
67
|
+
"encryption_type": str(suite),
|
|
68
|
+
}
|
|
69
|
+
# pylint: disable=broad-except
|
|
70
|
+
except Exception as e:
|
|
71
|
+
self.logger.exception(str(e))
|
|
72
|
+
self.logger.debug(f"Failed to decrypt with suite {suite}")
|
|
73
|
+
else:
|
|
74
|
+
raise ValueError("Could not decrypt with any suite")
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self.logger.exception(str(e))
|
|
77
|
+
# TODO: defer this http error to later, return a normal error here
|
|
78
|
+
raise HTTPException(status_code=400, detail="Decryption failed")
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def key_available(self) -> bool:
|
|
82
|
+
return self.suites[0].key_available
|
|
83
|
+
|
|
84
|
+
AVAILABLE_CIPHERS: dict[EncryptionType | Literal["default"], type[CipherSuite]] = {
|
|
85
|
+
EncryptionType.DISABLED: DisabledCipher,
|
|
86
|
+
EncryptionType.AESGCM: AESGCMCipher,
|
|
87
|
+
EncryptionType.FERNET: FernetCipher,
|
|
88
|
+
"default": FernetCipher,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def get_cipher_suite(cls, encryption_type: EncryptionType) -> type[CipherSuite]:
|
|
93
|
+
SelectedCipher = cls.AVAILABLE_CIPHERS.get(
|
|
94
|
+
encryption_type, cls.AVAILABLE_CIPHERS["default"]
|
|
95
|
+
)
|
|
96
|
+
return SelectedCipher
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def create_cipher_suite(
|
|
100
|
+
cls,
|
|
101
|
+
encryption_type: EncryptionType,
|
|
102
|
+
key: str,
|
|
103
|
+
logger: BoundLogger,
|
|
104
|
+
) -> CipherSuite:
|
|
105
|
+
kwargs = {
|
|
106
|
+
"secret_key": key,
|
|
107
|
+
}
|
|
108
|
+
try:
|
|
109
|
+
SelectedCipher = cls.get_cipher_suite(encryption_type)
|
|
110
|
+
return SelectedCipher(**kwargs)
|
|
111
|
+
except TypeError:
|
|
112
|
+
pass
|
|
113
|
+
except ValueError as e:
|
|
114
|
+
if key not in (b"", ""):
|
|
115
|
+
logger.error(
|
|
116
|
+
f"Encryption key was provided, but appears to be invalid: {repr(e)}"
|
|
117
|
+
)
|
|
118
|
+
return DisabledCipher(**kwargs)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_encryption_configs(
|
|
122
|
+
cls, encryption_configs: Sequence[EncryptionConfig], logger: BoundLogger
|
|
123
|
+
) -> Self:
|
|
124
|
+
cipher_suites: list[CipherSuite] = []
|
|
125
|
+
for encryption_config in encryption_configs:
|
|
126
|
+
cipher_suites.append(
|
|
127
|
+
cls.create_cipher_suite(
|
|
128
|
+
key=encryption_config.encryption_key,
|
|
129
|
+
encryption_type=encryption_config.encryption_type,
|
|
130
|
+
logger=logger,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
return cls(suites=cipher_suites, logger=logger)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from sovereign.utils.crypto.suites.aes_gcm_cipher import AESGCMCipher
|
|
4
|
+
from sovereign.utils.crypto.suites.base_cipher import CipherSuite
|
|
5
|
+
from sovereign.utils.crypto.suites.disabled_cipher import DisabledCipher
|
|
6
|
+
from sovereign.utils.crypto.suites.fernet_cipher import FernetCipher
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EncryptionType(StrEnum):
|
|
10
|
+
FERNET = "fernet"
|
|
11
|
+
AESGCM = "aesgcm"
|
|
12
|
+
DISABLED = "disabled"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AESGCMCipher",
|
|
17
|
+
"CipherSuite",
|
|
18
|
+
"DisabledCipher",
|
|
19
|
+
"FernetCipher",
|
|
20
|
+
"EncryptionType",
|
|
21
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
5
|
+
|
|
6
|
+
from .base_cipher import CipherSuite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AESGCMCipher(CipherSuite):
|
|
10
|
+
AUTHENTICATE_DATA = "sovereign".encode()
|
|
11
|
+
NONCE_LEN = 12
|
|
12
|
+
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return "aesgcm"
|
|
15
|
+
|
|
16
|
+
def __init__(self, secret_key: str):
|
|
17
|
+
self.secret_key = base64.urlsafe_b64decode(secret_key)
|
|
18
|
+
|
|
19
|
+
def encrypt(self, data: str) -> str:
|
|
20
|
+
aesgcm = AESGCM(self.secret_key)
|
|
21
|
+
nonce = os.urandom(self.NONCE_LEN)
|
|
22
|
+
ct = aesgcm.encrypt(nonce, data.encode(), self.AUTHENTICATE_DATA)
|
|
23
|
+
return base64.b64encode(nonce + ct).decode("utf-8")
|
|
24
|
+
|
|
25
|
+
def decrypt(self, data: str) -> str:
|
|
26
|
+
decoded_data = base64.b64decode(data)
|
|
27
|
+
nonce, ct = (
|
|
28
|
+
decoded_data[: self.NONCE_LEN],
|
|
29
|
+
decoded_data[self.NONCE_LEN :],
|
|
30
|
+
)
|
|
31
|
+
aesgcm = AESGCM(self.secret_key)
|
|
32
|
+
decrypted_output = aesgcm.decrypt(nonce, ct, self.AUTHENTICATE_DATA)
|
|
33
|
+
return decrypted_output.decode("utf-8")
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def key_available(self) -> bool:
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def generate_key(cls) -> bytes:
|
|
41
|
+
# Generate 256 bit length key
|
|
42
|
+
return base64.urlsafe_b64encode(os.urandom(32))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CipherSuite(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
8
|
+
...
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def encrypt(self, data: str) -> str:
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def decrypt(self, data: str) -> str:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def key_available(self) -> bool:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def generate_key(cls) -> bytes:
|
|
26
|
+
...
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .base_cipher import CipherSuite
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DisabledCipher(CipherSuite):
|
|
7
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
return "disabled"
|
|
12
|
+
|
|
13
|
+
def encrypt(self, _: str) -> str:
|
|
14
|
+
return "Unavailable (No Secret Key)"
|
|
15
|
+
|
|
16
|
+
def decrypt(self, _: str) -> str:
|
|
17
|
+
return "Unavailable (No Secret Key)"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def key_available(self) -> bool:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def generate_key(cls) -> bytes:
|
|
25
|
+
return b"Unavailable (No key to generate)"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from cryptography.fernet import Fernet
|
|
5
|
+
|
|
6
|
+
from .base_cipher import CipherSuite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FernetCipher(CipherSuite):
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
return "fernet"
|
|
12
|
+
|
|
13
|
+
def __init__(self, secret_key: str):
|
|
14
|
+
self.fernet = Fernet(secret_key)
|
|
15
|
+
|
|
16
|
+
def encrypt(self, data: str) -> str:
|
|
17
|
+
return self.fernet.encrypt(data.encode()).decode("utf-8")
|
|
18
|
+
|
|
19
|
+
def decrypt(self, data: str) -> str:
|
|
20
|
+
return self.fernet.decrypt(data).decode("utf-8")
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def key_available(self) -> bool:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def generate_key(cls) -> bytes:
|
|
28
|
+
# Generate 256 bit length key
|
|
29
|
+
return base64.urlsafe_b64encode(os.urandom(32))
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Body
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from sovereign import json_response_class, logs, server_cipher_container
|
|
8
|
+
from sovereign.schemas import EncryptionConfig
|
|
9
|
+
from sovereign.utils.crypto.crypto import CipherContainer
|
|
10
|
+
from sovereign.utils.crypto.suites import EncryptionType
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EncryptionRequest(BaseModel):
|
|
16
|
+
data: str = Field(..., title="Text to be encrypted", min_length=1, max_length=65535)
|
|
17
|
+
key: Optional[str] = Field(
|
|
18
|
+
None,
|
|
19
|
+
title="Optional encryption key to use to encrypt",
|
|
20
|
+
min_length=44,
|
|
21
|
+
max_length=44,
|
|
22
|
+
)
|
|
23
|
+
encryption_type: str = Field(default="fernet", title="Encryption type to be used")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DecryptionRequest(BaseModel):
|
|
27
|
+
data: str = Field(..., title="Text to be decrypted", min_length=1, max_length=65535)
|
|
28
|
+
key: str = Field(
|
|
29
|
+
...,
|
|
30
|
+
title="Encryption key to use to decrypt",
|
|
31
|
+
min_length=44,
|
|
32
|
+
max_length=44,
|
|
33
|
+
)
|
|
34
|
+
encryption_type: str = Field(default="fernet", title="Encryption type to be used")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DecryptableRequest(BaseModel):
|
|
38
|
+
data: str = Field(..., title="Text to be decrypted", min_length=1, max_length=65535)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.post(
|
|
42
|
+
"/decrypt",
|
|
43
|
+
summary="Decrypt provided encrypted data using a provided key",
|
|
44
|
+
response_class=json_response_class,
|
|
45
|
+
)
|
|
46
|
+
async def _decrypt(request: DecryptionRequest = Body(None)) -> dict[str, Any]:
|
|
47
|
+
user_cipher_container = CipherContainer.from_encryption_configs(
|
|
48
|
+
encryption_configs=[
|
|
49
|
+
EncryptionConfig(
|
|
50
|
+
encryption_key=request.key,
|
|
51
|
+
encryption_type=EncryptionType(request.encryption_type),
|
|
52
|
+
)
|
|
53
|
+
],
|
|
54
|
+
logger=logs.application_logger.logger,
|
|
55
|
+
)
|
|
56
|
+
return {**user_cipher_container.decrypt_with_type(request.data)}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.post(
|
|
60
|
+
"/encrypt",
|
|
61
|
+
summary="Encrypt provided data using this servers key or provided key",
|
|
62
|
+
response_class=json_response_class,
|
|
63
|
+
)
|
|
64
|
+
async def _encrypt(request: EncryptionRequest = Body(None)) -> dict[str, Any]:
|
|
65
|
+
if request.key:
|
|
66
|
+
user_cipher_container = CipherContainer.from_encryption_configs(
|
|
67
|
+
encryption_configs=[
|
|
68
|
+
EncryptionConfig(
|
|
69
|
+
encryption_key=request.key,
|
|
70
|
+
encryption_type=EncryptionType(request.encryption_type),
|
|
71
|
+
)
|
|
72
|
+
],
|
|
73
|
+
logger=logs.application_logger.logger,
|
|
74
|
+
)
|
|
75
|
+
return {**user_cipher_container.encrypt(request.data)}
|
|
76
|
+
return {**server_cipher_container.encrypt(request.data)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.post(
|
|
80
|
+
"/decryptable",
|
|
81
|
+
summary="Check whether data is decryptable by this server",
|
|
82
|
+
response_class=json_response_class,
|
|
83
|
+
)
|
|
84
|
+
async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse:
|
|
85
|
+
server_cipher_container.decrypt(request.data)
|
|
86
|
+
return json_response_class({})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.get(
|
|
90
|
+
"/generate_key",
|
|
91
|
+
summary="Generate a new asymmetric encryption key",
|
|
92
|
+
response_class=json_response_class,
|
|
93
|
+
)
|
|
94
|
+
def _generate_key(encryption_type: str = "fernet") -> Dict[str, str]:
|
|
95
|
+
cipher_suite = CipherContainer.get_cipher_suite(EncryptionType(encryption_type))
|
|
96
|
+
return {
|
|
97
|
+
"key": cipher_suite.generate_key().decode(),
|
|
98
|
+
"encryption_type": encryption_type,
|
|
99
|
+
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
from typing import Dict
|
|
2
2
|
|
|
3
3
|
from fastapi import Body, Header
|
|
4
|
-
from fastapi.routing import APIRouter
|
|
5
4
|
from fastapi.responses import Response
|
|
5
|
+
from fastapi.routing import APIRouter
|
|
6
6
|
|
|
7
|
-
from sovereign import discovery, logs
|
|
7
|
+
from sovereign import config, discovery, logs
|
|
8
|
+
from sovereign.schemas import DiscoveryRequest, DiscoveryResponse, ProcessedTemplate
|
|
8
9
|
from sovereign.utils.auth import authenticate
|
|
9
10
|
from sovereign.utils.version_info import compute_hash
|
|
10
|
-
from sovereign.schemas import (
|
|
11
|
-
DiscoveryRequest,
|
|
12
|
-
DiscoveryResponse,
|
|
13
|
-
ProcessedTemplate,
|
|
14
|
-
)
|
|
15
11
|
|
|
16
12
|
discovery_cache = config.discovery_cache
|
|
17
13
|
|
|
@@ -151,7 +147,7 @@ async def perform_discovery(
|
|
|
151
147
|
value=template,
|
|
152
148
|
expire=discovery_cache.ttl,
|
|
153
149
|
)
|
|
154
|
-
return template
|
|
150
|
+
return template
|
|
155
151
|
|
|
156
152
|
|
|
157
153
|
def not_modified(headers: Dict[str, str]) -> Response:
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
from functools import partial
|
|
2
|
-
from collections import namedtuple
|
|
3
|
-
from typing import Optional, List
|
|
4
|
-
from cryptography.fernet import Fernet, InvalidToken
|
|
5
|
-
from fastapi.exceptions import HTTPException
|
|
6
|
-
from structlog.stdlib import BoundLogger
|
|
7
|
-
|
|
8
|
-
CipherSuite = namedtuple("CipherSuite", "encrypt decrypt key_available")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CipherContainer:
|
|
12
|
-
"""
|
|
13
|
-
Object which intercepts encrypt/decryptions
|
|
14
|
-
Tries to decrypt data using the ciphers provided in order
|
|
15
|
-
Encrypts with the first suite available.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
def __init__(self, suites: List[CipherSuite]) -> None:
|
|
19
|
-
self.suites = suites
|
|
20
|
-
|
|
21
|
-
def encrypt(self, data: str, key: Optional[str] = None) -> str:
|
|
22
|
-
if key is not None:
|
|
23
|
-
return encrypt(Fernet(key.encode()), data)
|
|
24
|
-
return self.suites[0].encrypt(data) # type: ignore
|
|
25
|
-
|
|
26
|
-
def decrypt(self, data: str, key: Optional[str] = None) -> str:
|
|
27
|
-
if key is not None:
|
|
28
|
-
return decrypt(Fernet(key.encode()), data)
|
|
29
|
-
success = False
|
|
30
|
-
decrypted = None
|
|
31
|
-
error = ValueError("Unable to decrypt value, unknown error")
|
|
32
|
-
for suite in self.suites:
|
|
33
|
-
try:
|
|
34
|
-
decrypted = suite.decrypt(data)
|
|
35
|
-
success = True
|
|
36
|
-
break
|
|
37
|
-
except (InvalidToken, AttributeError, HTTPException) as e:
|
|
38
|
-
error = e # type: ignore
|
|
39
|
-
continue
|
|
40
|
-
if not success:
|
|
41
|
-
raise error
|
|
42
|
-
else:
|
|
43
|
-
return decrypted # type: ignore
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def key_available(self) -> bool:
|
|
47
|
-
return self.suites[0].key_available # type: ignore
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class DisabledSuite:
|
|
51
|
-
@staticmethod
|
|
52
|
-
def encrypt(_: bytes) -> bytes:
|
|
53
|
-
return b"Unavailable (No Secret Key)"
|
|
54
|
-
|
|
55
|
-
@staticmethod
|
|
56
|
-
def decrypt(*_: bytes) -> str:
|
|
57
|
-
return "Unavailable (No Secret Key)"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def create_cipher_suite(key: bytes, logger: BoundLogger) -> CipherSuite:
|
|
61
|
-
try:
|
|
62
|
-
fernet = Fernet(key)
|
|
63
|
-
return CipherSuite(partial(encrypt, fernet), partial(decrypt, fernet), True)
|
|
64
|
-
except TypeError:
|
|
65
|
-
pass
|
|
66
|
-
except ValueError as e:
|
|
67
|
-
if key not in (b"", ""):
|
|
68
|
-
logger.error(
|
|
69
|
-
f"Fernet key was provided, but appears to be invalid: {repr(e)}"
|
|
70
|
-
)
|
|
71
|
-
return CipherSuite(DisabledSuite.encrypt, DisabledSuite.decrypt, False)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def generate_key() -> str:
|
|
75
|
-
secret: bytes = Fernet.generate_key()
|
|
76
|
-
return secret.decode()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def encrypt(cipher_suite: Fernet, data: str, key: Optional[str] = None) -> str:
|
|
80
|
-
_local_cipher_suite = cipher_suite
|
|
81
|
-
if isinstance(key, str):
|
|
82
|
-
_local_cipher_suite = Fernet(key.encode())
|
|
83
|
-
try:
|
|
84
|
-
encrypted: bytes = _local_cipher_suite.encrypt(data.encode())
|
|
85
|
-
except (InvalidToken, AttributeError):
|
|
86
|
-
# TODO: defer this http error to later, return a normal error here
|
|
87
|
-
raise HTTPException(status_code=400, detail="Encryption failed")
|
|
88
|
-
return encrypted.decode()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def decrypt(cipher_suite: Fernet, data: str, key: Optional[str] = None) -> str:
|
|
92
|
-
_local_cipher_suite = cipher_suite
|
|
93
|
-
if key is not None:
|
|
94
|
-
_local_cipher_suite = Fernet(key.encode())
|
|
95
|
-
try:
|
|
96
|
-
decrypted = _local_cipher_suite.decrypt(data.encode())
|
|
97
|
-
except (InvalidToken, AttributeError):
|
|
98
|
-
# TODO: defer this http error to later, return a normal error here
|
|
99
|
-
raise HTTPException(status_code=400, detail="Decryption failed")
|
|
100
|
-
if isinstance(decrypted, bytes):
|
|
101
|
-
return decrypted.decode()
|
|
102
|
-
else:
|
|
103
|
-
return decrypted
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
from typing import Dict
|
|
2
|
-
from pydantic import BaseModel, Field
|
|
3
|
-
from fastapi import APIRouter, Body
|
|
4
|
-
from fastapi.responses import JSONResponse
|
|
5
|
-
from sovereign import json_response_class, cipher_suite
|
|
6
|
-
from sovereign.utils.crypto import generate_key
|
|
7
|
-
|
|
8
|
-
router = APIRouter()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class EncryptionRequest(BaseModel):
|
|
12
|
-
data: str = Field(..., title="Text to be encrypted", min_length=1, max_length=65535)
|
|
13
|
-
key: str = Field(
|
|
14
|
-
None,
|
|
15
|
-
title="Optional Fernet encryption key to use to encrypt",
|
|
16
|
-
min_length=44,
|
|
17
|
-
max_length=44,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class DecryptionRequest(BaseModel):
|
|
22
|
-
data: str = Field(..., title="Text to be decrypted", min_length=1, max_length=65535)
|
|
23
|
-
key: str = Field(
|
|
24
|
-
...,
|
|
25
|
-
title="Fernet encryption key to use to decrypt",
|
|
26
|
-
min_length=44,
|
|
27
|
-
max_length=44,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class DecryptableRequest(BaseModel):
|
|
32
|
-
data: str = Field(..., title="Text to be decrypted", min_length=1, max_length=65535)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@router.post(
|
|
36
|
-
"/decrypt",
|
|
37
|
-
summary="Decrypt provided encrypted data using a provided key",
|
|
38
|
-
response_class=json_response_class,
|
|
39
|
-
)
|
|
40
|
-
async def _decrypt(request: DecryptionRequest = Body(None)) -> Dict[str, str]:
|
|
41
|
-
return {"result": cipher_suite.decrypt(request.data, request.key)}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@router.post(
|
|
45
|
-
"/encrypt",
|
|
46
|
-
summary="Encrypt provided data using this servers key",
|
|
47
|
-
response_class=json_response_class,
|
|
48
|
-
)
|
|
49
|
-
async def _encrypt(request: EncryptionRequest = Body(None)) -> Dict[str, str]:
|
|
50
|
-
return {"result": cipher_suite.encrypt(data=request.data, key=request.key)}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@router.post(
|
|
54
|
-
"/decryptable",
|
|
55
|
-
summary="Check whether data is decryptable by this server",
|
|
56
|
-
response_class=json_response_class,
|
|
57
|
-
)
|
|
58
|
-
async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse:
|
|
59
|
-
cipher_suite.decrypt(request.data)
|
|
60
|
-
return json_response_class({})
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@router.get(
|
|
64
|
-
"/generate_key",
|
|
65
|
-
summary="Generate a new asymmetric encryption key",
|
|
66
|
-
response_class=json_response_class,
|
|
67
|
-
)
|
|
68
|
-
def _generate_key() -> Dict[str, str]:
|
|
69
|
-
return {"result": generate_key()}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sovereign-0.24.6/src/sovereign/views → sovereign-0.25.0/src/sovereign/utils/crypto}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|