# Security Specification

## Threat Model

### Assets
1. **Document Content**: Markdown files and attachments
2. **User Credentials**: Passkeys, passwords, session tokens
3. **Collaboration Data**: Comments, notifications, history
4. **System State**: Database, configuration, logs

### Threat Actors
1. **External Attacker**: No system access, attempts remote exploitation
2. **Malicious User**: Authenticated, attempts unauthorized access
3. **Compromised Client**: Legitimate user's device infected
4. **Insider**: Administrator or developer with system access

### Threats
1. **Supply Chain Attack**: Malicious dependency injection
2. **Authentication Bypass**: Session hijacking, credential theft
3. **Authorization Bypass**: Horizontal/vertical privilege escalation
4. **Data Exfiltration**: Unauthorized document access
5. **Data Tampering**: Modification without authorization
6. **Denial of Service**: Resource exhaustion
7. **Information Disclosure**: Secrets in logs/responses

## Security Controls

### 1. Supply Chain Security

**Policy**:
- Only dependencies with >1M downloads/month or official Go team
- All versions pinned in go.sum
- No transitive dependencies without review
- Prefer standard library when possible

**Implementation**:
```
# go.mod
require (
    github.com/go-chi/chi/v5 v5.0.12
    github.com/tursodatabase/libsql-client-go v0.0.0-20240416075031-555ce55511f7
    // All versions pinned
)

# Build
RUN go mod verify  # Verify checksums
RUN go mod vendor  # Optional: vendor for air-gapped
```

**Audit**:
- Weekly `govulncheck` scan
- Dependency review in CI
- No build-time network access (vendored or proxy with checksum)

### 2. Authentication Security

#### Passkey (WebAuthn)
```go
// Registration
func registerPasskey(user User) (*Credential, error) {
    options, session, err := webauthn.BeginRegistration(user)
    // Store session data server-side (never client-side)
    // Return options to client
}

func verifyRegistration(sessionID string, response ProtocolCredentialCreation) (*Credential, error) {
    session := getSessionData(sessionID)
    credential, err := webauthn.FinishRegistration(user, session, response)
    // Verify attestation
    // Store credential: ID, public key, sign count
}
```

#### Password (Argon2id)
```go
import "golang.org/x/crypto/argon2"

const (
    timeCost    = 3
    memoryCost  = 64 * 1024  // 64MB
    parallelism = 4
    saltLength  = 16
    keyLength   = 32
)

func hashPassword(password string) (string, error) {
    salt := make([]byte, saltLength)
    if _, err := rand.Read(salt); err != nil {
        return "", err
    }
    
    hash := argon2.IDKey([]byte(password), salt, timeCost, memoryCost, parallelism, keyLength)
    
    // Encode to string: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
    return encodeHash(salt, hash), nil
}

func verifyPassword(password, encodedHash string) (bool, error) {
    salt, hash, params, err := decodeHash(encodedHash)
    if err != nil {
        return false, err
    }
    
    computedHash := argon2.IDKey([]byte(password), salt, params.time, params.memory, params.parallelism, uint32(len(hash)))
    
    // Constant-time comparison
    return subtle.ConstantTimeCompare(hash, computedHash) == 1, nil
}
```

#### Session Management
```go
func createSession(userID string, r *http.Request) (*Session, error) {
    token := make([]byte, 32)
    if _, err := rand.Read(token); err != nil {
        return nil, err
    }
    
    session := &Session{
        ID:        uuid.New().String(),
        UserID:    userID,
        TokenHash: sha256.Sum256(token),
        CreatedAt: time.Now(),
        ExpiresAt: time.Now().Add(24 * time.Hour),
        IP:        r.RemoteAddr,
        UserAgent: r.UserAgent(),
    }
    
    // Store in database
    if err := db.CreateSession(session); err != nil {
        return nil, err
    }
    
    // Set cookie
    http.SetCookie(w, &http.Cookie{
        Name:     "session",
        Value:    base64.URLEncoding.EncodeToString(token),
        Expires:  session.ExpiresAt,
        HttpOnly: true,
        Secure:   true,        // HTTPS only
        SameSite: http.SameSiteStrictMode,
        Path:     "/",
    })
    
    return session, nil
}
```

### 3. Authorization

#### Permission Model
```go
type Permission string

const (
    PermissionRead  Permission = "read"
    PermissionWrite Permission = "write"
    PermissionAdmin Permission = "admin"
)

type ResourceType string

const (
    ResourceGlobal     ResourceType = "global"
    ResourceCollection ResourceType = "collection"
    ResourceDocument   ResourceType = "document"
)

func checkPermission(userID string, resourceType ResourceType, resourceID string, required Permission) (bool, error) {
    // Check explicit permission
    perm, err := db.GetPermission(userID, resourceType, resourceID)
    if err == nil && hasPermission(perm.Permission, required) {
        return true, nil
    }
    
    // Check collection permission for documents
    if resourceType == ResourceDocument {
        collectionID := getDocumentCollection(resourceID)
        perm, err = db.GetPermission(userID, ResourceCollection, collectionID)
        if err == nil && hasPermission(perm.Permission, required) {
            return true, nil
        }
    }
    
    // Check global permission
    perm, err = db.GetPermission(userID, ResourceGlobal, "")
    if err == nil && hasPermission(perm.Permission, required) {
        return true, nil
    }
    
    // Check document public status
    if resourceType == ResourceDocument && required == PermissionRead {
        doc, err := db.GetDocument(resourceID)
        if err == nil && doc.PublicRead {
            return true, nil
        }
    }
    
    return false, nil
}
```

#### Middleware
```go
func RequirePermission(resourceType ResourceType, required Permission) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userID := getUserID(r.Context())
            resourceID := chi.URLParam(r, "id")
            
            allowed, err := checkPermission(userID, resourceType, resourceID, required)
            if err != nil || !allowed {
                http.Error(w, "Forbidden", http.StatusForbidden)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

// Usage:
r.With(RequirePermission(ResourceDocument, PermissionRead)).
    Get("/docs/{id}", getDocumentHandler)
```

### 4. Input Validation

#### Markdown Sanitization
```go
import "github.com/yuin/goldmark"
import "github.com/yuin/goldmark/extension"
import "github.com/yuin/goldmark/renderer/html"

func renderMarkdown(source []byte) ([]byte, error) {
    md := goldmark.New(
        goldmark.WithExtensions(
            extension.Table,
            extension.Strikethrough,
            extension.Linkify,
            // No raw HTML extension — we control HTML output
        ),
        goldmark.WithRendererOptions(
            html.WithHardWraps(),
            html.WithXHTML(),
            html.WithUnsafe(), // DISABLED — no raw HTML passthrough
        ),
    )
    
    var buf bytes.Buffer
    if err := md.Convert(source, &buf); err != nil {
        return nil, err
    }
    
    return buf.Bytes(), nil
}
```

#### File Upload Validation
```go
func validateUpload(file io.Reader, filename string, maxSize int64) ([]byte, string, error) {
    // Size limit
    limited := io.LimitReader(file, maxSize+1)
    data, err := io.ReadAll(limited)
    if err != nil {
        return nil, "", err
    }
    if int64(len(data)) > maxSize {
        return nil, "", fmt.Errorf("file too large")
    }
    
    // Magic number validation
    contentType := detectContentType(data)
    allowedTypes := map[string]bool{
        "image/jpeg": true,
        "image/png": true,
        "image/gif": true,
        "image/webp": true,
        "application/pdf": true,
    }
    if !allowedTypes[contentType] {
        return nil, "", fmt.Errorf("file type not allowed")
    }
    
    // Extension whitelist
    ext := strings.ToLower(filepath.Ext(filename))
    allowedExts := map[string]bool{
        ".jpg": true, ".jpeg": true, ".png": true,
        ".gif": true, ".webp": true, ".pdf": true,
    }
    if !allowedExts[ext] {
        return nil, "", fmt.Errorf("file extension not allowed")
    }
    
    // Compute hash
    hash := sha256.Sum256(data)
    hashStr := hex.EncodeToString(hash[:])
    
    return data, hashStr, nil
}
```

#### Path Sanitization
```go
func sanitizePath(userPath string) (string, error) {
    // Clean the path
    clean := filepath.Clean(userPath)
    
    // Prevent traversal
    if strings.Contains(clean, "..") {
        return "", fmt.Errorf("path traversal detected")
    }
    
    // Ensure it's relative
    if filepath.IsAbs(clean) {
        return "", fmt.Errorf("absolute paths not allowed")
    }
    
    // Join with base and verify it's within base
    fullPath := filepath.Join(baseDir, clean)
    if !strings.HasPrefix(filepath.Clean(fullPath), filepath.Clean(baseDir)) {
        return "", fmt.Errorf("path outside allowed directory")
    }
    
    return clean, nil
}
```

### 5. Transport Security

#### TLS Configuration
```go
tlsConfig := &tls.Config{
    MinVersion: tls.VersionTLS13,
    CurvePreferences: []tls.CurveID{
        tls.X25519,
        tls.CurveP256,
    },
    CipherSuites: []uint16{
        tls.TLS_AES_256_GCM_SHA384,
        tls.TLS_CHACHA20_POLY1305_SHA256,
        tls.TLS_AES_128_GCM_SHA256,
    },
    PreferServerCipherSuites: true,
}
```

#### Security Headers
```go
func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Security-Policy",
            "default-src 'self'; "+
            "script-src 'self' 'nonce-{nonce}'; "+
            "style-src 'self' 'unsafe-inline'; "+
            "img-src 'self' data:; "+
            "connect-src 'self' wss:; "+
            "frame-ancestors 'none'; "+
            "base-uri 'self'; "+
            "form-action 'self';")
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
        w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
        
        next.ServeHTTP(w, r)
    })
}
```

### 6. Operational Security

#### Logging
```go
func logAudit(event AuditEvent) {
    entry := slog.With(
        "event_type", event.Type,
        "actor_id", redactIfSecret(event.ActorID),
        "resource_type", event.ResourceType,
        "resource_id", redactIfSecret(event.ResourceID),
        "ip", event.IP,
        "timestamp", event.Timestamp,
    )
    
    // Never log: passwords, tokens, session IDs, API keys
    entry.Info("audit event")
}

func redactIfSecret(value string) string {
    if looksLikeSecret(value) {
        return "[REDACTED]"
    }
    return value
}
```

#### Container Security
```dockerfile
# Dockerfile
FROM golang:1.22-alpine AS builder
# ... build ...

FROM alpine:3.19
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

# Read-only root filesystem
COPY --from=builder /app/server /server

# Data directory (writable)
RUN mkdir /data && chown appuser:appgroup /data
VOLUME /data

USER appuser
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget -q --spider http://localhost:8080/health || exit 1

ENTRYPOINT ["/server"]
```

## Incident Response

### Detection
- Automated alerts on: failed auth spikes, permission errors, large file uploads
- Log analysis for anomalies
- Health check failures

### Response
1. **Isolate**: Block IP, revoke sessions, disable account if compromised
2. **Assess**: Determine scope of breach, affected data
3. **Contain**: Patch vulnerability, rotate secrets
4. **Recover**: Restore from clean backup if necessary
5. **Document**: Post-incident review, update security measures

## Compliance

### GDPR
- Data minimization: only email stored, no other PII
- Right to erasure: `/api/user/delete` endpoint
- Data portability: `/api/user/export` endpoint
- Consent: explicit opt-in for email notifications

### Audit Trail
All security-relevant events logged:
- Authentication attempts (success/failure)
- Permission changes
- Document access (if private)
- Admin actions
- Configuration changes

Retention: 90 days active, 1 year archive
