mirror of
https://github.com/apache/superset.git
synced 2026-07-16 11:46:09 +00:00
App-encrypted fields (DB passwords, SSH tunnel creds, OAuth tokens) use
sqlalchemy_utils EncryptedType, which defaults to AES-CBC — unauthenticated
encryption vulnerable to bit-flipping/tamper of ciphertext at rest. AES-GCM is
authenticated and fails closed on tampering.
Phase 1 (this change, backward compatible): add a configurable encryption
engine via SQLALCHEMY_ENCRYPTED_FIELD_ENGINE ("aes" | "aes-gcm"), defaulting to
"aes" so existing deployments are unchanged. The default SQLAlchemyUtilsAdapter
now honors it; an explicit engine kwarg still wins. New installs can opt into
AES-GCM with one line instead of writing a custom adapter.
Includes docs/sip/authenticated-encryption-at-rest.md describing the full
proposal: Phase 2 (a CBC->GCM re-encryption migrator built on SecretsMigrator)
and Phase 3 (flip default for fresh installs). The instance-wide re-encryption
migration is intentionally NOT included here — it is the subject of the SIP.
DRAFT: flipping the engine on a populated DB without the Phase 2 migrator makes
existing secrets undecryptable; this PR only adds the safe opt-in plumbing for
discussion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
58 lines
2.2 KiB
Python
58 lines
2.2 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from sqlalchemy import String
|
|
from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine, AesGcmEngine
|
|
|
|
from superset.utils.encrypt import SQLAlchemyUtilsAdapter
|
|
|
|
SECRET = {"SECRET_KEY": "x" * 32}
|
|
|
|
|
|
def test_default_engine_is_aes_cbc() -> None:
|
|
"""Without config, the adapter keeps the historical AES-CBC engine."""
|
|
field = SQLAlchemyUtilsAdapter().create(SECRET, String(128))
|
|
assert isinstance(field.engine, AesEngine)
|
|
|
|
|
|
def test_aes_gcm_engine_selected_by_config() -> None:
|
|
"""SQLALCHEMY_ENCRYPTED_FIELD_ENGINE='aes-gcm' selects authenticated AES-GCM."""
|
|
field = SQLAlchemyUtilsAdapter().create(
|
|
{**SECRET, "SQLALCHEMY_ENCRYPTED_FIELD_ENGINE": "aes-gcm"},
|
|
String(128),
|
|
)
|
|
assert isinstance(field.engine, AesGcmEngine)
|
|
|
|
|
|
def test_unknown_engine_falls_back_to_aes_cbc() -> None:
|
|
"""An unrecognized engine name falls back to the safe historical default."""
|
|
field = SQLAlchemyUtilsAdapter().create(
|
|
{**SECRET, "SQLALCHEMY_ENCRYPTED_FIELD_ENGINE": "bogus"},
|
|
String(128),
|
|
)
|
|
assert isinstance(field.engine, AesEngine)
|
|
|
|
|
|
def test_explicit_engine_kwarg_takes_precedence() -> None:
|
|
"""An explicit engine kwarg overrides the config (used by the migrator)."""
|
|
field = SQLAlchemyUtilsAdapter().create(
|
|
{**SECRET, "SQLALCHEMY_ENCRYPTED_FIELD_ENGINE": "aes-gcm"},
|
|
String(128),
|
|
engine=AesEngine,
|
|
)
|
|
assert isinstance(field.engine, AesEngine)
|