sovereign 0.24.6__py3-none-any.whl → 0.25.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -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=logs.application_logger.logger,
76
+ logger=application_logger,
80
77
  stats=stats,
81
78
  )
82
79
 
83
- fernet_keys = config.authentication.encryption_key
84
- encryption_keys = fernet_keys.get_secret_value().encode().split()
85
- cipher_suite = CipherContainer(
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=cipher_suite,
100
- disabled_suite=create_cipher_suite(b"", logs.application_logger.logger),
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.schemas import (
7
- SovereignAsgiConfig,
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.statistics import configure_statsd
9
+ from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
13
10
  from sovereign.sources import SourcePoller
14
- from sovereign.context import TemplateContext
15
- from sovereign.utils.crypto import CipherContainer, create_cipher_suite
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
- FERNET_KEYS = CONFIG.authentication.encryption_key
44
- ENCRYPTION_KEYS = FERNET_KEYS.get_secret_value().encode().split()
45
- CIPHER_SUITE = CipherContainer(
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=CIPHER_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
  )
sovereign/context.py CHANGED
@@ -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, CipherSuite
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"] = self.disabled_suite
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:
sovereign/schemas.py CHANGED
@@ -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
- from typing import List, Any, Dict, Union, Optional, Tuple, Type
15
- from types import ModuleType
16
- from jinja2 import meta, Template
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"},
sovereign/utils/auth.py CHANGED
@@ -1,6 +1,7 @@
1
- from fastapi.exceptions import HTTPException
2
1
  from cryptography.fernet import InvalidToken
3
- from sovereign import config, stats, cipher_suite
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 = cipher_suite.decrypt(s)
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 cipher_suite.key_available:
27
+ if not server_cipher_container.key_available:
27
28
  raise RuntimeError(
28
- "No Fernet key loaded, and auth is enabled. "
29
- "A fernet key must be provided via SOVEREIGN_ENCRYPTION_KEY. "
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"]
File without changes
@@ -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))
sovereign/views/crypto.py CHANGED
@@ -1,31 +1,37 @@
1
- from typing import Dict
2
- from pydantic import BaseModel, Field
1
+ from typing import Any, Dict, Optional
2
+
3
3
  from fastapi import APIRouter, Body
4
4
  from fastapi.responses import JSONResponse
5
- from sovereign import json_response_class, cipher_suite
6
- from sovereign.utils.crypto import generate_key
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
7
11
 
8
12
  router = APIRouter()
9
13
 
10
14
 
11
15
  class EncryptionRequest(BaseModel):
12
16
  data: str = Field(..., title="Text to be encrypted", min_length=1, max_length=65535)
13
- key: str = Field(
17
+ key: Optional[str] = Field(
14
18
  None,
15
- title="Optional Fernet encryption key to use to encrypt",
19
+ title="Optional encryption key to use to encrypt",
16
20
  min_length=44,
17
21
  max_length=44,
18
22
  )
23
+ encryption_type: str = Field(default="fernet", title="Encryption type to be used")
19
24
 
20
25
 
21
26
  class DecryptionRequest(BaseModel):
22
27
  data: str = Field(..., title="Text to be decrypted", min_length=1, max_length=65535)
23
28
  key: str = Field(
24
29
  ...,
25
- title="Fernet encryption key to use to decrypt",
30
+ title="Encryption key to use to decrypt",
26
31
  min_length=44,
27
32
  max_length=44,
28
33
  )
34
+ encryption_type: str = Field(default="fernet", title="Encryption type to be used")
29
35
 
30
36
 
31
37
  class DecryptableRequest(BaseModel):
@@ -37,17 +43,37 @@ class DecryptableRequest(BaseModel):
37
43
  summary="Decrypt provided encrypted data using a provided key",
38
44
  response_class=json_response_class,
39
45
  )
40
- async def _decrypt(request: DecryptionRequest = Body(None)) -> Dict[str, str]:
41
- return {"result": cipher_suite.decrypt(request.data, request.key)}
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)}
42
57
 
43
58
 
44
59
  @router.post(
45
60
  "/encrypt",
46
- summary="Encrypt provided data using this servers key",
61
+ summary="Encrypt provided data using this servers key or provided key",
47
62
  response_class=json_response_class,
48
63
  )
49
- async def _encrypt(request: EncryptionRequest = Body(None)) -> Dict[str, str]:
50
- return {"result": cipher_suite.encrypt(data=request.data, key=request.key)}
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)}
51
77
 
52
78
 
53
79
  @router.post(
@@ -56,7 +82,7 @@ async def _encrypt(request: EncryptionRequest = Body(None)) -> Dict[str, str]:
56
82
  response_class=json_response_class,
57
83
  )
58
84
  async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse:
59
- cipher_suite.decrypt(request.data)
85
+ server_cipher_container.decrypt(request.data)
60
86
  return json_response_class({})
61
87
 
62
88
 
@@ -65,5 +91,9 @@ async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse
65
91
  summary="Generate a new asymmetric encryption key",
66
92
  response_class=json_response_class,
67
93
  )
68
- def _generate_key() -> Dict[str, str]:
69
- return {"result": generate_key()}
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, config
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 # type: ignore[no-any-return]
150
+ return template
155
151
 
156
152
 
157
153
  def not_modified(headers: Dict[str, str]) -> Response:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 0.24.6
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,9 +1,9 @@
1
- sovereign/__init__.py,sha256=IJscxrxp9cWH85Ee1x_5joMR0b-31l1-sXi6E6wgxOI,3436
1
+ sovereign/__init__.py,sha256=a8bCatvhtIQEw3_gZCdAbzXKP3fwA1AtyPlez-wQvGE,3274
2
2
  sovereign/app.py,sha256=uozeEQJjefBUV6dZfIooTBJgDhEE4bd2ozRxWVdVMK4,4085
3
3
  sovereign/config_loader.py,sha256=ZLKlyuvJ8Wqhsg6xZI-zp_5ABslB3Tc9zEdpds7-d7Q,6349
4
- sovereign/configuration.py,sha256=rpZWNgn5ySNO1bdHlUix02yzo3wOefJaWkif5_4HjyE,2665
4
+ sovereign/configuration.py,sha256=thiNe_6x3wR8wc4Jy0aeBlQe3kSsIqpTOIlOoxp5UJg,2497
5
5
  sovereign/constants.py,sha256=qdWD1lTvkaW5JGF7TmZhfksQHlRAJFVqbG7v6JQA9k8,46
6
- sovereign/context.py,sha256=dsKEUK7yrOpXGMY_YzxuaoBDmGXIh0fvtVBoh3Dd8SY,6509
6
+ sovereign/context.py,sha256=gY9q6rjfPaGKJzCM4BavaFHvRlmYR08TkvutK4wM0XA,6675
7
7
  sovereign/discovery.py,sha256=TPecAoUDHx6SbS5hE2K73uEgDro0Uzz3qsvOQMpFtGI,5899
8
8
  sovereign/error_info.py,sha256=r2KXBYq9Fo7AI2pmIpATWFm0pykr2MqfrKH0WWW5Sfk,1488
9
9
  sovereign/logging/access_logger.py,sha256=JMMzQvi7doFJGA__YYqyasdfAT9W31Ycu_oZ2ovAMis,2565
@@ -16,7 +16,7 @@ sovereign/modifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
16
16
  sovereign/modifiers/lib.py,sha256=DbXsxrrjnFE4Y7rbwpeiM5tS5w5NBwSdYH58AtDTP0I,2884
17
17
  sovereign/modifiers/test.py,sha256=7_c2hWXn_sYJ6997N1_uSWtClOikcOzu1yRCY56-l-4,361
18
18
  sovereign/response_class.py,sha256=beMAFV-4L6DwyWzJzy71GkEW4gb7fzH1jd8-Tul13cU,427
19
- sovereign/schemas.py,sha256=ylUlHeaLzpsNJnOQCfdcVKt5kaNh0oo8nkDwRuchIWk,27775
19
+ sovereign/schemas.py,sha256=JsL_qL5dMhiAH2FB9UHb9nkRT4sUcT5qwuk129pNm3o,28718
20
20
  sovereign/server.py,sha256=z8Uz1UYIZix0S40Srk774WIMDN2jl2SozO8irib0wc4,1402
21
21
  sovereign/sources/__init__.py,sha256=g9hEpFk8j5i1ApHQpbc9giTyJW41Ppgsqv5P9zGxOJk,78
22
22
  sovereign/sources/file.py,sha256=A4UWoRU39v2Ex5Mtdl_uw53iMkslYylF4CiiwW7LOpk,689
@@ -31,8 +31,14 @@ sovereign/templates/err.html,sha256=a3cEzOqyqWOIe3YxfTEjkxbTfxBxq1knD6GwzEFljfs,
31
31
  sovereign/templates/resources.html,sha256=NnrnamWg_vJjY88efsMcjNsldg-K9TZnp6tFS5tkZOU,6366
32
32
  sovereign/templates/ul_filter.html,sha256=LrzZv5408Qq5UP4lcHVRwY2G6lXd3IiSNiJn1aH7Yqo,666
33
33
  sovereign/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- sovereign/utils/auth.py,sha256=9rruWP-Ok8ec9l_MzWY3oUZKI8s7jt0Dmx3kHWnTRgQ,1772
35
- sovereign/utils/crypto.py,sha256=Xf6eBFOVDy1MWiOAmZTMYllOaSxi_JCGg6dFrceywvg,3465
34
+ sovereign/utils/auth.py,sha256=sQC8eLPWtk0RIXKwwxnYqILUvUCOaEGtGrtdJflat8E,1692
35
+ sovereign/utils/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ sovereign/utils/crypto/crypto.py,sha256=ONfMoFy0pFgS6LqdS85FxPqtDfMUhk3juTXpJxrhTpA,4473
37
+ sovereign/utils/crypto/suites/__init__.py,sha256=smMvNa1VsQ0PvsNj6lnRNh4ktB7dMnas1CqeTOFqgGA,526
38
+ sovereign/utils/crypto/suites/aes_gcm_cipher.py,sha256=Yjfj1LCQDGTzHBjrZR3-koh29L_N34v65kPoIfta0aw,1239
39
+ sovereign/utils/crypto/suites/base_cipher.py,sha256=xJWey-Wy7RFnIH6E3yBSSnn3OAi9PZEZNSJEY9R5ftQ,494
40
+ sovereign/utils/crypto/suites/disabled_cipher.py,sha256=0_vzydVdVIUlX4pYEAMgB_RvHpyZ25uDC4pz1jRJ5wE,573
41
+ sovereign/utils/crypto/suites/fernet_cipher.py,sha256=rP6M5ys1vctyadOxDGNFoyerWPUOunLQdZ2jjS1pxzc,701
36
42
  sovereign/utils/dictupdate.py,sha256=JkDjg16u7sW6A_4Q2oX1PY_MtJU7m1VivZWn9VLZ9V8,2559
37
43
  sovereign/utils/eds.py,sha256=sCEDj1y-0Crs40cHZLiPGVb7ed1f8vFqgHLY5R2LMbw,4377
38
44
  sovereign/utils/entry_point_loader.py,sha256=BEVodk-um70RvT1nSOu_IB-hr1K4ppthXod0VZEiZJ8,526
@@ -44,12 +50,12 @@ sovereign/utils/version_info.py,sha256=vbAiUyz6v3-zSOoS-7HwrvJie729RgIKy0Bt091Z6
44
50
  sovereign/utils/weighted_clusters.py,sha256=bPzuRE7Qgvv04HcR2AhMDvBrFlZ8AfteweLKhY9SvWg,1166
45
51
  sovereign/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
52
  sovereign/views/admin.py,sha256=GtjSipSfLwrvU1axX3pJfJXiSO82e2pkw8izykZNtGA,4306
47
- sovereign/views/crypto.py,sha256=nxp6bkPU9GZw_zOk0fsJdz_XRQPXxPI6cXQDL9-cigU,2041
48
- sovereign/views/discovery.py,sha256=DzFfG8fdFHKAZzmWZi9YzFP2PYLCf3tPlEAY3udNyNg,5980
53
+ sovereign/views/crypto.py,sha256=o8NSyiUBy7v1pMOXt_1UBi68FNcGkXSlEVg9C18y8kY,3324
54
+ sovereign/views/discovery.py,sha256=TVvWTMzWydsC-SNKL9WsSss_Hfnt2Ed4SVC2A8Na7Jo,5932
49
55
  sovereign/views/healthchecks.py,sha256=_WkMunlrFpqGTLgtNtRr7gCsDCv5kiuYxCyTi-dMEKM,1357
50
56
  sovereign/views/interface.py,sha256=Y2fbR26cSF8eKQHfTbnv5WKEdgqaGNwys0lEGUTjXqw,7041
51
- sovereign-0.24.6.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
52
- sovereign-0.24.6.dist-info/METADATA,sha256=g_JPBObGtgSbJgimhZdP_3Q9dS6gKZ_E__KiTcgFSKc,6420
53
- sovereign-0.24.6.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
54
- sovereign-0.24.6.dist-info/entry_points.txt,sha256=kOn848ucVbNvtsGABDuwzOHmNiOb0Ey8dV85Z3dLv3Y,222
55
- sovereign-0.24.6.dist-info/RECORD,,
57
+ sovereign-0.25.0.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
58
+ sovereign-0.25.0.dist-info/METADATA,sha256=vIWzrbn5aJUnPeNtlqJkGvb3bdJBO_f-SDOT07TRjYU,6463
59
+ sovereign-0.25.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
60
+ sovereign-0.25.0.dist-info/entry_points.txt,sha256=kOn848ucVbNvtsGABDuwzOHmNiOb0Ey8dV85Z3dLv3Y,222
61
+ sovereign-0.25.0.dist-info/RECORD,,
sovereign/utils/crypto.py DELETED
@@ -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