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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ env/
desktop.ini/
.ruff_cache/
.mypy_cache/
write_ups/
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test = [
]

[project.scripts]
fscrypt = "fscrypt.main:main"
fscrypt = "fscrypt.cli:app"

[tool.setuptools.packages.find]
where = ["src"]
69 changes: 69 additions & 0 deletions src/fscrypt/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from pathlib import Path
from typing import Annotated, Callable

import typer

from . import crypto_utils
from . import file_tools

app = typer.Typer(help="Encrypt and decrypt text files.")


@app.command()
def encrypt_file(
input_file: Annotated[Path, typer.Argument(help="plaintext input file")],
output_file: Annotated[Path, typer.Argument(help="plaintext input file")],
password: Annotated[
str | None,
typer.Option(
"--password",
"-p",
help="Password to use. If ommited the last line of the file is used",
),
] = None,
) -> None:
"""
Encrypt one file with last line.

If --password is omitted, the final line of the input file is used as the password.
"""

# Create a encryption function that either uses the last line of text, or password if password is given
encryption_function: Callable[[str], str]
if password is None:
encryption_function = file_tools.encrypt_file_text_from_last_line
else:
given_password: str = 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)

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"
)


@app.command()
def encrypt_dir(
input_directory: Annotated[
Path,
typer.Argument(
help="dir full of plaintext input files with the password to each as the last line of text"
),
],
output_directory: Annotated[
Path,
typer.Argument(help="output directory where encrypted files will be placed"),
],
) -> 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
)
15 changes: 11 additions & 4 deletions src/fscrypt/file_tools.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from . import crypto_utils
from typing import Callable
import os
from pathlib import Path


def map_file(oldPath: str, newPath: str, function: Callable[[str], str]) -> None:
def map_file(
oldPath: str | Path, newPath: str | Path, function: Callable[[str], str]
) -> None:
# Read the old file
with open(oldPath, "r") as oldFile:
oldFileText: str = oldFile.read()
Expand All @@ -16,7 +19,11 @@ def map_file(oldPath: str, newPath: str, function: Callable[[str], str]) -> None
newFile.write(encryptedText)


def map_dir(inputDirectory: str, outputDirectory: str, function: Callable[[str], str]):
def map_dir(
inputDirectory: str | Path,
outputDirectory: str | Path,
function: Callable[[str], str],
):
# Runs checks sanity checks on inputs
if not os.path.exists(inputDirectory):
raise RuntimeError(
Expand Down Expand Up @@ -58,7 +65,7 @@ def strip_commenting(commented_line: str) -> str:
return commented_line


def encrypt_file_text_from_answer(input_text: str) -> str:
def encrypt_file_text_from_last_line(input_text: str) -> str:
"""split text into first and last lines and use last line to encrypt others"""
lines: list[str] = input_text.split("\n")
body: str = "\n".join(lines[:-1])
Expand All @@ -68,4 +75,4 @@ def encrypt_file_text_from_answer(input_text: str) -> str:


def encrypt_all_files_in_dir(inputDirectory: str, outputDirectory: str):
map_dir(inputDirectory, outputDirectory, encrypt_file_text_from_answer)
map_dir(inputDirectory, outputDirectory, encrypt_file_text_from_last_line)
10 changes: 0 additions & 10 deletions src/fscrypt/main.py

This file was deleted.

8 changes: 1 addition & 7 deletions tests/test_crypto_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,12 @@
import unittest
import os
import random
import string
from testing_utils import create_random_string


b64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"


def create_random_string(
length: int, alphabet=string.ascii_letters + string.digits
) -> str:
return "".join([random.choice(alphabet) for _ in range(length)])


class TestB64StringUtils(unittest.TestCase):
def test_encode_decode(self) -> None:
"""Checks that you can encode then decode bytes and end up with the same bytes"""
Expand Down
58 changes: 58 additions & 0 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from pathlib import Path
import subprocess
from testing_utils import PLAINTEXT_TEST_FILES
from fscrypt.crypto_utils import decryptStringFromPassword


def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["fscrypt", *args], text=True, capture_output=True, check=False
)


def test_encrypt_file_with_pass_e2e(tmp_path: Path):
password: str = "S3cur3P4ss123!"
plaintext: str = "secrete message from alice to bob"
input_file = tmp_path / "plain_text.txt"
input_file.write_text(plaintext)
output_file = tmp_path / "output_file.txt"

result = run_cli("encrypt-file", str(input_file), str(output_file), "-p", password)

assert result.returncode == 0
assert output_file.exists()
assert decryptStringFromPassword(output_file.read_text(), password) == plaintext
assert "Encrypted" in result.stdout


def test_encrypt_file_with_no_pass_e2e(tmp_path: Path):
password: str = "S3cur3P4ss123!"
plaintext: str = "secrete message from alice to bob"
body: str = plaintext + "\n" + password
input_file = tmp_path / "plain_text.txt"
input_file.write_text(body)
output_file = tmp_path / "output_file.txt"

result = run_cli("encrypt-file", str(input_file), str(output_file))

assert result.returncode == 0
assert output_file.exists()
assert decryptStringFromPassword(output_file.read_text(), password) == plaintext
assert "Encrypted" in result.stdout


def test_encrypt_dir_recursive_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
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"))

for path, contents, password 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
31 changes: 5 additions & 26 deletions tests/test_file_tools.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
from pathlib import Path
from pyfakefs.fake_filesystem import FakeFilesystem
from testing_utils import fake_encryption, PLAINTEXT_TEST_FILES

import fscrypt.file_tools as file_tools
import fscrypt.crypto_utils as crypto_utils


def fake_encryption(plaintext: str) -> str:
return f"ENCRYPTED({plaintext})"


plaintext_test_files: list[tuple[str, str, str]] = [
("message1.txt", "Love letter from Bob to Alice, very secure!", "pass1"),
("message2.txt", "Dear Bob, I am glad to hear you feel the same way", "pass2"),
("message3.txt", "Let's go watch a movie together on friday?\npass3", "pass3"),
("diary/entry1.txt", "I have such a crush on bob", "AKfmcIodlp98720"),
(
"diary/entry2.txt",
"I hope he feels the same way about me\npass3",
"Very Long Password!!!udksPOldSuper3Secure",
),
(
"diary/super_secret/self_only.txt",
"my RSA primes are 67 and 722896686382073903939970876463",
"67722896686382073903939970876463",
),
]


def test_map_file_transforms_file(fs: FakeFilesystem) -> None:
plaintext_test_message: str = (
"Love letter from Bob to Alice, Very important to be kept Secure!"
Expand All @@ -41,24 +20,24 @@ def test_map_file_transforms_file(fs: FakeFilesystem) -> None:


def test_map_dir_transforms_nested_dir(fs: FakeFilesystem) -> None:
for path, contents, _ in plaintext_test_files:
for path, contents, _ in PLAINTEXT_TEST_FILES:
fs.create_file(Path("/plaintext_dir") / path, contents=contents)

file_tools.map_dir("/plaintext_dir", "/encrypted_dir", fake_encryption)

for path, contents, _ in plaintext_test_files:
for path, contents, _ in PLAINTEXT_TEST_FILES:
assert (Path("/encrypted_dir") / path).read_text() == fake_encryption(contents)


def test_encrypt_all_files_in_dir_with_fake_dir(fs: FakeFilesystem) -> None:
for path, contents, password in plaintext_test_files:
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")

for path, contents, password in plaintext_test_files:
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
29 changes: 29 additions & 0 deletions tests/testing_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import random
import string

PLAINTEXT_TEST_FILES: list[tuple[str, str, str]] = [
("message1.txt", "Love letter from Bob to Alice, very secure!", "pass1"),
("message2.txt", "Dear Bob, I am glad to hear you feel the same way", "pass2"),
("message3.txt", "Let's go watch a movie together on friday?\npass3", "pass3"),
("diary/entry1.txt", "I have such a crush on bob", "AKfmcIodlp98720"),
(
"diary/entry2.txt",
"I hope he feels the same way about me\npass3",
"Very Long Password!!!udksPOldSuper3Secure",
),
(
"diary/super_secret/self_only.txt",
"my RSA primes are 67 and 722896686382073903939970876463",
"67722896686382073903939970876463",
),
]


def create_random_string(
length: int, alphabet=string.ascii_letters + string.digits
) -> str:
return "".join([random.choice(alphabet) for _ in range(length)])


def fake_encryption(plaintext: str) -> str:
return f"ENCRYPTED({plaintext})"
1 change: 0 additions & 1 deletion write_ups/project-euler/1.py

This file was deleted.

Loading