Forward Secrecy in a Reconnectable Protocol: Two Key Pairs Per Session
TermOnMac uses two X25519 key pairs per device: a persistent identity key and a per-connection ephemeral key. The session encryption key is derived from the ephemeral keys, not the identity keys. This provides forward secrecy: compromising an identity key doesn’t expose past sessions.
Two Key Pairs
Identity key (persistent, stored on disk):
// mac_agent/Sources/RemoteDevCore/SessionCrypto.swift
public final class SessionCrypto {
public let privateKey: Curve25519.KeyAgreement.PrivateKey
public let publicKeyBase64: String
public init() {
self.privateKey = Curve25519.KeyAgreement.PrivateKey()
self.publicKeyBase64 = privateKey.publicKey.rawRepresentation.base64EncodedString()
}
/// Load from existing private key data (for persistent identity keys).
public init(privateKeyData: Data) throws {
self.privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData)
self.publicKeyBase64 = privateKey.publicKey.rawRepresentation.base64EncodedString()
}
}
The identity key is loaded from disk on startup. It identifies the device and is used for TOFU verification at the relay level.
Ephemeral key (per-connection, in memory only):
// mac_agent/Sources/MacAgentLib/RelayConnection.swift
// Generate fresh session nonce and ephemeral key per connection
localSessionNonce = SessionCrypto.randomBytes(32).base64EncodedString()
let ephKey = Curve25519.KeyAgreement.PrivateKey()
ephemeralPrivateKey = ephKey
localEphemeralPubKey = ephKey.publicKey.rawRepresentation.base64EncodedString()
peerEphemeralPubKey = ""
A new ephemeral private key is generated on every connection attempt. It exists only in memory and is never written to disk.
Ephemeral Keys Drive Session Key Derivation
The session key is derived from the ephemeral keys, not the identity keys:
// mac_agent/Sources/RemoteDevCore/SessionCrypto.swift
/// Derive session key using an ephemeral key pair for forward secrecy.
/// The persistent identity key is NOT used for ECDH — only the ephemeral keys.
public func deriveSessionKeyEphemeral(
ephemeralPrivateKey: Curve25519.KeyAgreement.PrivateKey,
peerEphemeralKeyBase64: String,
localNonce: String,
remoteNonce: String
) throws {
let shared = try ephemeralPrivateKey.sharedSecretFromKeyAgreement(with: peerKey)
let salt = Data("remotedev-v2".utf8) + Data(sortedNonces.joined().utf8)
self.sessionKey = shared.hkdfDerivedSymmetricKey(...)
}
The identity key is passed to the relay in register_room / join_room for TOFU, but it is not used in ECDH.
What Forward Secrecy Protects Against
If an attacker records all encrypted traffic and later compromises the identity private key, they still cannot decrypt past sessions because:
- Each session’s encryption key was derived from a unique ephemeral key pair
- The ephemeral private keys were never written to disk
- The ephemeral private keys are cleared from memory when the connection ends
Compromising the identity key allows future impersonation (the attacker can register the same room with the stolen key), but it cannot retroactively decrypt recorded traffic.
Session Key Cleanup
The session key is explicitly cleared when a connection ends:
// mac_agent/Sources/RemoteDevCore/SessionCrypto.swift
/// Clear the derived session key from memory.
public func clearSessionKey() {
sessionKey = nil
}
clearSessionKey() is called in RelayConnection.disconnect():
// mac_agent/Sources/MacAgentLib/RelayConnection.swift
public func disconnect() {
// ...
crypto.clearSessionKey()
}
And in iOSRelayConnection.disconnect():
// ios_remote_dev_ios_app/RemoteDevApp/Network/iOSRelayConnection.swift
func disconnect() {
// ...
crypto.clearSessionKey()
}
Identity Key Role: TOFU Only
The identity key is used exclusively for TOFU at the relay:
// relay_server/src/types.ts
export interface RoomState {
mac_public_key: string; // stored on first register, compared on every subsequent register
// ...
}
On reconnect, the relay checks that the public_key in register_room matches mac_public_key stored in room state. If they don’t match, the registration is rejected. This prevents a different device from taking over an existing room.
The identity key never participates in ECDH or session key derivation.