diff --git a/changelog.md b/changelog.md index 7a8aa10d..ee5b0c7f 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ Internal --------- * Improve test coverage for DSN variable expansion. * Test on all platforms in the Publish GitHub Action. +* Remove unused support for writing `.mylogin.cnf` files. 1.76.0 (2026/06/20) diff --git a/mycli/config.py b/mycli/config.py index 1053d50e..b42f2b92 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -138,56 +138,6 @@ def open_mylogin_cnf(name: str) -> TextIOWrapper | None: return TextIOWrapper(plaintext) -# TODO reuse code between encryption an decryption -def encrypt_mylogin_cnf(plaintext: IO[str]) -> BytesIO: - """Encryption of .mylogin.cnf file, analogous to calling - mysql_config_editor. - - Code is based on the python implementation by Kristian Koehntopp - https://github.com/isotopp/mysql-config-coder - - """ - - def realkey(key: bytes) -> bytes: - """Create the AES key from the login key.""" - rkey = bytearray(16) - for i in range(len(key)): - rkey[i % 16] ^= key[i] - return bytes(rkey) - - def encode_line(plaintext: str, real_key: bytes, buf_len: int) -> bytes: - aes = AES.new(real_key, AES.MODE_ECB) - text_len = len(plaintext) - pad_len = buf_len - text_len - pad_chr = bytes(chr(pad_len), "utf8") - plaintext_b = plaintext.encode() + pad_chr * pad_len - encrypted_text = b"".join([aes.encrypt(plaintext_b[i : i + 16]) for i in range(0, len(plaintext_b), 16)]) - return encrypted_text - - LOGIN_KEY_LENGTH = 20 - key = os.urandom(LOGIN_KEY_LENGTH) - real_key = realkey(key) - - outfile = BytesIO() - - outfile.write(struct.pack("i", 0)) - outfile.write(key) - - while True: - line = plaintext.readline() - if not line: - break - real_len = len(line) - pad_len = (int(real_len / 16) + 1) * 16 - - outfile.write(struct.pack("i", pad_len)) - x = encode_line(line, real_key, pad_len) - outfile.write(x) - - outfile.seek(0) - return outfile - - def read_and_decrypt_mylogin_cnf(f: BinaryIO) -> BytesIO | None: """Read and decrypt the contents of .mylogin.cnf. diff --git a/test/features/connection.feature b/test/features/connection.feature index eb54e02b..04d041d3 100644 --- a/test/features/connection.feature +++ b/test/features/connection.feature @@ -21,10 +21,3 @@ Feature: connect to a database: When we run mycli without arguments "host port" When we query "status" Then status contains "via UNIX socket" - - Scenario: run mycli with mylogin.cnf configuration - When we create mylogin.cnf file - When we run mycli with arguments "login_path=test_login_path" without arguments "host port user pass defaults_file" - Then we are logged in - - diff --git a/test/features/steps/connection.py b/test/features/steps/connection.py index 732a0938..74df2883 100644 --- a/test/features/steps/connection.py +++ b/test/features/steps/connection.py @@ -1,15 +1,10 @@ # type: ignore -import io -import os - from behave import then, when import wrappers -from mycli.config import encrypt_mylogin_cnf -from test.features.environment import MYLOGIN_CNF_PATH, get_db_name_from_context +from test.features.environment import get_db_name_from_context from test.features.steps.utils import parse_cli_args_to_dict -from test.utils import HOST, PASSWORD, PORT, USER TEST_LOGIN_PATH = "test_login_path" @@ -31,15 +26,6 @@ def status_contains(context, expression): context.atprompt = True -@when("we create mylogin.cnf file") -def step_create_mylogin_cnf_file(context): - os.environ.pop("MYSQL_TEST_LOGIN_FILE", None) - mylogin_cnf = f"[{TEST_LOGIN_PATH}]\nhost = {HOST}\nport = {PORT}\nuser = {USER}\npassword = {PASSWORD}\n" - with open(MYLOGIN_CNF_PATH, "wb") as f: - input_file = io.StringIO(mylogin_cnf) - f.write(encrypt_mylogin_cnf(input_file).read()) - - @then("we are logged in") def we_are_logged_in(context): db_name = get_db_name_from_context(context) diff --git a/test/pytests/test_config.py b/test/pytests/test_config.py index b51717bd..3abc1cce 100644 --- a/test/pytests/test_config.py +++ b/test/pytests/test_config.py @@ -16,7 +16,6 @@ from mycli.config import ( _remove_pad, create_default_config, - encrypt_mylogin_cnf, get_mylogin_cnf_path, log, open_mylogin_cnf, @@ -244,16 +243,6 @@ def test_open_mylogin_cnf_error_paths(monkeypatch, tmp_path, caplog) -> None: assert 'Unable to read login path file.' in caplog.text -def test_encrypt_mylogin_cnf_round_trip() -> None: - plaintext = StringIO('[client]\nuser=test\npassword=secret\n') - - encrypted = encrypt_mylogin_cnf(plaintext) - decrypted = read_and_decrypt_mylogin_cnf(encrypted) - - assert isinstance(encrypted, BytesIO) - assert decrypted.read().decode('utf8') == '[client]\nuser=test\npassword=secret\n' - - def test_read_and_decrypt_mylogin_cnf_error_branches(caplog) -> None: incomplete_key = BytesIO(struct.pack('i', 0) + b'a') with caplog.at_level(logging.ERROR, logger='mycli.config'):