mirror of
https://github.com/fr33n0w/lxmf-cli.git
synced 2025-12-22 05:57:07 +00:00
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:
351
plugins/PGP_PLUGIN_README.md
Normal file
351
plugins/PGP_PLUGIN_README.md
Normal 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
|
||||
373
plugins/PGP_USAGE_EXAMPLES.md
Normal file
373
plugins/PGP_USAGE_EXAMPLES.md
Normal 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
|
||||
```
|
||||
178
plugins/install_pgp_plugin.sh
Normal file
178
plugins/install_pgp_plugin.sh
Normal 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
616
plugins/pgp.py
Normal 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}")
|
||||
Reference in New Issue
Block a user