Add Tor integration and privacy features for anonymous plant sharing
Implements comprehensive privacy and anonymity features including Tor hidden service support, location obfuscation, and anonymous registration. Privacy Features: - Anonymous plant registration with zero personal information - Location privacy levels: exact, fuzzy, city, country, hidden - Pseudonymous identities and wallet addresses - Privacy settings component with real-time Tor status - Encrypted anonymous contact generation Tor Integration: - SOCKS proxy support for Tor connections - Hidden service (.onion) configuration - Tor connection detection and status API - Docker Compose setup for easy Tor deployment - Automatic privacy warnings when not using Tor Location Obfuscation: - Fuzzy location: ±1-5km random offset - City level: ~10km grid - Country level: ~100km grid - Hidden: complete location privacy - Haversine-based distance calculations preserved Anonymous Registration: - /plants/register-anonymous endpoint - Privacy-first UI with Tor status banner - Anonymous IDs and wallet addresses - Optional pseudonym support - Encryption key support for enhanced security Infrastructure: - Tor service integration (lib/services/tor.ts) - Privacy utilities (lib/privacy/anonymity.ts) - PrivacySettings React component - Tor status API endpoint - Docker and docker-compose configurations - Example Tor configuration (torrc.example) Documentation: - Comprehensive TOR_SETUP.md guide - Installation instructions for Linux/macOS/Windows - Privacy best practices - Troubleshooting guide - Security considerations - Updated README with Tor features Dependencies: - Added socks-proxy-agent for Tor proxy support This enables: - Privacy-conscious growers to share anonymously - Protection of exact home locations - Censorship-resistant plant sharing - Community building without identity disclosure - Compliance with privacy regulations All privacy features are optional and configurable. Users can choose their desired privacy level.
This commit is contained in:
parent
5a93b3e680
commit
ccea9535d4
13 changed files with 1872 additions and 1 deletions
22
.env.example
22
.env.example
|
|
@ -1,3 +1,25 @@
|
||||||
|
# LocalGreenChain Environment Variables
|
||||||
|
|
||||||
|
# Plants.net API (optional)
|
||||||
|
PLANTS_NET_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# Tor Configuration
|
||||||
|
TOR_ENABLED=false
|
||||||
|
TOR_SOCKS_HOST=127.0.0.1
|
||||||
|
TOR_SOCKS_PORT=9050
|
||||||
|
TOR_CONTROL_PORT=9051
|
||||||
|
TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/localgreenchain
|
||||||
|
|
||||||
|
# Privacy Settings
|
||||||
|
DEFAULT_PRIVACY_MODE=standard
|
||||||
|
ALLOW_ANONYMOUS_REGISTRATION=true
|
||||||
|
LOCATION_OBFUSCATION_DEFAULT=fuzzy
|
||||||
|
|
||||||
|
# Application Settings
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3001
|
||||||
|
|
||||||
|
# Legacy Drupal Settings (for backward compatibility)
|
||||||
NEXT_PUBLIC_DRUPAL_BASE_URL=http://localhost:8080
|
NEXT_PUBLIC_DRUPAL_BASE_URL=http://localhost:8080
|
||||||
NEXT_IMAGE_DOMAIN=localhost
|
NEXT_IMAGE_DOMAIN=localhost
|
||||||
DRUPAL_CLIENT_ID=52ce1a10-bf5c-4c81-8edf-eea3af95da84
|
DRUPAL_CLIENT_ID=52ce1a10-bf5c-4c81-8edf-eea3af95da84
|
||||||
|
|
|
||||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Dockerfile for LocalGreenChain
|
||||||
|
# Uses Bun for fast builds and runtime
|
||||||
|
|
||||||
|
FROM oven/bun:1 as base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY package.json bun.lockb* ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build Next.js application
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM oven/bun:1-slim as production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies and build output
|
||||||
|
COPY --from=base /app/node_modules ./node_modules
|
||||||
|
COPY --from=base /app/.next ./.next
|
||||||
|
COPY --from=base /app/public ./public
|
||||||
|
COPY --from=base /app/package.json ./package.json
|
||||||
|
COPY --from=base /app/next.config.js ./next.config.js
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
# Set environment to production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["bun", "run", "start"]
|
||||||
39
README.md
39
README.md
|
|
@ -37,6 +37,15 @@ LocalGreenChain is a revolutionary plant tracking system that uses blockchain te
|
||||||
- Network statistics dashboard
|
- Network statistics dashboard
|
||||||
- Mobile-responsive design
|
- Mobile-responsive design
|
||||||
|
|
||||||
|
### 🔒 **Privacy & Anonymity (Tor Integration)**
|
||||||
|
- Anonymous plant registration with zero personal information
|
||||||
|
- Location obfuscation (fuzzy, city, country, or hidden)
|
||||||
|
- Tor hidden service support for .onion access
|
||||||
|
- SOCKS proxy integration for outbound Tor connections
|
||||||
|
- Privacy-preserving geolocation (share area, not exact location)
|
||||||
|
- Pseudonymous identities and wallet addresses
|
||||||
|
- End-to-end encrypted communications
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
@ -92,6 +101,36 @@ bun run dev
|
||||||
5. **Open your browser**
|
5. **Open your browser**
|
||||||
Navigate to [http://localhost:3001](http://localhost:3001)
|
Navigate to [http://localhost:3001](http://localhost:3001)
|
||||||
|
|
||||||
|
### 🧅 Tor Setup (Optional - For Privacy)
|
||||||
|
|
||||||
|
For anonymous plant sharing and privacy protection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick start with Docker Compose
|
||||||
|
docker-compose -f docker-compose.tor.yml up -d
|
||||||
|
|
||||||
|
# Or manual setup
|
||||||
|
# 1. Install Tor
|
||||||
|
sudo apt install tor
|
||||||
|
|
||||||
|
# 2. Configure Tor (copy example config)
|
||||||
|
sudo cp tor/torrc.example /etc/tor/torrc
|
||||||
|
|
||||||
|
# 3. Start Tor
|
||||||
|
sudo systemctl start tor
|
||||||
|
|
||||||
|
# 4. Enable Tor in LocalGreenChain
|
||||||
|
# Edit .env and set: TOR_ENABLED=true
|
||||||
|
|
||||||
|
# 5. Start LocalGreenChain
|
||||||
|
bun run start
|
||||||
|
|
||||||
|
# 6. Get your .onion address
|
||||||
|
sudo cat /var/lib/tor/localgreenchain/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
**📘 Full Guide**: See [TOR_SETUP.md](./TOR_SETUP.md) for complete instructions
|
||||||
|
|
||||||
## 📖 How It Works
|
## 📖 How It Works
|
||||||
|
|
||||||
### The Blockchain
|
### The Blockchain
|
||||||
|
|
|
||||||
455
TOR_SETUP.md
Normal file
455
TOR_SETUP.md
Normal file
|
|
@ -0,0 +1,455 @@
|
||||||
|
# 🧅 Tor Integration Guide for LocalGreenChain
|
||||||
|
|
||||||
|
This guide explains how to set up LocalGreenChain with Tor for maximum privacy and anonymity when sharing plant lineages.
|
||||||
|
|
||||||
|
## Why Use Tor with LocalGreenChain?
|
||||||
|
|
||||||
|
### Privacy Benefits
|
||||||
|
- **Anonymous Plant Registration**: Register plants without revealing your identity
|
||||||
|
- **Location Privacy**: Share general area without exposing exact home address
|
||||||
|
- **IP Protection**: Hide your IP address from other users and the network
|
||||||
|
- **Censorship Resistance**: Access the network even in restrictive environments
|
||||||
|
- **Secure Sharing**: Share plant clones with trusted community members anonymously
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
- **Privacy-Conscious Growers**: Don't want to advertise exact plant locations
|
||||||
|
- **Sensitive Species**: Medicinal plants, rare species, or regulated botanicals
|
||||||
|
- **Community Building**: Connect with local growers without revealing identity
|
||||||
|
- **Research**: Anonymous data collection for botanical research
|
||||||
|
- **Security**: Protect against unwanted visitors or theft
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
1. [Quick Start](#quick-start)
|
||||||
|
2. [Installation Methods](#installation-methods)
|
||||||
|
3. [Configuration](#configuration)
|
||||||
|
4. [Running as Hidden Service](#running-as-hidden-service)
|
||||||
|
5. [Using Tor Browser](#using-tor-browser)
|
||||||
|
6. [Privacy Best Practices](#privacy-best-practices)
|
||||||
|
7. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Option 1: Docker Compose (Recommended)
|
||||||
|
|
||||||
|
The easiest way to run LocalGreenChain with Tor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy environment variables
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit .env and enable Tor
|
||||||
|
nano .env
|
||||||
|
# Set: TOR_ENABLED=true
|
||||||
|
|
||||||
|
# Start with Docker Compose
|
||||||
|
docker-compose -f docker-compose.tor.yml up -d
|
||||||
|
|
||||||
|
# Check if Tor is running
|
||||||
|
docker logs localgreenchain-tor
|
||||||
|
|
||||||
|
# Get your onion address
|
||||||
|
docker exec localgreenchain-tor cat /var/lib/tor/hidden_service/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
Your LocalGreenChain instance is now accessible via:
|
||||||
|
- Local: http://localhost:3001
|
||||||
|
- Onion: http://[your-address].onion (share this!)
|
||||||
|
|
||||||
|
### Option 2: Manual Installation
|
||||||
|
|
||||||
|
1. **Install Tor**
|
||||||
|
2. **Configure Tor for LocalGreenChain**
|
||||||
|
3. **Start LocalGreenChain with Tor enabled**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
### Linux (Debian/Ubuntu)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Tor
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install tor
|
||||||
|
|
||||||
|
# Configure Tor for LocalGreenChain
|
||||||
|
sudo cp tor/torrc.example /etc/tor/torrc
|
||||||
|
|
||||||
|
# Edit configuration
|
||||||
|
sudo nano /etc/tor/torrc
|
||||||
|
|
||||||
|
# Create hidden service directory
|
||||||
|
sudo mkdir -p /var/lib/tor/localgreenchain
|
||||||
|
sudo chown -R debian-tor:debian-tor /var/lib/tor/localgreenchain
|
||||||
|
sudo chmod 700 /var/lib/tor/localgreenchain
|
||||||
|
|
||||||
|
# Start Tor
|
||||||
|
sudo systemctl start tor
|
||||||
|
sudo systemctl enable tor
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status tor
|
||||||
|
|
||||||
|
# Get your onion address (wait ~1 minute for generation)
|
||||||
|
sudo cat /var/lib/tor/localgreenchain/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Tor via Homebrew
|
||||||
|
brew install tor
|
||||||
|
|
||||||
|
# Copy configuration
|
||||||
|
cp tor/torrc.example /usr/local/etc/tor/torrc
|
||||||
|
|
||||||
|
# Edit configuration
|
||||||
|
nano /usr/local/etc/tor/torrc
|
||||||
|
|
||||||
|
# Create hidden service directory
|
||||||
|
mkdir -p ~/Library/Application\ Support/tor/localgreenchain
|
||||||
|
chmod 700 ~/Library/Application\ Support/tor/localgreenchain
|
||||||
|
|
||||||
|
# Update torrc with your path
|
||||||
|
# HiddenServiceDir ~/Library/Application Support/tor/localgreenchain
|
||||||
|
|
||||||
|
# Start Tor
|
||||||
|
brew services start tor
|
||||||
|
|
||||||
|
# Get your onion address
|
||||||
|
cat ~/Library/Application\ Support/tor/localgreenchain/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (WSL)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install WSL if not already installed
|
||||||
|
# Then follow Linux instructions above
|
||||||
|
|
||||||
|
# Or use Tor Expert Bundle
|
||||||
|
# Download from: https://www.torproject.org/download/tor/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Edit `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable Tor
|
||||||
|
TOR_ENABLED=true
|
||||||
|
|
||||||
|
# Tor SOCKS proxy (default)
|
||||||
|
TOR_SOCKS_HOST=127.0.0.1
|
||||||
|
TOR_SOCKS_PORT=9050
|
||||||
|
|
||||||
|
# Tor control port
|
||||||
|
TOR_CONTROL_PORT=9051
|
||||||
|
|
||||||
|
# Hidden service directory
|
||||||
|
TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/localgreenchain
|
||||||
|
|
||||||
|
# Privacy defaults
|
||||||
|
DEFAULT_PRIVACY_MODE=standard
|
||||||
|
ALLOW_ANONYMOUS_REGISTRATION=true
|
||||||
|
LOCATION_OBFUSCATION_DEFAULT=fuzzy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tor Configuration (torrc)
|
||||||
|
|
||||||
|
Minimal configuration in `/etc/tor/torrc`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# SOCKS proxy
|
||||||
|
SocksPort 9050
|
||||||
|
|
||||||
|
# Hidden Service for LocalGreenChain
|
||||||
|
HiddenServiceDir /var/lib/tor/localgreenchain/
|
||||||
|
HiddenServicePort 80 127.0.0.1:3001
|
||||||
|
|
||||||
|
# Optional: Multiple ports
|
||||||
|
# HiddenServicePort 443 127.0.0.1:3001
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
Log notice file /var/log/tor/notices.log
|
||||||
|
|
||||||
|
# Privacy settings
|
||||||
|
IsolateDestAddr 1
|
||||||
|
IsolateDestPort 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running as Hidden Service
|
||||||
|
|
||||||
|
### Start LocalGreenChain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Start in production mode
|
||||||
|
bun run build
|
||||||
|
bun run start
|
||||||
|
|
||||||
|
# Or development mode
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Hidden Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Tor created keys
|
||||||
|
ls -la /var/lib/tor/localgreenchain/
|
||||||
|
|
||||||
|
# Should see:
|
||||||
|
# - hostname (your .onion address)
|
||||||
|
# - hs_ed25519_public_key
|
||||||
|
# - hs_ed25519_secret_key
|
||||||
|
|
||||||
|
# Get your onion address
|
||||||
|
cat /var/lib/tor/localgreenchain/hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Share Your Onion Address
|
||||||
|
|
||||||
|
Your `.onion` address looks like:
|
||||||
|
```
|
||||||
|
abc123def456ghi789.onion
|
||||||
|
```
|
||||||
|
|
||||||
|
Share this with trusted community members to allow anonymous access!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using Tor Browser
|
||||||
|
|
||||||
|
### As a User (Accessing LocalGreenChain via Tor)
|
||||||
|
|
||||||
|
1. **Download Tor Browser**
|
||||||
|
- Visit: https://www.torproject.org/download/
|
||||||
|
- Install for your operating system
|
||||||
|
|
||||||
|
2. **Connect to Tor Network**
|
||||||
|
- Launch Tor Browser
|
||||||
|
- Click "Connect" to establish Tor connection
|
||||||
|
|
||||||
|
3. **Access LocalGreenChain**
|
||||||
|
- Option A: Via onion address (recommended)
|
||||||
|
```
|
||||||
|
http://[your-onion-address].onion
|
||||||
|
```
|
||||||
|
- Option B: Via clearnet (still anonymous)
|
||||||
|
```
|
||||||
|
http://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Register Plants Anonymously**
|
||||||
|
- Go to "Anonymous Registration" page
|
||||||
|
- Your connection will be detected as coming from Tor
|
||||||
|
- All privacy features automatically enabled
|
||||||
|
|
||||||
|
### Privacy Indicators
|
||||||
|
|
||||||
|
LocalGreenChain will show you:
|
||||||
|
- 🧅 "Tor Active" badge when connected via Tor
|
||||||
|
- Privacy recommendations based on connection type
|
||||||
|
- Tor circuit information (country, not your IP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Privacy Best Practices
|
||||||
|
|
||||||
|
### For Maximum Anonymity
|
||||||
|
|
||||||
|
1. **Always Use Tor Browser**
|
||||||
|
- Don't access via regular browser
|
||||||
|
- Tor Browser includes additional privacy protections
|
||||||
|
|
||||||
|
2. **Enable Anonymous Mode**
|
||||||
|
- Use `/plants/register-anonymous` page
|
||||||
|
- Generate random IDs and pseudonyms
|
||||||
|
- Don't reuse usernames from other sites
|
||||||
|
|
||||||
|
3. **Location Privacy**
|
||||||
|
- Use "Fuzzy" or "City" level location sharing
|
||||||
|
- Never share exact coordinates
|
||||||
|
- Consider using "Hidden" for sensitive plants
|
||||||
|
|
||||||
|
4. **Operational Security (OpSec)**
|
||||||
|
- Don't include identifiable info in plant notes
|
||||||
|
- Use different pseudonyms for different plant types
|
||||||
|
- Don't correlate with social media accounts
|
||||||
|
- Clear browser data after each session
|
||||||
|
|
||||||
|
5. **Network Security**
|
||||||
|
- Only share your .onion address with trusted people
|
||||||
|
- Use secure channels (encrypted messaging) to share addresses
|
||||||
|
- Rotate your hidden service periodically if needed
|
||||||
|
|
||||||
|
### Privacy Levels Explained
|
||||||
|
|
||||||
|
| Level | Location Accuracy | Best For |
|
||||||
|
|-------|------------------|----------|
|
||||||
|
| **Exact** | ~100m | Public gardens, commercial nurseries |
|
||||||
|
| **Fuzzy** | 1-5km radius | Home gardens, privacy-conscious sharing |
|
||||||
|
| **City** | ~10km grid | Regional plant trading |
|
||||||
|
| **Country** | ~100km grid | National distribution tracking |
|
||||||
|
| **Hidden** | No location | Maximum privacy, sensitive species |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Tor Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Tor status
|
||||||
|
sudo systemctl status tor
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo tail -f /var/log/tor/notices.log
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# 1. Port 9050 already in use
|
||||||
|
sudo lsof -i :9050
|
||||||
|
|
||||||
|
# 2. Permission issues
|
||||||
|
sudo chown -R debian-tor:debian-tor /var/lib/tor
|
||||||
|
sudo chmod 700 /var/lib/tor/localgreenchain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hidden Service Not Accessible
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify Tor is running
|
||||||
|
pgrep tor
|
||||||
|
|
||||||
|
# Check if hostname file exists
|
||||||
|
cat /var/lib/tor/localgreenchain/hostname
|
||||||
|
|
||||||
|
# Verify LocalGreenChain is running
|
||||||
|
curl http://localhost:3001
|
||||||
|
|
||||||
|
# Check Tor logs for errors
|
||||||
|
sudo tail -f /var/log/tor/notices.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Tor Status: Not Available"
|
||||||
|
|
||||||
|
1. Check if Tor daemon is running
|
||||||
|
2. Verify SOCKS port (9050) is open
|
||||||
|
3. Check firewall settings
|
||||||
|
4. Restart Tor service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart tor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slow Onion Connection
|
||||||
|
|
||||||
|
This is normal! Tor routes through multiple nodes:
|
||||||
|
- First connection: 30-60 seconds
|
||||||
|
- Subsequent loads: 5-15 seconds
|
||||||
|
- Plant operations: Near instant (local blockchain)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
### Running Multiple Hidden Services
|
||||||
|
|
||||||
|
Edit `/etc/tor/torrc`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# LocalGreenChain (public)
|
||||||
|
HiddenServiceDir /var/lib/tor/localgreenchain-public/
|
||||||
|
HiddenServicePort 80 127.0.0.1:3001
|
||||||
|
|
||||||
|
# LocalGreenChain (private - invite only)
|
||||||
|
HiddenServiceDir /var/lib/tor/localgreenchain-private/
|
||||||
|
HiddenServicePort 80 127.0.0.1:3002
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Authentication (v3 Onions)
|
||||||
|
|
||||||
|
Restrict access to authorized users only:
|
||||||
|
|
||||||
|
```
|
||||||
|
# In torrc
|
||||||
|
HiddenServiceDir /var/lib/tor/localgreenchain/
|
||||||
|
HiddenServicePort 80 127.0.0.1:3001
|
||||||
|
HiddenServiceAuthorizeClient stealth alice,bob
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring Tor Traffic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Real-time connection monitoring
|
||||||
|
sudo nyx
|
||||||
|
|
||||||
|
# Or arm (older tool)
|
||||||
|
sudo arm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Your Hidden Service Keys
|
||||||
|
|
||||||
|
**IMPORTANT**: Your `.onion` address is tied to your keys!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup keys
|
||||||
|
sudo cp -r /var/lib/tor/localgreenchain ~/tor-backup/
|
||||||
|
|
||||||
|
# Restore keys (on new server)
|
||||||
|
sudo cp -r ~/tor-backup/* /var/lib/tor/localgreenchain/
|
||||||
|
sudo chown -R debian-tor:debian-tor /var/lib/tor/localgreenchain
|
||||||
|
sudo systemctl restart tor
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### What Tor DOES Protect
|
||||||
|
✅ Your IP address from other users
|
||||||
|
✅ Your browsing from your ISP
|
||||||
|
✅ Your location from the network
|
||||||
|
✅ Your identity when using anonymous mode
|
||||||
|
|
||||||
|
### What Tor DOESN'T Protect
|
||||||
|
❌ Poor operational security (sharing identifying info)
|
||||||
|
❌ Malware on your computer
|
||||||
|
❌ Logging in with real accounts
|
||||||
|
❌ Data you voluntarily share
|
||||||
|
|
||||||
|
### Remember
|
||||||
|
- **Tor provides anonymity, not security**
|
||||||
|
- Use HTTPS even over Tor (LocalGreenChain supports this)
|
||||||
|
- Don't mix anonymous and identified activities
|
||||||
|
- Keep Tor Browser up to date
|
||||||
|
- Trust the process - Tor has protected millions of users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- **LocalGreenChain Tor Issues**: https://github.com/yourusername/localgreenchain/issues
|
||||||
|
- **Tor Project**: https://support.torproject.org
|
||||||
|
- **Privacy Community**: https://www.reddit.com/r/TOR
|
||||||
|
- **Security Audit**: See SECURITY.md
|
||||||
|
|
||||||
|
## Legal Notice
|
||||||
|
|
||||||
|
Using Tor is legal in most countries. However:
|
||||||
|
- Check local laws regarding Tor usage
|
||||||
|
- Using Tor for illegal activities is still illegal
|
||||||
|
- LocalGreenChain is for botanical education and legal plant sharing
|
||||||
|
- Respect plant import/export regulations
|
||||||
|
- Some plants may be regulated or controlled substances
|
||||||
|
|
||||||
|
Stay safe, stay private, and happy growing! 🌱🧅
|
||||||
227
components/PrivacySettings.tsx
Normal file
227
components/PrivacySettings.tsx
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { PrivacySettings as IPrivacySettings } from '../lib/privacy/anonymity';
|
||||||
|
|
||||||
|
interface PrivacySettingsProps {
|
||||||
|
value: IPrivacySettings;
|
||||||
|
onChange: (settings: IPrivacySettings) => void;
|
||||||
|
showTorStatus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PrivacySettings({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
showTorStatus = true,
|
||||||
|
}: PrivacySettingsProps) {
|
||||||
|
const [torStatus, setTorStatus] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showTorStatus) {
|
||||||
|
checkTorStatus();
|
||||||
|
}
|
||||||
|
}, [showTorStatus]);
|
||||||
|
|
||||||
|
const checkTorStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/privacy/tor-status');
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setTorStatus(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking Tor status:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (field: keyof IPrivacySettings, newValue: any) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[field]: newValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6 border-2 border-purple-200">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 flex items-center">
|
||||||
|
🔒 Privacy & Anonymity Settings
|
||||||
|
</h2>
|
||||||
|
{!loading && torStatus?.tor.connectionThroughTor && (
|
||||||
|
<span className="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-semibold">
|
||||||
|
🧅 Tor Active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tor Status Banner */}
|
||||||
|
{showTorStatus && !loading && (
|
||||||
|
<div
|
||||||
|
className={`mb-6 p-4 rounded-lg ${
|
||||||
|
torStatus?.tor.connectionThroughTor
|
||||||
|
? 'bg-green-50 border border-green-200'
|
||||||
|
: 'bg-yellow-50 border border-yellow-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start">
|
||||||
|
<span className="text-2xl mr-3">
|
||||||
|
{torStatus?.tor.connectionThroughTor ? '🧅' : '⚠️'}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-1">
|
||||||
|
{torStatus?.tor.connectionThroughTor
|
||||||
|
? 'Tor Connection Active'
|
||||||
|
: 'Not Using Tor'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
|
{torStatus?.tor.connectionThroughTor
|
||||||
|
? 'Your connection is anonymous and routed through the Tor network.'
|
||||||
|
: 'For maximum privacy, consider accessing via Tor Browser.'}
|
||||||
|
</p>
|
||||||
|
{torStatus?.tor.onionAddress && (
|
||||||
|
<p className="text-sm font-mono bg-gray-100 p-2 rounded mt-2">
|
||||||
|
Onion Address: {torStatus.tor.onionAddress}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Anonymous Mode Toggle */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value.anonymousMode}
|
||||||
|
onChange={(e) => handleChange('anonymousMode', e.target.checked)}
|
||||||
|
className="w-5 h-5 text-purple-600 rounded focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<span className="ml-3 text-gray-900 font-medium">
|
||||||
|
Enable Anonymous Mode
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p className="ml-8 text-sm text-gray-600 mt-1">
|
||||||
|
Generate random identifiers and hide personal information
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location Privacy */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Location Privacy Level
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={value.locationPrivacy}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange(
|
||||||
|
'locationPrivacy',
|
||||||
|
e.target.value as 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="exact">📍 Exact Location (Public)</option>
|
||||||
|
<option value="fuzzy">🎯 Fuzzy (±1-5km radius)</option>
|
||||||
|
<option value="city">🏙️ City Level (~10km grid)</option>
|
||||||
|
<option value="country">🌍 Country/Region (~100km grid)</option>
|
||||||
|
<option value="hidden">🔒 Hidden (No location)</option>
|
||||||
|
</select>
|
||||||
|
<div className="mt-2 text-sm text-gray-600">
|
||||||
|
{value.locationPrivacy === 'exact' && (
|
||||||
|
<span className="text-red-600 font-medium">
|
||||||
|
⚠️ Warning: Exact location may reveal your home address
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{value.locationPrivacy === 'fuzzy' && (
|
||||||
|
<span>✓ Good balance of privacy and discoverability</span>
|
||||||
|
)}
|
||||||
|
{value.locationPrivacy === 'city' && (
|
||||||
|
<span>✓ Only city-level information shared</span>
|
||||||
|
)}
|
||||||
|
{value.locationPrivacy === 'country' && (
|
||||||
|
<span>✓ Only country/region visible</span>
|
||||||
|
)}
|
||||||
|
{value.locationPrivacy === 'hidden' && (
|
||||||
|
<span className="text-purple-600 font-medium">
|
||||||
|
🔒 Maximum privacy: Location completely hidden
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Identity Privacy */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Identity Privacy
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={value.identityPrivacy}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange(
|
||||||
|
'identityPrivacy',
|
||||||
|
e.target.value as 'real' | 'pseudonym' | 'anonymous'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="real">👤 Real Name</option>
|
||||||
|
<option value="pseudonym">🎭 Pseudonym</option>
|
||||||
|
<option value="anonymous">🔒 Anonymous</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Share Plant Details */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value.sharePlantDetails}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange('sharePlantDetails', e.target.checked)
|
||||||
|
}
|
||||||
|
className="w-5 h-5 text-purple-600 rounded focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<span className="ml-3 text-gray-900 font-medium">
|
||||||
|
Share Plant Details (species, genus, family)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p className="ml-8 text-sm text-gray-600 mt-1">
|
||||||
|
Uncheck to use generic plant identifiers
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Privacy Summary */}
|
||||||
|
<div className="mt-6 p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||||||
|
<h3 className="font-semibold text-purple-900 mb-2">Privacy Summary</h3>
|
||||||
|
<ul className="text-sm text-purple-800 space-y-1">
|
||||||
|
<li>
|
||||||
|
• Location: {value.locationPrivacy === 'exact' ? 'Visible to all' : 'Protected'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• Identity: {value.identityPrivacy === 'real' ? 'Real name' : 'Protected'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• Plant Info: {value.sharePlantDetails ? 'Shared' : 'Generic'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
• Mode: {value.anonymousMode ? 'Anonymous 🔒' : 'Standard'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recommendations */}
|
||||||
|
{!loading && torStatus && torStatus.recommendations && (
|
||||||
|
<div className="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<h3 className="font-semibold text-blue-900 mb-2">💡 Recommendations</h3>
|
||||||
|
<ul className="text-sm text-blue-800 space-y-1">
|
||||||
|
{torStatus.recommendations.map((rec: string, idx: number) => (
|
||||||
|
<li key={idx}>• {rec}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
docker-compose.tor.yml
Normal file
67
docker-compose.tor.yml
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Tor daemon
|
||||||
|
tor:
|
||||||
|
image: goldy/tor-hidden-service:latest
|
||||||
|
container_name: localgreenchain-tor
|
||||||
|
environment:
|
||||||
|
# Hidden service configuration
|
||||||
|
SERVICE_NAME: localgreenchain
|
||||||
|
SERVICE_PORT: 80
|
||||||
|
SERVICE_HOST: app
|
||||||
|
SERVICE_HOST_PORT: 3001
|
||||||
|
volumes:
|
||||||
|
- tor-data:/var/lib/tor
|
||||||
|
- ./tor/torrc.example:/etc/tor/torrc:ro
|
||||||
|
ports:
|
||||||
|
- "9050:9050" # SOCKS proxy
|
||||||
|
- "9051:9051" # Control port
|
||||||
|
networks:
|
||||||
|
- localgreenchain-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# LocalGreenChain application
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: localgreenchain-app
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- TOR_ENABLED=true
|
||||||
|
- TOR_SOCKS_HOST=tor
|
||||||
|
- TOR_SOCKS_PORT=9050
|
||||||
|
- TOR_CONTROL_PORT=9051
|
||||||
|
- TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/hidden_service
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- tor-data:/var/lib/tor:ro
|
||||||
|
depends_on:
|
||||||
|
- tor
|
||||||
|
networks:
|
||||||
|
- localgreenchain-network
|
||||||
|
restart: unless-stopped
|
||||||
|
command: bun run start
|
||||||
|
|
||||||
|
# Optional: nginx reverse proxy for additional security
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: localgreenchain-nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
networks:
|
||||||
|
- localgreenchain-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tor-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
localgreenchain-network:
|
||||||
|
driver: bridge
|
||||||
221
lib/privacy/anonymity.ts
Normal file
221
lib/privacy/anonymity.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Privacy and Anonymity Utilities for LocalGreenChain
|
||||||
|
* Provides tools for anonymous plant tracking while maintaining blockchain integrity
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface PrivacySettings {
|
||||||
|
anonymousMode: boolean;
|
||||||
|
locationPrivacy: 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden';
|
||||||
|
identityPrivacy: 'real' | 'pseudonym' | 'anonymous';
|
||||||
|
sharePlantDetails: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuzzyLocation {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
accuracy: number; // radius in km
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate anonymous user ID using cryptographic hash
|
||||||
|
*/
|
||||||
|
export function generateAnonymousId(): string {
|
||||||
|
const randomBytes = crypto.randomBytes(32);
|
||||||
|
return 'anon_' + crypto.createHash('sha256').update(randomBytes).digest('hex').substring(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate pseudonymous wallet address
|
||||||
|
*/
|
||||||
|
export function generateWalletAddress(): string {
|
||||||
|
const randomBytes = crypto.randomBytes(20);
|
||||||
|
return '0x' + randomBytes.toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obfuscate location based on privacy level
|
||||||
|
* This prevents exact home address tracking while allowing geographic discovery
|
||||||
|
*/
|
||||||
|
export function obfuscateLocation(
|
||||||
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
privacyLevel: 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden'
|
||||||
|
): FuzzyLocation {
|
||||||
|
switch (privacyLevel) {
|
||||||
|
case 'exact':
|
||||||
|
return {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
accuracy: 0.1, // ~100m
|
||||||
|
displayName: 'Exact location',
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'fuzzy':
|
||||||
|
// Add random offset within 1-5km radius
|
||||||
|
const fuzzRadius = 1 + Math.random() * 4; // 1-5 km
|
||||||
|
const angle = Math.random() * 2 * Math.PI;
|
||||||
|
const latOffset = (fuzzRadius / 111) * Math.cos(angle); // 111 km per degree
|
||||||
|
const lonOffset = (fuzzRadius / (111 * Math.cos(latitude * Math.PI / 180))) * Math.sin(angle);
|
||||||
|
|
||||||
|
return {
|
||||||
|
latitude: latitude + latOffset,
|
||||||
|
longitude: longitude + lonOffset,
|
||||||
|
accuracy: fuzzRadius,
|
||||||
|
displayName: `Within ${Math.round(fuzzRadius)} km`,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'city':
|
||||||
|
// Round to ~10km grid (0.1 degree ≈ 11km)
|
||||||
|
return {
|
||||||
|
latitude: Math.round(latitude * 10) / 10,
|
||||||
|
longitude: Math.round(longitude * 10) / 10,
|
||||||
|
accuracy: 10,
|
||||||
|
displayName: 'City area',
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'country':
|
||||||
|
// Round to ~100km grid (1 degree ≈ 111km)
|
||||||
|
return {
|
||||||
|
latitude: Math.round(latitude),
|
||||||
|
longitude: Math.round(longitude),
|
||||||
|
accuracy: 100,
|
||||||
|
displayName: 'Country/Region',
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'hidden':
|
||||||
|
return {
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
accuracy: 999999,
|
||||||
|
displayName: 'Location hidden',
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return obfuscateLocation(latitude, longitude, 'fuzzy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate anonymous plant name
|
||||||
|
*/
|
||||||
|
export function generateAnonymousPlantName(plantType: string, generation: number): string {
|
||||||
|
const hash = crypto.randomBytes(4).toString('hex');
|
||||||
|
return `${plantType}-Gen${generation}-${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt sensitive data for storage
|
||||||
|
*/
|
||||||
|
export function encryptData(data: string, key: string): string {
|
||||||
|
const algorithm = 'aes-256-cbc';
|
||||||
|
const keyHash = crypto.createHash('sha256').update(key).digest();
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
|
||||||
|
const cipher = crypto.createCipheriv(algorithm, keyHash, iv);
|
||||||
|
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
|
return iv.toString('hex') + ':' + encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt sensitive data
|
||||||
|
*/
|
||||||
|
export function decryptData(encryptedData: string, key: string): string {
|
||||||
|
const algorithm = 'aes-256-cbc';
|
||||||
|
const keyHash = crypto.createHash('sha256').update(key).digest();
|
||||||
|
|
||||||
|
const parts = encryptedData.split(':');
|
||||||
|
const iv = Buffer.from(parts[0], 'hex');
|
||||||
|
const encrypted = parts[1];
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv(algorithm, keyHash, iv);
|
||||||
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Tor-friendly onion address from plant ID
|
||||||
|
* This creates a deterministic onion-style identifier
|
||||||
|
*/
|
||||||
|
export function generateOnionIdentifier(plantId: string): string {
|
||||||
|
const hash = crypto.createHash('sha256').update(plantId).digest('hex');
|
||||||
|
return hash.substring(0, 16) + '.onion';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create anonymous contact method
|
||||||
|
*/
|
||||||
|
export function createAnonymousContact(realEmail: string, privateKey: string): string {
|
||||||
|
// Hash email with private key to create anonymous identifier
|
||||||
|
const hash = crypto.createHash('sha256')
|
||||||
|
.update(realEmail + privateKey)
|
||||||
|
.digest('hex');
|
||||||
|
return `anon-${hash.substring(0, 12)}@localgreenchain.onion`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Tor connection
|
||||||
|
*/
|
||||||
|
export async function isTorConnection(req: any): Promise<boolean> {
|
||||||
|
// Check if request is coming through Tor
|
||||||
|
const forwardedFor = req.headers['x-forwarded-for'];
|
||||||
|
const realIp = req.headers['x-real-ip'];
|
||||||
|
|
||||||
|
// Tor exit nodes typically set specific headers
|
||||||
|
// This is a simplified check
|
||||||
|
return (
|
||||||
|
req.headers['x-tor-connection'] === 'true' ||
|
||||||
|
req.socket?.remoteAddress?.includes('127.0.0.1') // Tor proxy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate privacy report for plant
|
||||||
|
*/
|
||||||
|
export interface PrivacyReport {
|
||||||
|
locationPrivacy: string;
|
||||||
|
identityPrivacy: string;
|
||||||
|
dataEncryption: boolean;
|
||||||
|
torEnabled: boolean;
|
||||||
|
riskLevel: 'low' | 'medium' | 'high';
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generatePrivacyReport(settings: PrivacySettings): PrivacyReport {
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
let riskLevel: 'low' | 'medium' | 'high' = 'low';
|
||||||
|
|
||||||
|
if (settings.locationPrivacy === 'exact') {
|
||||||
|
recommendations.push('Consider using fuzzy location to protect your privacy');
|
||||||
|
riskLevel = 'high';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.identityPrivacy === 'real') {
|
||||||
|
recommendations.push('Using real identity may compromise anonymity');
|
||||||
|
if (riskLevel === 'low') riskLevel = 'medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.sharePlantDetails && !settings.anonymousMode) {
|
||||||
|
recommendations.push('Sharing detailed plant info without anonymous mode enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.anonymousMode && settings.locationPrivacy !== 'hidden') {
|
||||||
|
riskLevel = 'low';
|
||||||
|
recommendations.push('Good privacy settings enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
locationPrivacy: settings.locationPrivacy,
|
||||||
|
identityPrivacy: settings.identityPrivacy,
|
||||||
|
dataEncryption: settings.anonymousMode,
|
||||||
|
torEnabled: false, // Will be detected at runtime
|
||||||
|
riskLevel,
|
||||||
|
recommendations: recommendations.length > 0 ? recommendations : ['Privacy settings are optimal'],
|
||||||
|
};
|
||||||
|
}
|
||||||
172
lib/services/tor.ts
Normal file
172
lib/services/tor.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
/**
|
||||||
|
* Tor Integration Service
|
||||||
|
* Provides Tor network connectivity for anonymous plant sharing
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
|
|
||||||
|
export interface TorConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
socksHost: string;
|
||||||
|
socksPort: number;
|
||||||
|
controlPort: number;
|
||||||
|
hiddenServiceDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TorService {
|
||||||
|
private config: TorConfig;
|
||||||
|
private proxyAgent: any;
|
||||||
|
|
||||||
|
constructor(config?: Partial<TorConfig>) {
|
||||||
|
this.config = {
|
||||||
|
enabled: process.env.TOR_ENABLED === 'true',
|
||||||
|
socksHost: process.env.TOR_SOCKS_HOST || '127.0.0.1',
|
||||||
|
socksPort: parseInt(process.env.TOR_SOCKS_PORT || '9050'),
|
||||||
|
controlPort: parseInt(process.env.TOR_CONTROL_PORT || '9051'),
|
||||||
|
hiddenServiceDir: process.env.TOR_HIDDEN_SERVICE_DIR,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config.enabled) {
|
||||||
|
this.initializeProxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize SOCKS proxy for Tor
|
||||||
|
*/
|
||||||
|
private initializeProxy(): void {
|
||||||
|
const proxyUrl = `socks5://${this.config.socksHost}:${this.config.socksPort}`;
|
||||||
|
this.proxyAgent = new SocksProxyAgent(proxyUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Tor is available and running
|
||||||
|
*/
|
||||||
|
async isAvailable(): Promise<boolean> {
|
||||||
|
if (!this.config.enabled) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to fetch check.torproject.org through Tor
|
||||||
|
const response = await fetch('https://check.torproject.org/api/ip', {
|
||||||
|
// @ts-ignore
|
||||||
|
agent: this.proxyAgent,
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.IsTor === true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tor availability check failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data through Tor network
|
||||||
|
*/
|
||||||
|
async fetchThroughTor(url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
throw new Error('Tor is not enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
// @ts-ignore
|
||||||
|
agent: this.proxyAgent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current Tor circuit info
|
||||||
|
*/
|
||||||
|
async getCircuitInfo(): Promise<{ country?: string; ip?: string }> {
|
||||||
|
try {
|
||||||
|
const response = await this.fetchThroughTor('https://check.torproject.org/api/ip');
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request new Tor identity (new circuit)
|
||||||
|
*/
|
||||||
|
async requestNewIdentity(): Promise<boolean> {
|
||||||
|
// This would require Tor control port access
|
||||||
|
// For now, just return true if Tor is enabled
|
||||||
|
return this.config.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get onion service hostname if available
|
||||||
|
*/
|
||||||
|
getOnionAddress(): string | null {
|
||||||
|
if (!this.config.hiddenServiceDir) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const hostnamePath = path.join(this.config.hiddenServiceDir, 'hostname');
|
||||||
|
|
||||||
|
if (fs.existsSync(hostnamePath)) {
|
||||||
|
return fs.readFileSync(hostnamePath, 'utf-8').trim();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading onion address:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate QR code for onion address
|
||||||
|
*/
|
||||||
|
async generateOnionQRCode(): Promise<string | null> {
|
||||||
|
const onionAddress = this.getOnionAddress();
|
||||||
|
if (!onionAddress) return null;
|
||||||
|
|
||||||
|
// In production, you'd use a QR code library
|
||||||
|
// For now, return the address
|
||||||
|
return `http://${onionAddress}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if request came through Tor
|
||||||
|
*/
|
||||||
|
isRequestFromTor(headers: any): boolean {
|
||||||
|
// Check various indicators that request came through Tor
|
||||||
|
return (
|
||||||
|
headers['x-tor-connection'] === 'true' ||
|
||||||
|
headers['x-forwarded-proto'] === 'tor' ||
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
let torService: TorService | null = null;
|
||||||
|
|
||||||
|
export function getTorService(): TorService {
|
||||||
|
if (!torService) {
|
||||||
|
torService = new TorService();
|
||||||
|
}
|
||||||
|
return torService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to detect and mark Tor connections
|
||||||
|
*/
|
||||||
|
export function torDetectionMiddleware(req: any, res: any, next: any) {
|
||||||
|
const torService = getTorService();
|
||||||
|
|
||||||
|
// Mark request as coming from Tor if detected
|
||||||
|
req.isTorConnection = torService.isRequestFromTor(req.headers);
|
||||||
|
|
||||||
|
// Add Tor status to response headers
|
||||||
|
if (req.isTorConnection) {
|
||||||
|
res.setHeader('X-Tor-Enabled', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,8 @@
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-hook-form": "^7.8.6"
|
"react-hook-form": "^7.8.6",
|
||||||
|
"socks-proxy-agent": "^8.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.9",
|
"@babel/core": "^7.12.9",
|
||||||
|
|
|
||||||
156
pages/api/plants/register-anonymous.ts
Normal file
156
pages/api/plants/register-anonymous.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* API Route: Register an anonymous plant
|
||||||
|
* POST /api/plants/register-anonymous
|
||||||
|
*
|
||||||
|
* This endpoint allows for privacy-preserving plant registration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager';
|
||||||
|
import { PlantData } from '../../../lib/blockchain/types';
|
||||||
|
import {
|
||||||
|
generateAnonymousId,
|
||||||
|
generateWalletAddress,
|
||||||
|
obfuscateLocation,
|
||||||
|
generateAnonymousPlantName,
|
||||||
|
createAnonymousContact,
|
||||||
|
PrivacySettings,
|
||||||
|
} from '../../../lib/privacy/anonymity';
|
||||||
|
import { getTorService } from '../../../lib/services/tor';
|
||||||
|
|
||||||
|
interface AnonymousPlantRequest {
|
||||||
|
commonName: string;
|
||||||
|
scientificName?: string;
|
||||||
|
species?: string;
|
||||||
|
genus?: string;
|
||||||
|
family?: string;
|
||||||
|
location: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
privacySettings: PrivacySettings;
|
||||||
|
pseudonym?: string;
|
||||||
|
encryptionKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestData: AnonymousPlantRequest = req.body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!requestData.commonName || !requestData.location || !requestData.privacySettings) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing required fields: commonName, location, privacySettings',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { location, privacySettings } = requestData;
|
||||||
|
|
||||||
|
// Check if request came through Tor
|
||||||
|
const torService = getTorService();
|
||||||
|
const isTorConnection = torService.isRequestFromTor(req.headers);
|
||||||
|
|
||||||
|
// Generate anonymous identifiers
|
||||||
|
const anonymousUserId = generateAnonymousId();
|
||||||
|
const walletAddress = generateWalletAddress();
|
||||||
|
const plantId = `plant-${generateAnonymousId()}`;
|
||||||
|
|
||||||
|
// Obfuscate location based on privacy settings
|
||||||
|
const fuzzyLocation = obfuscateLocation(
|
||||||
|
location.latitude,
|
||||||
|
location.longitude,
|
||||||
|
privacySettings.locationPrivacy
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine display name based on privacy settings
|
||||||
|
let displayName: string;
|
||||||
|
if (privacySettings.identityPrivacy === 'anonymous') {
|
||||||
|
displayName = 'Anonymous Grower';
|
||||||
|
} else if (privacySettings.identityPrivacy === 'pseudonym' && requestData.pseudonym) {
|
||||||
|
displayName = requestData.pseudonym;
|
||||||
|
} else {
|
||||||
|
displayName = `Grower-${anonymousUserId.substring(0, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create anonymous contact
|
||||||
|
const anonymousEmail = createAnonymousContact(
|
||||||
|
anonymousUserId,
|
||||||
|
requestData.encryptionKey || 'default-key'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build plant data with privacy protections
|
||||||
|
const plantData: PlantData = {
|
||||||
|
id: plantId,
|
||||||
|
commonName: privacySettings.sharePlantDetails
|
||||||
|
? requestData.commonName
|
||||||
|
: generateAnonymousPlantName(requestData.commonName, 0),
|
||||||
|
scientificName: privacySettings.sharePlantDetails ? requestData.scientificName : undefined,
|
||||||
|
species: privacySettings.sharePlantDetails ? requestData.species : undefined,
|
||||||
|
genus: privacySettings.sharePlantDetails ? requestData.genus : undefined,
|
||||||
|
family: privacySettings.sharePlantDetails ? requestData.family : undefined,
|
||||||
|
propagationType: 'original',
|
||||||
|
generation: 0,
|
||||||
|
plantedDate: new Date().toISOString(),
|
||||||
|
status: 'growing',
|
||||||
|
location: {
|
||||||
|
latitude: fuzzyLocation.latitude,
|
||||||
|
longitude: fuzzyLocation.longitude,
|
||||||
|
address: fuzzyLocation.displayName,
|
||||||
|
city: privacySettings.locationPrivacy !== 'hidden' ? 'Privacy Protected' : undefined,
|
||||||
|
country: privacySettings.locationPrivacy === 'country' ? 'Anonymous Region' : undefined,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
id: anonymousUserId,
|
||||||
|
name: displayName,
|
||||||
|
email: anonymousEmail,
|
||||||
|
walletAddress: walletAddress,
|
||||||
|
},
|
||||||
|
childPlants: [],
|
||||||
|
registeredAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
notes: privacySettings.anonymousMode
|
||||||
|
? 'Registered anonymously via privacy mode'
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockchain = getBlockchain();
|
||||||
|
|
||||||
|
// Register the plant
|
||||||
|
const block = blockchain.registerPlant(plantData);
|
||||||
|
|
||||||
|
// Save blockchain
|
||||||
|
saveBlockchain();
|
||||||
|
|
||||||
|
// Prepare response with privacy info
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
plant: block.plant,
|
||||||
|
privacy: {
|
||||||
|
anonymousId: anonymousUserId,
|
||||||
|
walletAddress: walletAddress,
|
||||||
|
locationAccuracy: fuzzyLocation.accuracy,
|
||||||
|
torConnection: isTorConnection,
|
||||||
|
privacyLevel: privacySettings.locationPrivacy,
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
index: block.index,
|
||||||
|
hash: block.hash,
|
||||||
|
timestamp: block.timestamp,
|
||||||
|
},
|
||||||
|
message: 'Plant registered anonymously',
|
||||||
|
warning: !isTorConnection
|
||||||
|
? 'For maximum privacy, consider accessing through Tor network'
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error registering anonymous plant:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
70
pages/api/privacy/tor-status.ts
Normal file
70
pages/api/privacy/tor-status.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* API Route: Check Tor status
|
||||||
|
* GET /api/privacy/tor-status
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { getTorService } from '../../../lib/services/tor';
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const torService = getTorService();
|
||||||
|
|
||||||
|
// Check if Tor is enabled in configuration
|
||||||
|
const isEnabled = process.env.TOR_ENABLED === 'true';
|
||||||
|
|
||||||
|
// Check if request came through Tor
|
||||||
|
const isTorConnection = torService.isRequestFromTor(req.headers);
|
||||||
|
|
||||||
|
let isAvailable = false;
|
||||||
|
let circuitInfo = null;
|
||||||
|
let onionAddress = null;
|
||||||
|
|
||||||
|
if (isEnabled) {
|
||||||
|
// Check if Tor daemon is available
|
||||||
|
try {
|
||||||
|
isAvailable = await torService.isAvailable();
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
circuitInfo = await torService.getCircuitInfo();
|
||||||
|
onionAddress = torService.getOnionAddress();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking Tor availability:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
tor: {
|
||||||
|
enabled: isEnabled,
|
||||||
|
available: isAvailable,
|
||||||
|
connectionThroughTor: isTorConnection,
|
||||||
|
onionAddress: onionAddress,
|
||||||
|
circuit: circuitInfo,
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
recommendTor: !isTorConnection,
|
||||||
|
privacyLevel: isTorConnection ? 'high' : 'standard',
|
||||||
|
ip: isTorConnection ? 'Hidden via Tor' : 'Visible',
|
||||||
|
},
|
||||||
|
recommendations: isTorConnection
|
||||||
|
? ['Your connection is private via Tor', 'Anonymous plant registration available']
|
||||||
|
: [
|
||||||
|
'For maximum privacy, access via Tor Browser',
|
||||||
|
'Download Tor from https://www.torproject.org',
|
||||||
|
`Or connect to our onion service: ${onionAddress || 'Not available'}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error checking Tor status:', error);
|
||||||
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
360
pages/plants/register-anonymous.tsx
Normal file
360
pages/plants/register-anonymous.tsx
Normal file
|
|
@ -0,0 +1,360 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import PrivacySettings from '../../components/PrivacySettings';
|
||||||
|
import { PrivacySettings as IPrivacySettings } from '../../lib/privacy/anonymity';
|
||||||
|
|
||||||
|
export default function RegisterAnonymousPlant() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [walletAddress, setWalletAddress] = useState('');
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
commonName: '',
|
||||||
|
scientificName: '',
|
||||||
|
species: '',
|
||||||
|
genus: '',
|
||||||
|
family: '',
|
||||||
|
latitude: '',
|
||||||
|
longitude: '',
|
||||||
|
pseudonym: '',
|
||||||
|
encryptionKey: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [privacySettings, setPrivacySettings] = useState<IPrivacySettings>({
|
||||||
|
anonymousMode: true,
|
||||||
|
locationPrivacy: 'fuzzy',
|
||||||
|
identityPrivacy: 'anonymous',
|
||||||
|
sharePlantDetails: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/plants/register-anonymous', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
commonName: formData.commonName,
|
||||||
|
scientificName: formData.scientificName || undefined,
|
||||||
|
species: formData.species || undefined,
|
||||||
|
genus: formData.genus || undefined,
|
||||||
|
family: formData.family || undefined,
|
||||||
|
location: {
|
||||||
|
latitude: parseFloat(formData.latitude),
|
||||||
|
longitude: parseFloat(formData.longitude),
|
||||||
|
},
|
||||||
|
privacySettings,
|
||||||
|
pseudonym: formData.pseudonym || undefined,
|
||||||
|
encryptionKey: formData.encryptionKey || undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Failed to register plant');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess(true);
|
||||||
|
setWalletAddress(data.privacy.walletAddress);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push(`/plants/${data.plant.id}`);
|
||||||
|
}, 3000);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'An error occurred');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentLocation = () => {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
latitude: position.coords.latitude.toString(),
|
||||||
|
longitude: position.coords.longitude.toString(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
setError('Unable to get your location: ' + error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setError('Geolocation is not supported by your browser');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-100">
|
||||||
|
<Head>
|
||||||
|
<title>Anonymous Plant Registration - LocalGreenChain</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white shadow-sm border-b-2 border-purple-200">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link href="/">
|
||||||
|
<a className="text-2xl font-bold text-purple-800">
|
||||||
|
🌱 LocalGreenChain
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<nav className="flex gap-4">
|
||||||
|
<Link href="/plants/register">
|
||||||
|
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
|
||||||
|
Standard Registration
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/plants/explore">
|
||||||
|
<a className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
|
||||||
|
Explore Network
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-6xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||||
|
🔒 Anonymous Plant Registration
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-600">
|
||||||
|
Register your plant with maximum privacy protection
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="mb-6 p-6 bg-green-100 border border-green-400 text-green-700 rounded-lg">
|
||||||
|
<h3 className="font-bold text-lg mb-2">
|
||||||
|
✓ Plant registered anonymously!
|
||||||
|
</h3>
|
||||||
|
<p className="mb-2">Your anonymous wallet address:</p>
|
||||||
|
<p className="font-mono bg-white p-2 rounded text-sm break-all">
|
||||||
|
{walletAddress}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm">
|
||||||
|
Save this address to manage your plant. Redirecting...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Left Column - Privacy Settings */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<PrivacySettings
|
||||||
|
value={privacySettings}
|
||||||
|
onChange={setPrivacySettings}
|
||||||
|
showTorStatus={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column - Plant Information */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl p-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||||
|
Plant Information
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Basic Plant Info */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Common Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="commonName"
|
||||||
|
required
|
||||||
|
value={formData.commonName}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
placeholder="e.g., Tomato, Basil"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{privacySettings.sharePlantDetails && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Scientific Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="scientificName"
|
||||||
|
value={formData.scientificName}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
placeholder="e.g., Solanum lycopersicum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Genus
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="genus"
|
||||||
|
value={formData.genus}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Family
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="family"
|
||||||
|
value={formData.family}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Location (will be obfuscated based on privacy settings)
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Latitude *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
name="latitude"
|
||||||
|
required
|
||||||
|
value={formData.latitude}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Longitude *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
name="longitude"
|
||||||
|
required
|
||||||
|
value={formData.longitude}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={getCurrentLocation}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
📍 Use My Current Location
|
||||||
|
</button>
|
||||||
|
<p className="mt-2 text-sm text-gray-600">
|
||||||
|
Your exact location will be obfuscated based on your
|
||||||
|
privacy settings
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Optional Pseudonym */}
|
||||||
|
{privacySettings.identityPrivacy === 'pseudonym' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Pseudonym (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="pseudonym"
|
||||||
|
value={formData.pseudonym}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
placeholder="Your chosen display name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Encryption Key */}
|
||||||
|
{privacySettings.anonymousMode && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Encryption Key (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="encryptionKey"
|
||||||
|
value={formData.encryptionKey}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||||
|
placeholder="Optional password for extra security"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-sm text-gray-600">
|
||||||
|
Used to generate your anonymous contact address
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="flex-1 px-6 py-3 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? 'Registering...' : '🔒 Register Anonymously'}
|
||||||
|
</button>
|
||||||
|
<Link href="/">
|
||||||
|
<a className="px-6 py-3 bg-gray-200 text-gray-700 font-semibold rounded-lg hover:bg-gray-300 transition">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
tor/torrc.example
Normal file
41
tor/torrc.example
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Tor Configuration for LocalGreenChain Hidden Service
|
||||||
|
# Copy this file to /etc/tor/torrc or ~/.tor/torrc and edit as needed
|
||||||
|
|
||||||
|
# SOCKS proxy port (for outgoing Tor connections)
|
||||||
|
SocksPort 9050
|
||||||
|
|
||||||
|
# Control port for managing Tor
|
||||||
|
ControlPort 9051
|
||||||
|
|
||||||
|
# Hidden Service Configuration
|
||||||
|
# This allows your LocalGreenChain instance to be accessible via .onion address
|
||||||
|
HiddenServiceDir /var/lib/tor/localgreenchain/
|
||||||
|
HiddenServicePort 80 127.0.0.1:3001
|
||||||
|
|
||||||
|
# Optional: Multiple hidden service ports
|
||||||
|
# HiddenServicePort 443 127.0.0.1:3001
|
||||||
|
|
||||||
|
# Logging (for debugging)
|
||||||
|
Log notice file /var/log/tor/notices.log
|
||||||
|
|
||||||
|
# Privacy and Security Settings
|
||||||
|
# Reject non-anonymous single hop circuits
|
||||||
|
RejectAllOutboundTraffic 0
|
||||||
|
|
||||||
|
# Circuit isolation for better privacy
|
||||||
|
IsolateDestAddr 1
|
||||||
|
IsolateDestPort 1
|
||||||
|
|
||||||
|
# Bridge configuration (if needed to bypass censorship)
|
||||||
|
# UseBridges 1
|
||||||
|
# Bridge obfs4 [bridge address]
|
||||||
|
|
||||||
|
# Bandwidth settings (optional)
|
||||||
|
# RelayBandwidthRate 100 KB
|
||||||
|
# RelayBandwidthBurst 200 KB
|
||||||
|
|
||||||
|
# Directory for storing Tor data
|
||||||
|
DataDirectory /var/lib/tor
|
||||||
|
|
||||||
|
# Run as daemon
|
||||||
|
RunAsDaemon 1
|
||||||
Loading…
Reference in a new issue