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_IMAGE_DOMAIN=localhost
|
||||
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
|
||||
- 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
|
||||
|
||||
### Prerequisites
|
||||
|
|
@ -92,6 +101,36 @@ bun run dev
|
|||
5. **Open your browser**
|
||||
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
|
||||
|
||||
### 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",
|
||||
"react": "^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": {
|
||||
"@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