Files
superset2/tests/unit_tests/utils/encrypt_test.py
Claude Code f748de3423 feat(encrypt): make encryption engine selectable (AES-GCM support) [DRAFT/SIP]
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>
2026-06-05 11:41:12 -07:00

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)