Marix SSH Client - Security Documentation
Version: 1.0.10 | Last Updated: January 2026
This document describes the security architecture, cryptographic implementations, and data protection mechanisms used in Marix SSH Client.
- Overview
- Credential Storage
- Backup Encryption
- SSH/SFTP Security
- Known Hosts Verification
- SSH Key Management
- OAuth & Cloud Integrations
- LAN Sharing Security
- Electron Security Model
- Cryptographic Summary
- Security Recommendations
Marix is designed to provide high usability while minimizing the attack surface.
Marix PROTECTS against:
- Accidental credential disclosure (via shoulder surfing or plain text files).
- Local credential theft from casual malware (via OS Keychain binding).
- Backup leakage (encrypted at rest before upload).
- Offline brute-force attacks against stolen backups.
- MITM attacks via SSH host key verification.
Marix does NOT claim protection against:
- A malicious or fully compromised SSH server.
- Kernel-level malware (Rootkits/Keyloggers) on the local machine.
- Physical access to an unlocked device while the app is running.
- Supply-chain attacks at the OS level.
Disclaimer: This is not a formal security audit. If your threat model includes nation-state adversaries, you should use OpenSSH CLI directly in a hardened environment.
All sensitive data (passwords, private keys, passphrases) are encrypted using Electron's safeStorage API, which leverages the operating system's native keychain:
| Platform | Backend |
|---|---|
| macOS | Keychain |
| Windows | DPAPI (Data Protection API) |
| Linux | libsecret (GNOME Keyring / KWallet) |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SecureStorage Flow β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Plain Password ββββββββββββββββββββββββββββββββββββββΊ β
β β β
β βΌ β
β βββββββββββββββββββ β
β β safeStorage API β β
β β (OS Keychain) β β
β ββββββββββ¬βββββββββ β
β β β
β βΌ β
β enc:XXXXXXXXXXXXXX (Base64 encrypted string) β
β β β
β βΌ β
β Stored in electron-store (config.json) β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Security Properties:
- β Data encrypted on one machine cannot be decrypted on another
- β Credentials are tied to the device and user account
- β Automatic migration from plaintext on first run
- β
Protected fields:
password,privateKey,passphrase
// Fields automatically encrypted/decrypted
const SENSITIVE_FIELDS = ['password', 'privateKey', 'passphrase'];
// Storage format example
{
"id": "server-001",
"host": "example.com",
"username": "admin",
"password": "enc:AQAAANCMnd8BFdERjHoAwE...", // β Encrypted
"privateKey": "enc:AQAAANCMnd8BFdERjHoAwE..." // β Encrypted
}| Component | Algorithm | Parameters |
|---|---|---|
| KDF | Argon2id | Auto-tuned (~1s target) |
| Encryption | AES-256-GCM | Authenticated encryption |
| Salt | CSPRNG | 32 bytes per backup |
| IV/Nonce | CSPRNG | 16 bytes per operation |
| Auth Tag | GCM | 16 bytes |
Marix dynamically calibrates Argon2id parameters based on the user's hardware to achieve approximately 1 second of key derivation time. This provides consistent security regardless of machine performance.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Argon2id Auto-Tuning Algorithm β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Detect System RAM β
β βββ β₯16 GB β Start at 512 MB memory β
β βββ β₯8 GB β Start at 256 MB memory β
β βββ β₯4 GB β Start at 128 MB memory β
β βββ <4 GB β Start at 64 MB memory (minimum) β
β β
β 2. Benchmark with timeCost=1 β
β β
β 3. Adjust memory if baseline is off β
β βββ Too slow (>600ms) β Reduce memory β
β βββ Too fast (<100ms) β Increase memory β
β β
β 4. Scale timeCost to hit ~1000ms target β
β β
β 5. Fine-tune Β±200ms tolerance β
β β
β 6. Store parameters with backup for decryption β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Security Floors (Minimum Values):
- Memory Cost: 64 MB minimum
- Time Cost: 2 iterations minimum
- Parallelism: min(4, CPU cores)
Backup passwords must meet strict requirements:
| Requirement | Minimum |
|---|---|
| Length | 10 characters |
| Uppercase | At least 1 |
| Lowercase | At least 1 |
| Number | At least 1 |
| Special Character | At least 1 (!@#$%^&*()_+-=[]{}...) |
// Password validation regex
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]).{10,}$/{
"version": "2.2",
"encrypted": "Base64(AES-256-GCM ciphertext)",
"salt": "Base64(32 random bytes)",
"iv": "Base64(16 random bytes)",
"authTag": "Base64(16 bytes GCM tag)",
"kdf": "argon2id",
"memoryCost": 262144,
"parallelism": 4,
"timeCost": 3
}Cross-Machine Compatibility:
- KDF parameters are stored with the backup
- Decryption uses stored parameters, not current machine's calibration
- Legacy backups (v2.0) use fixed parameters for compatibility
| Data Type | Encrypted in Backup |
|---|---|
| Server credentials | β Yes |
| SSH Keys (public + private) | β Yes |
| 2FA TOTP secrets | β Yes |
| Cloudflare API tokens | β Yes |
| Port forward configs | β Yes |
| Command snippets | β Yes |
| Tag colors | β Yes |
| Theme settings | β Yes |
const connectConfig: ConnectConfig = {
host: config.host,
port: config.port,
username: config.username,
password: config.password,
privateKey: config.privateKey,
passphrase: config.passphrase,
readyTimeout: 30000,
keepaliveInterval: 10000,
keepaliveCountMax: 3,
};| Method | Security Level | Recommendation |
|---|---|---|
| SSH Key (Ed25519) | π’ Highest | Recommended |
| SSH Key (ECDSA) | π’ High | Good |
| SSH Key (RSA 4096-bit) | π’ High | Good |
| Password | π‘ Medium | Use with strong passwords |
For terminal sessions, Marix uses the system's native SSH client via node-pty:
const sshArgs = [
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'LogLevel=ERROR',
'-p', port.toString(),
'-i', privateKeyPath, // If using key auth
`${username}@${host}`
];Temporary Key Handling:
- Private keys are written to temp files with
mode: 0o600 - Keys are normalized (LF line endings, trailing newline)
- Temp files are cleaned up after session ends
Marix implements SSH host key verification to prevent MITM attacks:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Host Key Verification Flow β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Connect to server β
β β
β 2. Fetch host key via ssh-keyscan β
β βββ Preference: ed25519 > ecdsa > rsa β
β β
β 3. Calculate SHA256 fingerprint β
β β
β 4. Compare with stored fingerprint β
β βββ NEW: Prompt user to trust β
β βββ MATCH: Allow connection β
β βββ CHANGED: β οΈ Security warning! β
β β
β 5. Store in ~/.marix/known_hosts.json β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
{
"example.com:22": {
"host": "example.com",
"port": 22,
"keyType": "ssh-ed25519",
"fingerprint": "SHA256:AAAA...",
"fullKey": "ssh-ed25519 AAAA...",
"addedAt": "2026-01-21T10:00:00Z"
}
}Marix supports generating SSH keys via ssh-keygen:
| Type | Bits | Use Case |
|---|---|---|
| Ed25519 | 256 | Modern, recommended |
| ECDSA | 521 | Strong, wide support |
| RSA | 4096 | Legacy compatibility |
- Location:
~/.marix/ssh_keys/ - Private keys:
mode: 0o600(owner read/write only) - Public keys: stored alongside private
- Metadata:
ssh_keys_meta.json
Both public and private keys are backed up:
exportAllKeysForBackup(): {
id: string;
name: string;
type: string;
publicKey: string; // Full public key
privateKey: string; // Full private key (encrypted in backup)
fingerprint: string;
createdAt: string;
}[]Import Deduplication:
- Keys are matched by fingerprint
- Duplicate keys are skipped during restore
- Preserves original key IDs and metadata
| Provider | Auth Method | Scopes |
|---|---|---|
| Google Drive | OAuth 2.0 (PKCE) | drive.file |
| GitHub | Device Flow | repo |
| GitLab | OAuth 2.0 | api |
| Box | OAuth 2.0 | root_readwrite |
OAuth tokens are stored securely:
// GitHub tokens
class SecureStore {
async setPassword(service, account, password) {
const encrypted = safeStorage.encryptString(password);
fs.writeFileSync(filePath, encrypted);
}
}
// Google Drive tokens
// Stored in user data directory, encrypted
TOKEN_PATH = path.join(app.getPath('userData'), 'google-drive-token.json');Local callback server for OAuth:
- Runs on
localhost:3000only - Stops immediately after receiving callback
- Uses PKCE flow where supported
- Protocol: UDP Multicast
- Address:
224.0.0.88:45678 - Peer timeout: 30 seconds
// Stable device ID from hostname + MAC address
const deviceId = crypto.createHash('sha256')
.update(`${hostname}-${macAddress}`)
.digest('hex')
.substring(0, 32);- 6-digit random code for each transfer
- Must match to initiate file transfer
- Prevents unauthorized connections
- Protocol: TCP (port 45679)
- Chunk size: 64 KB
- No encryption (relies on LAN trust)
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
backgroundThrottling: false,
spellcheck: false,
}<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline';">Preload script exposes minimal IPC surface:
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
on: (channel, func) => ipcRenderer.on(channel, ...args),
},
});- V8 memory limit: 256 MB
- Periodic cache clearing (5 minutes)
- Manual GC triggers
| Operation | Algorithm | Key Size | Notes |
|---|---|---|---|
| Credential encryption | OS Keychain | Platform-dependent | safeStorage API |
| Backup KDF | Argon2id | 256-bit output | Auto-tuned |
| Backup encryption | AES-256-GCM | 256-bit | Authenticated |
| SSH key generation | Ed25519/ECDSA/RSA | 256/521/4096-bit | Via ssh-keygen |
| Host fingerprint | SHA256 | 256-bit | Via ssh-keyscan |
| Device ID | SHA256 | 256-bit | From hostname+MAC |
| Random generation | CSPRNG | N/A | crypto.randomBytes |
- Use SSH Keys instead of passwords whenever possible
- Use strong backup passwords (10+ chars, mixed case, numbers, symbols)
- Verify host fingerprints on first connection
- Use Port Knocking for additional stealth
- Backup regularly to multiple cloud providers
- Disable password authentication on SSH servers
- Use Ed25519 keys for best security/performance
- Configure fail2ban for brute-force protection
- Keep OpenSSH updated on all servers
- Use VPN for sensitive LAN transfers
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Port Knocking Flow β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. SSH port (22) is CLOSED by default β
β β
β 2. Client sends TCP SYN to sequence: 7000 β 8000 β 9000 β
β β
β 3. Server daemon (knockd) detects sequence β
β β
β 4. Firewall opens port 22 for client IP β
β β
β 5. Client connects via SSH β
β β
β Benefits: β
β βββ Port scanners see port 22 as closed β
β βββ Prevents brute-force attacks β
β βββ Adds stealth layer before authentication β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
If you discover a security vulnerability in Marix, please report it responsibly:
- Do not open a public GitHub issue
- Contact the maintainer directly
- Provide detailed reproduction steps
- Allow reasonable time for a fix before disclosure
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2026 | Initial security implementation |
| 1.0.5 | 2026 | Added Argon2id auto-tuning |
| 1.0.10 | 2026 | Added Snippets to backup |
This document is maintained as part of the Marix SSH Client project.