Skip to content

Commit c0ae212

Browse files
kada2004kada2004
andauthored
Fix passphrase quoting in decrypt_remote_file_to_string for Windows (#69908)
* Fix passphrase quoting in decrypt_remote_file_to_string for Windows remotes * Restored shlex.quote on remote path and drop misleading password = None --------- Co-authored-by: kada2004 <dankalenga3@gmail,com>
1 parent 3c011c8 commit c0ae212

2 files changed

Lines changed: 30 additions & 38 deletions

File tree

providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,12 @@
2424

2525

2626
def generate_random_password(length=12):
27-
# Define the character set: letters, digits, and special characters
2827
characters = string.ascii_letters + string.digits + string.punctuation
29-
# Generate a random password
3028
password = "".join(secrets.choice(characters) for _ in range(length))
3129
return password
3230

3331

3432
def generate_encrypted_file_with_openssl(file_path: str, password: str, out_file: str):
35-
# Write plaintext temporarily to file
36-
37-
# Run openssl enc with AES-256-CBC, pbkdf2, salt
3833
cmd = [
3934
"openssl",
4035
"enc",
@@ -54,25 +49,17 @@ def generate_encrypted_file_with_openssl(file_path: str, password: str, out_file
5449

5550

5651
def decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, bteq_command_str):
57-
# Run openssl decrypt command on remote machine
58-
quoted_password = shell_quote_single(password)
59-
52+
# Use -pass stdin to avoid shell quoting on any OS and keep the passphrase
53+
# out of the remote process table where ps could expose it.
6054
decrypt_cmd = (
61-
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass pass:{quoted_password} -in {shlex.quote(remote_enc_file)} | "
55+
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass stdin -in {shlex.quote(remote_enc_file)} | "
6256
+ bteq_command_str
6357
)
64-
# Clear password to prevent lingering sensitive data
65-
password = None
66-
quoted_password = None
6758
stdin, stdout, stderr = ssh_client.exec_command(decrypt_cmd)
68-
# Wait for command to finish
59+
stdin.write(password + "\n")
60+
stdin.flush()
61+
stdin.channel.shutdown_write()
6962
exit_status = stdout.channel.recv_exit_status()
7063
output = stdout.read().decode()
7164
err = stderr.read().decode()
7265
return exit_status, output, err
73-
74-
75-
def shell_quote_single(s):
76-
# Escape single quotes in s, then wrap in single quotes
77-
# In shell, to include a single quote inside single quotes, close, add '\'' and reopen
78-
return "'" + s.replace("'", "'\\''") + "'"

providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,15 @@
2323
decrypt_remote_file_to_string,
2424
generate_encrypted_file_with_openssl,
2525
generate_random_password,
26-
shell_quote_single,
2726
)
2827

2928

3029
class TestEncryptionUtils:
3130
def test_generate_random_password_length(self):
3231
pwd = generate_random_password(16)
3332
assert len(pwd) == 16
34-
# Check characters are in allowed set
3533
allowed_chars = string.ascii_letters + string.digits + string.punctuation
36-
assert (all(c in allowed_chars for c in pwd)) is True
34+
assert all(c in allowed_chars for c in pwd) is True
3735

3836
@patch("subprocess.run")
3937
def test_generate_encrypted_file_with_openssl_calls_subprocess(self, mock_run):
@@ -72,16 +70,6 @@ def test_generate_encrypted_file_passphrase_not_on_argv(self, mock_run):
7270
assert not any(password in str(part) for part in cmd), "passphrase leaked onto argv"
7371
assert kwargs["input"] == f"{password}\n".encode()
7472

75-
def test_shell_quote_single_simple(self):
76-
s = "simple"
77-
quoted = shell_quote_single(s)
78-
assert quoted == "'simple'"
79-
80-
def test_shell_quote_single_with_single_quote(self):
81-
s = "O'Reilly"
82-
quoted = shell_quote_single(s)
83-
assert quoted == "'O'\\''Reilly'"
84-
8573
def test_decrypt_remote_file_to_string(self):
8674
password = "mysecret"
8775
remote_enc_file = "/remote/encrypted.enc"
@@ -91,25 +79,42 @@ def test_decrypt_remote_file_to_string(self):
9179
mock_stdin = MagicMock()
9280
mock_stdout = MagicMock()
9381
mock_stderr = MagicMock()
94-
95-
# Setup mock outputs and exit code
9682
mock_stdout.channel.recv_exit_status.return_value = 0
9783
mock_stdout.read.return_value = b"decrypted output"
9884
mock_stderr.read.return_value = b""
99-
10085
ssh_client.exec_command.return_value = (mock_stdin, mock_stdout, mock_stderr)
10186

10287
exit_status, output, err = decrypt_remote_file_to_string(
10388
ssh_client, remote_enc_file, password, bteq_command_str
10489
)
10590

106-
quoted_password = shell_quote_single(password)
10791
expected_cmd = (
108-
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass pass:{quoted_password} -in {remote_enc_file} | "
92+
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass stdin -in {remote_enc_file} | "
10993
+ bteq_command_str
11094
)
111-
11295
ssh_client.exec_command.assert_called_once_with(expected_cmd)
96+
mock_stdin.write.assert_called_once_with(password + "\n")
97+
mock_stdin.flush.assert_called_once()
98+
mock_stdin.channel.shutdown_write.assert_called_once()
11399
assert exit_status == 0
114100
assert output == "decrypted output"
115101
assert err == ""
102+
103+
def test_decrypt_remote_file_passphrase_not_on_argv(self):
104+
"""The passphrase is passed via stdin, never on the remote command line."""
105+
password = "s3cr3t&rm -rf ~"
106+
remote_enc_file = "/remote/encrypted.enc"
107+
ssh_client = MagicMock()
108+
mock_stdin = MagicMock()
109+
mock_stdout = MagicMock()
110+
mock_stderr = MagicMock()
111+
mock_stdout.channel.recv_exit_status.return_value = 0
112+
mock_stdout.read.return_value = b""
113+
mock_stderr.read.return_value = b""
114+
ssh_client.exec_command.return_value = (mock_stdin, mock_stdout, mock_stderr)
115+
116+
decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, "bteq")
117+
118+
cmd = ssh_client.exec_command.call_args[0][0]
119+
assert password not in cmd, "passphrase leaked onto remote command line"
120+
assert "-pass stdin" in cmd

0 commit comments

Comments
 (0)