Security
Krema's security model protects users while enabling powerful desktop capabilities.
Permission System
Krema uses declarative permissions configured in krema.toml:
[permissions]
allow = [
"fs:read",
"fs:write",
"clipboard:read",
"clipboard:write",
"notification",
"shell:open"
]
Available Permissions
| Permission | Description |
|---|---|
fs:read | Read files from the filesystem |
fs:write | Write files to the filesystem |
fs:* | Full filesystem access |
clipboard:read | Read from clipboard |
clipboard:write | Write to clipboard |
notification | Show desktop notifications |
shell:execute | Execute shell commands |
shell:open | Open files/URLs with default app |
system-tray | System tray access |
system-info | Read system information |
network | Make HTTP requests |
dialog | Show file dialogs |
sql:read | Read from SQLite databases |
sql:write | Write to SQLite databases |
autostart:manage | Manage launch-at-login |
deep-link:read | Handle custom URL schemes |
Permission Enforcement
Commands requiring permissions must be annotated:
import build.krema.security.RequiresPermission;
import build.krema.security.Permission;
@KremaCommand
@RequiresPermission(Permission.FS_READ)
public String readFile(String path) throws IOException {
return Files.readString(Path.of(path));
}
@KremaCommand
@RequiresPermission(Permission.SHELL_EXECUTE)
public String runCommand(String command) {
return Shell.execute(command).output();
}
If a command is called without the required permission, an error is returned to the frontend.
Content Security Policy
Configure CSP headers in krema.toml:
[security]
csp = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
Default CSP restricts:
- Only load resources from the app itself
- No inline scripts (unless explicitly allowed)
- No eval()
- No external connections (unless
networkpermission granted)
Development Mode
In dev mode, CSP is relaxed to allow:
- Hot module replacement
- Source maps
- Dev server connections
Secure Storage
Store sensitive data using the OS keychain:
// Store a secret
SecureStorage.set("api-key", "sk-secret123");
// Retrieve a secret
String apiKey = SecureStorage.get("api-key").orElse(null);
// Delete a secret
SecureStorage.delete("api-key");
Secrets are stored in:
- macOS: Keychain Services
- Windows: Credential Manager
- Linux: Secret Service (libsecret)
Never store secrets in:
- Plain text files
- localStorage
- The regular key-value store
- Environment variables (for runtime secrets)
Input Validation
Path Traversal
Always validate file paths:
@KremaCommand
@RequiresPermission(Permission.FS_READ)
public String readFile(String path) throws IOException {
Path normalized = Path.of(path).normalize();
Path appDir = AppPaths.appDataDir();
// Ensure path is within allowed directory
if (!normalized.startsWith(appDir)) {
throw new SecurityException("Access denied: path outside app directory");
}
return Files.readString(normalized);
}
Command Injection
Avoid string concatenation for shell commands:
// DANGEROUS - command injection possible
@KremaCommand
public String unsafeSearch(String query) {
return Shell.execute("grep " + query + " /var/log/*").output();
}
// SAFE - use argument arrays
@KremaCommand
public String safeSearch(String query) {
return Shell.execute("grep", query, "/var/log/app.log").output();
}
SQL Injection
Use parameterized queries:
// DANGEROUS - SQL injection
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
// SAFE - parameterized query
String sql = "SELECT * FROM users WHERE name = ?";
connection.prepareStatement(sql).setString(1, name);
Update Security
Signature Verification
All updates are verified with Ed25519 signatures:
- Generate a keypair:
krema signer generate - Configure public key in
krema.toml - Sign builds with private key
- Updates are rejected if signatures don't match
[updater]
pubkey = "MCowBQYDK2VwAyEA..."
endpoints = ["https://releases.example.com/..."]
Secure Update Delivery
- Always serve updates over HTTPS
- Use content checksums in addition to signatures
- Pin TLS certificates in production (future feature)
Code Signing
Sign your application to:
- Establish developer identity
- Prevent tampering warnings
- Enable macOS notarization
See the Code Signing Guide for details.
Network Security
CORS Bypass
Backend HTTP requests bypass CORS, enabling API calls that would be blocked in a browser. This is intentional but requires caution:
// Backend can access any API
@KremaCommand
public String fetchData(String url) {
return HttpClient.fetch(url).get().body();
}
Validate URLs if accepting user input:
@KremaCommand
public String fetchData(String url) {
// Only allow specific domains
if (!url.startsWith("https://api.example.com/")) {
throw new SecurityException("Invalid URL");
}
return HttpClient.fetch(url).get().body();
}
Secrets in Requests
Never expose secrets to the frontend:
// Backend handles auth, frontend never sees the key
@KremaCommand
public String fetchProtectedData() {
String apiKey = SecureStorage.get("api-key").orElseThrow();
return HttpClient.fetch("https://api.example.com/data")
.header("Authorization", "Bearer " + apiKey)
.get()
.body();
}
Best Practices
- Principle of Least Privilege: Only request permissions you need
- Validate All Input: Never trust frontend data
- Use Secure Storage: For API keys, tokens, and secrets
- Sign Your Releases: Enable signature verification for updates
- Keep Dependencies Updated: Monitor for security advisories
- Audit Third-Party Plugins: Review permissions and code
- Log Security Events: Track authentication failures, permission denials
- Test Security: Include security tests in your CI pipeline