mirror of
https://github.com/zadam/trilium.git
synced 2025-11-08 15:39:02 +01:00
835 lines
18 KiB
Markdown
Vendored
835 lines
18 KiB
Markdown
Vendored
# Trilium Security Architecture
|
|
|
|
> **Related:** [ARCHITECTURE.md](ARCHITECTURE.md) | [SECURITY.md](../SECURITY.md)
|
|
|
|
## Overview
|
|
|
|
Trilium implements a **defense-in-depth security model** with multiple layers of protection for user data. The security architecture covers authentication, authorization, encryption, input sanitization, and secure communication.
|
|
|
|
## Security Principles
|
|
|
|
1. **Data Privacy**: User data is protected at rest and in transit
|
|
2. **Encryption**: Per-note encryption for sensitive content
|
|
3. **Authentication**: Multiple authentication methods supported
|
|
4. **Authorization**: Single-user model with granular protected sessions
|
|
5. **Input Validation**: All user input sanitized
|
|
6. **Secure Defaults**: Security features enabled by default
|
|
7. **Transparency**: Open source allows security audits
|
|
|
|
## Threat Model
|
|
|
|
### Threats Considered
|
|
|
|
1. **Unauthorized Access**
|
|
- Physical access to device
|
|
- Network eavesdropping
|
|
- Stolen credentials
|
|
- Session hijacking
|
|
|
|
2. **Data Exfiltration**
|
|
- Malicious scripts
|
|
- XSS attacks
|
|
- SQL injection
|
|
- CSRF attacks
|
|
|
|
3. **Data Corruption**
|
|
- Malicious modifications
|
|
- Database tampering
|
|
- Sync conflicts
|
|
|
|
4. **Privacy Leaks**
|
|
- Unencrypted backups
|
|
- Search indexing
|
|
- Temporary files
|
|
- Memory dumps
|
|
|
|
### Out of Scope
|
|
|
|
- Nation-state attackers
|
|
- Zero-day vulnerabilities in dependencies
|
|
- Hardware vulnerabilities (Spectre, Meltdown)
|
|
- Physical access with unlimited time
|
|
- Quantum computing attacks
|
|
|
|
## Authentication
|
|
|
|
### Password Authentication
|
|
|
|
**Implementation:** `apps/server/src/services/password.ts`
|
|
|
|
**Password Storage:**
|
|
```typescript
|
|
// Password is never stored directly
|
|
const salt = crypto.randomBytes(32)
|
|
const derivedKey = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256')
|
|
const verificationHash = crypto.createHash('sha256')
|
|
.update(derivedKey)
|
|
.digest('hex')
|
|
|
|
// Store only salt and verification hash
|
|
sql.insert('user_data', {
|
|
salt: salt.toString('hex'),
|
|
derivedKey: derivedKey.toString('hex') // Used for encryption
|
|
})
|
|
|
|
sql.insert('options', {
|
|
name: 'passwordVerificationHash',
|
|
value: verificationHash
|
|
})
|
|
```
|
|
|
|
**Password Requirements:**
|
|
- Minimum length: 4 characters (configurable)
|
|
- No maximum length
|
|
- All characters allowed
|
|
- Can be changed by user
|
|
|
|
**Login Process:**
|
|
```typescript
|
|
// 1. User submits password
|
|
POST /api/login/password
|
|
Body: { password: "user-password" }
|
|
|
|
// 2. Server derives key
|
|
const derivedKey = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256')
|
|
|
|
// 3. Verify against stored hash
|
|
const verificationHash = crypto.createHash('sha256')
|
|
.update(derivedKey)
|
|
.digest('hex')
|
|
|
|
if (verificationHash === storedHash) {
|
|
// 4. Create session
|
|
req.session.loggedIn = true
|
|
req.session.regenerate()
|
|
}
|
|
```
|
|
|
|
### TOTP (Two-Factor Authentication)
|
|
|
|
**Implementation:** `apps/server/src/routes/api/login.ts`
|
|
|
|
**Setup Process:**
|
|
```typescript
|
|
// 1. Generate secret
|
|
const secret = speakeasy.generateSecret({
|
|
name: `Trilium (${username})`,
|
|
length: 32
|
|
})
|
|
|
|
// 2. Store encrypted secret
|
|
const encryptedSecret = encrypt(secret.base32, dataKey)
|
|
sql.insert('options', {
|
|
name: 'totpSecret',
|
|
value: encryptedSecret
|
|
})
|
|
|
|
// 3. Generate QR code
|
|
const qrCodeUrl = secret.otpauth_url
|
|
```
|
|
|
|
**Verification:**
|
|
```typescript
|
|
// User submits TOTP token
|
|
POST /api/login/totp
|
|
Body: { token: "123456" }
|
|
|
|
// Verify token
|
|
const secret = decrypt(encryptedSecret, dataKey)
|
|
const verified = speakeasy.totp.verify({
|
|
secret: secret,
|
|
encoding: 'base32',
|
|
token: token,
|
|
window: 1 // Allow 1 time step tolerance
|
|
})
|
|
```
|
|
|
|
### OpenID Connect
|
|
|
|
**Implementation:** `apps/server/src/routes/api/login.ts`
|
|
|
|
**Supported Providers:**
|
|
- Any OpenID Connect compatible provider
|
|
- Google, GitHub, Auth0, etc.
|
|
|
|
**Flow:**
|
|
```typescript
|
|
// 1. Redirect to provider
|
|
GET /api/login/openid
|
|
|
|
// 2. Provider redirects back with code
|
|
GET /api/login/openid/callback?code=...
|
|
|
|
// 3. Exchange code for tokens
|
|
const tokens = await openidClient.callback(redirectUri, req.query)
|
|
|
|
// 4. Verify ID token
|
|
const claims = tokens.claims()
|
|
|
|
// 5. Create session
|
|
req.session.loggedIn = true
|
|
```
|
|
|
|
### Session Management
|
|
|
|
**Session Storage:** SQLite database (sessions table)
|
|
|
|
**Session Configuration:**
|
|
```typescript
|
|
app.use(session({
|
|
secret: sessionSecret,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
rolling: true,
|
|
cookie: {
|
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
httpOnly: true,
|
|
secure: isHttps,
|
|
sameSite: 'lax'
|
|
},
|
|
store: new SqliteStore({
|
|
db: db,
|
|
table: 'sessions'
|
|
})
|
|
}))
|
|
```
|
|
|
|
**Session Invalidation:**
|
|
- Automatic timeout after inactivity
|
|
- Manual logout clears session
|
|
- Server restart invalidates all sessions (optional)
|
|
|
|
## Authorization
|
|
|
|
### Single-User Model
|
|
|
|
**Desktop:**
|
|
- Single user (owner of device)
|
|
- No multi-user support
|
|
- Full access to all notes
|
|
|
|
**Server:**
|
|
- Single user per installation
|
|
- Authentication required for all operations
|
|
- No user roles or permissions
|
|
|
|
### Protected Sessions
|
|
|
|
**Purpose:** Temporary access to encrypted (protected) notes
|
|
|
|
**Implementation:** `apps/server/src/services/protected_session.ts`
|
|
|
|
**Workflow:**
|
|
```typescript
|
|
// 1. User enters password for protected notes
|
|
POST /api/protected-session/enter
|
|
Body: { password: "protected-password" }
|
|
|
|
// 2. Derive encryption key
|
|
const protectedDataKey = deriveKey(password)
|
|
|
|
// 3. Verify password (decrypt known encrypted value)
|
|
const decrypted = decrypt(testValue, protectedDataKey)
|
|
if (decrypted === expectedValue) {
|
|
// 4. Store in memory (not in session)
|
|
protectedSessionHolder.setProtectedDataKey(protectedDataKey)
|
|
|
|
// 5. Set timeout
|
|
setTimeout(() => {
|
|
protectedSessionHolder.clearProtectedDataKey()
|
|
}, timeout)
|
|
}
|
|
```
|
|
|
|
**Protected Session Timeout:**
|
|
- Default: 10 minutes (configurable)
|
|
- Extends on activity
|
|
- Cleared on browser close
|
|
- Separate from main session
|
|
|
|
### API Authorization
|
|
|
|
**Internal API:**
|
|
- Requires authenticated session
|
|
- CSRF token validation
|
|
- Same-origin policy
|
|
|
|
**ETAPI (External API):**
|
|
- Token-based authentication
|
|
- No session required
|
|
- Rate limiting
|
|
|
|
## Encryption
|
|
|
|
### Note Encryption
|
|
|
|
**Encryption Algorithm:** AES-256-CBC
|
|
|
|
**Key Hierarchy:**
|
|
```
|
|
User Password
|
|
↓ (PBKDF2)
|
|
Data Key (for protected notes)
|
|
↓ (AES-256)
|
|
Protected Note Content
|
|
```
|
|
|
|
**Encryption Process:**
|
|
```typescript
|
|
// 1. Generate IV (initialization vector)
|
|
const iv = crypto.randomBytes(16)
|
|
|
|
// 2. Encrypt content
|
|
const cipher = crypto.createCipheriv('aes-256-cbc', dataKey, iv)
|
|
let encrypted = cipher.update(content, 'utf8', 'base64')
|
|
encrypted += cipher.final('base64')
|
|
|
|
// 3. Prepend IV to encrypted content
|
|
const encryptedBlob = iv.toString('base64') + ':' + encrypted
|
|
|
|
// 4. Store in database
|
|
sql.insert('blobs', {
|
|
blobId: blobId,
|
|
content: encryptedBlob
|
|
})
|
|
```
|
|
|
|
**Decryption Process:**
|
|
```typescript
|
|
// 1. Split IV and encrypted content
|
|
const [ivBase64, encryptedData] = encryptedBlob.split(':')
|
|
const iv = Buffer.from(ivBase64, 'base64')
|
|
|
|
// 2. Decrypt
|
|
const decipher = crypto.createDecipheriv('aes-256-cbc', dataKey, iv)
|
|
let decrypted = decipher.update(encryptedData, 'base64', 'utf8')
|
|
decrypted += decipher.final('utf8')
|
|
|
|
return decrypted
|
|
```
|
|
|
|
**Protected Note Metadata:**
|
|
- Title is NOT encrypted (for tree display)
|
|
- Type and MIME are NOT encrypted
|
|
- Content IS encrypted
|
|
- Attributes CAN be encrypted (optional)
|
|
|
|
### Data Key Management
|
|
|
|
**Master Data Key:**
|
|
```typescript
|
|
// Generated once during setup
|
|
const dataKey = crypto.randomBytes(32) // 256 bits
|
|
|
|
// Encrypted with derived key from user password
|
|
const derivedKey = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256')
|
|
const encryptedDataKey = encrypt(dataKey, derivedKey)
|
|
|
|
// Stored in database
|
|
sql.insert('options', {
|
|
name: 'encryptedDataKey',
|
|
value: encryptedDataKey.toString('hex')
|
|
})
|
|
```
|
|
|
|
**Key Rotation:**
|
|
- Not currently supported
|
|
- Requires re-encrypting all protected notes
|
|
- Planned for future version
|
|
|
|
### Transport Encryption
|
|
|
|
**HTTPS:**
|
|
- Required for server installations (recommended)
|
|
- TLS 1.2+ only
|
|
- Strong cipher suites preferred
|
|
- Certificate validation enabled
|
|
|
|
**Desktop:**
|
|
- Local communication (no network)
|
|
- No HTTPS required
|
|
|
|
### Backup Encryption
|
|
|
|
**Database Backups:**
|
|
- Protected notes remain encrypted in backup
|
|
- Backup file should be protected separately
|
|
- Consider encrypting backup storage location
|
|
|
|
## Input Sanitization
|
|
|
|
### XSS Prevention
|
|
|
|
**HTML Sanitization:**
|
|
|
|
Location: `apps/client/src/services/dompurify.ts`
|
|
|
|
```typescript
|
|
import DOMPurify from 'dompurify'
|
|
|
|
// Configure DOMPurify
|
|
DOMPurify.setConfig({
|
|
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'div', ...],
|
|
ALLOWED_ATTR: ['href', 'title', 'class', 'id', ...],
|
|
ALLOW_DATA_ATTR: false
|
|
})
|
|
|
|
// Sanitize HTML before rendering
|
|
const cleanHtml = DOMPurify.sanitize(userHtml)
|
|
```
|
|
|
|
**CKEditor Configuration:**
|
|
```typescript
|
|
// apps/client/src/widgets/type_widgets/text_type_widget.ts
|
|
ClassicEditor.create(element, {
|
|
// Restrict allowed content
|
|
htmlSupport: {
|
|
allow: [
|
|
{ name: /./, attributes: true, classes: true, styles: true }
|
|
],
|
|
disallow: [
|
|
{ name: 'script' },
|
|
{ name: 'iframe', attributes: /^(?!src$).*/ }
|
|
]
|
|
}
|
|
})
|
|
```
|
|
|
|
**Content Security Policy:**
|
|
```typescript
|
|
// apps/server/src/main.ts
|
|
app.use((req, res, next) => {
|
|
res.setHeader('Content-Security-Policy',
|
|
"default-src 'self'; " +
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
|
|
"style-src 'self' 'unsafe-inline'; " +
|
|
"img-src 'self' data: blob:;"
|
|
)
|
|
next()
|
|
})
|
|
```
|
|
|
|
### SQL Injection Prevention
|
|
|
|
**Parameterized Queries:**
|
|
```typescript
|
|
// GOOD - Safe from SQL injection
|
|
const notes = sql.getRows(
|
|
'SELECT * FROM notes WHERE title = ?',
|
|
[userInput]
|
|
)
|
|
|
|
// BAD - Vulnerable to SQL injection
|
|
const notes = sql.getRows(
|
|
`SELECT * FROM notes WHERE title = '${userInput}'`
|
|
)
|
|
```
|
|
|
|
**ORM Usage:**
|
|
```typescript
|
|
// Entity-based access prevents SQL injection
|
|
const note = becca.getNote(noteId)
|
|
note.title = userInput // Sanitized by entity
|
|
note.save() // Parameterized query
|
|
```
|
|
|
|
### CSRF Prevention
|
|
|
|
**CSRF Token Validation:**
|
|
|
|
Location: `apps/server/src/routes/middleware/csrf.ts`
|
|
|
|
```typescript
|
|
// Generate CSRF token
|
|
const csrfToken = crypto.randomBytes(32).toString('hex')
|
|
req.session.csrfToken = csrfToken
|
|
|
|
// Validate on state-changing requests
|
|
app.use((req, res, next) => {
|
|
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
|
|
const token = req.headers['x-csrf-token']
|
|
if (token !== req.session.csrfToken) {
|
|
return res.status(403).json({ error: 'CSRF token mismatch' })
|
|
}
|
|
}
|
|
next()
|
|
})
|
|
```
|
|
|
|
**Client-Side:**
|
|
```typescript
|
|
// apps/client/src/services/server.ts
|
|
const csrfToken = getCsrfToken()
|
|
|
|
fetch('/api/notes', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
```
|
|
|
|
### File Upload Validation
|
|
|
|
**Validation:**
|
|
```typescript
|
|
// apps/server/src/routes/api/attachments.ts
|
|
const allowedMimeTypes = [
|
|
'image/jpeg',
|
|
'image/png',
|
|
'application/pdf',
|
|
// ...
|
|
]
|
|
|
|
if (!allowedMimeTypes.includes(file.mimetype)) {
|
|
throw new Error('File type not allowed')
|
|
}
|
|
|
|
// Validate file size
|
|
const maxSize = 100 * 1024 * 1024 // 100 MB
|
|
if (file.size > maxSize) {
|
|
throw new Error('File too large')
|
|
}
|
|
|
|
// Sanitize filename
|
|
const sanitizedFilename = path.basename(file.originalname)
|
|
.replace(/[^a-z0-9.-]/gi, '_')
|
|
```
|
|
|
|
## Network Security
|
|
|
|
### HTTPS Configuration
|
|
|
|
**Server Setup:**
|
|
```typescript
|
|
// apps/server/src/main.ts
|
|
const httpsOptions = {
|
|
key: fs.readFileSync('server.key'),
|
|
cert: fs.readFileSync('server.cert')
|
|
}
|
|
|
|
https.createServer(httpsOptions, app).listen(443)
|
|
```
|
|
|
|
**Certificate Validation:**
|
|
- Require valid certificates in production
|
|
- Self-signed certificates allowed for development
|
|
- Certificate pinning not implemented
|
|
|
|
### Secure Headers
|
|
|
|
```typescript
|
|
// apps/server/src/main.ts
|
|
app.use((req, res, next) => {
|
|
// Prevent clickjacking
|
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
|
|
|
|
// Prevent MIME sniffing
|
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
|
|
// XSS protection
|
|
res.setHeader('X-XSS-Protection', '1; mode=block')
|
|
|
|
// Referrer policy
|
|
res.setHeader('Referrer-Policy', 'same-origin')
|
|
|
|
// HTTPS upgrade
|
|
if (req.secure) {
|
|
res.setHeader('Strict-Transport-Security', 'max-age=31536000')
|
|
}
|
|
|
|
next()
|
|
})
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
**API Rate Limiting:**
|
|
```typescript
|
|
// apps/server/src/routes/middleware/rate_limit.ts
|
|
const rateLimit = require('express-rate-limit')
|
|
|
|
const apiLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 1000, // Limit each IP to 1000 requests per window
|
|
message: 'Too many requests from this IP'
|
|
})
|
|
|
|
app.use('/api/', apiLimiter)
|
|
```
|
|
|
|
**Login Rate Limiting:**
|
|
```typescript
|
|
const loginLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000,
|
|
max: 5, // 5 failed attempts
|
|
skipSuccessfulRequests: true
|
|
})
|
|
|
|
app.post('/api/login/password', loginLimiter, loginHandler)
|
|
```
|
|
|
|
## Data Security
|
|
|
|
### Secure Data Deletion
|
|
|
|
**Soft Delete:**
|
|
```typescript
|
|
// Mark as deleted (sync first)
|
|
note.isDeleted = 1
|
|
note.deleteId = generateUUID()
|
|
note.save()
|
|
|
|
// Entity change tracked for sync
|
|
addEntityChange('notes', noteId, note)
|
|
```
|
|
|
|
**Hard Delete (Erase):**
|
|
```typescript
|
|
// After sync completed
|
|
sql.execute('DELETE FROM notes WHERE noteId = ?', [noteId])
|
|
sql.execute('DELETE FROM branches WHERE noteId = ?', [noteId])
|
|
sql.execute('DELETE FROM attributes WHERE noteId = ?', [noteId])
|
|
|
|
// Mark entity change as erased
|
|
sql.execute('UPDATE entity_changes SET isErased = 1 WHERE entityId = ?', [noteId])
|
|
```
|
|
|
|
**Blob Cleanup:**
|
|
```typescript
|
|
// Find orphaned blobs (not referenced by any note/revision/attachment)
|
|
const orphanedBlobs = sql.getRows(`
|
|
SELECT blobId FROM blobs
|
|
WHERE blobId NOT IN (SELECT blobId FROM notes WHERE blobId IS NOT NULL)
|
|
AND blobId NOT IN (SELECT blobId FROM revisions WHERE blobId IS NOT NULL)
|
|
AND blobId NOT IN (SELECT blobId FROM attachments WHERE blobId IS NOT NULL)
|
|
`)
|
|
|
|
// Delete orphaned blobs
|
|
for (const blob of orphanedBlobs) {
|
|
sql.execute('DELETE FROM blobs WHERE blobId = ?', [blob.blobId])
|
|
}
|
|
```
|
|
|
|
### Memory Security
|
|
|
|
**Protected Data in Memory:**
|
|
- Protected data keys stored in memory only
|
|
- Cleared on timeout
|
|
- Not written to disk
|
|
- Not in session storage
|
|
|
|
**Memory Cleanup:**
|
|
```typescript
|
|
// Clear sensitive data
|
|
const clearSensitiveData = () => {
|
|
protectedDataKey = null
|
|
|
|
// Force garbage collection if available
|
|
if (global.gc) {
|
|
global.gc()
|
|
}
|
|
}
|
|
```
|
|
|
|
### Temporary Files
|
|
|
|
**Secure Temporary Files:**
|
|
```typescript
|
|
const tempDir = os.tmpdir()
|
|
const tempFile = path.join(tempDir, `trilium-${crypto.randomBytes(16).toString('hex')}`)
|
|
|
|
// Write temp file
|
|
fs.writeFileSync(tempFile, data, { mode: 0o600 }) // Owner read/write only
|
|
|
|
// Clean up after use
|
|
fs.unlinkSync(tempFile)
|
|
```
|
|
|
|
## Dependency Security
|
|
|
|
### Vulnerability Scanning
|
|
|
|
**Tools:**
|
|
- `npm audit` - Check for known vulnerabilities
|
|
- Renovate bot - Automatic dependency updates
|
|
- GitHub Dependabot alerts
|
|
|
|
**Process:**
|
|
```bash
|
|
# Check for vulnerabilities
|
|
npm audit
|
|
|
|
# Fix automatically
|
|
npm audit fix
|
|
|
|
# Manual review for breaking changes
|
|
npm audit fix --force
|
|
```
|
|
|
|
### Dependency Pinning
|
|
|
|
**package.json:**
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"express": "4.18.2", // Exact version
|
|
"better-sqlite3": "^9.2.2" // Compatible versions
|
|
}
|
|
}
|
|
```
|
|
|
|
**pnpm Overrides:**
|
|
```json
|
|
{
|
|
"pnpm": {
|
|
"overrides": {
|
|
"lodash@<4.17.21": ">=4.17.21", // Force minimum version
|
|
"axios@<0.21.2": ">=0.21.2"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Patch Management
|
|
|
|
**pnpm Patches:**
|
|
```bash
|
|
# Create patch
|
|
pnpm patch @ckeditor/ckeditor5
|
|
|
|
# Edit files in temporary directory
|
|
# ...
|
|
|
|
# Generate patch file
|
|
pnpm patch-commit /tmp/ckeditor5-patch
|
|
|
|
# Patch applied automatically on install
|
|
```
|
|
|
|
## Security Best Practices
|
|
|
|
### For Users
|
|
|
|
1. **Strong Passwords**
|
|
- Use unique password for Trilium
|
|
- Enable TOTP 2FA
|
|
- Protect password manager
|
|
|
|
2. **Protected Notes**
|
|
- Use for sensitive information
|
|
- Set reasonable session timeout
|
|
- Don't leave sessions unattended
|
|
|
|
3. **Backups**
|
|
- Regular backups to secure location
|
|
- Encrypt backup storage
|
|
- Test backup restoration
|
|
|
|
4. **Server Setup**
|
|
- Use HTTPS only
|
|
- Keep software updated
|
|
- Firewall configuration
|
|
- Use reverse proxy (nginx, Caddy)
|
|
|
|
5. **Scripts**
|
|
- Review scripts before using
|
|
- Be cautious with external scripts
|
|
- Understand script permissions
|
|
|
|
### For Developers
|
|
|
|
1. **Code Review**
|
|
- Review all security-related changes
|
|
- Test authentication/authorization changes
|
|
- Validate input sanitization
|
|
|
|
2. **Testing**
|
|
- Write security tests
|
|
- Test edge cases
|
|
- Penetration testing
|
|
|
|
3. **Dependencies**
|
|
- Regular updates
|
|
- Audit new dependencies
|
|
- Monitor security advisories
|
|
|
|
4. **Secrets**
|
|
- No secrets in source code
|
|
- Use environment variables
|
|
- Secure key generation
|
|
|
|
## Security Auditing
|
|
|
|
### Logs
|
|
|
|
**Security Events Logged:**
|
|
- Login attempts (success/failure)
|
|
- Protected session access
|
|
- Password changes
|
|
- ETAPI token usage
|
|
- Failed CSRF validations
|
|
|
|
**Log Location:**
|
|
- Desktop: Console output
|
|
- Server: Log files or stdout
|
|
|
|
### Monitoring
|
|
|
|
**Metrics to Monitor:**
|
|
- Failed login attempts
|
|
- API error rates
|
|
- Unusual database changes
|
|
- Large exports/imports
|
|
|
|
## Incident Response
|
|
|
|
### Security Issue Reporting
|
|
|
|
**Process:**
|
|
1. Email security@triliumnext.com
|
|
2. Include vulnerability details
|
|
3. Provide reproduction steps
|
|
4. Allow reasonable disclosure time
|
|
|
|
**Response:**
|
|
1. Acknowledge within 48 hours
|
|
2. Investigate and validate
|
|
3. Develop fix
|
|
4. Coordinate disclosure
|
|
5. Release patch
|
|
|
|
### Breach Response
|
|
|
|
**If Compromised:**
|
|
1. Change password immediately
|
|
2. Review recent activity
|
|
3. Check for unauthorized changes
|
|
4. Restore from backup if needed
|
|
5. Update security settings
|
|
|
|
## Future Security Enhancements
|
|
|
|
**Planned:**
|
|
- Hardware security key support (U2F/WebAuthn)
|
|
- End-to-end encryption for sync
|
|
- Zero-knowledge architecture option
|
|
- Encryption key rotation
|
|
- Audit log enhancements
|
|
- Per-note access controls
|
|
|
|
**Under Consideration:**
|
|
- Multi-user support with permissions
|
|
- Blockchain-based sync verification
|
|
- Homomorphic encryption for search
|
|
- Quantum-resistant encryption
|
|
|
|
---
|
|
|
|
**See Also:**
|
|
- [SECURITY.md](../SECURITY.md) - Security policy
|
|
- [ARCHITECTURE.md](ARCHITECTURE.md) - Overall architecture
|
|
- [Protected Notes Guide](https://triliumnext.github.io/Docs/Wiki/protected-notes)
|