PyCypherLib 1.4.3
Secure file encryption and decryption library with fluent API. Install as PyCypherLib; import with: from PyCypher import Cy
Requires Python: >=3.12

# PyCypher

Python library for **secure file encryption and decryption** with a fluent API. Built on [cryptography](https://pypi.org/project/cryptography/) (Fernet) and [argon2-cffi](https://pypi.org/project/argon2-cffi/). Published on PyPI as **PyCypherLib**; you **import** with `from PyCypher import Cy`.

- **Fluent API**: chain methods like `Cy().enc("file.txt").newName("out.cy").P("password")`.
- **Two KDFs**: Argon2 (default, recommended) or PBKDF2; format auto-detected on decrypt.
- **File and in-memory**: encrypt/decrypt files or get bytes with `.toData()`.
- **Lines mode**: encrypt/decrypt single strings or lists of strings (e.g. credentials).
- **Decrypt & run**: execute encrypted `.py.cy` Python scripts with `.run()`.
- **Password change**: re-encrypt an existing `.cy` file with a new password (`.changeP()`).
- **Best-effort wiping**: in-memory and optional input-file wiping (not guaranteed on all systems).

---

## Install

```bash
pip install PyCypherLib
```

Dependencies: `cryptography`, `argon2-cffi`.

Import in code:

```python
from PyCypher import Cy
```

---

## Quick start

Encrypt and decrypt a file (default: Argon2, output file gets `.cy` suffix):

```python
from PyCypher import Cy

# Encrypt file.txt -> file.txt.cy
Cy().enc("file.txt").P("yourpassword")

# Decrypt file.txt.cy -> file.txt (or use .newName() for custom output)
Cy().dec("file.txt.cy").P("yourpassword")
```

Encrypt/decrypt **in memory** (get bytes, no file written):

```python
encrypted_bytes = Cy().enc("file.txt").toData().P("yourpassword")
decrypted_bytes = Cy().dec("file.txt.cy").toData().P("yourpassword")
```

---

## KDF selection

- **`Cy()`** – Default: Argon2 if available, otherwise PBKDF2.
- **`Cy("A")`** – Argon2 (recommended).
- **`Cy("P")`** – PBKDF2.

Decryption **auto-detects** the format (Argon2 vs PBKDF2) from the file header, so you can decrypt with any constructor.

```python
Cy("A").enc("sensitive.txt").P("strongpassword")   # Force Argon2
Cy("P").enc("legacy.txt").P("oldpassword")           # Force PBKDF2
Cy().dec("file.cy").P("password")                    # Auto-detect on decrypt
```

---

## File operations

| Method | Description |
|--------|-------------|
| **`.enc(input_file)`** | Encrypt a file; output is `input_file.cy` unless `.newName()` is used. |
| **`.dec(input_file)`** | Decrypt a `.cy` file; output name is derived from input or set with `.newName()`. |
| **`.newName(output_file)`** | Set the output file name. |
| **`.delInput()`** | After encrypt/decrypt, wipe and delete the input file (best-effort). |
| **`.toData()`** | Do not write to file; return encrypted/decrypted bytes (enc) or bytes (dec). |
| **`.P(password)`** | Set password and run the chain (min 8 characters). |
| **`.terminalP(msg="Password: ")`** | Prompt for password in the terminal (hidden input) and run. |

**Examples:**

```python
# Custom output name
Cy().enc("file.txt").newName("file.enc").P("mypassword")
Cy().dec("file.enc").newName("file.txt").P("mypassword")

# Encrypt and delete original
Cy().enc("file.txt").newName("file.cy").delInput().P("mypassword")

# Password from terminal
Cy().dec("file.enc").newName("file.txt").delInput().terminalP("Enter password: ")
```

---

## Lines (strings / lists)

Encrypt or decrypt **one string** or **a list of strings** (e.g. API key + secret). Default output file is `cyfile.cy` unless you pass a name to `.encLines(file)`.

| Method | Description |
|--------|-------------|
| **`.encLines(output_file=None)`** | Encrypt lines; output file optional (default `cyfile.cy`). |
| **`.decLines(input_file)`** | Decrypt a lines file; returns one string or a list of strings. |
| **`.Lines(data)`** | Set data: a single string or a list of strings. |
| **`.terminalL(msg="Enter line: ")`** | Append one line read from the terminal (can be chained). |

**Examples:**

```python
# Single string
Cy().encLines().Lines("yoursecretkey").P("yourpassword")

# List of strings
Cy().encLines().Lines(["yoursecretkey", "yoursecrettoken"]).P("yourpassword")

# Custom output file
Cy().encLines("credentials.cy").Lines(["key", "token"]).P("yourpassword")

# Prompt for lines and password in terminal
Cy().encLines("credentials.cy")\
    .terminalL("Enter your secret key: ")\
    .terminalL("Enter your secret token: ")\
    .terminalP("Enter a password to encrypt: ")

# Decrypt (returns string or list of strings)
key = Cy().decLines("key.cy").terminalP("Enter password: ")
key, token = Cy().decLines("credentials.cy").P("yourpassword")
```

---

## Decrypt and run (.py.cy)

Run an **encrypted Python script** (file must end with `.py.cy`). The script is decrypted in memory, executed, then cleared.

```python
Cy().run("script.py.cy").P("yourpassword")
# Or prompt for password:
Cy().run("script.py.cy").terminalP("Password: ")
```

---

## Change password

Re-encrypt an existing `.cy` file with a new password (same KDF as current instance). New password must be at least 8 characters.

```python
Cy().changeP("credentials.cy").newP("newpassword").P("oldpassword")
```

---

## Validation and errors

- **Password**: minimum 8 characters; empty password raises `ValueError`.
- **Filenames**: empty or invalid characters (`<>:"/\|?*`) raise `ValueError`.
- **`.run()`**: input must end with `.py.cy`.
- **`.changeP()`**: input must end with `.cy`.
- **Wrong password or corrupted data**: decryption raises `ValueError` with message `"Decryption failed: wrong password or corrupted data."` (or similar).
- **Missing file**: `FileNotFoundError`. **Permission**: `PermissionError`.

---

## Exports

- **`Cy`** – Main class. Use `Cy()` or `Cy("A")` / `Cy("P")` for KDF selection.
- **`__version__`** – Version string (e.g. `"1.4.2"`).

```python
from PyCypher import Cy, __version__
```

---

## Development status

PyCypher is maintained as a personal project. Suggestions, feature requests, and constructive feedback are welcome (issues or pull requests).