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
40 changes: 24 additions & 16 deletions src/fscrypt/cli.py
Original file line number Diff line number Diff line change
@@ -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.")
Expand All @@ -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"
)
Expand All @@ -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
)
27 changes: 26 additions & 1 deletion src/fscrypt/file_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
28 changes: 26 additions & 2 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
50 changes: 48 additions & 2 deletions tests/test_file_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading