Skip to content

SSH Connection

Understanding SSH connections in Flutter Server Box.

User Input → Spi Config → genClient() → SSH Client → Session

The Spi (Server Parameter Info) model contains:

class Spi {
String name; // Server name
String ip; // IP address
int port; // SSH port (default 22)
String user; // Username
String? pwd; // Password (encrypted)
String? keyId; // SSH key ID
String? jumpId; // Jump server ID
String? alterUrl; // Alternative URL
}

genClient(spi) creates SSH client:

Future<SSHClient> genClient(Spi spi) async {
// 1. Establish socket
final socket = await connect(spi.ip, spi.port);
// 2. Try alternative URL if failed
if (socket == null && spi.alterUrl != null) {
socket = await connect(spi.alterUrl, spi.port);
}
// 3. Authenticate
final client = SSHClient(
socket: socket,
username: spi.user,
onPasswordRequest: () => spi.pwd,
onIdentityRequest: () => loadKey(spi.keyId),
);
// 4. Verify host key
await verifyHostKey(client, spi);
return client;
}

For jump servers, recursive connection:

if (spi.jumpId != null) {
final jumpClient = await genClient(getJumpSpi(spi.jumpId));
final forwarded = await jumpClient.forwardLocal(
spi.ip,
spi.port,
);
// Connect through forwarded socket
}
onPasswordRequest: () => spi.pwd
  • Password stored encrypted in Hive
  • Decrypted on connection
  • Sent to server for verification
onIdentityRequest: () async {
final key = await KeyStore.get(spi.keyId);
return decyptPem(key.pem, key.password);
}

Key Loading Process:

  1. Retrieve encrypted key from KeyStore
  2. Decrypt password (biometric/prompt)
  3. Parse PEM format
  4. Standardize line endings (LF)
  5. Return for authentication
onUserInfoRequest: (instructions) async {
// Handle challenge-response
return responses;
}

Supports:

  • Password authentication
  • OTP tokens
  • Two-factor authentication

Prevents Man-in-the-Middle (MITM) attacks by ensuring you’re connecting to the same server.

{spi.id}::{keyType}

Example:

my-server::ssh-ed25519
my-server::ecdsa-sha2-nistp256

MD5 Hex:

aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99

Base64:

SHA256:AbCdEf1234567890...=
Future<void> verifyHostKey(SSHClient client, Spi spi) async {
final key = await client.hostKey;
final fingerprint = md5Hex(key); // or base64
final stored = SettingStore.sshKnownHostsFingerprints
['$keyId::$keyType'];
if (stored == null) {
// New host - prompt user
final trust = await promptUser(
'Unknown host',
'Fingerprint: $fingerprint',
);
if (trust) {
SettingStore.sshKnownHostsFingerprints
['$keyId::$keyType'] = fingerprint;
}
} else if (stored != fingerprint) {
// Changed - warn user
await warnUser(
'Host key changed!',
'Possible MITM attack',
);
}
}

Active clients maintained in ServerProvider:

class ServerProvider {
final Map<String, SSHClient> _clients = {};
SSHClient getClient(String spiId) {
return _clients[spiId] ??= connect(spiId);
}
}

Maintain connection during inactivity:

Timer.periodic(
Duration(seconds: 30),
(_) => client.sendKeepAlive(),
);

On connection loss:

client.onError.listen((error) async {
await Future.delayed(Duration(seconds: 5));
reconnect();
});
┌─────────────┐
│ Initial │
└──────┬──────┘
│ connect()
┌─────────────┐
│ Connecting │ ←──┐
└──────┬──────┘ │
│ success │
↓ │ fail (retry)
┌─────────────┐ │
│ Connected │───┘
└──────┬──────┘
┌─────────────┐
│ Active │ ──→ Send commands
└──────┬──────┘
↓ (error/disconnect)
┌─────────────┐
│ Disconnected│
└─────────────┘
try {
await client.connect().timeout(
Duration(seconds: 30),
);
} on TimeoutException {
throw ConnectionException('Connection timeout');
}
onAuthFail: (error) {
if (error.contains('password')) {
return 'Invalid password';
} else if (error.contains('key')) {
return 'Invalid SSH key';
}
return 'Authentication failed';
}
onHostKeyMismatch: (stored, current) {
showSecurityWarning(
'Host key has changed!',
'Possible MITM attack',
);
}
  • Reuse clients across features
  • Don’t disconnect/reconnect unnecessarily
  • Pool connections for concurrent operations
  • Timeout: 30 seconds (adjustable)
  • Keep-alive: Every 30 seconds
  • Retry delay: 5 seconds
  • Single connection for multiple operations
  • Pipeline commands when possible
  • Avoid opening multiple connections