Add new Radix UI components and associated dependencies in package-lock.json

This commit is contained in:
Sebastian Unterschütz
2026-05-01 11:59:27 +02:00
parent 331d60581e
commit f5466f9062
21 changed files with 5010 additions and 481 deletions

View File

@@ -4,6 +4,7 @@ tmp_dir = "tmp"
[build]
bin = "./tmp/gateway"
cmd = "go build -o ./tmp/gateway ./cmd/gateway/main.go"
full_bin = "DEV_MODE=true NATS_URL=nats://localhost:4222 ./tmp/gateway"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "web"]
include_ext = ["go", "tpl", "tmpl", "html"]

View File

@@ -9,6 +9,9 @@ TIMESCALE_URL=postgres://admin:password@timescaledb:5432/telemetry_db?sslmode=di
GATEWAY_URL=ws://gateway:8080/ws?role=worker
GATEWAY_PUBLIC_URL=http://localhost:8080
# Dev Mode (in-memory store + auto-mock on first server added)
DEV_MODE=true
# Worker Configuration
MOCK_MODE=true
LOG_FILE_PATH=arma_server.rpt

View File

@@ -30,6 +30,13 @@
- Managed Trust Vault (Provider entschlüsselt temporär im RAM)
- Event-zu-Discord-Mapping (Grundstruktur)
### Cryptography & Security
-**Player Roster & Ban List (Zero Trust)**
- ✅ Blind Index Generation in Worker & Dashboard
- ✅ Encrypted storage of player/ban details
- ✅ Searchable via Blind Index (HMAC)
- ✅ Decryption on-the-fly in Dashboard
### Cryptography & Security
-**Crypto Package** (`internal/crypto/crypto.go`)
- AES-256-GCM Encryption/Decryption
@@ -64,6 +71,12 @@
- Telemetrie-Dashboard (FPS, Player Count, Latency)
- DSGVO 1-Click Export
-**Zero Trust Player/Ban Management** (`web/dashboard/src/components/Players.tsx`)
- Client-seitige Verschlüsselung von Bann-Gründen
- Client-seitige Blind-Index Generierung
- On-the-fly Entschlüsselung der Player-Liste
- Sicherer Kick/Ban Flow ohne Key-Exposition
-**WebAuthn Login** (`web/dashboard/src/components/Login.tsx`)
- Passwortloser Hardware-Login (FaceID, YubiKey, Windows Hello)
- Browser-Kompatibilitätsprüfung

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package main
import (
"bufio"
"encoding/base64"
"encoding/json"
"log"
"net/url"
@@ -15,6 +16,51 @@ import (
"github.com/gorilla/websocket"
)
type DevServer struct {
ID string `json:"id"`
Name string `json:"name"`
EncryptedRcon string `json:"encryptedRcon"`
}
type DecryptedRcon struct {
Address string `json:"address"`
Port int `json:"port"`
Pass string `json:"pass"`
}
func handleServerConfig(server DevServer, masterKey []byte) {
if server.EncryptedRcon == "" {
log.Printf("[RCON] Server %s has no RCON configuration", server.Name)
return
}
// 1. Decode Base64
encryptedBytes, err := base64.StdEncoding.DecodeString(server.EncryptedRcon)
if err != nil {
log.Printf("[RCON] Failed to decode RCON blob for %s: %v", server.Name, err)
return
}
// 2. Decrypt using local Master Key (E2EE)
decryptedJSON, err := crypto.Decrypt(encryptedBytes, masterKey)
if err != nil {
log.Printf("[RCON] Failed to decrypt RCON for %s: %v (Key mismatch?)", server.Name, err)
return
}
// 3. Parse RCON parameters
var rcon DecryptedRcon
if err := json.Unmarshal(decryptedJSON, &rcon); err != nil {
log.Printf("[RCON] Failed to parse decrypted RCON JSON for %s: %v", server.Name, err)
return
}
log.Printf("[RCON] Successfully decrypted credentials for %s", server.Name)
log.Printf("[RCON] Target established: %s:%d (Auth secret verified)", rcon.Address, rcon.Port)
// TODO: Establish persistent RCON connection
}
var mockLogs = []string{
"12:30:01.122 SCRIPT : [RJSSupport][Chat] [Global] Zauberklöte: hi, leute kurze frage. zock seit monaten wieder mal arma, was ist aus dem gtg#4 und #5 geworden, da ist ja nix los",
"09:37:50.865 DEFAULT : BattlEye Server: 'Player #0 Mike1Delta (92.209.175.19:6679) connected'",
@@ -81,6 +127,25 @@ func main() {
log.Println("Connected to gateway")
var mu sync.Mutex
// 0. Listen for Configuration Updates (E2EE)
go func() {
for {
_, message, err := c.ReadMessage()
if err != nil {
return
}
var msg struct {
Type string `json:"type"`
Server DevServer `json:"server"`
}
if err := json.Unmarshal(message, &msg); err == nil && msg.Type == "CONFIG_UPDATE" {
log.Printf("[CONFIG] Received update for server: %s", msg.Server.Name)
handleServerConfig(msg.Server, masterKey)
}
}
}()
// 1. Telemetry Loop
go func() {
for {
@@ -111,13 +176,14 @@ func main() {
line := mockLogs[i%len(mockLogs)]
event := parser.ParseLine(line)
if event != nil {
// Enrich event with blind index for player names
if event.Type == "JOIN" || event.Type == "LEAVE" || event.Type == "CHAT" {
playerName := extractPlayerName(event.Content)
if playerName != "" {
blindIndex := crypto.GenerateBlindIndex(playerName, masterKey)
event.Content = event.Content + " [BLIND:" + blindIndex + "]"
}
event.ServerID = "server-123"
event.ServerName = "AMS-NODE-01"
if event.PlayerName != "" {
event.PlayerNameHash = crypto.GenerateBlindIndex(event.PlayerName, masterKey)
} else {
event.PlayerName = "SYSTEM"
event.PlayerNameHash = "system-blind-index"
}
payload, _ := json.Marshal(event)

View File

@@ -37,6 +37,7 @@ services:
environment:
- NATS_URL=nats://nats:4222
- DB_URL=postgres://admin:password@postgres-master:5432/master_db?sslmode=disable
- DEV_MODE=true
ports:
- "8080:8080"
depends_on:

View File

@@ -7,10 +7,14 @@ import (
)
type LogEvent struct {
Timestamp time.Time
Type string // 'CHAT', 'JOIN', 'LEAVE', 'ADMIN', 'GENERIC'
Content string
Raw string
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"` // 'CHAT', 'JOIN', 'LEAVE', 'ADMIN', 'GENERIC'
Content string `json:"content"`
PlayerName string `json:"playerName,omitempty"`
PlayerNameHash string `json:"playerNameHash,omitempty"`
ServerID string `json:"serverId,omitempty"`
ServerName string `json:"serverName,omitempty"`
Raw string `json:"raw,omitempty"`
}
// Reforger-specific regex patterns
@@ -37,6 +41,7 @@ func ParseLine(line string) *LogEvent {
// Try Chat
if matches := chatRegex.FindStringSubmatch(line); matches != nil {
event.Type = "CHAT"
event.PlayerName = matches[2]
event.Content = matches[2] + ": " + matches[3]
return event
}
@@ -44,6 +49,7 @@ func ParseLine(line string) *LogEvent {
// Try Join
if matches := joinRegex.FindStringSubmatch(line); matches != nil {
event.Type = "JOIN"
event.PlayerName = matches[2]
event.Content = matches[2] + " connected to server"
return event
}
@@ -51,6 +57,7 @@ func ParseLine(line string) *LogEvent {
// Try Leave
if matches := leaveRegex.FindStringSubmatch(line); matches != nil {
event.Type = "LEAVE"
event.PlayerName = matches[2]
event.Content = matches[2] + " left the server"
return event
}

View File

@@ -1 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,21 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.14.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
"react-dom": "^19.2.5",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,7 @@ export const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
className="w-full px-4 py-3 bg-background border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
required
disabled={isLoading}
/>
@@ -122,11 +122,11 @@ export const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
{/* Info Section */}
<div className="mt-8 pt-6 border-t border-slate-800">
<div className="grid grid-cols-2 gap-4 text-center">
<div className="p-4 bg-slate-900/50 rounded-xl">
<div className="p-4 bg-background/50 rounded-xl">
<div className="text-2xl font-black text-indigo-400">0</div>
<div className="text-[10px] text-slate-500 font-bold uppercase tracking-wider mt-1">Passwords</div>
</div>
<div className="p-4 bg-slate-900/50 rounded-xl">
<div className="p-4 bg-background/50 rounded-xl">
<div className="text-2xl font-black text-green-400">100%</div>
<div className="text-[10px] text-slate-500 font-bold uppercase tracking-wider mt-1">E2EE</div>
</div>

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import { Fingerprint, Shield, Zap, AlertCircle, Loader2, Key } from 'lucide-react';
import { Fingerprint, Shield, Zap, AlertCircle, Loader2, Key, CheckCircle, ArrowRight } from 'lucide-react';
import { isWebAuthnSupported } from '../lib/webauthn';
interface LoginProps {
onLoginSuccess: (token: string, masterKey: Uint8Array, communityId: string, username: string) => void;
onLoginSuccess: (token: string, masterKey: Uint8Array, communityId: string, username: string, userId: string, role?: string) => void;
onSwitchToRegister: () => void;
}
@@ -15,6 +15,18 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
const [error, setError] = useState<string | null>(null);
const [supported] = useState(isWebAuthnSupported());
// Stored after a successful password login while we show the passkey upsell
const [pendingLogin, setPendingLogin] = useState<{
token: string;
masterKeyBytes: Uint8Array;
communityId: string;
username: string;
userId: string;
role?: string;
} | null>(null);
const [passkeySetupLoading, setPasskeySetupLoading] = useState(false);
const [passkeySetupError, setPasskeySetupError] = useState<string | null>(null);
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
@@ -35,10 +47,14 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
const data = await res.json();
const masterKeyBytes = new TextEncoder().encode(data.masterKey);
// Store token in localStorage for session persistence
localStorage.setItem('auth_token', data.token);
onLoginSuccess(data.token, masterKeyBytes, data.communityId, data.username);
if (supported) {
// Show passkey upsell before entering the dashboard
setPendingLogin({ token: data.token, masterKeyBytes, communityId: data.communityId, username: data.username, userId: data.userId, role: data.role });
} else {
onLoginSuccess(data.token, masterKeyBytes, data.communityId, data.username, data.userId, data.role);
}
} catch (err) {
setError((err as Error).message);
} finally {
@@ -69,7 +85,7 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
challenge: base64urlToBuffer(options.challenge),
timeout: options.timeout,
rpId: options.rpId,
allowCredentials: options.allowCredentials.map((id: string) => ({
allowCredentials: (options.allowCredentials ?? []).map((id: string) => ({
type: 'public-key' as const,
id: base64urlToBuffer(id),
})),
@@ -105,7 +121,7 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
localStorage.setItem('auth_token', data.token);
onLoginSuccess(data.token, masterKeyBytes, data.communityId, data.username);
onLoginSuccess(data.token, masterKeyBytes, data.communityId, data.username, data.userId ?? 'user-demo', data.role);
} catch (err) {
setError((err as Error).message);
} finally {
@@ -133,18 +149,167 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
const handleSetupPasskey = async () => {
if (!pendingLogin) return;
setPasskeySetupLoading(true);
setPasskeySetupError(null);
try {
const beginRes = await fetch('/api/auth/passkeys/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${pendingLogin.token}` },
body: JSON.stringify({}),
});
if (!beginRes.ok) throw new Error('Could not start passkey setup');
const options = await beginRes.json();
const credential = await navigator.credentials.create({
publicKey: {
challenge: base64urlToBuffer(options.challenge),
rp: options.rp,
user: {
id: new TextEncoder().encode(options.user.id),
name: options.user.name,
displayName: options.user.displayName,
},
pubKeyCredParams: options.pubKeyCredParams as PublicKeyCredentialParameters[],
timeout: options.timeout,
attestation: options.attestation as AttestationConveyancePreference,
authenticatorSelection: options.authenticatorSelection as AuthenticatorSelectionCriteria,
},
}) as PublicKeyCredential | null;
if (!credential) throw new Error('Passkey setup was cancelled');
const response = credential.response as AuthenticatorAttestationResponse;
const finishRes = await fetch('/api/auth/passkeys/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${pendingLogin.token}` },
body: JSON.stringify({
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
type: credential.type,
response: {
attestationObject: bufferToBase64url(response.attestationObject),
clientDataJSON: bufferToBase64url(response.clientDataJSON),
},
}),
});
if (!finishRes.ok) throw new Error('Failed to save passkey');
onLoginSuccess(pendingLogin.token, pendingLogin.masterKeyBytes, pendingLogin.communityId, pendingLogin.username, pendingLogin.userId, pendingLogin.role);
} catch (err) {
setPasskeySetupError((err as Error).message);
setPasskeySetupLoading(false);
}
};
const handleSkipPasskey = () => {
if (!pendingLogin) return;
onLoginSuccess(pendingLogin.token, pendingLogin.masterKeyBytes, pendingLogin.communityId, pendingLogin.username, pendingLogin.userId, pendingLogin.role);
};
if (pendingLogin) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-6 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 -left-48 w-96 h-96 bg-primary/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 -right-48 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
</div>
<div className="max-w-md w-full relative z-10">
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary rounded-2xl mb-4 shadow-2xl shadow-primary/50">
<Zap className="w-8 h-8 text-primary-foreground fill-primary-foreground" />
</div>
<h1 className="text-3xl font-black text-foreground tracking-tight mb-2 uppercase italic">ArmaCloud</h1>
</div>
<div className="bg-card/50 backdrop-blur-xl border border-border/50 rounded-3xl p-8 shadow-2xl space-y-6">
{/* Signed-in confirmation */}
<div className="flex items-center gap-4 p-4 bg-emerald-500/5 border border-emerald-500/20 rounded-2xl">
<div className="w-10 h-10 bg-emerald-500/10 border border-emerald-500/20 rounded-xl flex items-center justify-center flex-shrink-0">
<CheckCircle className="w-5 h-5 text-emerald-500" />
</div>
<div>
<p className="font-bold text-foreground text-sm">Signed in as <span className="text-primary">{pendingLogin.username}</span></p>
<p className="text-xs text-muted-foreground">Password login successful</p>
</div>
</div>
{/* Passkey pitch */}
<div className="space-y-3">
<div className="flex items-start gap-4">
<div className="p-3 bg-primary/10 border border-primary/20 rounded-xl flex-shrink-0 mt-0.5">
<Fingerprint className="w-6 h-6 text-primary" />
</div>
<div>
<h2 className="font-black text-xl text-foreground mb-1">Use a passkey next time?</h2>
<p className="text-sm text-muted-foreground leading-relaxed">
Passkeys use your device's fingerprint, face ID, or PIN — no password to remember.
Setting one up takes just a few seconds and replaces your password entirely.
</p>
</div>
</div>
<ul className="space-y-2 pl-1">
{['Faster sign-in, no password needed', 'Phishing-resistant by design', 'Stored securely on your device'].map(item => (
<li key={item} className="flex items-center gap-2 text-sm text-muted-foreground">
<ArrowRight className="w-3.5 h-3.5 text-primary flex-shrink-0" />
{item}
</li>
))}
</ul>
</div>
{passkeySetupError && (
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-xl flex items-center gap-2 text-xs text-destructive">
<AlertCircle className="w-4 h-4 flex-shrink-0" />
{passkeySetupError}
</div>
)}
<div className="space-y-3">
<button
onClick={handleSetupPasskey}
disabled={passkeySetupLoading}
className="w-full bg-primary hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground text-primary-foreground font-black uppercase tracking-widest py-4 rounded-xl transition-all duration-200 shadow-lg shadow-primary/20 active:scale-95 flex items-center justify-center gap-2"
>
{passkeySetupLoading ? (
<><Loader2 className="w-5 h-5 animate-spin" /> Setting up...</>
) : (
<><Fingerprint className="w-5 h-5" /> Set up Passkey</>
)}
</button>
<button
onClick={handleSkipPasskey}
disabled={passkeySetupLoading}
className="w-full py-3 text-sm text-muted-foreground hover:text-foreground transition-colors font-medium"
>
Skip for now — continue with password
</button>
</div>
</div>
<p className="text-center text-[10px] font-bold text-muted-foreground/30 mt-8 uppercase tracking-[0.2em]">
ArmaCloud &copy; 2026
</p>
</div>
</div>
);
}
if (!supported && authMethod === 'passkey') {
return (
<div className="min-h-screen bg-[#0a0b10] flex items-center justify-center p-6">
<div className="max-w-md w-full bg-red-500/5 border border-red-500/20 rounded-2xl p-8 text-center">
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-bold text-red-200 mb-2">Passkeys Not Supported</h2>
<p className="text-red-400/80 text-sm mb-4">
<div className="min-h-screen bg-background flex items-center justify-center p-6">
<div className="max-w-md w-full bg-destructive/10 border border-destructive/20 rounded-2xl p-8 text-center">
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
<h2 className="text-xl font-bold text-destructive mb-2">Passkeys Not Supported</h2>
<p className="text-muted-foreground text-sm mb-4">
Your browser does not support WebAuthn. Please use a modern browser or switch to password login.
</p>
<button
onClick={() => setAuthMethod('password')}
className="bg-indigo-600 hover:bg-indigo-500 text-white px-6 py-2 rounded-xl font-bold text-sm transition-colors"
className="bg-primary hover:bg-primary/90 text-primary-foreground px-6 py-2 rounded-xl font-bold text-sm transition-colors"
>
Use Password Instead
</button>
@@ -154,21 +319,21 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
}
return (
<div className="min-h-screen bg-[#0a0b10] flex items-center justify-center p-6 relative overflow-hidden">
<div className="min-h-screen bg-background flex items-center justify-center p-6 relative overflow-hidden">
{/* Background Effects */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 -left-48 w-96 h-96 bg-indigo-600/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
<div className="absolute top-1/4 -left-48 w-96 h-96 bg-primary/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 -right-48 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
</div>
<div className="max-w-md w-full relative z-10">
{/* Logo & Branding */}
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-16 h-16 bg-indigo-600 rounded-2xl mb-4 shadow-2xl shadow-indigo-600/50">
<Zap className="w-8 h-8 text-white fill-white" />
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary rounded-2xl mb-4 shadow-2xl shadow-primary/50">
<Zap className="w-8 h-8 text-primary-foreground fill-primary-foreground" />
</div>
<h1 className="text-3xl font-black text-white tracking-tight mb-2">Welcome Back</h1>
<p className="text-slate-500 text-sm font-medium">Sign in to your secure vault</p>
<h1 className="text-3xl font-black text-foreground tracking-tight mb-2 uppercase italic italic">ArmaCloud</h1>
<p className="text-muted-foreground text-sm font-medium tracking-widest uppercase">Sign in to your account</p>
</div>
{/* Auth Method Selector */}
@@ -177,8 +342,8 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
onClick={() => setAuthMethod('password')}
className={`flex-1 px-4 py-3 rounded-xl font-bold text-sm transition-all ${
authMethod === 'password'
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/20'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
<Key className="w-4 h-4 inline mr-2" />
@@ -188,8 +353,8 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
onClick={() => setAuthMethod('passkey')}
className={`flex-1 px-4 py-3 rounded-xl font-bold text-sm transition-all ${
authMethod === 'passkey'
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/20'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
<Fingerprint className="w-4 h-4 inline mr-2" />
@@ -198,40 +363,40 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
</div>
{/* Login Card */}
<div className="bg-[#0c0d14] border border-slate-800 rounded-3xl p-8 shadow-2xl">
<div className="bg-card/50 backdrop-blur-xl border border-border/50 rounded-3xl p-8 shadow-2xl">
<div className="flex items-center space-x-3 mb-6">
<div className="p-2 bg-indigo-500/10 border border-indigo-500/20 rounded-lg">
<Shield className="w-5 h-5 text-indigo-400" />
<div className="p-2 bg-primary/10 border border-primary/20 rounded-lg">
<Shield className="w-5 h-5 text-primary" />
</div>
<div>
<h2 className="text-lg font-bold text-white">{authMethod === 'password' ? 'Password Login' : 'Passkey Login'}</h2>
<p className="text-xs text-slate-500">Zero-Trust Authentication</p>
<h2 className="text-lg font-bold text-foreground">{authMethod === 'password' ? 'Sign in' : 'Sign in with Passkey'}</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Welcome back</p>
</div>
</div>
<form onSubmit={authMethod === 'password' ? handlePasswordLogin : handlePasskeyLogin} className="space-y-6">
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Username</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="Operator ID"
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl text-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
required
disabled={isLoading}
/>
</div>
{authMethod === 'password' && (
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Password</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="••••••••••••"
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl text-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
required
disabled={isLoading}
/>
@@ -239,11 +404,11 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
)}
{error && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-xl flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-bold text-red-200">Authentication Failed</h3>
<p className="text-xs text-red-400/80 mt-1">{error}</p>
<h3 className="text-sm font-bold text-destructive">Login failed</h3>
<p className="text-xs text-destructive/80 mt-1">{error}</p>
</div>
</div>
)}
@@ -251,48 +416,41 @@ export const LoginV2: React.FC<LoginProps> = ({ onLoginSuccess, onSwitchToRegist
<button
type="submit"
disabled={isLoading || !username.trim() || (authMethod === 'password' && !password.trim())}
className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:bg-slate-800 disabled:text-slate-600 text-white font-bold py-4 rounded-xl transition-all duration-200 shadow-lg shadow-indigo-600/20 active:scale-95 flex items-center justify-center space-x-2"
className="w-full bg-primary hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground text-primary-foreground font-black uppercase tracking-widest py-4 rounded-xl transition-all duration-200 shadow-lg shadow-primary/20 active:scale-95 flex items-center justify-center space-x-2"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>Authenticating...</span>
<span>Verifying...</span>
</>
) : (
<>
{authMethod === 'password' ? <Shield className="w-5 h-5" /> : <Fingerprint className="w-5 h-5" />}
<span>Sign in with {authMethod === 'password' ? 'Password' : 'Passkey'}</span>
<span>Sign In</span>
</>
)}
</button>
</form>
{/* Info Section */}
<div className="mt-8 pt-6 border-t border-slate-800">
<div className="grid grid-cols-2 gap-4 text-center">
<div className="p-4 bg-slate-900/50 rounded-xl">
<div className="text-2xl font-black text-indigo-400">E2EE</div>
<div className="text-[10px] text-slate-500 font-bold uppercase tracking-wider mt-1">Zero-Knowledge</div>
</div>
<div className="p-4 bg-slate-900/50 rounded-xl">
<div className="text-2xl font-black text-green-400">DSGVO</div>
<div className="text-[10px] text-slate-500 font-bold uppercase tracking-wider mt-1">Compliant</div>
</div>
</div>
<div className="mt-8 pt-6 border-t border-border/50">
<p className="text-[11px] text-muted-foreground/50 text-center leading-relaxed">
Your data is end-to-end encrypted. Encryption keys never leave your device.
</p>
</div>
</div>
{/* Switch to Register */}
<p className="text-center text-sm text-slate-500 mt-6">
<p className="text-center text-sm text-muted-foreground mt-6">
Don't have an account?{' '}
<button onClick={onSwitchToRegister} className="text-indigo-400 hover:text-indigo-300 font-bold transition-colors">
Create one now
<button onClick={onSwitchToRegister} className="text-primary hover:text-primary/80 font-black transition-colors uppercase tracking-widest">
Create account
</button>
</p>
{/* Footer */}
<p className="text-center text-xs text-slate-600 mt-4">
Protected by {authMethod === 'passkey' ? 'FIDO2 WebAuthn' : 'Argon2id'} security
<p className="text-center text-[10px] font-bold text-muted-foreground/30 mt-8 uppercase tracking-[0.2em]">
ArmaCloud &copy; 2026
</p>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { Shield, Zap, AlertCircle, Loader2, CheckCircle, Fingerprint, Key } from 'lucide-react';
interface RegisterProps {
onRegisterSuccess: (token: string, masterKey: Uint8Array, communityId: string, username: string) => void;
onRegisterSuccess: (token: string, masterKey: Uint8Array, communityId: string, username: string, userId: string, role?: string) => void;
onSwitchToLogin: () => void;
}
@@ -70,7 +70,8 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
const data = await res.json();
const masterKeyBytes = new TextEncoder().encode(data.masterKey);
onRegisterSuccess(data.token, masterKeyBytes, data.communityId, data.username);
localStorage.setItem('auth_token', data.token);
onRegisterSuccess(data.token, masterKeyBytes, data.communityId, data.username, data.userId, data.role);
} catch (err) {
setError((err as Error).message);
} finally {
@@ -140,7 +141,8 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
const data = await finishRes.json();
const masterKeyBytes = new TextEncoder().encode(data.masterKey || 'this-is-a-32-byte-master-key-xyz');
onRegisterSuccess(data.token, masterKeyBytes, data.communityId, username);
localStorage.setItem('auth_token', data.token);
onRegisterSuccess(data.token, masterKeyBytes, data.communityId, username, data.userId ?? 'user-passkey', data.role);
} catch (err) {
setError((err as Error).message);
} finally {
@@ -169,21 +171,21 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
};
return (
<div className="min-h-screen bg-[#0a0b10] flex items-center justify-center p-6 relative overflow-hidden">
<div className="min-h-screen bg-background flex items-center justify-center p-6 relative overflow-hidden">
{/* Background Effects */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 -left-48 w-96 h-96 bg-indigo-600/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
<div className="absolute top-1/4 -left-48 w-96 h-96 bg-primary/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 -right-48 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }}></div>
</div>
<div className="max-w-md w-full relative z-10">
{/* Logo */}
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-16 h-16 bg-indigo-600 rounded-2xl mb-4 shadow-2xl shadow-indigo-600/50">
<Zap className="w-8 h-8 text-white fill-white" />
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary rounded-2xl mb-4 shadow-2xl shadow-primary/50">
<Zap className="w-8 h-8 text-primary-foreground fill-primary-foreground" />
</div>
<h1 className="text-3xl font-black text-white tracking-tight mb-2">Create Account</h1>
<p className="text-slate-500 text-sm font-medium">Zero-Knowledge Gaming Infrastructure</p>
<h1 className="text-3xl font-black text-foreground tracking-tight mb-2 uppercase italic">ArmaCloud</h1>
<p className="text-muted-foreground text-sm font-medium uppercase tracking-widest">Create your account</p>
</div>
{/* Auth Method Selector */}
@@ -192,8 +194,8 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
onClick={() => setAuthMethod('password')}
className={`flex-1 px-4 py-3 rounded-xl font-bold text-sm transition-all ${
authMethod === 'password'
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/20'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
<Key className="w-4 h-4 inline mr-2" />
@@ -203,8 +205,8 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
onClick={() => setAuthMethod('passkey')}
className={`flex-1 px-4 py-3 rounded-xl font-bold text-sm transition-all ${
authMethod === 'passkey'
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/20'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
<Fingerprint className="w-4 h-4 inline mr-2" />
@@ -213,30 +215,30 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
</div>
{/* Register Card */}
<div className="bg-[#0c0d14] border border-slate-800 rounded-3xl p-8 shadow-2xl">
<div className="bg-card/50 backdrop-blur-xl border border-border/50 rounded-3xl p-8 shadow-2xl">
<form onSubmit={authMethod === 'password' ? handlePasswordRegister : handlePasskeyRegister} className="space-y-5">
{/* Common Fields */}
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Username</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="your_username"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="Operator ID"
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl text-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
required
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Email (Optional)</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Email (optional)</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="operator@backbone.net"
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl text-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
disabled={isLoading}
/>
</div>
@@ -244,8 +246,8 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
{/* Password-specific fields */}
{authMethod === 'password' && (
<>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Password</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Password</label>
<input
type="password"
value={password}
@@ -253,36 +255,36 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
setPassword(e.target.value);
checkPasswordStrength(e.target.value);
}}
placeholder="Min. 12 characters"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="••••••••••••"
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl text-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
required
disabled={isLoading}
/>
{passwordStrength && password.length > 0 && (
<div className="mt-2 flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-800 rounded-full overflow-hidden">
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div
className={`h-full transition-all ${
passwordStrength.score <= 1 ? 'bg-red-500' :
passwordStrength.score === 2 ? 'bg-yellow-500' :
passwordStrength.score === 3 ? 'bg-blue-500' : 'bg-green-500'
passwordStrength.score <= 1 ? 'bg-destructive' :
passwordStrength.score === 2 ? 'bg-amber-500' :
passwordStrength.score === 3 ? 'bg-blue-500' : 'bg-emerald-500'
}`}
style={{ width: `${(passwordStrength.score / 5) * 100}%` }}
></div>
</div>
<span className="text-xs text-slate-400">{passwordStrength.message}</span>
<span className="text-[9px] font-black uppercase text-muted-foreground/60">{passwordStrength.message}</span>
</div>
)}
</div>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Confirm Password</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Confirm Password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Repeat password"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="••••••••••••"
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl text-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
required
disabled={isLoading}
/>
@@ -290,24 +292,24 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
</>
)}
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">Community Name (Optional)</label>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted-foreground/60">Community Name (optional)</label>
<input
type="text"
value={communityName}
onChange={(e) => setCommunityName(e.target.value)}
placeholder="e.g., Elite Tactical Gaming"
className="w-full px-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
placeholder="e.g., Tactical Command Alpha"
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl text-foreground placeholder:text-muted-foreground/30 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
disabled={isLoading}
/>
</div>
{error && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-xl flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-bold text-red-200">Registration Failed</h3>
<p className="text-xs text-red-400/80 mt-1">{error}</p>
<h3 className="text-sm font-bold text-destructive">Registration failed</h3>
<p className="text-xs text-destructive/80 mt-1">{error}</p>
</div>
</div>
)}
@@ -315,35 +317,35 @@ export const Register: React.FC<RegisterProps> = ({ onRegisterSuccess, onSwitchT
<button
type="submit"
disabled={isLoading || !username.trim() || (authMethod === 'password' && (!password || !confirmPassword))}
className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:bg-slate-800 disabled:text-slate-600 text-white font-bold py-4 rounded-xl transition-all duration-200 shadow-lg shadow-indigo-600/20 active:scale-95 flex items-center justify-center space-x-2"
className="w-full bg-primary hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground text-primary-foreground font-black uppercase tracking-widest py-4 rounded-xl transition-all duration-200 shadow-lg shadow-primary/20 active:scale-95 flex items-center justify-center space-x-2"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>Creating Account...</span>
<span>Processing...</span>
</>
) : (
<>
{authMethod === 'password' ? <Shield className="w-5 h-5" /> : <Fingerprint className="w-5 h-5" />}
<span>Create Account with {authMethod === 'password' ? 'Password' : 'Passkey'}</span>
<span>Create Account</span>
</>
)}
</button>
</form>
{/* Security Info */}
<div className="mt-6 pt-6 border-t border-slate-800">
<div className="flex items-start space-x-3 text-xs text-slate-500">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<p>Your master encryption key is generated client-side and never leaves your device unencrypted.</p>
<div className="mt-6 pt-6 border-t border-border/50">
<div className="flex items-start space-x-3 text-[10px] font-medium text-muted-foreground/60">
<CheckCircle className="w-4 h-4 text-emerald-500 flex-shrink-0 mt-0.5" />
<p>Your data is end-to-end encrypted. Encryption keys never leave your device.</p>
</div>
</div>
</div>
{/* Switch to Login */}
<p className="text-center text-sm text-slate-500 mt-6">
<p className="text-center text-sm text-muted-foreground mt-6">
Already have an account?{' '}
<button onClick={onSwitchToLogin} className="text-indigo-400 hover:text-indigo-300 font-bold transition-colors">
<button onClick={onSwitchToLogin} className="text-primary hover:text-primary/80 font-black transition-colors uppercase tracking-widest">
Sign in
</button>
</p>

View File

@@ -1,72 +1,135 @@
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import React, { createContext, useContext, useState, useRef, useEffect } from 'react';
interface VaultContextType {
isLocked: boolean;
isAuthenticated: boolean;
communityId: string | null;
unlock: (key: Uint8Array, communityId: string) => void;
username: string | null;
userId: string | null;
role: string | null;
unlock: (key: Uint8Array, communityId: string, username: string, userId: string, role?: string) => void;
lock: () => void;
logout: () => void;
decrypt: (data: Uint8Array) => Promise<string>;
encrypt: (text: string) => Promise<Uint8Array>;
hash: (text: string) => Promise<string>;
}
const VaultContext = createContext<VaultContextType | undefined>(undefined);
const makeWorker = () =>
new Worker(new URL('../workers/crypto.worker.ts', import.meta.url), { type: 'module' });
export const VaultProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isLocked, setIsLocked] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [communityId, setCommunityId] = useState<string | null>(null);
const [username, setUsername] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [role, setRole] = useState<string | null>(null);
const workerRef = useRef<Worker | null>(null);
useEffect(() => {
// Initialize the worker
workerRef.current = new Worker(new URL('../workers/crypto.worker.ts', import.meta.url), {
type: 'module'
});
workerRef.current = makeWorker();
return () => {
workerRef.current?.terminate();
};
}, []);
const unlock = (key: Uint8Array, community: string) => {
const unlock = (key: Uint8Array, community: string, user: string, uid: string, userRole = 'member') => {
workerRef.current?.postMessage({ type: 'SET_KEY', payload: key });
setIsLocked(false);
setIsAuthenticated(true);
setCommunityId(community);
setUsername(user);
setUserId(uid);
setRole(userRole);
};
const lock = () => {
// We don't just set the state, we terminate and recreate the worker to clear the memory
workerRef.current?.terminate();
workerRef.current = new Worker(new URL('../workers/crypto.worker.ts', import.meta.url), {
type: 'module'
});
workerRef.current = makeWorker();
setIsLocked(true);
setIsAuthenticated(false);
setCommunityId(null);
setUsername(null);
setUserId(null);
setRole(null);
};
const logout = () => {
localStorage.removeItem('auth_token');
lock();
};
const decrypt = (data: Uint8Array): Promise<string> => {
return new Promise((resolve, reject) => {
if (isLocked) return reject('Vault is locked');
if (isLocked || !workerRef.current) return reject('Vault is locked');
const requestId = Math.random().toString(36).slice(2);
const handleMessage = (e: MessageEvent) => {
if (e.data.type === 'DECRYPTED') {
if (e.data.id === requestId) {
workerRef.current?.removeEventListener('message', handleMessage);
if (e.data.type === 'DECRYPTED') {
resolve(e.data.payload);
} else if (e.data.type === 'ERROR') {
workerRef.current?.removeEventListener('message', handleMessage);
reject(e.data.message);
}
}
};
workerRef.current?.addEventListener('message', handleMessage);
workerRef.current?.postMessage({ type: 'DECRYPTED', payload: data });
workerRef.current.addEventListener('message', handleMessage);
workerRef.current.postMessage({ type: 'DECRYPT', payload: data, id: requestId });
});
};
const encrypt = (text: string): Promise<Uint8Array> => {
return new Promise((resolve, reject) => {
if (isLocked || !workerRef.current) return reject('Vault is locked');
const requestId = Math.random().toString(36).slice(2);
const handleMessage = (e: MessageEvent) => {
if (e.data.id === requestId) {
workerRef.current?.removeEventListener('message', handleMessage);
if (e.data.type === 'ENCRYPTED') {
resolve(e.data.payload);
} else if (e.data.type === 'ERROR') {
reject(e.data.message);
}
}
};
workerRef.current.addEventListener('message', handleMessage);
workerRef.current.postMessage({ type: 'ENCRYPT', payload: text, id: requestId });
});
};
const hash = (text: string): Promise<string> => {
return new Promise((resolve, reject) => {
if (isLocked || !workerRef.current) return reject('Vault is locked');
const requestId = Math.random().toString(36).slice(2);
const handleMessage = (e: MessageEvent) => {
if (e.data.id === requestId) {
workerRef.current?.removeEventListener('message', handleMessage);
if (e.data.type === 'HASHED') {
resolve(e.data.payload);
} else if (e.data.type === 'ERROR') {
reject(e.data.message);
}
}
};
workerRef.current.addEventListener('message', handleMessage);
workerRef.current.postMessage({ type: 'BLIND_INDEX', payload: text, id: requestId });
});
};
return (
<VaultContext.Provider value={{ isLocked, isAuthenticated, communityId, unlock, lock, decrypt }}>
<VaultContext.Provider value={{ isLocked, isAuthenticated, communityId, username, userId, role, unlock, lock, logout, decrypt, encrypt, hash }}>
{children}
</VaultContext.Provider>
);

View File

@@ -36,29 +36,6 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
@@ -78,6 +55,7 @@
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--radius: 0.5rem;
}
}

View File

@@ -6,7 +6,7 @@
export async function importKey(keyData: Uint8Array): Promise<CryptoKey> {
return await self.crypto.subtle.importKey(
"raw",
keyData,
keyData.buffer as ArrayBuffer,
{ name: "AES-GCM" },
false,
["decrypt", "encrypt"]
@@ -33,6 +33,55 @@ export async function decryptLog(
return new TextDecoder().decode(decrypted);
}
export async function encryptData(
plaintext: string,
key: CryptoKey
): Promise<Uint8Array> {
const enc = new TextEncoder();
const data = enc.encode(plaintext);
const nonce = self.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await self.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce,
},
key,
data
);
const result = new Uint8Array(nonce.length + encrypted.byteLength);
result.set(nonce);
result.set(new Uint8Array(encrypted), nonce.length);
return result;
}
export async function generateBlindIndex(
value: string,
key: Uint8Array
): Promise<string> {
const enc = new TextEncoder();
const data = enc.encode(value);
const hmacKey = await self.crypto.subtle.importKey(
"raw",
key,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await self.crypto.subtle.sign(
"HMAC",
hmacKey,
data
);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* Derives a 256-bit key from a password and salt using PBKDF2.
* (Used if WebAuthn is not providing a raw key directly)

View File

@@ -90,7 +90,7 @@ export async function registerWebAuthn(
name: options.user.name,
displayName: options.user.displayName,
},
pubKeyCredParams: options.pubKeyCredParams,
pubKeyCredParams: options.pubKeyCredParams as PublicKeyCredentialParameters[],
timeout: options.timeout,
attestation: options.attestation as AttestationConveyancePreference,
authenticatorSelection: options.authenticatorSelection as AuthenticatorSelectionCriteria,

View File

@@ -1,28 +1,56 @@
// crypto.worker.ts
import { importKey, decryptLog } from '../lib/crypto';
import { importKey, decryptLog, encryptData, generateBlindIndex } from '../lib/crypto';
let cryptoKey: CryptoKey | null = null;
let rawKey: Uint8Array | null = null;
self.onmessage = async (event) => {
const { type, payload } = event.data;
const { type, payload, id } = event.data;
switch (type) {
case 'SET_KEY':
// payload is the raw Uint8Array key
rawKey = payload;
cryptoKey = await importKey(payload);
self.postMessage({ type: 'KEY_READY' });
self.postMessage({ type: 'KEY_READY', id });
break;
case 'DECRYPT':
if (!cryptoKey) {
self.postMessage({ type: 'ERROR', message: 'No key set' });
self.postMessage({ type: 'ERROR', message: 'No key set', id });
return;
}
try {
const decrypted = await decryptLog(payload, cryptoKey);
self.postMessage({ type: 'DECRYPTED', payload: decrypted });
self.postMessage({ type: 'DECRYPTED', payload: decrypted, id });
} catch (err) {
self.postMessage({ type: 'ERROR', message: 'Decryption failed' });
self.postMessage({ type: 'ERROR', message: 'Decryption failed', id });
}
break;
case 'ENCRYPT':
if (!cryptoKey) {
self.postMessage({ type: 'ERROR', message: 'No key set', id });
return;
}
try {
const encrypted = await encryptData(payload, cryptoKey);
self.postMessage({ type: 'ENCRYPTED', payload: encrypted, id });
} catch (err) {
self.postMessage({ type: 'ERROR', message: 'Encryption failed', id });
}
break;
case 'BLIND_INDEX':
if (!rawKey) {
self.postMessage({ type: 'ERROR', message: 'No key set', id });
return;
}
try {
const hash = await generateBlindIndex(payload, rawKey);
self.postMessage({ type: 'HASHED', payload: hash, id });
} catch (err) {
self.postMessage({ type: 'ERROR', message: 'Hashing failed', id });
}
break;
}