diff --git a/src/fscrypt/cli.py b/src/fscrypt/cli.py index aea50f5..5961571 100644 --- a/src/fscrypt/cli.py +++ b/src/fscrypt/cli.py @@ -1,9 +1,8 @@ from pathlib import Path -from typing import Annotated, Callable +from typing import Annotated import typer -from . import crypto_utils from . import file_tools app = typer.Typer(help="Encrypt and decrypt text files.") @@ -27,24 +26,20 @@ def encrypt_file( If --password is omitted, the final line of the input file is used as the password. """ + # Creates the parent directory of the output file + output_file.parent.mkdir(parents=True, exist_ok=True) - # Create a encryption function that either uses the last line of text, or password if password is given - encryption_function: Callable[[str], str] + # If there is a password create a encrypted file using it, if not encrypt using the last line of the file if password is None: - encryption_function = file_tools.encrypt_file_text_from_last_line + file_tools.encrypt_file_from_last_line(input_file, output_file) else: given_password: str = password + file_tools.encrypt_file_from_password(input_file, output_file, given_password) - def encryption_function(plaintext: str) -> str: - return crypto_utils.encryptStringFromPassword(plaintext, given_password) - - output_file.parent.mkdir(parents=True, exist_ok=True) - file_tools.map_file(input_file, output_file, encryption_function) - + # Prints out a message saying what file was encrypted and using what password: password_explaination: str = "the last line in in the file" if password is not None: password_explaination = password - typer.echo( f"Encrypted {input_file} -> {output_file} using {password_explaination} as the password" ) @@ -62,8 +57,21 @@ def encrypt_dir( Path, typer.Argument(help="output directory where encrypted files will be placed"), ], + password: Annotated[ + str | None, + typer.Option( + "--password", + "-p", + help="Password to use. If ommited the last line of each file is used", + ), + ] = None, ) -> None: - "Recreate structure of input directory in output director, but every file is encrypted" " with the last line of text used as a password." - file_tools.map_dir( - input_directory, output_directory, file_tools.encrypt_file_text_from_last_line - ) + "Recreate structure of input directory in output director, but every file is encrypted with the last line of text used as a password." + if password is None: + file_tools.encrypt_files_in_dir_from_last_lines( + input_directory, output_directory + ) + else: + file_tools.encrypt_files_in_dir_from_password( + input_directory, output_directory, password + ) diff --git a/src/fscrypt/file_tools.py b/src/fscrypt/file_tools.py index 504de99..0ad24c3 100644 --- a/src/fscrypt/file_tools.py +++ b/src/fscrypt/file_tools.py @@ -74,5 +74,30 @@ def encrypt_file_text_from_last_line(input_text: str) -> str: return outputText -def encrypt_all_files_in_dir(inputDirectory: str, outputDirectory: str): +def encryptor_from_password(password: str) -> Callable[[str], str]: + def encrypt_with_curried_password(plaintext: str) -> str: + return crypto_utils.encryptStringFromPassword(plaintext, password) + + return encrypt_with_curried_password + + +def encrypt_file_from_last_line(inputFile: str | Path, outputFile: str | Path) -> None: + map_file(inputFile, outputFile, encrypt_file_text_from_last_line) + + +def encrypt_file_from_password( + inputFile: str | Path, outputFile: str | Path, password: str +) -> None: + map_file(inputFile, outputFile, encryptor_from_password(password)) + + +def encrypt_files_in_dir_from_last_lines( + inputDirectory: str | Path, outputDirectory: str | Path +) -> None: map_dir(inputDirectory, outputDirectory, encrypt_file_text_from_last_line) + + +def encrypt_files_in_dir_from_password( + inputDirectory: str | Path, outputDirectory: str | Path, password: str +) -> None: + map_dir(inputDirectory, outputDirectory, encryptor_from_password(password)) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 3909a2e..0439aa1 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -25,7 +25,7 @@ def test_encrypt_file_with_pass_e2e(tmp_path: Path): assert "Encrypted" in result.stdout -def test_encrypt_file_with_no_pass_e2e(tmp_path: Path): +def test_encrypt_file_with_last_line_e2e(tmp_path: Path): password: str = "S3cur3P4ss123!" plaintext: str = "secrete message from alice to bob" body: str = plaintext + "\n" + password @@ -41,7 +41,31 @@ def test_encrypt_file_with_no_pass_e2e(tmp_path: Path): assert "Encrypted" in result.stdout -def test_encrypt_dir_recursive_e2e(tmp_path: Path): +def test_encrypt_dir_with_password_e2e(tmp_path: Path): + password: str = "FakePass18fdsjnK" + for filepath, plaintext, _ in PLAINTEXT_TEST_FILES: + filebody: str = plaintext + full_filepath: Path = tmp_path / "plain_dir" / filepath + full_filepath.parent.mkdir(parents=True, exist_ok=True) + full_filepath.write_text(filebody) + + run_cli( + "encrypt-dir", + str(tmp_path / "plain_dir"), + str(tmp_path / "crypt_dir"), + "-p", + password, + ) + + for path, contents, _ in PLAINTEXT_TEST_FILES: + full_filepath = tmp_path / "crypt_dir" / path + assert full_filepath.exists() + encrypted: str = full_filepath.read_text() + decrypted: str = decryptStringFromPassword(encrypted, password) + assert decrypted == contents + + +def test_encrypt_dir_with_last_line_e2e(tmp_path: Path): for filepath, plaintext, password in PLAINTEXT_TEST_FILES: filebody: str = plaintext + "\n" + password full_filepath: Path = tmp_path / "plain_dir" / filepath diff --git a/tests/test_file_tools.py b/tests/test_file_tools.py index 5ac7e6b..7bfefa3 100644 --- a/tests/test_file_tools.py +++ b/tests/test_file_tools.py @@ -29,15 +29,61 @@ def test_map_dir_transforms_nested_dir(fs: FakeFilesystem) -> None: assert (Path("/encrypted_dir") / path).read_text() == fake_encryption(contents) -def test_encrypt_all_files_in_dir_with_fake_dir(fs: FakeFilesystem) -> None: +def test_encrypt_file_with_last_line(fs: FakeFilesystem) -> None: + body: str = "Dear alice, u up? - Bob" + password: str = "aljfSLK£nc3&yvHSnk?" + plainPath: Path = Path("/plain.txt") + cryptPath: Path = Path("/crypt.txt") + + fs.create_file(plainPath, contents=body + "\n" + password) + file_tools.encrypt_file_from_last_line(plainPath, cryptPath) + + assert cryptPath.exists() + assert ( + crypto_utils.decryptStringFromPassword(cryptPath.read_text(), password) == body + ) + + +def test_encrypt_file_with_password(fs: FakeFilesystem) -> None: + body: str = "Dear alice, u up? - Bob" + password: str = "aljfSLK£nc3&yvHSnk?" + plainPath: Path = Path("/plain.txt") + cryptPath: Path = Path("/crypt.txt") + + fs.create_file(plainPath, contents=body) + file_tools.encrypt_file_from_password(plainPath, cryptPath, password) + + assert cryptPath.exists() + assert ( + crypto_utils.decryptStringFromPassword(cryptPath.read_text(), password) == body + ) + + +def test_encrypt_all_files_in_dir_with_last_line(fs: FakeFilesystem) -> None: for path, contents, password in PLAINTEXT_TEST_FILES: fs.create_file( Path("/plaintext_dir") / path, contents=contents + "\n" + password ) - file_tools.encrypt_all_files_in_dir("/plaintext_dir", "/encrypted_dir") + file_tools.encrypt_files_in_dir_from_last_lines("/plaintext_dir", "/encrypted_dir") for path, contents, password in PLAINTEXT_TEST_FILES: encrypted: str = (Path("/encrypted_dir") / path).read_text() decrypted: str = crypto_utils.decryptStringFromPassword(encrypted, password) assert decrypted == contents + + +def test_encrypt_all_files_in_dir_with_password(fs: FakeFilesystem) -> None: + for path, contents, _ in PLAINTEXT_TEST_FILES: + fs.create_file(Path("/plaintext_dir") / path, contents=contents) + + password: str = "COMMON_PASSWORD" + + file_tools.encrypt_files_in_dir_from_password( + "/plaintext_dir", "/encrypted_dir", password + ) + + for path, contents, _ in PLAINTEXT_TEST_FILES: + encrypted: str = (Path("/encrypted_dir") / path).read_text() + decrypted: str = crypto_utils.decryptStringFromPassword(encrypted, password) + assert decrypted == contents