codeant-ai-for-open-source[bot] commented on code in PR #40654:
URL: https://github.com/apache/superset/pull/40654#discussion_r3338482244


##########
superset/cli/update.py:
##########
@@ -110,17 +110,37 @@ def update_api_docs() -> None:
     help="An optional previous secret key, if PREVIOUS_SECRET_KEY "
     "is not set on the config",
 )
-def re_encrypt_secrets(previous_secret_key: Optional[str] = None) -> None:
[email protected](
+    "--engine",
+    "-e",
+    "target_engine_name",
+    required=False,
+    type=click.Choice(sorted(ENCRYPTION_ENGINES)),
+    help="Re-encrypt all app-encrypted fields with this encryption engine "
+    "(e.g. 'aes-gcm' for authenticated encryption). The SECRET_KEY is "
+    "unchanged. Take a metadata-DB backup first, then set "
+    "SQLALCHEMY_ENCRYPTED_FIELD_ENGINE to the same value and restart.",
+)
+def re_encrypt_secrets(
+    previous_secret_key: Optional[str] = None,
+    target_engine_name: Optional[str] = None,
+) -> None:
     previous_secret_key = previous_secret_key or current_app.config.get(
         "PREVIOUS_SECRET_KEY"
     )
-    if previous_secret_key is None:
+    target_engine = (
+        ENCRYPTION_ENGINES[target_engine_name] if target_engine_name else None
+    )
+    if previous_secret_key is None and target_engine is None:
         click.secho(
-            "No previous secret key provided; nothing to re-encrypt.",
+            "No previous secret key or target engine provided; nothing to 
re-encrypt.",
             fg="yellow",
         )
         return
-    secrets_migrator = SecretsMigrator(previous_secret_key=previous_secret_key)
+    secrets_migrator = SecretsMigrator(
+        previous_secret_key=previous_secret_key,
+        target_engine=target_engine,
+    )

Review Comment:
   **Suggestion:** In engine-migration mode, always forwarding 
`previous_secret_key` into `SecretsMigrator` can override the migrator's 
built-in fallback to the current `SECRET_KEY`. If `PREVIOUS_SECRET_KEY` is 
still set in config, this can make `--engine` runs fail to decrypt AES-CBC rows 
(especially if config was already flipped), because decryption is attempted 
with a stale key instead of the current one. Pass `previous_secret_key` only 
when explicitly provided for key rotation, or ignore config 
`PREVIOUS_SECRET_KEY` when running engine-only migration so the migrator can 
default correctly. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ `re_encrypt_secrets --engine` fails when PREVIOUS_SECRET_KEY set.
   - ❌ AES-CBC secrets cannot be migrated to AES-GCM.
   - ⚠️ Authenticated-encryption adoption blocked for affected deployments.
   - ⚠️ Operators see unexplained decryption failures during migration.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction βœ… </b></summary>
   
   ```mdx
   1. Configure a Superset instance so that app-encrypted fields use AES-CBC 
under the
   current `SECRET_KEY` but the engine config has already been flipped to 
AES-GCM: set
   `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes-gcm"` in config (used by
   `SQLAlchemyUtilsAdapter.create()` at `superset/utils/encrypt.py:71-88`) 
while the
   underlying database still holds AES-CBC ciphertext from the historical 
default engine.
   
   2. Ensure that `PREVIOUS_SECRET_KEY` remains set in config to an older key 
(different from
   the current `SECRET_KEY`), e.g. after a prior key rotation, so
   `current_app.config["PREVIOUS_SECRET_KEY"]` is non-empty when 
`re_encrypt_secrets()` runs
   at `superset/cli/update.py:104-124`.
   
   3. Run the CLI command `superset re-encrypt-secrets --engine aes-gcm` (wired 
to
   `re_encrypt_secrets` at `superset/cli/update.py:104-124`) without providing
   `--previous_secret_key`; inside this function, `previous_secret_key` is 
populated from
   `current_app.config.get("PREVIOUS_SECRET_KEY")` at `update.py:128-130`, and
   `SecretsMigrator` is constructed with `previous_secret_key=<stale config 
value>` and
   `target_engine=AesGcmEngine` at `update.py:140-143`.
   
   4. In `SecretsMigrator.__init__` at `superset/utils/encrypt.py:141-157`, the
   engine-migration fallback `if target_engine is not None and not 
previous_secret_key:
   previous_secret_key = self._secret_key` is bypassed because 
`previous_secret_key` is
   already set from config; `_previous_secret_key` is thus the stale key 
instead of the
   current `SECRET_KEY`. When `run()` (at `encrypt.py:376-385`) iterates over 
rows and calls
   `_re_encrypt_row()` (`encrypt.py:265-361`), `_source_decryptors()` 
(`encrypt.py:244-263`)
   builds decryptors `[encrypted_type(current_engine=AES-GCM, current key), 
AES-CBC with
   stale previous key]`. Both fail to decrypt AES-CBC ciphertext encrypted with 
the current
   key, so each value is counted as `failed` and logged, and `run()` raises once
   `stats.failed` is non-zero (`encrypt.py:387-385`), causing 
`re_encrypt_secrets()` to print
   `Re-encryption failed: ...` and exit with code 1 at 
`superset/cli/update.py:145-148`,
   blocking AES-CBC→AES-GCM migration for deployments where 
`PREVIOUS_SECRET_KEY` is still
   configured.
   ```
   </details>
   
   [Fix in 
Cursor](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=0a77a939b12e48b2868183aca5dcea5b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 | [Fix in VSCode 
Claude](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=0a77a939b12e48b2868183aca5dcea5b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent πŸ€– </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/cli/update.py
   **Line:** 140:143
   **Comment:**
        *Logic Error: In engine-migration mode, always forwarding 
`previous_secret_key` into `SecretsMigrator` can override the migrator's 
built-in fallback to the current `SECRET_KEY`. If `PREVIOUS_SECRET_KEY` is 
still set in config, this can make `--engine` runs fail to decrypt AES-CBC rows 
(especially if config was already flipped), because decryption is attempted 
with a stale key instead of the current one. Pass `previous_secret_key` only 
when explicitly provided for key rotation, or ignore config 
`PREVIOUS_SECRET_KEY` when running engine-only migration so the migrator can 
default correctly.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40654&comment_hash=057fc5941ab247e3c6c451e9f34efd1fb7ea6add4080b3e1e2122924251e2fa5&reaction=like'>πŸ‘</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40654&comment_hash=057fc5941ab247e3c6c451e9f34efd1fb7ea6add4080b3e1e2122924251e2fa5&reaction=dislike'>πŸ‘Ž</a>



##########
superset/utils/encrypt.py:
##########
@@ -177,6 +226,42 @@ def _select_columns_from_table(
         cols = ",".join(pk_columns + column_names)
         return conn.execute(f"SELECT {cols} FROM {table_name}")  # noqa: S608
 
+    def _target_type(self, encrypted_type: EncryptedType) -> EncryptedType:
+        """The EncryptedType to re-encrypt a value *into*.
+
+        For a key rotation this is the column's own configured type (current
+        engine + lazily-resolved current key). For an engine migration it is a
+        type pinned to the requested target engine under the current key.
+        """
+        if self._target_engine is None:
+            return encrypted_type
+        return EncryptedType(
+            type_in=encrypted_type.underlying_type,
+            key=self._secret_key,
+            engine=self._target_engine,
+        )
+
+    def _source_decryptors(self, encrypted_type: EncryptedType) -> 
list[EncryptedType]:
+        """Candidate decryptors, tried in order, to recover a value's 
plaintext.
+
+        1. The column's configured type β€” current key + currently-configured
+           engine. During an engine migration this reads the source ciphertext
+           while the config still points at the source engine.
+        2. The previous key with the historical AES-CBC engine β€” covers
+           ``SECRET_KEY`` rotation, and (since the previous key defaults to the
+           current one in engine-migration mode) reading AES-CBC source data
+           after the config has already been flipped to the target engine.
+        """
+        decryptors = [encrypted_type]
+        if self._previous_secret_key:
+            decryptors.append(
+                EncryptedType(
+                    type_in=encrypted_type.underlying_type,
+                    key=self._previous_secret_key,
+                )
+            )
+        return decryptors

Review Comment:
   **Suggestion:** The previous-key fallback decryptor is built without an 
`engine`, so it always uses the default AES-CBC engine. That breaks 
`SECRET_KEY` rotation for deployments configured with AES-GCM, because 
ciphertext encrypted with the old key and GCM cannot be decrypted by this 
fallback and is incorrectly counted as failed. Build the previous-key decryptor 
with the same engine as the column's `encrypted_type` (or include both relevant 
engines in order) so key rotation works for non-CBC configurations. [logic 
error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ re_encrypt_secrets CLI fails rotating AES-GCM deployments.
   - ❌ AES-GCM secrets remain unreadable under new SECRET_KEY.
   - ⚠️ Operators cannot complete documented AES-GCM key rotation.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction βœ… </b></summary>
   
   ```mdx
   1. Configure Superset with AES-GCM as the encrypted field engine by setting
   `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes-gcm"`, which 
`SQLAlchemyUtilsAdapter.create()`
   in `superset/utils/encrypt.py:71-87` reads to pass `engine=AesGcmEngine` into
   `EncryptedType`.
   
   2. Store app-encrypted secrets (e.g. DB passwords) so they are written using
   `EncryptedType` instances discovered by 
`SecretsMigrator.discover_encrypted_fields()` in
   `superset/utils/encrypt.py:160-198`; these ciphertexts are produced with
   `SECRET_KEY=KEY_A` and the AES-GCM engine, as also validated by the 
integration tests that
   ensure encrypted fields exist on the `dbs` table in
   `tests/integration_tests/utils/encrypt_tests.py:34-48`.
   
   3. Rotate the secret key by changing `current_app.config["SECRET_KEY"]` to 
`KEY_B` and
   configuring `PREVIOUS_SECRET_KEY=KEY_A`, then run the CLI command bound to
   `re_encrypt_secrets()` in `superset/cli/update.py:25-48` (invoked via 
`superset
   re-encrypt-secrets --previous-secret-key KEY_A` or using 
`PREVIOUS_SECRET_KEY`), which
   constructs `SecretsMigrator(previous_secret_key=KEY_A, target_engine=None)` 
at
   `superset/cli/update.py:61-64`.
   
   4. During `SecretsMigrator.run()` in `superset/utils/encrypt.py:117-163`, 
each row is
   processed by `_re_encrypt_row()` which calls `_source_decryptors()` at
   `superset/utils/encrypt.py:244-260`; this builds two decryptors: (a) the 
column's
   configured AES-GCM + KEY_B type, and (b) a previous-key fallback
   `EncryptedType(type_in=encrypted_type.underlying_type, 
key=self._previous_secret_key)`
   without an `engine`, which defaults to AES-CBC. Both decryptors fail on 
AES-GCM+KEY_A
   ciphertext, causing `_re_encrypt_row()` to mark the column as failed and 
`run()` to raise
   an exception when `stats.failed` is non-zero at 
`superset/utils/encrypt.py:151-163`, which
   `re_encrypt_secrets()` catches and reports as a failed re-encryption in
   `superset/cli/update.py:65-69`, leaving AES-GCM deployments unable to 
complete key
   rotation.
   ```
   </details>
   
   [Fix in 
Cursor](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=86676e1926fe404bb871eb03e9f3591a&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 | [Fix in VSCode 
Claude](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=86676e1926fe404bb871eb03e9f3591a&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent πŸ€– </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/utils/encrypt.py
   **Line:** 255:263
   **Comment:**
        *Logic Error: The previous-key fallback decryptor is built without an 
`engine`, so it always uses the default AES-CBC engine. That breaks 
`SECRET_KEY` rotation for deployments configured with AES-GCM, because 
ciphertext encrypted with the old key and GCM cannot be decrypted by this 
fallback and is incorrectly counted as failed. Build the previous-key decryptor 
with the same engine as the column's `encrypted_type` (or include both relevant 
engines in order) so key rotation works for non-CBC configurations.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40654&comment_hash=99a0cedb63f76d9e9f2e27f7e16cbdad3c0c296c6c37007e7b1d014be618932c&reaction=like'>πŸ‘</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F40654&comment_hash=99a0cedb63f76d9e9f2e27f7e16cbdad3c0c296c6c37007e7b1d014be618932c&reaction=dislike'>πŸ‘Ž</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to