Add PGP plugin

PGP plugin for added security layer in messaging, included readme and easy installation script.
Automatically generate PGP signature keys on first load and auto encrypt / decript messages using PGP.
 
After install usage:

──────────────────────────────────────────────────────────────────────
PGP PLUGIN - COMMANDS
──────────────────────────────────────────────────────────────────────

📊 Status & Info:
  pgp status              - Show PGP status and settings
  pgp list                - List all keys in keyring

🔑 Key Management:
  pgp keygen              - Generate new PGP key pair
  pgp export              - Export your public key
  pgp import <contact>    - Request public key from contact
  pgp trust <contact> <key> - Import and trust a public key

📨 Messaging:
  pgp send <contact> <msg> - Send encrypted message

⚙️  Settings:
  pgp set auto_encrypt on/off    - Auto-encrypt outgoing
  pgp set auto_sign on/off        - Auto-sign outgoing
  pgp set auto_decrypt on/off     - Auto-decrypt incoming
  pgp set auto_verify on/off      - Auto-verify signatures
  pgp set reject_unsigned on/off  - Reject unsigned messages
  pgp set reject_unencrypted on/off - Reject unencrypted

──────────────────────────────────────────────────────────────────────
This commit is contained in:
F
2025-12-20 12:46:02 +01:00
committed by GitHub
parent 3a0f5d6ea9
commit e5787f5ef8
4 changed files with 1518 additions and 0 deletions

View File

@@ -0,0 +1,351 @@
# PGP Plugin for LXMF CLI
End-to-end encryption and digital signatures for LXMF messages using PGP/GPG.
## Features
- 🔐 **Automatic Encryption/Decryption**: Seamlessly encrypt outgoing and decrypt incoming messages
- ✍️ **Digital Signatures**: Sign messages to prove authenticity and verify incoming signatures
- 🔑 **Key Management**: Generate, import, export, and manage PGP keys
- 🛡️ **Security Policies**: Reject unsigned or unencrypted messages
- 🤖 **Autonomous Operation**: Works automatically in the background once configured
- 👥 **Contact Key Mapping**: Automatically associates PGP keys with LXMF contacts
## Installation
### Prerequisites
1. **Python GnuPG library**:
```bash
pip install python-gnupg --break-system-packages
```
2. **GnuPG binary** (if not already installed):
**Termux/Android:**
```bash
pkg install gnupg
```
**Debian/Ubuntu:**
```bash
sudo apt install gnupg
```
**macOS:**
```bash
brew install gnupg
```
**Windows:**
Download and install from: https://gnupg.org/download/
### Installing the Plugin
1. Copy `pgp.py` to your LXMF client's plugins directory:
```bash
# Default location
cp pgp.py ~/.local/share/lxmf_client_storage/plugins/
```
2. Start the LXMF client - the plugin will auto-load
3. First run will automatically generate a PGP key pair for you
## Quick Start
### Basic Usage
1. **Check PGP status:**
```
pgp status
```
2. **Export your public key** (to share with contacts):
```
pgp export
```
3. **Import a contact's public key:**
```
pgp trust Alice <paste their public key>
```
4. **Send encrypted message:**
```
pgp send Alice Hello, this is encrypted!
```
### Automatic Mode
Enable automatic encryption for all messages:
```
pgp set auto_encrypt on
pgp set auto_sign on
```
Now all outgoing messages will be automatically encrypted and signed!
## Commands Reference
### Status & Information
| Command | Description |
|---------|-------------|
| `pgp status` | Show PGP configuration and statistics |
| `pgp list` | List all keys in keyring |
| `pgp help` | Show command help |
### Key Management
| Command | Description |
|---------|-------------|
| `pgp keygen` | Generate new PGP key pair (replaces current) |
| `pgp export` | Display your public key for sharing |
| `pgp import <contact>` | Request public key from contact |
| `pgp trust <contact> <key>` | Import and trust a public key |
### Messaging
| Command | Description |
|---------|-------------|
| `pgp send <contact> <message>` | Send encrypted & signed message |
### Settings
| Setting | Values | Description |
|---------|--------|-------------|
| `auto_encrypt` | on/off | Automatically encrypt all outgoing messages |
| `auto_sign` | on/off | Automatically sign all outgoing messages |
| `auto_decrypt` | on/off | Automatically decrypt incoming messages |
| `auto_verify` | on/off | Automatically verify incoming signatures |
| `reject_unsigned` | on/off | Reject messages without valid signatures |
| `reject_unencrypted` | on/off | Reject unencrypted messages |
**Example:**
```
pgp set auto_encrypt on
pgp set reject_unencrypted on
```
## Usage Scenarios
### Scenario 1: Secure Communication Setup
**You and Alice want secure communications:**
1. **You export your key:**
```
pgp export
```
Copy the output
2. **Send it to Alice via regular LXMF:**
```
send Alice -----BEGIN PGP PUBLIC KEY BLOCK----- <rest of key>
```
3. **Alice imports your key:**
```
pgp trust <your_name> <your_public_key>
```
4. **Alice sends you her key the same way**
5. **You import Alice's key:**
```
pgp trust Alice <alice_public_key>
```
6. **Now send encrypted messages:**
```
pgp send Alice This is secret!
```
### Scenario 2: Automatic Encryption
**Enable full automatic mode:**
```
pgp set auto_encrypt on
pgp set auto_sign on
pgp set auto_decrypt on
pgp set auto_verify on
```
Now:
- All outgoing messages are automatically encrypted & signed
- All incoming encrypted messages are automatically decrypted
- All signatures are automatically verified
- You can use normal `send` command and it works transparently!
### Scenario 3: High Security Mode
**Only accept encrypted & signed messages:**
```
pgp set reject_unencrypted on
pgp set reject_unsigned on
```
This will automatically reject any message that isn't both encrypted AND signed.
## How It Works
### Outgoing Messages
1. **Manual encryption** (`pgp send`):
- Message is signed with your private key
- Signed message is encrypted with recipient's public key
- Encrypted blob is sent via LXMF
2. **Automatic encryption** (`auto_encrypt on`):
- Intercepts normal `send` commands
- Automatically encrypts if recipient's key is known
- Falls back to unencrypted if no key available
### Incoming Messages
1. **Automatic decryption** (`auto_decrypt on`):
- Detects PGP encrypted messages
- Decrypts using your private key
- Shows decrypted content
2. **Automatic verification** (`auto_verify on`):
- Detects PGP signed messages
- Verifies signature
- Shows validity status and signer info
### Key Exchange
The plugin supports multiple key exchange methods:
1. **Manual exchange**: Copy/paste keys via any channel
2. **LXMF exchange**: Send keys as regular messages
3. **Out-of-band**: QR codes, files, etc.
## Security Considerations
### Key Storage
- Keys are stored in `~/.local/share/lxmf_client_storage/plugins/pgp/keyring/`
- This directory should be kept secure (use encryption at rest if possible)
- Private keys are never transmitted
### Trust Model
- **Manual trust**: You explicitly trust each imported key
- **No key servers**: Keys are exchanged directly between users
- **Contact mapping**: Keys are linked to LXMF addresses for convenience
### Limitations
- **No forward secrecy**: PGP doesn't provide forward secrecy like Signal/OTR
- **Key compromise**: If private key is compromised, all past messages are readable
- **No group encryption**: Each message encrypted separately for each recipient
### Best Practices
1. **Verify fingerprints** out-of-band when possible
2. **Backup your private key** securely
3. **Use strong passphrases** (future feature)
4. **Regenerate keys periodically** for long-term security
5. **Enable reject policies** for sensitive communications
## Troubleshooting
### "gnupg not found"
Install python-gnupg:
```bash
pip install python-gnupg --break-system-packages
```
### "Failed to generate key"
Ensure GnuPG is installed:
```bash
gpg --version
```
### "No public key for contact"
Import their public key first:
```bash
pgp trust <contact> <their_public_key>
```
### "Decryption failed"
- Message wasn't encrypted for your key
- Check your key is properly configured: `pgp status`
### Key not working after restart
- Keys are persistent in the keyring
- Check: `pgp list`
- Reimport if needed
## Advanced Usage
### Multiple Keys
You can have multiple keys in your keyring. Set the active one:
```
pgp keygen
```
### Export Specific Key
```bash
pgp export
```
### Key Fingerprint Verification
List all keys with full fingerprints:
```
pgp list
```
Compare fingerprints out-of-band (phone call, in person, etc.)
## Integration with LXMF CLI
The plugin seamlessly integrates with your LXMF client:
- **Works with all commands**: `send`, `reply`, `sendpeer`, etc.
- **Contact resolution**: Use contact names, numbers, or hashes
- **Message history**: Decrypted messages are stored decrypted
- **Notifications**: Normal notification system works with encrypted messages
## File Locations
```
~/.local/share/lxmf_client_storage/plugins/pgp/
├── keyring/ # GnuPG keyring (private & public keys)
├── config.json # Plugin settings
└── trusted_keys.json # Contact->Key mappings
```
## Privacy & Security
- ✅ End-to-end encryption
- ✅ Digital signatures for authenticity
- ✅ Local key storage only
- ✅ No key servers (no metadata leakage)
- ✅ Works offline
- ❌ No forward secrecy (use Signal Protocol plugin for that)
- ❌ No key expiration (manual rotation recommended)
## Contributing
Found a bug? Have a feature request? Please open an issue!
## License
Same as LXMF CLI client
## Credits
Built on:
- **GnuPG**: The GNU Privacy Guard
- **python-gnupg**: Python wrapper for GnuPG
- **LXMF**: Lightweight Extensible Message Format
- **Reticulum**: The cryptography-based networking stack

View File

@@ -0,0 +1,373 @@
# PGP Plugin - Usage Examples
## Example 1: First Time Setup
```bash
# Start LXMF CLI - PGP plugin auto-generates key on first run
$ python lxmf_cli.py
[PGP Plugin loads]
PGP PLUGIN - FIRST TIME SETUP
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
No PGP key found. Let's create one for you.
This will be used to sign and encrypt your messages.
Name: Alice
Email: a1b2c3d4e5f6@lxmf.local
Generating 2048-bit RSA key pair...
This may take a minute...
✓ PGP key pair generated!
✓ Key ID: 1234567890ABCDEF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Check status
> pgp status
PGP STATUS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔑 Your Key:
Key ID: 1234567890ABCDEF
Name: Alice <a1b2c3d4e5f6@lxmf.local>
Type: RSA 2048-bit
⚙️ Settings:
Auto-encrypt: OFF
Auto-sign: ON
Auto-decrypt: ON
Auto-verify: ON
Reject unsigned: OFF
Reject unencrypted: OFF
👥 Trusted Keys: 0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## Example 2: Exchanging Keys with Bob
```bash
# Alice exports her public key
> pgp export
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
YOUR PUBLIC KEY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGX1... [full key data] ...=abcd
-----END PGP PUBLIC KEY BLOCK-----
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 Share this with contacts so they can send you encrypted messages
You can send it via: send <contact> <paste key here>
# Alice sends her public key to Bob
> send Bob -----BEGIN PGP PUBLIC KEY BLOCK----- mQENBGX1...=abcd -----END PGP PUBLIC KEY BLOCK-----
📤 Sending to: Bob...
✅ Delivered to Bob (2.3s)
# Bob receives the key and imports it
[Bob's terminal]
📨 NEW MESSAGE from: Alice
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGX1...=abcd
-----END PGP PUBLIC KEY BLOCK-----
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> pgp trust Alice -----BEGIN PGP PUBLIC KEY BLOCK----- mQENBGX1...=abcd -----END PGP PUBLIC KEY BLOCK-----
✓ [PGP] Imported public key: 1234567890ABCDEF
✓ [PGP] Trusted key for Alice
# Bob sends his key to Alice (same process)
> pgp export
[copies key]
> send Alice -----BEGIN PGP PUBLIC KEY BLOCK----- ...
# Alice imports Bob's key
> pgp trust Bob -----BEGIN PGP PUBLIC KEY BLOCK----- ...
[PGP] Imported public key: FEDCBA0987654321
[PGP] Trusted key for Bob
```
## Example 3: Sending Encrypted Messages
```bash
# Alice sends encrypted message to Bob
> pgp send Bob Hey Bob! This message is encrypted and signed!
[PGP] Sent encrypted & signed message
📤 Sending to: Bob...
✅ Delivered to Bob (3.1s)
# Bob receives and automatically decrypts
[Bob's terminal]
🔐 Encrypted message from Alice
✓ [PGP] Message decrypted
✓ [PGP] ✓ Signature valid - From: Alice <a1b2c3d4e5f6@lxmf.local>
Key ID: 1234567890ABCDEF
📨 NEW MESSAGE from: Alice
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Hey Bob! This message is encrypted and signed!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 Type 'reply <message>' to respond
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## Example 4: Automatic Mode
```bash
# Enable automatic encryption and signing
> pgp set auto_encrypt on
[PGP] auto_encrypt: enabled
> pgp set auto_sign on
[PGP] auto_sign: enabled
# Now regular send commands are automatically encrypted!
> send Bob This is automatically encrypted!
[Behind the scenes: message is signed, encrypted, then sent]
✅ Delivered to Bob (2.8s)
# Bob receives it - automatically decrypted
[Bob's terminal]
🔐 Encrypted message from Alice
[PGP] Message decrypted
[PGP] ✓ Signature valid - From: Alice <a1b2c3d4e5f6@lxmf.local>
📨 NEW MESSAGE from: Alice
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This is automatically encrypted!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## Example 5: High Security Mode
```bash
# Only accept encrypted and signed messages
> pgp set reject_unencrypted on
[PGP] reject_unencrypted: enabled
> pgp set reject_unsigned on
[PGP] reject_unsigned: enabled
# Charlie (who doesn't have PGP) tries to send unencrypted message
[Charlie sends: "Hey Alice!"]
[Alice's terminal - message rejected]
⚠ [PGP] Rejected unencrypted message from <charlie_hash>
Enable 'pgp set reject_unencrypted off' to receive unencrypted messages
# The message is blocked - Alice never sees it
```
## Example 6: Multi-User Scenario
```bash
# Alice has keys for multiple contacts
> pgp status
PGP STATUS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔑 Your Key:
Key ID: 1234567890ABCDEF
Name: Alice <a1b2c3d4e5f6@lxmf.local>
Type: RSA 2048-bit
⚙️ Settings:
Auto-encrypt: ON
Auto-sign: ON
Auto-decrypt: ON
Auto-verify: ON
👥 Trusted Keys: 3
Bob: FEDCBA09876543...
Charlie: AABBCCDD112233...
Dave: 99887766554433...
# Send to multiple people - each message encrypted separately
> send Bob Secret for Bob only
> send Charlie Different secret for Charlie
> send Dave Yet another secret for Dave
# Each recipient gets their own encrypted copy
# Even if they intercept each other's messages, they can't decrypt them
```
## Example 7: Signed but Unencrypted Messages
```bash
# Sometimes you want authentication but not secrecy
> pgp set auto_encrypt off
[PGP] auto_encrypt: disabled
> pgp set auto_sign on
[PGP] auto_sign: enabled
> send Bob This is signed but readable by anyone who intercepts it
[Message is signed but not encrypted - proves it's from Alice]
[Bob receives]
[PGP] ✓ Signature valid - From: Alice <a1b2c3d4e5f6@lxmf.local>
📨 NEW MESSAGE from: Alice
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This is signed but readable by anyone who intercepts it
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## Example 8: Verifying Fingerprints Out-of-Band
```bash
# Alice and Bob meet in person to verify keys
> pgp list
PGP KEYRING
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Key ID Type Name
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
★ 1234567890ABCDEF RSA 2048-bit Alice <a1b2c3d4e5f6@lxmf.local>
FEDCBA0987654321 RSA 2048-bit Bob <b2c3d4e5f6a7@lxmf.local>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
= Your key
# Alice reads out her fingerprint: "1234 5678 90AB CDEF"
# Bob verifies it matches what he has
# Bob reads his: "FEDC BA09 8765 4321"
# Alice confirms match
# Now they know the keys are authentic!
```
## Example 9: Emergency Key Rotation
```bash
# Private key compromised! Generate new one
> pgp keygen
⚠ Warning: This will replace your current key!
Continue? [y/N]: y
PGP PLUGIN - FIRST TIME SETUP
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Name: Alice
Email: a1b2c3d4e5f6@lxmf.local
Generating 2048-bit RSA key pair...
✓ PGP key pair generated!
✓ Key ID: ABCDEF1234567890
# Export new key and send to all contacts
> pgp export
[copy key]
> send Bob KEY ROTATION - please import this new key
> send Charlie KEY ROTATION - please import this new key
> send Dave KEY ROTATION - please import this new key
```
## Example 10: Plugin Management
```bash
# List all plugins
> plugin list
PLUGINS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Plugin Status Description
──────────────────── ─────────────── ──────────────────────────────
pgp Loaded End-to-end PGP encryption
echo Disabled Echo bot example
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Disable PGP temporarily
> plugin disable pgp
✓ Plugin pgp disabled
⚠ Use 'plugin reload' to deactivate
> plugin reload
✓ Plugins reloaded
# Re-enable
> plugin enable pgp
✓ Plugin pgp enabled
⚠ Use 'plugin reload' to activate
> plugin reload
✓ Plugins reloaded
[PGP] PGP plugin loaded
[PGP] Using key: 1234567890ABCDEF
```
## Common Workflows
### Workflow 1: Initial Contact Setup
1. `pgp export` - Get your public key
2. `send <contact> <your_key>` - Send to contact
3. Wait for their key
4. `pgp trust <contact> <their_key>` - Import their key
5. `pgp send <contact> test` - Test encrypted messaging
### Workflow 2: Daily Use (Auto Mode)
1. `pgp set auto_encrypt on` - One time setup
2. `pgp set auto_sign on`
3. Use normal `send` commands - encryption is automatic!
### Workflow 3: Paranoid Mode
1. `pgp set reject_unencrypted on`
2. `pgp set reject_unsigned on`
3. `pgp set auto_encrypt on`
4. `pgp set auto_sign on`
5. Only encrypted & signed messages allowed
## Troubleshooting Examples
### Problem: Can't decrypt received message
```bash
> [Message shows encrypted blob]
# Check if you have the right key
> pgp status
# Look at "Your Key" - should match recipient
# Try manual decrypt (shouldn't be needed with auto_decrypt on)
> pgp set auto_decrypt on
```
### Problem: Recipient can't decrypt your message
```bash
# Did you import their public key?
> pgp list
# Check if their key is listed
# If not:
> pgp trust <contact> <their_public_key>
```
### Problem: Signature verification fails
```bash
[PGP] Invalid or missing signature!
# Possible causes:
# 1. Wrong key imported for this contact
# 2. Message was tampered with
# 3. Sender didn't actually sign it
# Solution: Re-verify fingerprints out-of-band
```

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env bash
# PGP Plugin Installer for LXMF CLI
set -e
echo "======================================"
echo "PGP Plugin Installer for LXMF CLI"
echo "======================================"
echo ""
# Detect OS
OS="$(uname -s)"
case "${OS}" in
Linux*) MACHINE=Linux;;
Darwin*) MACHINE=Mac;;
CYGWIN*) MACHINE=Windows;;
MINGW*) MACHINE=Windows;;
*) MACHINE="UNKNOWN:${OS}"
esac
echo "Detected OS: ${MACHINE}"
echo ""
# Check for Termux
if [ -d "/data/data/com.termux" ]; then
echo "Termux environment detected"
IS_TERMUX=true
else
IS_TERMUX=false
fi
# Step 1: Check for Python
echo "Checking Python installation..."
if command -v python3 &> /dev/null; then
PYTHON_VERSION=$(python3 --version)
echo "✓ Found: ${PYTHON_VERSION}"
else
echo "❌ Python 3 not found!"
echo "Please install Python 3 first"
exit 1
fi
echo ""
# Step 2: Check for GnuPG
echo "Checking GnuPG installation..."
if command -v gpg &> /dev/null; then
GPG_VERSION=$(gpg --version | head -n 1)
echo "✓ Found: ${GPG_VERSION}"
else
echo "❌ GnuPG not found!"
echo ""
echo "Installation instructions:"
if [ "$IS_TERMUX" = true ]; then
echo " pkg install gnupg"
elif [ "$MACHINE" = "Linux" ]; then
echo " Debian/Ubuntu: sudo apt install gnupg"
echo " Fedora: sudo dnf install gnupg"
echo " Arch: sudo pacman -S gnupg"
elif [ "$MACHINE" = "Mac" ]; then
echo " brew install gnupg"
else
echo " Download from: https://gnupg.org/download/"
fi
echo ""
read -p "Install GnuPG now? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
if [ "$IS_TERMUX" = true ]; then
pkg install gnupg -y
elif [ "$MACHINE" = "Linux" ]; then
if command -v apt &> /dev/null; then
sudo apt update && sudo apt install gnupg -y
elif command -v dnf &> /dev/null; then
sudo dnf install gnupg -y
elif command -v pacman &> /dev/null; then
sudo pacman -S gnupg --noconfirm
fi
elif [ "$MACHINE" = "Mac" ]; then
brew install gnupg
fi
else
echo "Please install GnuPG manually and run this script again"
exit 1
fi
fi
echo ""
# Step 3: Install python-gnupg
echo "Installing python-gnupg..."
if [ "$IS_TERMUX" = true ]; then
pip install python-gnupg --break-system-packages
else
if command -v pip3 &> /dev/null; then
pip3 install python-gnupg --user
else
python3 -m pip install python-gnupg --user
fi
fi
echo "✓ python-gnupg installed"
echo ""
# Step 4: Find LXMF storage directory
echo "Locating LXMF client storage..."
# Common locations
POSSIBLE_PATHS=(
"$HOME/.local/share/lxmf_client_storage"
"$HOME/lxmf_client_storage"
"./lxmf_client_storage"
)
STORAGE_PATH=""
for path in "${POSSIBLE_PATHS[@]}"; do
if [ -d "$path" ]; then
STORAGE_PATH="$path"
break
fi
done
if [ -z "$STORAGE_PATH" ]; then
echo "⚠ Could not auto-detect LXMF storage directory"
echo ""
read -p "Enter path to LXMF storage directory: " STORAGE_PATH
if [ ! -d "$STORAGE_PATH" ]; then
echo "❌ Directory not found: $STORAGE_PATH"
exit 1
fi
fi
echo "✓ Found: $STORAGE_PATH"
echo ""
# Step 5: Create plugins directory
PLUGINS_DIR="${STORAGE_PATH}/plugins"
echo "Setting up plugins directory..."
mkdir -p "$PLUGINS_DIR"
echo "✓ Created: $PLUGINS_DIR"
echo ""
# Step 6: Copy plugin file
echo "Installing PGP plugin..."
if [ -f "pgp.py" ]; then
cp pgp.py "$PLUGINS_DIR/"
echo "✓ Copied pgp.py to $PLUGINS_DIR/"
else
echo "❌ pgp.py not found in current directory!"
echo "Please run this script from the directory containing pgp.py"
exit 1
fi
echo ""
# Step 7: Set permissions
chmod 644 "$PLUGINS_DIR/pgp.py"
echo "✓ Set file permissions"
echo ""
# Done!
echo "======================================"
echo "Installation Complete!"
echo "======================================"
echo ""
echo "Next steps:"
echo "1. Start your LXMF client"
echo "2. The PGP plugin will auto-load"
echo "3. Run 'pgp status' to verify"
echo "4. Run 'pgp help' for commands"
echo ""
echo "Quick start:"
echo " pgp export - Get your public key"
echo " pgp trust <contact> <key> - Import contact's key"
echo " pgp send <contact> <msg> - Send encrypted message"
echo ""
echo "For full documentation, see: PGP_PLUGIN_README.md"
echo ""

616
plugins/pgp.py Normal file
View File

@@ -0,0 +1,616 @@
#!/usr/bin/env python3
"""
PGP Plugin for LXMF CLI
Provides end-to-end encryption and signing for LXMF messages using PGP/GPG
"""
import os
import json
import time
import gnupg
from datetime import datetime
class Plugin:
def __init__(self, client):
self.client = client
self.commands = ['pgp']
self.description = "End-to-end PGP encryption and signing for messages"
# Setup plugin storage
self.plugin_dir = os.path.join(client.storage_path, "plugins", "pgp")
self.keyring_dir = os.path.join(self.plugin_dir, "keyring")
self.config_file = os.path.join(self.plugin_dir, "config.json")
self.trusted_keys_file = os.path.join(self.plugin_dir, "trusted_keys.json")
os.makedirs(self.keyring_dir, exist_ok=True)
# Initialize GPG
self.gpg = gnupg.GPG(gnupghome=self.keyring_dir)
# Load configuration
self.config = self.load_config()
self.trusted_keys = self.load_trusted_keys()
# Auto-enable settings
self.auto_encrypt = self.config.get('auto_encrypt', False)
self.auto_sign = self.config.get('auto_sign', True)
self.auto_verify = self.config.get('auto_verify', True)
self.auto_decrypt = self.config.get('auto_decrypt', True)
self.reject_unsigned = self.config.get('reject_unsigned', False)
self.reject_unencrypted = self.config.get('reject_unencrypted', False)
# Current user's key
self.my_key_id = self.config.get('my_key_id', None)
# Initialize key if needed
if not self.my_key_id:
self._first_time_setup()
self._print_success("PGP plugin loaded")
if self.my_key_id:
self._print_success(f"Using key: {self.my_key_id[:16]}...")
def _print_success(self, msg):
"""Print success message"""
if hasattr(self.client, '_print_success'):
self.client._print_success(f"[PGP] {msg}")
else:
print(f"✓ [PGP] {msg}")
def _print_error(self, msg):
"""Print error message"""
if hasattr(self.client, '_print_error'):
self.client._print_error(f"[PGP] {msg}")
else:
print(f"❌ [PGP] {msg}")
def _print_warning(self, msg):
"""Print warning message"""
if hasattr(self.client, '_print_warning'):
self.client._print_warning(f"[PGP] {msg}")
else:
print(f"⚠ [PGP] {msg}")
def load_config(self):
"""Load plugin configuration"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r') as f:
return json.load(f)
except Exception as e:
self._print_warning(f"Error loading config: {e}")
return {}
def save_config(self):
"""Save plugin configuration"""
try:
config = {
'my_key_id': self.my_key_id,
'auto_encrypt': self.auto_encrypt,
'auto_sign': self.auto_sign,
'auto_verify': self.auto_verify,
'auto_decrypt': self.auto_decrypt,
'reject_unsigned': self.reject_unsigned,
'reject_unencrypted': self.reject_unencrypted
}
with open(self.config_file, 'w') as f:
json.dump(config, f, indent=2)
except Exception as e:
self._print_warning(f"Error saving config: {e}")
def load_trusted_keys(self):
"""Load trusted public keys mapping (hash -> key_id)"""
if os.path.exists(self.trusted_keys_file):
try:
with open(self.trusted_keys_file, 'r') as f:
return json.load(f)
except Exception as e:
self._print_warning(f"Error loading trusted keys: {e}")
return {}
def save_trusted_keys(self):
"""Save trusted keys mapping"""
try:
with open(self.trusted_keys_file, 'w') as f:
json.dump(self.trusted_keys, f, indent=2)
except Exception as e:
self._print_warning(f"Error saving trusted keys: {e}")
def _first_time_setup(self):
"""First time setup - generate PGP key"""
print("\n" + ""*60)
print("PGP PLUGIN - FIRST TIME SETUP")
print(""*60)
print("\nNo PGP key found. Let's create one for you.")
print("This will be used to sign and encrypt your messages.\n")
# Use display name from client
name = self.client.display_name if hasattr(self.client, 'display_name') else "LXMF User"
# Generate email from LXMF address
if hasattr(self.client.destination, 'hash'):
import RNS
lxmf_addr = RNS.prettyhexrep(self.client.destination.hash).replace(":", "")
email = f"{lxmf_addr[:16]}@lxmf.local"
else:
email = "user@lxmf.local"
print(f"Name: {name}")
print(f"Email: {email}")
print("\nGenerating 2048-bit RSA key pair...")
print("This may take a minute...\n")
try:
key_input = self.gpg.gen_key_input(
name_real=name,
name_email=email,
key_type='RSA',
key_length=2048,
expire_date=0 # Never expire
)
key = self.gpg.gen_key(key_input)
if key:
self.my_key_id = str(key)
self.save_config()
self._print_success("PGP key pair generated!")
self._print_success(f"Key ID: {self.my_key_id}")
print("\n" + ""*60 + "\n")
else:
self._print_error("Failed to generate key")
except Exception as e:
self._print_error(f"Key generation failed: {e}")
def get_recipient_key(self, dest_hash):
"""Get recipient's public key ID"""
# Normalize hash
clean_hash = dest_hash.replace(":", "").replace(" ", "").replace("<", "").replace(">", "").lower()
return self.trusted_keys.get(clean_hash)
def import_public_key(self, dest_hash, key_data):
"""Import a recipient's public key"""
try:
result = self.gpg.import_keys(key_data)
if result.count > 0:
key_id = result.fingerprints[0]
# Store mapping
clean_hash = dest_hash.replace(":", "").replace(" ", "").replace("<", "").replace(">", "").lower()
self.trusted_keys[clean_hash] = key_id
self.save_trusted_keys()
self._print_success(f"Imported public key: {key_id[:16]}...")
return key_id
else:
self._print_error("Failed to import key")
return None
except Exception as e:
self._print_error(f"Import failed: {e}")
return None
def export_my_public_key(self):
"""Export current user's public key"""
if not self.my_key_id:
return None
try:
ascii_key = self.gpg.export_keys(self.my_key_id)
return ascii_key
except Exception as e:
self._print_error(f"Export failed: {e}")
return None
def encrypt_message(self, content, recipient_key_id):
"""Encrypt message content for recipient"""
try:
encrypted = self.gpg.encrypt(
content,
recipient_key_id,
always_trust=True,
armor=True
)
if encrypted.ok:
return str(encrypted)
else:
self._print_error(f"Encryption failed: {encrypted.status}")
return None
except Exception as e:
self._print_error(f"Encryption error: {e}")
return None
def decrypt_message(self, encrypted_content):
"""Decrypt encrypted message"""
try:
decrypted = self.gpg.decrypt(encrypted_content)
if decrypted.ok:
return str(decrypted)
else:
self._print_error(f"Decryption failed: {decrypted.status}")
return None
except Exception as e:
self._print_error(f"Decryption error: {e}")
return None
def sign_message(self, content):
"""Sign message content"""
try:
signed = self.gpg.sign(
content,
keyid=self.my_key_id,
clearsign=True
)
if signed:
return str(signed)
else:
self._print_error("Signing failed")
return None
except Exception as e:
self._print_error(f"Signing error: {e}")
return None
def verify_signature(self, signed_content):
"""Verify signed message"""
try:
verified = self.gpg.verify(signed_content)
if verified.valid:
# Extract original message
lines = signed_content.split('\n')
message_lines = []
in_message = False
for line in lines:
if line.startswith('-----BEGIN PGP SIGNED MESSAGE-----'):
in_message = True
continue
elif line.startswith('-----BEGIN PGP SIGNATURE-----'):
break
elif in_message and not line.startswith('Hash: '):
if line or message_lines: # Skip initial empty lines
message_lines.append(line)
original_message = '\n'.join(message_lines).strip()
return {
'valid': True,
'key_id': verified.key_id,
'username': verified.username,
'message': original_message
}
else:
return {
'valid': False,
'message': signed_content
}
except Exception as e:
self._print_error(f"Verification error: {e}")
return {'valid': False, 'message': signed_content}
def on_message(self, message, msg_data):
"""Handle incoming messages - auto decrypt/verify"""
try:
content = msg_data['content']
source_hash = msg_data['source_hash']
# Check if message is encrypted
is_encrypted = '-----BEGIN PGP MESSAGE-----' in content
is_signed = '-----BEGIN PGP SIGNED MESSAGE-----' in content
# Check rejection policies
if self.reject_unencrypted and not is_encrypted:
self._print_warning(f"Rejected unencrypted message from {source_hash[:16]}...")
print(" Enable 'pgp set reject_unencrypted off' to receive unencrypted messages")
return True # Suppress normal notification
if self.reject_unsigned and not is_signed and not is_encrypted:
self._print_warning(f"Rejected unsigned message from {source_hash[:16]}...")
print(" Enable 'pgp set reject_unsigned off' to receive unsigned messages")
return True # Suppress normal notification
modified = False
# Auto-decrypt if enabled
if self.auto_decrypt and is_encrypted:
print(f"\n🔐 Encrypted message from {self.client.format_contact_display_short(source_hash)}")
decrypted = self.decrypt_message(content)
if decrypted:
msg_data['content'] = decrypted
content = decrypted
modified = True
self._print_success("Message decrypted")
# Check if decrypted content is also signed
is_signed = '-----BEGIN PGP SIGNED MESSAGE-----' in content
else:
self._print_error("Failed to decrypt message")
return True # Suppress - couldn't decrypt
# Auto-verify if enabled
if self.auto_verify and is_signed:
result = self.verify_signature(content)
if result['valid']:
msg_data['content'] = result['message']
modified = True
self._print_success(f"✓ Signature valid - From: {result.get('username', 'Unknown')}")
print(f" Key ID: {result['key_id'][:16]}...")
else:
self._print_warning("⚠ Invalid or missing signature!")
if self.reject_unsigned:
return True # Suppress
return False # Let normal notification proceed if we modified it
except Exception as e:
self._print_error(f"Message processing error: {e}")
return False
def handle_command(self, cmd, parts):
"""Handle PGP commands"""
if len(parts) < 2:
self.show_help()
return
subcmd = parts[1].lower()
if subcmd == 'help':
self.show_help()
elif subcmd == 'status':
self.show_status()
elif subcmd == 'keygen':
self.generate_new_key()
elif subcmd == 'export':
self.export_key_command()
elif subcmd == 'import':
self.import_key_command(parts)
elif subcmd == 'trust':
self.trust_key_command(parts)
elif subcmd == 'list':
self.list_keys()
elif subcmd == 'send':
self.send_encrypted_command(parts)
elif subcmd == 'set':
self.change_setting(parts)
else:
print(f"Unknown subcommand: {subcmd}")
self.show_help()
def show_help(self):
"""Show plugin help"""
print("\n" + ""*70)
print("PGP PLUGIN - COMMANDS")
print(""*70)
print("\n📊 Status & Info:")
print(" pgp status - Show PGP status and settings")
print(" pgp list - List all keys in keyring")
print("\n🔑 Key Management:")
print(" pgp keygen - Generate new PGP key pair")
print(" pgp export - Export your public key")
print(" pgp import <contact> - Request public key from contact")
print(" pgp trust <contact> <key> - Import and trust a public key")
print("\n📨 Messaging:")
print(" pgp send <contact> <msg> - Send encrypted message")
print("\n⚙️ Settings:")
print(" pgp set auto_encrypt on/off - Auto-encrypt outgoing")
print(" pgp set auto_sign on/off - Auto-sign outgoing")
print(" pgp set auto_decrypt on/off - Auto-decrypt incoming")
print(" pgp set auto_verify on/off - Auto-verify signatures")
print(" pgp set reject_unsigned on/off - Reject unsigned messages")
print(" pgp set reject_unencrypted on/off - Reject unencrypted")
print("\n" + ""*70 + "\n")
def show_status(self):
"""Show PGP status"""
print("\n" + ""*70)
print("PGP STATUS")
print(""*70)
print(f"\n🔑 Your Key:")
if self.my_key_id:
print(f" Key ID: {self.my_key_id}")
keys = self.gpg.list_keys()
my_key = next((k for k in keys if k['fingerprint'] == self.my_key_id), None)
if my_key:
print(f" Name: {my_key['uids'][0] if my_key['uids'] else 'Unknown'}")
print(f" Type: {my_key['type']} {my_key['length']}-bit")
else:
print(" No key configured")
print(f"\n⚙️ Settings:")
print(f" Auto-encrypt: {'ON' if self.auto_encrypt else 'OFF'}")
print(f" Auto-sign: {'ON' if self.auto_sign else 'OFF'}")
print(f" Auto-decrypt: {'ON' if self.auto_decrypt else 'OFF'}")
print(f" Auto-verify: {'ON' if self.auto_verify else 'OFF'}")
print(f" Reject unsigned: {'ON' if self.reject_unsigned else 'OFF'}")
print(f" Reject unencrypted: {'ON' if self.reject_unencrypted else 'OFF'}")
print(f"\n👥 Trusted Keys: {len(self.trusted_keys)}")
if self.trusted_keys:
for hash_str, key_id in list(self.trusted_keys.items())[:5]:
contact_name = self.client.format_contact_display_short(hash_str)
print(f" {contact_name}: {key_id[:16]}...")
if len(self.trusted_keys) > 5:
print(f" ... and {len(self.trusted_keys) - 5} more")
print("\n" + ""*70 + "\n")
def generate_new_key(self):
"""Generate a new PGP key"""
print("\n⚠ Warning: This will replace your current key!")
confirm = input("Continue? [y/N]: ").strip().lower()
if confirm != 'y':
print("Cancelled")
return
self.my_key_id = None
self._first_time_setup()
def export_key_command(self):
"""Export public key and prepare for sending"""
public_key = self.export_my_public_key()
if public_key:
print("\n" + ""*70)
print("YOUR PUBLIC KEY")
print(""*70)
print(public_key)
print(""*70)
print("\n💡 Share this with contacts so they can send you encrypted messages")
print(" You can send it via: send <contact> <paste key here>")
print()
def import_key_command(self, parts):
"""Request public key from contact"""
if len(parts) < 3:
print("💡 Usage: pgp import <contact>")
return
contact = parts[2]
# Send request message
request_msg = "PGP_KEY_REQUEST"
self.client.send_message(contact, request_msg, title="PGP Key Request")
print(f"\n📨 Sent key request to {contact}")
print(" Waiting for their public key...")
def trust_key_command(self, parts):
"""Import and trust a public key"""
if len(parts) < 4:
print("💡 Usage: pgp trust <contact> <key_data>")
print(" Or paste the key on the next line")
return
contact = parts[2]
key_data = ' '.join(parts[3:])
# Resolve contact to hash
dest_hash = self.client.resolve_contact_or_hash(contact)
if not dest_hash:
self._print_error(f"Unknown contact: {contact}")
return
# Import the key
result = self.import_public_key(dest_hash, key_data)
if result:
contact_display = self.client.format_contact_display_short(dest_hash)
self._print_success(f"Trusted key for {contact_display}")
def list_keys(self):
"""List all keys in keyring"""
print("\n" + ""*70)
print("PGP KEYRING")
print(""*70)
keys = self.gpg.list_keys()
if not keys:
print("\nNo keys in keyring\n")
return
print(f"\n{'Key ID':<18} {'Type':<12} {'Name'}")
print(""*70)
for key in keys:
key_id = key['keyid'][-16:]
key_type = f"{key['type']} {key['length']}-bit"
name = key['uids'][0] if key['uids'] else 'Unknown'
marker = "" if key['fingerprint'] == self.my_key_id else " "
print(f"{marker}{key_id:<16} {key_type:<12} {name}")
print(""*70)
print("\n★ = Your key\n")
def send_encrypted_command(self, parts):
"""Send encrypted and signed message"""
if len(parts) < 4:
print("💡 Usage: pgp send <contact> <message>")
return
contact = parts[2]
message = ' '.join(parts[3:])
# Resolve contact to hash
dest_hash = self.client.resolve_contact_or_hash(contact)
if not dest_hash:
self._print_error(f"Unknown contact: {contact}")
return
# Get recipient's public key
recipient_key = self.get_recipient_key(dest_hash)
if not recipient_key:
self._print_error(f"No public key for {contact}")
print(" Use 'pgp import <contact>' to request their key")
print(" Or 'pgp trust <contact> <key>' to import manually")
return
# Sign the message first
signed = self.sign_message(message)
if not signed:
return
# Then encrypt the signed message
encrypted = self.encrypt_message(signed, recipient_key)
if not encrypted:
return
# Send via normal LXMF
self.client.send_message(dest_hash, encrypted, title="🔐 Encrypted")
self._print_success("Sent encrypted & signed message")
def change_setting(self, parts):
"""Change plugin settings"""
if len(parts) < 4:
print("💡 Usage: pgp set <setting> <on/off>")
print("\nAvailable settings:")
print(" auto_encrypt, auto_sign, auto_decrypt, auto_verify")
print(" reject_unsigned, reject_unencrypted")
return
setting = parts[2].lower()
value = parts[3].lower() in ['on', 'yes', 'true', '1']
if setting == 'auto_encrypt':
self.auto_encrypt = value
elif setting == 'auto_sign':
self.auto_sign = value
elif setting == 'auto_decrypt':
self.auto_decrypt = value
elif setting == 'auto_verify':
self.auto_verify = value
elif setting == 'reject_unsigned':
self.reject_unsigned = value
elif setting == 'reject_unencrypted':
self.reject_unencrypted = value
else:
self._print_error(f"Unknown setting: {setting}")
return
self.save_config()
status = "enabled" if value else "disabled"
self._print_success(f"{setting}: {status}")