Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,12 @@


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


def generate_encrypted_file_with_openssl(file_path: str, password: str, out_file: str):
# Write plaintext temporarily to file

# Run openssl enc with AES-256-CBC, pbkdf2, salt
cmd = [
"openssl",
"enc",
Expand All @@ -54,25 +49,17 @@ def generate_encrypted_file_with_openssl(file_path: str, password: str, out_file


def decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, bteq_command_str):
# Run openssl decrypt command on remote machine
quoted_password = shell_quote_single(password)

# Use -pass stdin to avoid shell quoting on any OS and keep the passphrase
# out of the remote process table where ps could expose it.
decrypt_cmd = (
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass pass:{quoted_password} -in {shlex.quote(remote_enc_file)} | "
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass stdin -in {shlex.quote(remote_enc_file)} | "
+ bteq_command_str
)
# Clear password to prevent lingering sensitive data
password = None
quoted_password = None
stdin, stdout, stderr = ssh_client.exec_command(decrypt_cmd)
# Wait for command to finish
stdin.write(password + "\n")
stdin.flush()
stdin.channel.shutdown_write()
exit_status = stdout.channel.recv_exit_status()
output = stdout.read().decode()
err = stderr.read().decode()
return exit_status, output, err


def shell_quote_single(s):
# Escape single quotes in s, then wrap in single quotes
# In shell, to include a single quote inside single quotes, close, add '\'' and reopen
return "'" + s.replace("'", "'\\''") + "'"
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,15 @@
decrypt_remote_file_to_string,
generate_encrypted_file_with_openssl,
generate_random_password,
shell_quote_single,
)


class TestEncryptionUtils:
def test_generate_random_password_length(self):
pwd = generate_random_password(16)
assert len(pwd) == 16
# Check characters are in allowed set
allowed_chars = string.ascii_letters + string.digits + string.punctuation
assert (all(c in allowed_chars for c in pwd)) is True
assert all(c in allowed_chars for c in pwd) is True

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

def test_shell_quote_single_simple(self):
s = "simple"
quoted = shell_quote_single(s)
assert quoted == "'simple'"

def test_shell_quote_single_with_single_quote(self):
s = "O'Reilly"
quoted = shell_quote_single(s)
assert quoted == "'O'\\''Reilly'"

def test_decrypt_remote_file_to_string(self):
password = "mysecret"
remote_enc_file = "/remote/encrypted.enc"
Expand All @@ -91,25 +79,42 @@ def test_decrypt_remote_file_to_string(self):
mock_stdin = MagicMock()
mock_stdout = MagicMock()
mock_stderr = MagicMock()

# Setup mock outputs and exit code
mock_stdout.channel.recv_exit_status.return_value = 0
mock_stdout.read.return_value = b"decrypted output"
mock_stderr.read.return_value = b""

ssh_client.exec_command.return_value = (mock_stdin, mock_stdout, mock_stderr)

exit_status, output, err = decrypt_remote_file_to_string(
ssh_client, remote_enc_file, password, bteq_command_str
)

quoted_password = shell_quote_single(password)
expected_cmd = (
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass pass:{quoted_password} -in {remote_enc_file} | "
f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass stdin -in {remote_enc_file} | "
Comment thread
potiuk marked this conversation as resolved.
+ bteq_command_str
)

ssh_client.exec_command.assert_called_once_with(expected_cmd)
mock_stdin.write.assert_called_once_with(password + "\n")
mock_stdin.flush.assert_called_once()
mock_stdin.channel.shutdown_write.assert_called_once()
assert exit_status == 0
assert output == "decrypted output"
assert err == ""

def test_decrypt_remote_file_passphrase_not_on_argv(self):
"""The passphrase is passed via stdin, never on the remote command line."""
password = "s3cr3t&rm -rf ~"
remote_enc_file = "/remote/encrypted.enc"
ssh_client = MagicMock()
mock_stdin = MagicMock()
mock_stdout = MagicMock()
mock_stderr = MagicMock()
mock_stdout.channel.recv_exit_status.return_value = 0
mock_stdout.read.return_value = b""
mock_stderr.read.return_value = b""
ssh_client.exec_command.return_value = (mock_stdin, mock_stdout, mock_stderr)

decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, "bteq")

cmd = ssh_client.exec_command.call_args[0][0]
assert password not in cmd, "passphrase leaked onto remote command line"
assert "-pass stdin" in cmd