Skip to content

Commit 1f705c5

Browse files
committed
added code to store encrypted secrets
1 parent d1fbe9a commit 1f705c5

6 files changed

Lines changed: 182 additions & 8 deletions

File tree

state-manager/app/controller/upsert_graph_template.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async def upsert_graph_template(namespace_name: str, graph_name: str, body: Upse
1818
namespace_name=namespace_name,
1919
x_exosphere_request_id=x_exosphere_request_id)
2020

21-
await graph_template.update(
21+
await graph_template.set_secrets(body.secrets).update(
2222
Set({
2323
GraphTemplate.nodes: body.nodes, # type: ignore
2424
GraphTemplate.validation_status: GraphTemplateValidationStatus.PENDING, # type: ignore
@@ -40,15 +40,14 @@ async def upsert_graph_template(namespace_name: str, graph_name: str, body: Upse
4040
nodes=body.nodes,
4141
validation_status=GraphTemplateValidationStatus.PENDING,
4242
validation_errors=[]
43-
)
43+
).set_secrets(body.secrets)
4444
)
4545

4646
return UpsertGraphTemplateResponse(
47-
name=graph_template.name,
48-
namespace=graph_template.namespace,
4947
nodes=graph_template.nodes,
5048
validation_status=graph_template.validation_status,
5149
validation_errors=graph_template.validation_errors,
50+
secrets={secret_name: True for secret_name in graph_template.get_secrets().keys()},
5251
created_at=graph_template.created_at,
5352
updated_at=graph_template.updated_at
5453
)
Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
import re
2+
import base64
3+
14
from .base import BaseDatabaseModel
2-
from pydantic import Field
5+
from pydantic import Field, field_validator
36
from typing import Optional, List
47
from ..graph_template_validation_status import GraphTemplateValidationStatus
58
from ..node_template_model import NodeTemplate
69
from pymongo import IndexModel
7-
10+
from typing import Dict
11+
from app.utils.encrypter import encrypter
812

913
class GraphTemplate(BaseDatabaseModel):
1014
name: str = Field(..., description="Name of the graph")
1115
namespace: str = Field(..., description="Namespace of the graph")
1216
nodes: List[NodeTemplate] = Field(..., description="Nodes of the graph")
1317
validation_status: GraphTemplateValidationStatus = Field(..., description="Validation status of the graph")
1418
validation_errors: Optional[List[str]] = Field(None, description="Validation errors of the graph")
19+
secrets: Dict[str, str] = Field(default_factory=dict, description="Secrets of the graph")
1520

1621
class Settings:
1722
indexes = [
@@ -20,4 +25,58 @@ class Settings:
2025
unique=True,
2126
name="unique_name_namespace"
2227
)
23-
]
28+
]
29+
30+
@field_validator('secrets')
31+
@classmethod
32+
def validate_secrets(cls, v: Dict[str, str]) -> Dict[str, str]:
33+
for secret_name, secret_value in v.items():
34+
if not secret_name or not secret_value:
35+
raise ValueError("Secrets cannot be empty")
36+
if not isinstance(secret_name, str):
37+
raise ValueError("Secret name must be a string")
38+
if not isinstance(secret_value, str):
39+
raise ValueError("Secret value must be a string")
40+
cls._validate_secret_value(secret_value)
41+
42+
return v
43+
44+
@classmethod
45+
def _validate_secret_value(cls, secret_value: str) -> None:
46+
# Check minimum length for AES-GCM encrypted string
47+
# 12 bytes nonce + minimum ciphertext + base64 encoding
48+
if len(secret_value) < 32: # Minimum length for encrypted string
49+
raise ValueError("Value appears to be too short for an encrypted string")
50+
51+
# Check if the string contains only URL-safe base64 characters
52+
url_safe_base64_pattern = r'^[A-Za-z0-9_-]+$'
53+
if not re.match(url_safe_base64_pattern, secret_value):
54+
raise ValueError("Value must be URL-safe base64 encoded")
55+
56+
# Check if the string length is valid for base64 encoding
57+
# Base64 encoding increases size by ~33%, and we need at least 12 bytes nonce
58+
if len(secret_value) % 4 != 0:
59+
raise ValueError("Value length is not valid for base64 encoding")
60+
61+
# Try to decode as base64 to ensure it's valid
62+
try:
63+
decoded = base64.urlsafe_b64decode(secret_value)
64+
if len(decoded) < 12:
65+
raise ValueError("Decoded value is too short to contain valid nonce")
66+
except Exception:
67+
raise ValueError("Value is not valid URL-safe base64 encoded")
68+
69+
70+
def set_secrets(self, secrets: Dict[str, str]) -> "GraphTemplate":
71+
self.secrets = {secret_name: encrypter.encrypt(secret_value) for secret_name, secret_value in secrets.items()}
72+
return self
73+
74+
def get_secrets(self) -> Dict[str, str]:
75+
if not self.secrets:
76+
return {}
77+
return {secret_name: encrypter.decrypt(secret_value) for secret_name, secret_value in self.secrets.items()}
78+
79+
def get_secret(self, secret_name: str) -> str:
80+
if not self.secrets:
81+
return ""
82+
return encrypter.decrypt(self.secrets[secret_name])

state-manager/app/models/graph_models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from .node_template_model import NodeTemplate
22
from pydantic import BaseModel, Field
3-
from typing import List, Optional
3+
from typing import Dict, List, Optional
44
from datetime import datetime
55
from .graph_template_validation_status import GraphTemplateValidationStatus
66

77

88
class UpsertGraphTemplateRequest(BaseModel):
9+
secrets: Dict[str, str] = Field(..., description="Dictionary of secrets that are used while graph execution")
910
nodes: List[NodeTemplate] = Field(..., description="List of node templates that define the graph structure")
1011

1112

1213
class UpsertGraphTemplateResponse(BaseModel):
1314
nodes: List[NodeTemplate] = Field(..., description="List of node templates that define the graph structure")
15+
secrets: Dict[str, bool] = Field(..., description="Dictionary of secrets that are used while graph execution")
1416
created_at: datetime = Field(..., description="Timestamp when the graph template was created")
1517
updated_at: datetime = Field(..., description="Timestamp when the graph template was last updated")
1618
validation_status: GraphTemplateValidationStatus = Field(..., description="Current validation status of the graph template")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import os
2+
import base64
3+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
4+
5+
class Encrypter:
6+
7+
@staticmethod
8+
def generate_key() -> str:
9+
return base64.urlsafe_b64encode(AESGCM.generate_key(bit_length=256)).decode()
10+
11+
def __init__(self):
12+
key_b64 = os.getenv("SECRETS_ENCRYPTION_KEY")
13+
if not key_b64:
14+
raise ValueError("SECRETS_ENCRYPTION_KEY is not set")
15+
try:
16+
self._key = base64.urlsafe_b64decode(key_b64)
17+
except Exception as exc:
18+
raise ValueError("Key must be URL-safe base64 (44 chars for 32-byte key)") from exc
19+
if len(self._key) != 32:
20+
raise ValueError("Key must be 32 raw bytes (256 bits)")
21+
self._aesgcm = AESGCM(self._key)
22+
23+
def encrypt(self, secret: str) -> str:
24+
nonce = os.urandom(12)
25+
ciphertext = self._aesgcm.encrypt(nonce, secret.encode(), None)
26+
return base64.urlsafe_b64encode(nonce + ciphertext).decode()
27+
28+
def decrypt(self, encrypted_secret: str) -> str:
29+
encrypted_secret_bytes = base64.urlsafe_b64decode(encrypted_secret)
30+
nonce = encrypted_secret_bytes[:12]
31+
ciphertext = encrypted_secret_bytes[12:]
32+
return self._aesgcm.decrypt(nonce, ciphertext, None).decode()
33+
34+
encrypter = Encrypter()

state-manager/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"beanie>=2.0.0",
9+
"cryptography>=45.0.5",
910
"fastapi>=0.116.1",
1011
"python-dotenv>=1.1.1",
1112
"structlog>=25.4.0",

0 commit comments

Comments
 (0)