RDP Credentials in the Vault
Credential vault
Storage
With credential_mode='user_only', only username_encrypted is set (the user enters the password locally). With credential_mode='full', username and password are encrypted. Encryption runs via src/utils/crypto.js:encrypt (AES-256-GCM with the server master key).
E2EE retrieval by the desktop client
Credentials are NEVER delivered in plaintext over the API. The flow:
- Client generates an ephemeral ECDH keypair (P-256), sends the public key with the connection request (
GET /api/v1/client/rdp/:id/connection-info?ecdhPublicKey=<base64>). - Server decrypts the credentials from the DB (AES-GCM with master key, server-side).
- Server calls
ecdhEncrypt(credentialsJson, ecdhPublicKey):- Generates an ephemeral server ECDH keypair.
- Derives an AES-256 key via HKDF-SHA256 (
salt = clientPub || serverPub,info = 'gatecontrol-rdp-e2ee-v1'). - AES-256-GCM encryption of the credentials JSON.
- Response contains
{data, iv, authTag, serverPublicKey}. - Client derives the same AES key from its private key + the received server public and decrypts.
Nuance: Android/Java clients supply public keys in X.509/SPKI format (91 bytes), Node clients supply raw uncompressed (65 bytes). ecdhEncrypt detects this and uses the same encoding for the HKDF salt that the client uses — otherwise the derived key would not be symmetric.
The RSA pubkey endpoint GET /api/v1/rdp/pubkey is a separate legacy path (no ECDH, but RSA-OAEP). Used for older client flows where the client, instead of a credentials download, performs a server-side encrypted blob operation.
Credential rotation
If credential_rotation_enabled=1, the cycle credential_rotation_days applies. GET /api/v1/rdp/rotation/pending lists routes whose credential_rotation_last + credential_rotation_days has elapsed:
WHERE credential_rotation_enabled = 1 AND (
credential_rotation_last IS NULL
OR datetime(credential_rotation_last, '+' || credential_rotation_days || ' days') < datetime('now')
)
The admin then sets new values via PATCH /api/v1/rdp/:id/credentials and confirms via POST /api/v1/rdp/:id/rotation/ack — this only sets credential_rotation_last = datetime('now') and marks the rotation cycle as completed.
On rotation of the server master key (when the AES-GCM key changes), all encrypted credentials become invalid. The admin sees them as "rotation pending" and must re-enter them.