chore: enhance security configuration across applications

- Add root-level *.json to .gitignore to prevent wallet backup leaks
- Replace wildcard CORS origins with explicit localhost URLs across all apps
- Add OPTIONS method to CORS allowed methods for preflight requests
- Update coordinator database to use absolute path in data/ directory to prevent duplicates
- Add JWT secret validation in coordinator config (must be set via environment)
- Replace deprecated get_session dependency with Session
This commit is contained in:
oib
2026-02-13 16:07:03 +01:00
parent e9646cc7dd
commit c984a1e052
13 changed files with 434 additions and 120 deletions

3
.gitignore vendored
View File

@@ -150,6 +150,9 @@ home/client/client_wallet.json
home/genesis_wallet.json home/genesis_wallet.json
home/miner/miner_wallet.json home/miner/miner_wallet.json
# Root-level wallet backups (contain private keys)
*.json
# =================== # ===================
# Stale source copies # Stale source copies
# =================== # ===================

View File

@@ -111,8 +111,13 @@ def create_app() -> FastAPI:
app.add_middleware(RateLimitMiddleware, max_requests=200, window_seconds=60) app.add_middleware(RateLimitMiddleware, max_requests=200, window_seconds=60)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=[
allow_methods=["GET", "POST"], "http://localhost:3000",
"http://localhost:8080",
"http://localhost:8000",
"http://localhost:8011"
],
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"], allow_headers=["*"],
) )

View File

@@ -70,7 +70,16 @@ def create_app() -> Starlette:
] ]
middleware = [ middleware = [
Middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"]) Middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:8080",
"http://localhost:8000",
"http://localhost:8011"
],
allow_methods=["POST", "GET", "OPTIONS"]
)
] ]
return Starlette(routes=routes, middleware=middleware) return Starlette(routes=routes, middleware=middleware)

View File

@@ -1,5 +1,7 @@
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List, Optional from typing import List, Optional
from pathlib import Path
import os
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -9,14 +11,35 @@ class Settings(BaseSettings):
app_host: str = "127.0.0.1" app_host: str = "127.0.0.1"
app_port: int = 8011 app_port: int = 8011
database_url: str = "sqlite:///./coordinator.db" # Use absolute path to avoid database duplicates in different working directories
@property
def database_url(self) -> str:
# Find project root by looking for .git directory
current = Path(__file__).resolve()
while current.parent != current:
if (current / ".git").exists():
project_root = current
break
current = current.parent
else:
# Fallback to relative path if .git not found
project_root = Path(__file__).resolve().parents[3]
db_path = project_root / "data" / "coordinator.db"
db_path.parent.mkdir(parents=True, exist_ok=True)
return f"sqlite:///{db_path}"
client_api_keys: List[str] = [] client_api_keys: List[str] = []
miner_api_keys: List[str] = [] miner_api_keys: List[str] = []
admin_api_keys: List[str] = [] admin_api_keys: List[str] = []
hmac_secret: Optional[str] = None hmac_secret: Optional[str] = None
allow_origins: List[str] = ["*"] allow_origins: List[str] = [
"http://localhost:3000",
"http://localhost:8080",
"http://localhost:8000",
"http://localhost:8011"
]
job_ttl_seconds: int = 900 job_ttl_seconds: int = 900
heartbeat_interval_seconds: int = 10 heartbeat_interval_seconds: int = 10

View File

@@ -17,7 +17,7 @@ class Settings(BaseSettings):
database_url: str = "postgresql://localhost:5432/aitbc_coordinator" database_url: str = "postgresql://localhost:5432/aitbc_coordinator"
# JWT Configuration # JWT Configuration
jwt_secret: str = "change-me-in-production" jwt_secret: str = "" # Must be provided via environment
jwt_algorithm: str = "HS256" jwt_algorithm: str = "HS256"
jwt_expiration_hours: int = 24 jwt_expiration_hours: int = 24
@@ -51,7 +51,17 @@ class Settings(BaseSettings):
class Config: class Config:
env_file = ".env" env_file = ".env"
env_file_encoding = "utf-8" env_file_encoding = "utf-8"
def validate_secrets(self) -> None:
"""Validate that all required secrets are provided"""
if not self.jwt_secret:
raise ValueError("JWT_SECRET environment variable is required")
if self.jwt_secret == "change-me-in-production":
raise ValueError("JWT_SECRET must be changed from default value")
# Create global settings instance # Create global settings instance
settings = Settings() settings = Settings()
# Validate secrets on import
settings.validate_secrets()

View File

@@ -1,21 +1,9 @@
from typing import Callable, Generator, Annotated from typing import Callable, Annotated
from fastapi import Depends, Header, HTTPException from fastapi import Depends, Header, HTTPException
from sqlmodel import Session
from .config import settings from .config import settings
def get_session() -> Generator[Session, None, None]:
"""Get database session"""
from .database import engine
with Session(engine) as session:
yield session
# Type alias for session dependency
SessionDep = Annotated[Session, Depends(get_session)]
class APIKeyValidator: class APIKeyValidator:
def __init__(self, allowed_keys: list[str]): def __init__(self, allowed_keys: list[str]):
self.allowed_keys = {key.strip() for key in allowed_keys if key} self.allowed_keys = {key.strip() for key in allowed_keys if key}

View File

@@ -3,7 +3,6 @@ from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import make_asgi_app from prometheus_client import make_asgi_app
from .config import settings from .config import settings
from .database import create_db_and_tables
from .storage import init_db from .storage import init_db
from .routers import ( from .routers import (
client, client,
@@ -38,8 +37,8 @@ def create_app() -> FastAPI:
CORSMiddleware, CORSMiddleware,
allow_origins=settings.allow_origins, allow_origins=settings.allow_origins,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"] allow_headers=["*"] # Allow all headers for API keys and content types
) )
app.include_router(client, prefix="/v1") app.include_router(client, prefix="/v1")

View File

@@ -10,7 +10,7 @@ import time
import hashlib import hashlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ..deps import get_session from ..storage import SessionDep
from ..domain import User, Wallet from ..domain import User, Wallet
from ..schemas import UserCreate, UserLogin, UserProfile, UserBalance from ..schemas import UserCreate, UserLogin, UserProfile, UserBalance
@@ -50,7 +50,7 @@ def verify_session_token(token: str) -> Optional[str]:
@router.post("/register", response_model=UserProfile) @router.post("/register", response_model=UserProfile)
async def register_user( async def register_user(
user_data: UserCreate, user_data: UserCreate,
session: Session = Depends(get_session) session: SessionDep
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Register a new user""" """Register a new user"""
@@ -103,7 +103,7 @@ async def register_user(
@router.post("/login", response_model=UserProfile) @router.post("/login", response_model=UserProfile)
async def login_user( async def login_user(
login_data: UserLogin, login_data: UserLogin,
session: Session = Depends(get_session) session: SessionDep
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Login user with wallet address""" """Login user with wallet address"""
@@ -161,7 +161,7 @@ async def login_user(
@router.get("/users/me", response_model=UserProfile) @router.get("/users/me", response_model=UserProfile)
async def get_current_user( async def get_current_user(
token: str, token: str,
session: Session = Depends(get_session) session: SessionDep
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get current user profile""" """Get current user profile"""
@@ -190,7 +190,7 @@ async def get_current_user(
@router.get("/users/{user_id}/balance", response_model=UserBalance) @router.get("/users/{user_id}/balance", response_model=UserBalance)
async def get_user_balance( async def get_user_balance(
user_id: str, user_id: str,
session: Session = Depends(get_session) session: SessionDep
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get user's AITBC balance""" """Get user's AITBC balance"""
@@ -223,7 +223,7 @@ async def logout_user(token: str) -> Dict[str, str]:
@router.get("/users/{user_id}/transactions") @router.get("/users/{user_id}/transactions")
async def get_user_transactions( async def get_user_transactions(
user_id: str, user_id: str,
session: Session = Depends(get_session) session: SessionDep
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get user's transaction history""" """Get user's transaction history"""

View File

@@ -30,12 +30,16 @@ Base = declarative_base()
# Direct PostgreSQL connection for performance # Direct PostgreSQL connection for performance
def get_pg_connection(): def get_pg_connection():
"""Get direct PostgreSQL connection""" """Get direct PostgreSQL connection"""
# Parse database URL from settings
from urllib.parse import urlparse
parsed = urlparse(settings.database_url)
return psycopg2.connect( return psycopg2.connect(
host="localhost", host=parsed.hostname or "localhost",
database="aitbc_coordinator", database=parsed.path[1:] if parsed.path else "aitbc_coordinator",
user="aitbc_user", user=parsed.username or "aitbc_user",
password="aitbc_password", password=parsed.password or "aitbc_password",
port=5432, port=parsed.port or 5432,
cursor_factory=RealDictCursor cursor_factory=RealDictCursor
) )
@@ -194,8 +198,16 @@ class PostgreSQLAdapter:
if self.connection: if self.connection:
self.connection.close() self.connection.close()
# Global adapter instance # Global adapter instance (lazy initialization)
db_adapter = PostgreSQLAdapter() db_adapter: Optional[PostgreSQLAdapter] = None
def get_db_adapter() -> PostgreSQLAdapter:
"""Get or create database adapter instance"""
global db_adapter
if db_adapter is None:
db_adapter = PostgreSQLAdapter()
return db_adapter
# Database initialization # Database initialization
def init_db(): def init_db():
@@ -212,7 +224,8 @@ def init_db():
def check_db_health() -> Dict[str, Any]: def check_db_health() -> Dict[str, Any]:
"""Check database health""" """Check database health"""
try: try:
result = db_adapter.execute_query("SELECT 1 as health_check") adapter = get_db_adapter()
result = adapter.execute_query("SELECT 1 as health_check")
return { return {
"status": "healthy", "status": "healthy",
"database": "postgresql", "database": "postgresql",

View File

@@ -5,11 +5,14 @@ FastAPI backend for the AITBC Trade Exchange
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional from typing import List, Optional
from fastapi import FastAPI, Depends, HTTPException, status from fastapi import FastAPI, Depends, HTTPException, status, Header
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import desc, func, and_ from sqlalchemy import desc, func, and_
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import hashlib
import time
from typing import Annotated
from database import init_db, get_db_session from database import init_db, get_db_session
from models import User, Order, Trade, Balance from models import User, Order, Trade, Balance
@@ -17,13 +20,59 @@ from models import User, Order, Trade, Balance
# Initialize FastAPI app # Initialize FastAPI app
app = FastAPI(title="AITBC Trade Exchange API", version="1.0.0") app = FastAPI(title="AITBC Trade Exchange API", version="1.0.0")
# In-memory session storage (use Redis in production)
user_sessions = {}
def verify_session_token(token: str = Header(..., alias="Authorization")) -> int:
"""Verify session token and return user_id"""
# Remove "Bearer " prefix if present
if token.startswith("Bearer "):
token = token[7:]
if token not in user_sessions:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
session = user_sessions[token]
# Check if expired
if int(time.time()) > session["expires_at"]:
del user_sessions[token]
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired"
)
return session["user_id"]
def optional_auth(token: Optional[str] = Header(None, alias="Authorization")) -> Optional[int]:
"""Optional authentication - returns user_id if token is valid, None otherwise"""
if not token:
return None
try:
return verify_session_token(token)
except HTTPException:
return None
# Type annotations for dependencies
UserDep = Annotated[int, Depends(verify_session_token)]
OptionalUserDep = Annotated[Optional[int], Depends(optional_auth)]
# Add CORS middleware # Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=[
"http://localhost:3000",
"http://localhost:8080",
"http://localhost:8000",
"http://localhost:3003"
],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"], allow_headers=["*"], # Allow all headers for auth tokens
) )
# Pydantic models # Pydantic models
@@ -110,6 +159,41 @@ def get_recent_trades(limit: int = 20, db: Session = Depends(get_db_session)):
trades = db.query(Trade).order_by(desc(Trade.created_at)).limit(limit).all() trades = db.query(Trade).order_by(desc(Trade.created_at)).limit(limit).all()
return trades return trades
@app.get("/api/orders", response_model=List[OrderResponse])
def get_orders(
status_filter: Optional[str] = None,
user_only: bool = False,
db: Session = Depends(get_db_session),
user_id: OptionalUserDep = None
):
"""Get all orders with optional status filter"""
query = db.query(Order)
# Filter by user if requested and authenticated
if user_only and user_id:
query = query.filter(Order.user_id == user_id)
if status_filter:
query = query.filter(Order.status == status_filter.upper())
orders = query.order_by(Order.created_at.desc()).all()
return orders
@app.get("/api/my/orders", response_model=List[OrderResponse])
def get_my_orders(
user_id: UserDep,
status_filter: Optional[str] = None,
db: Session = Depends(get_db_session)
):
"""Get current user's orders"""
query = db.query(Order).filter(Order.user_id == user_id)
if status_filter:
query = query.filter(Order.status == status_filter.upper())
orders = query.order_by(Order.created_at.desc()).all()
return orders
@app.get("/api/orders/orderbook", response_model=OrderBookResponse) @app.get("/api/orders/orderbook", response_model=OrderBookResponse)
def get_orderbook(db: Session = Depends(get_db_session)): def get_orderbook(db: Session = Depends(get_db_session)):
"""Get current order book""" """Get current order book"""
@@ -127,7 +211,11 @@ def get_orderbook(db: Session = Depends(get_db_session)):
return OrderBookResponse(buys=buys, sells=sells) return OrderBookResponse(buys=buys, sells=sells)
@app.post("/api/orders", response_model=OrderResponse) @app.post("/api/orders", response_model=OrderResponse)
def create_order(order: OrderCreate, db: Session = Depends(get_db_session)): def create_order(
order: OrderCreate,
db: Session = Depends(get_db_session),
user_id: UserDep
):
"""Create a new order""" """Create a new order"""
# Validate order type # Validate order type
@@ -140,7 +228,7 @@ def create_order(order: OrderCreate, db: Session = Depends(get_db_session)):
# Create order # Create order
total = order.amount * order.price total = order.amount * order.price
db_order = Order( db_order = Order(
user_id=1, # TODO: Get from authentication user_id=user_id, # Use authenticated user_id
order_type=order.order_type, order_type=order.order_type,
amount=order.amount, amount=order.amount,
price=order.price, price=order.price,
@@ -219,6 +307,45 @@ def try_match_order(order: Order, db: Session):
db.commit() db.commit()
@app.post("/api/auth/login")
def login_user(wallet_address: str, db: Session = Depends(get_db_session)):
"""Login with wallet address"""
# Find or create user
user = db.query(User).filter(User.wallet_address == wallet_address).first()
if not user:
user = User(
wallet_address=wallet_address,
email=f"{wallet_address}@aitbc.local",
is_active=True
)
db.add(user)
db.commit()
db.refresh(user)
# Create session token
token_data = f"{user.id}:{int(time.time())}"
token = hashlib.sha256(token_data.encode()).hexdigest()
# Store session
user_sessions[token] = {
"user_id": user.id,
"created_at": int(time.time()),
"expires_at": int(time.time()) + 86400 # 24 hours
}
return {"token": token, "user_id": user.id}
@app.post("/api/auth/logout")
def logout_user(token: str = Header(..., alias="Authorization")):
"""Logout user"""
if token.startswith("Bearer "):
token = token[7:]
if token in user_sessions:
del user_sessions[token]
return {"message": "Logged out successfully"}
@app.get("/api/health") @app.get("/api/health")
def health_check(): def health_check():
"""Health check endpoint""" """Health check endpoint"""

View File

@@ -9,7 +9,70 @@ import yaml
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ..utils import output, error, success from ..utils import output, error, success, encrypt_value, decrypt_value
import getpass
def _get_wallet_password(wallet_name: str) -> str:
"""Get or prompt for wallet encryption password"""
# Try to get from keyring first
try:
import keyring
password = keyring.get_password("aitbc-wallet", wallet_name)
if password:
return password
except:
pass
# Prompt for password
while True:
password = getpass.getpass(f"Enter password for wallet '{wallet_name}': ")
if not password:
error("Password cannot be empty")
continue
confirm = getpass.getpass("Confirm password: ")
if password != confirm:
error("Passwords do not match")
continue
# Store in keyring for future use
try:
import keyring
keyring.set_password("aitbc-wallet", wallet_name, password)
except:
pass
return password
def _save_wallet(wallet_path: Path, wallet_data: Dict[str, Any], password: str = None):
"""Save wallet with encrypted private key"""
# Encrypt private key if provided
if password and 'private_key' in wallet_data:
wallet_data['private_key'] = encrypt_value(wallet_data['private_key'], password)
wallet_data['encrypted'] = True
# Save wallet
with open(wallet_path, 'w') as f:
json.dump(wallet_data, f, indent=2)
def _load_wallet(wallet_path: Path, wallet_name: str) -> Dict[str, Any]:
"""Load wallet and decrypt private key if needed"""
with open(wallet_path, 'r') as f:
wallet_data = json.load(f)
# Decrypt private key if encrypted
if wallet_data.get('encrypted') and 'private_key' in wallet_data:
password = _get_wallet_password(wallet_name)
try:
wallet_data['private_key'] = decrypt_value(wallet_data['private_key'], password)
except Exception:
error("Invalid password for wallet")
raise click.Abort()
return wallet_data
@click.group() @click.group()
@@ -56,8 +119,9 @@ def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str]):
@wallet.command() @wallet.command()
@click.argument('name') @click.argument('name')
@click.option('--type', 'wallet_type', default='hd', help='Wallet type (hd, simple)') @click.option('--type', 'wallet_type', default='hd', help='Wallet type (hd, simple)')
@click.option('--no-encrypt', is_flag=True, help='Skip wallet encryption (not recommended)')
@click.pass_context @click.pass_context
def create(ctx, name: str, wallet_type: str): def create(ctx, name: str, wallet_type: str, no_encrypt: bool):
"""Create a new wallet""" """Create a new wallet"""
wallet_dir = ctx.obj['wallet_dir'] wallet_dir = ctx.obj['wallet_dir']
wallet_path = wallet_dir / f"{name}.json" wallet_path = wallet_dir / f"{name}.json"
@@ -70,10 +134,29 @@ def create(ctx, name: str, wallet_type: str):
if wallet_type == 'hd': if wallet_type == 'hd':
# Hierarchical Deterministic wallet # Hierarchical Deterministic wallet
import secrets import secrets
seed = secrets.token_hex(32) from cryptography.hazmat.primitives import hashes
address = f"aitbc1{seed[:40]}" from cryptography.hazmat.primitives.asymmetric import ec
private_key = f"0x{seed}" from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, NoEncryption, PrivateFormat
public_key = f"0x{secrets.token_hex(32)}" import base64
# Generate private key
private_key_bytes = secrets.token_bytes(32)
private_key = f"0x{private_key_bytes.hex()}"
# Derive public key from private key using ECDSA
priv_key = ec.derive_private_key(int.from_bytes(private_key_bytes, 'big'), ec.SECP256K1())
pub_key = priv_key.public_key()
pub_key_bytes = pub_key.public_bytes(
encoding=Encoding.X962,
format=PublicFormat.UncompressedPoint
)
public_key = f"0x{pub_key_bytes.hex()}"
# Generate address from public key (simplified)
digest = hashes.Hash(hashes.SHA256())
digest.update(pub_key_bytes)
address_hash = digest.finalize()
address = f"aitbc1{address_hash[:20].hex()}"
else: else:
# Simple wallet # Simple wallet
import secrets import secrets
@@ -92,9 +175,14 @@ def create(ctx, name: str, wallet_type: str):
"transactions": [] "transactions": []
} }
# Get password for encryption unless skipped
password = None
if not no_encrypt:
success("Wallet encryption is enabled. Your private key will be encrypted at rest.")
password = _get_wallet_password(name)
# Save wallet # Save wallet
with open(wallet_path, 'w') as f: _save_wallet(wallet_path, wallet_data, password)
json.dump(wallet_data, f, indent=2)
success(f"Wallet '{name}' created successfully") success(f"Wallet '{name}' created successfully")
output({ output({
@@ -123,13 +211,16 @@ def list(ctx):
for wallet_file in wallet_dir.glob("*.json"): for wallet_file in wallet_dir.glob("*.json"):
with open(wallet_file, 'r') as f: with open(wallet_file, 'r') as f:
wallet_data = json.load(f) wallet_data = json.load(f)
wallets.append({ wallet_info = {
"name": wallet_data['wallet_id'], "name": wallet_data['wallet_id'],
"type": wallet_data.get('type', 'simple'), "type": wallet_data.get('type', 'simple'),
"address": wallet_data['address'], "address": wallet_data['address'],
"created_at": wallet_data['created_at'], "created_at": wallet_data['created_at'],
"active": wallet_data['wallet_id'] == active_wallet "active": wallet_data['wallet_id'] == active_wallet
}) }
if wallet_data.get('encrypted'):
wallet_info['encrypted'] = True
wallets.append(wallet_info)
output(wallets, ctx.obj.get('output_format', 'table')) output(wallets, ctx.obj.get('output_format', 'table'))
@@ -163,9 +254,11 @@ def switch(ctx, name: str):
yaml.dump(config, f, default_flow_style=False) yaml.dump(config, f, default_flow_style=False)
success(f"Switched to wallet '{name}'") success(f"Switched to wallet '{name}'")
# Load wallet to get address (will handle encryption)
wallet_data = _load_wallet(wallet_path, name)
output({ output({
"active_wallet": name, "active_wallet": name,
"address": json.load(open(wallet_path))['address'] "address": wallet_data['address']
}, ctx.obj.get('output_format', 'table')) }, ctx.obj.get('output_format', 'table'))
@@ -255,7 +348,8 @@ def restore(ctx, backup_path: str, name: str, force: bool):
wallet_data['wallet_id'] = name wallet_data['wallet_id'] = name
wallet_data['restored_at'] = datetime.utcnow().isoformat() + "Z" wallet_data['restored_at'] = datetime.utcnow().isoformat() + "Z"
# Save restored wallet # Save restored wallet (preserve encryption state)
# If wallet was encrypted, we save it as-is (still encrypted with original password)
with open(wallet_path, 'w') as f: with open(wallet_path, 'w') as f:
json.dump(wallet_data, f, indent=2) json.dump(wallet_data, f, indent=2)
@@ -279,8 +373,7 @@ def info(ctx):
error(f"Wallet '{wallet_name}' not found. Use 'aitbc wallet create' to create one.") error(f"Wallet '{wallet_name}' not found. Use 'aitbc wallet create' to create one.")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
# Get active wallet from config # Get active wallet from config
active_wallet = 'default' active_wallet = 'default'
@@ -317,22 +410,46 @@ def balance(ctx):
# Auto-create wallet if it doesn't exist # Auto-create wallet if it doesn't exist
if not wallet_path.exists(): if not wallet_path.exists():
import secrets import secrets
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
# Generate proper key pair
private_key_bytes = secrets.token_bytes(32)
private_key = f"0x{private_key_bytes.hex()}"
# Derive public key from private key
priv_key = ec.derive_private_key(int.from_bytes(private_key_bytes, 'big'), ec.SECP256K1())
pub_key = priv_key.public_key()
pub_key_bytes = pub_key.public_bytes(
encoding=Encoding.X962,
format=PublicFormat.UncompressedPoint
)
public_key = f"0x{pub_key_bytes.hex()}"
# Generate address from public key
digest = hashes.Hash(hashes.SHA256())
digest.update(pub_key_bytes)
address_hash = digest.finalize()
address = f"aitbc1{address_hash[:20].hex()}"
wallet_data = { wallet_data = {
"wallet_id": wallet_name, "wallet_id": wallet_name,
"type": "simple", "type": "simple",
"address": f"aitbc1{secrets.token_hex(20)}", "address": address,
"public_key": f"0x{secrets.token_hex(32)}", "public_key": public_key,
"private_key": f"0x{secrets.token_hex(32)}", "private_key": private_key,
"created_at": datetime.utcnow().isoformat() + "Z", "created_at": datetime.utcnow().isoformat() + "Z",
"balance": 0.0, "balance": 0.0,
"transactions": [] "transactions": []
} }
wallet_path.parent.mkdir(parents=True, exist_ok=True) wallet_path.parent.mkdir(parents=True, exist_ok=True)
with open(wallet_path, 'w') as f: # Auto-create with encryption
json.dump(wallet_data, f, indent=2) success("Creating new wallet with encryption enabled")
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
else: else:
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
# Try to get balance from blockchain if available # Try to get balance from blockchain if available
if config: if config:
@@ -377,8 +494,7 @@ def history(ctx, limit: int):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
transactions = wallet_data.get('transactions', [])[-limit:] transactions = wallet_data.get('transactions', [])[-limit:]
@@ -413,8 +529,7 @@ def earn(ctx, amount: float, job_id: str, desc: Optional[str]):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
# Add transaction # Add transaction
transaction = { transaction = {
@@ -428,9 +543,11 @@ def earn(ctx, amount: float, job_id: str, desc: Optional[str]):
wallet_data['transactions'].append(transaction) wallet_data['transactions'].append(transaction)
wallet_data['balance'] = wallet_data.get('balance', 0) + amount wallet_data['balance'] = wallet_data.get('balance', 0) + amount
# Save wallet # Save wallet with encryption
with open(wallet_path, 'w') as f: password = None
json.dump(wallet_data, f, indent=2) if wallet_data.get('encrypted'):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
success(f"Earnings added: {amount} AITBC") success(f"Earnings added: {amount} AITBC")
output({ output({
@@ -454,8 +571,7 @@ def spend(ctx, amount: float, description: str):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
balance = wallet_data.get('balance', 0) balance = wallet_data.get('balance', 0)
if balance < amount: if balance < amount:
@@ -474,9 +590,11 @@ def spend(ctx, amount: float, description: str):
wallet_data['transactions'].append(transaction) wallet_data['transactions'].append(transaction)
wallet_data['balance'] = balance - amount wallet_data['balance'] = balance - amount
# Save wallet # Save wallet with encryption
with open(wallet_path, 'w') as f: password = None
json.dump(wallet_data, f, indent=2) if wallet_data.get('encrypted'):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
success(f"Spent: {amount} AITBC") success(f"Spent: {amount} AITBC")
output({ output({
@@ -498,8 +616,7 @@ def address(ctx):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
output({ output({
"wallet": wallet_name, "wallet": wallet_name,
@@ -522,8 +639,7 @@ def send(ctx, to_address: str, amount: float, description: Optional[str]):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
balance = wallet_data.get('balance', 0) balance = wallet_data.get('balance', 0)
if balance < amount: if balance < amount:
@@ -589,8 +705,11 @@ def send(ctx, to_address: str, amount: float, description: Optional[str]):
wallet_data['transactions'].append(transaction) wallet_data['transactions'].append(transaction)
wallet_data['balance'] = balance - amount wallet_data['balance'] = balance - amount
with open(wallet_path, 'w') as f: # Save wallet with encryption
json.dump(wallet_data, f, indent=2) password = None
if wallet_data.get('encrypted'):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
output({ output({
"wallet": wallet_name, "wallet": wallet_name,
@@ -615,8 +734,7 @@ def request_payment(ctx, to_address: str, amount: float, description: Optional[s
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
# Create payment request # Create payment request
request = { request = {
@@ -645,8 +763,7 @@ def stats(ctx):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
transactions = wallet_data.get('transactions', []) transactions = wallet_data.get('transactions', [])
@@ -680,8 +797,7 @@ def stake(ctx, amount: float, duration: int):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
balance = wallet_data.get('balance', 0) balance = wallet_data.get('balance', 0)
if balance < amount: if balance < amount:
@@ -714,8 +830,11 @@ def stake(ctx, amount: float, duration: int):
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat()
}) })
with open(wallet_path, 'w') as f: # Save wallet with encryption
json.dump(wallet_data, f, indent=2) password = None
if wallet_data.get('encrypted'):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
success(f"Staked {amount} AITBC for {duration} days") success(f"Staked {amount} AITBC for {duration} days")
output({ output({
@@ -774,8 +893,11 @@ def unstake(ctx, stake_id: str):
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat()
}) })
with open(wallet_path, 'w') as f: # Save wallet with encryption
json.dump(wallet_data, f, indent=2) password = None
if wallet_data.get('encrypted'):
password = _get_wallet_password(wallet_name)
_save_wallet(wallet_path, wallet_data, password)
success(f"Unstaked {stake_record['amount']} AITBC + {rewards:.4f} rewards") success(f"Unstaked {stake_record['amount']} AITBC + {rewards:.4f} rewards")
output({ output({
@@ -800,8 +922,7 @@ def staking_info(ctx):
error(f"Wallet '{wallet_name}' not found") error(f"Wallet '{wallet_name}' not found")
return return
with open(wallet_path, 'r') as f: wallet_data = _load_wallet(wallet_path, wallet_name)
wallet_data = json.load(f)
staking = wallet_data.get('staking', []) staking = wallet_data.get('staking', [])
active_stakes = [s for s in staking if s['status'] == 'active'] active_stakes = [s for s in staking if s['status'] == 'active']
@@ -995,14 +1116,14 @@ def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
@click.pass_context @click.pass_context
def liquidity_stake(ctx, amount: float, pool: str, lock_days: int): def liquidity_stake(ctx, amount: float, pool: str, lock_days: int):
"""Stake tokens into a liquidity pool""" """Stake tokens into a liquidity pool"""
wallet_name = ctx.obj['wallet_name']
wallet_path = ctx.obj.get('wallet_path') wallet_path = ctx.obj.get('wallet_path')
if not wallet_path or not Path(wallet_path).exists(): if not wallet_path or not Path(wallet_path).exists():
error("Wallet not found") error("Wallet not found")
ctx.exit(1) ctx.exit(1)
return return
with open(wallet_path) as f: wallet_data = _load_wallet(Path(wallet_path), wallet_name)
wallet_data = json.load(f)
balance = wallet_data.get('balance', 0) balance = wallet_data.get('balance', 0)
if balance < amount: if balance < amount:
@@ -1051,8 +1172,11 @@ def liquidity_stake(ctx, amount: float, pool: str, lock_days: int):
"timestamp": now.isoformat() "timestamp": now.isoformat()
}) })
with open(wallet_path, "w") as f: # Save wallet with encryption
json.dump(wallet_data, f, indent=2) password = None
if wallet_data.get('encrypted'):
password = _get_wallet_password(wallet_name)
_save_wallet(Path(wallet_path), wallet_data, password)
success(f"Staked {amount} AITBC into '{pool}' pool ({tier} tier, {apy}% APY)") success(f"Staked {amount} AITBC into '{pool}' pool ({tier} tier, {apy}% APY)")
output({ output({
@@ -1071,14 +1195,14 @@ def liquidity_stake(ctx, amount: float, pool: str, lock_days: int):
@click.pass_context @click.pass_context
def liquidity_unstake(ctx, stake_id: str): def liquidity_unstake(ctx, stake_id: str):
"""Withdraw from a liquidity pool with rewards""" """Withdraw from a liquidity pool with rewards"""
wallet_name = ctx.obj['wallet_name']
wallet_path = ctx.obj.get('wallet_path') wallet_path = ctx.obj.get('wallet_path')
if not wallet_path or not Path(wallet_path).exists(): if not wallet_path or not Path(wallet_path).exists():
error("Wallet not found") error("Wallet not found")
ctx.exit(1) ctx.exit(1)
return return
with open(wallet_path) as f: wallet_data = _load_wallet(Path(wallet_path), wallet_name)
wallet_data = json.load(f)
liquidity = wallet_data.get('liquidity', []) liquidity = wallet_data.get('liquidity', [])
record = next((r for r in liquidity if r["stake_id"] == stake_id and r["status"] == "active"), None) record = next((r for r in liquidity if r["stake_id"] == stake_id and r["status"] == "active"), None)
@@ -1118,8 +1242,11 @@ def liquidity_unstake(ctx, stake_id: str):
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat()
}) })
with open(wallet_path, "w") as f: # Save wallet with encryption
json.dump(wallet_data, f, indent=2) password = None
if wallet_data.get('encrypted'):
password = _get_wallet_password(wallet_name)
_save_wallet(Path(wallet_path), wallet_data, password)
success(f"Withdrawn {total:.6f} AITBC (principal: {record['amount']}, rewards: {rewards:.6f})") success(f"Withdrawn {total:.6f} AITBC (principal: {record['amount']}, rewards: {rewards:.6f})")
output({ output({
@@ -1138,14 +1265,14 @@ def liquidity_unstake(ctx, stake_id: str):
@click.pass_context @click.pass_context
def rewards(ctx): def rewards(ctx):
"""View all earned rewards (staking + liquidity)""" """View all earned rewards (staking + liquidity)"""
wallet_name = ctx.obj['wallet_name']
wallet_path = ctx.obj.get('wallet_path') wallet_path = ctx.obj.get('wallet_path')
if not wallet_path or not Path(wallet_path).exists(): if not wallet_path or not Path(wallet_path).exists():
error("Wallet not found") error("Wallet not found")
ctx.exit(1) ctx.exit(1)
return return
with open(wallet_path) as f: wallet_data = _load_wallet(Path(wallet_path), wallet_name)
wallet_data = json.load(f)
staking = wallet_data.get('staking', []) staking = wallet_data.get('staking', [])
liquidity = wallet_data.get('liquidity', []) liquidity = wallet_data.get('liquidity', [])

View File

@@ -76,20 +76,40 @@ class AuditLogger:
return entries[-limit:] return entries[-limit:]
def encrypt_value(value: str, key: str = None) -> str: def _get_fernet_key(key: str = None) -> bytes:
"""Simple XOR-based obfuscation for config values (not cryptographic security)""" """Derive a Fernet key from a password or use default"""
from cryptography.fernet import Fernet
import base64 import base64
key = key or "aitbc_config_key_2026" import hashlib
encrypted = bytes([ord(c) ^ ord(key[i % len(key)]) for i, c in enumerate(value)])
if key is None:
# Use a default key (should be overridden in production)
key = "aitbc_config_key_2026_default"
# Derive a 32-byte key suitable for Fernet
return base64.urlsafe_b64encode(hashlib.sha256(key.encode()).digest())
def encrypt_value(value: str, key: str = None) -> str:
"""Encrypt a value using Fernet symmetric encryption"""
from cryptography.fernet import Fernet
import base64
fernet_key = _get_fernet_key(key)
f = Fernet(fernet_key)
encrypted = f.encrypt(value.encode())
return base64.b64encode(encrypted).decode() return base64.b64encode(encrypted).decode()
def decrypt_value(encrypted: str, key: str = None) -> str: def decrypt_value(encrypted: str, key: str = None) -> str:
"""Decrypt an XOR-obfuscated config value""" """Decrypt a Fernet-encrypted value"""
from cryptography.fernet import Fernet
import base64 import base64
key = key or "aitbc_config_key_2026"
fernet_key = _get_fernet_key(key)
f = Fernet(fernet_key)
data = base64.b64decode(encrypted) data = base64.b64decode(encrypted)
return ''.join(chr(b ^ ord(key[i % len(key)])) for i, b in enumerate(data)) return f.decrypt(data).decode()
def setup_logging(verbosity: int, debug: bool = False) -> str: def setup_logging(verbosity: int, debug: bool = False) -> str:

View File

@@ -1,10 +0,0 @@
{
"wallet_id": "my-wallet",
"type": "hd",
"address": "aitbc1e9056b875c03773b067fd0345248abd3db762af5",
"public_key": "0x2619e06fca452e9a524a688ceccbbb07ebdaaa824c3e78c4a406ed2a60cee47f",
"private_key": "0xe9056b875c03773b067fd0345248abd3db762af5274c8e30ab304cb04a7b4c79",
"created_at": "2026-02-12T16:41:34.206767Z",
"balance": 0,
"transactions": []
}