Addressed SA-3 by encrypting the sqlite content with a .env secret
This commit is contained in:
@@ -7,6 +7,7 @@ import json
|
||||
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.database import get_connection
|
||||
from backend.app.services.secret_store import decrypt_secret, encrypt_secret
|
||||
|
||||
|
||||
def get_smtp_settings() -> dict:
|
||||
@@ -22,6 +23,7 @@ def get_smtp_settings() -> dict:
|
||||
row = conn.execute('SELECT value FROM app_settings WHERE name = ?', ('smtp',)).fetchone()
|
||||
if row:
|
||||
values.update(json.loads(row['value']))
|
||||
values['smtp_password'] = decrypt_secret(values['smtp_password'])
|
||||
return values
|
||||
|
||||
|
||||
@@ -30,7 +32,7 @@ def save_smtp_settings(values: dict) -> None:
|
||||
conn.execute(
|
||||
'''INSERT INTO app_settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP''',
|
||||
('smtp', json.dumps(values)),
|
||||
('smtp', json.dumps({**values, 'smtp_password': encrypt_secret(values['smtp_password'])})),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from uuid import uuid4
|
||||
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.database import get_connection
|
||||
from backend.app.services.secret_store import decrypt_secret, encrypt_secret
|
||||
|
||||
|
||||
def normalize_instance(instance: str) -> str:
|
||||
@@ -39,6 +40,8 @@ def start_authorization(user_id: str, instance: str) -> str:
|
||||
with get_connection() as conn:
|
||||
row = conn.execute('SELECT value FROM app_settings WHERE name = ?', (setting_name,)).fetchone()
|
||||
app = json.loads(row['value']) if row else None
|
||||
if app and app.get('client_secret'):
|
||||
app['client_secret'] = decrypt_secret(app['client_secret'])
|
||||
if not app:
|
||||
app = post_form(f'{instance}/api/v1/apps', {
|
||||
'client_name': settings.mastodon_client_name,
|
||||
@@ -50,7 +53,7 @@ def start_authorization(user_id: str, instance: str) -> str:
|
||||
conn.execute(
|
||||
'''INSERT INTO app_settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP''',
|
||||
(setting_name, json.dumps(app)),
|
||||
(setting_name, json.dumps({**app, 'client_secret': encrypt_secret(app['client_secret'])})),
|
||||
)
|
||||
conn.commit()
|
||||
state = token_urlsafe(32)
|
||||
@@ -62,7 +65,7 @@ def start_authorization(user_id: str, instance: str) -> str:
|
||||
(id, user_id, state_hash, instance, client_id, client_secret, redirect_uri, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||
(str(uuid4()), user_id, sha256(state.encode()).hexdigest(), instance,
|
||||
app['client_id'], app['client_secret'], redirect_uri, expires_at.isoformat()),
|
||||
app['client_id'], encrypt_secret(app['client_secret']), redirect_uri, expires_at.isoformat()),
|
||||
)
|
||||
conn.commit()
|
||||
return f'{instance}/oauth/authorize?' + urlencode({
|
||||
@@ -90,7 +93,7 @@ def finish_authorization(code: str, state: str) -> str:
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'client_id': record['client_id'],
|
||||
'client_secret': record['client_secret'],
|
||||
'client_secret': decrypt_secret(record['client_secret']),
|
||||
'redirect_uri': record['redirect_uri'],
|
||||
})
|
||||
access_token = token.get('access_token')
|
||||
@@ -102,7 +105,7 @@ def finish_authorization(code: str, state: str) -> str:
|
||||
(record['user_id'], 'mastodon'),
|
||||
).fetchone()
|
||||
config = json.loads(current['config']) if current and current['config'] else {}
|
||||
config.update({'instance': record['instance'], 'access_token': access_token})
|
||||
config.update({'instance': record['instance'], 'access_token': encrypt_secret(access_token)})
|
||||
if current:
|
||||
conn.execute(
|
||||
'UPDATE user_plugin_config SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND plugin_name = ?',
|
||||
|
||||
@@ -8,6 +8,7 @@ from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from backend.app.plugins.base import BasePlugin
|
||||
from backend.app.services.secret_store import decrypt_secret
|
||||
|
||||
DEFAULT_POST_PREFIX = 'From my #LinkLog: '
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -47,6 +48,7 @@ class MastodonPlugin(BasePlugin):
|
||||
).fetchone()
|
||||
if row and row['config']:
|
||||
config.update(json.loads(row['config']))
|
||||
config['access_token'] = decrypt_secret(config.get('access_token', ''))
|
||||
|
||||
instance = str(config.get('instance', '')).strip().rstrip('/')
|
||||
if instance and '://' not in instance:
|
||||
@@ -135,6 +137,7 @@ class MastodonPlugin(BasePlugin):
|
||||
).fetchone()
|
||||
if row and row['config']:
|
||||
config.update(json.loads(row['config']))
|
||||
config['access_token'] = decrypt_secret(config.get('access_token', ''))
|
||||
|
||||
instance = str(config.get('instance', '')).strip().rstrip('/')
|
||||
if instance and '://' not in instance:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
## Copyright © 2026 Olaf Kolkman
|
||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
from backend.app.core.config import settings
|
||||
|
||||
|
||||
def _cipher() -> Fernet:
|
||||
if not settings.data_encryption_key:
|
||||
raise RuntimeError('LINKLOG_DATA_ENCRYPTION_KEY is required to access encrypted secrets')
|
||||
try:
|
||||
return Fernet(settings.data_encryption_key.encode('ascii'))
|
||||
except (ValueError, UnicodeEncodeError) as error:
|
||||
raise RuntimeError('LINKLOG_DATA_ENCRYPTION_KEY must be a valid Fernet key') from error
|
||||
|
||||
|
||||
def encrypt_secret(value: str) -> str:
|
||||
if not value:
|
||||
return value
|
||||
if value.startswith('enc:v1:'):
|
||||
return value
|
||||
return 'enc:v1:' + _cipher().encrypt(value.encode('utf-8')).decode('ascii')
|
||||
|
||||
|
||||
def decrypt_secret(value: str) -> str:
|
||||
if not value or not value.startswith('enc:v1:'):
|
||||
return value
|
||||
try:
|
||||
return _cipher().decrypt(value[7:].encode('ascii')).decode('utf-8')
|
||||
except (InvalidToken, UnicodeEncodeError) as error:
|
||||
raise RuntimeError('Encrypted secret cannot be decrypted with LINKLOG_DATA_ENCRYPTION_KEY') from error
|
||||
Reference in New Issue
Block a user