Encrypted MessagePack over WebSocket.
- Identity signing: Ed25519
- Key exchange: X25519 and ECDH for session token
- Key derivation: HKDF-SHA256 (AES keys from shared secret)
- Transport encryption: AES-256-GCM
- Serialization: MessagePack
Client has the ServerIdentityPublicKey Ed25519 token.
Private key (ServerIdentityPrivateKey) is stored securely on the server.
Client should trust only data signed by the server key.
From now on, all MessagePack packets will be represented as JSON for reading convenience.
- Connect to wss://skyline-prod.k8s.telepower.pro/ws Request data (sent in plain unencrypted MessagePack):
{
"r": "client-hello",
"d": {
"clientEcdhPublic": "<ClientEcdhPublic>",
"nonce": "<Nonce>"
}
}- Server generates X25519 pair:
ServerEcdhPrivate,ServerEcdhPublic - Server calculates
SharedSecretby combiningServerEcdhPrivatewithClientEcdhPublic - Server creates proof of identity: combines
ClientEcdhPublic+ServerEcdhPublic+ClientNonce+ServerNonce - Server signs the proof by
ServerIdentityPublicKey.
Response data:
{
"S": "OK",
"e": null,
"R": {
"serverEcdhPublic": "<bytes>",
"signature": "<signed proof of identity bytes>",
"serverNonce": "<server nonce>"
}
}- Client verification
- Client combines the same proof of identity:
ClientEcdhPublic+ReceivedServerPublic+GeneratedNonce - Client checks the signature with
ServerIdentityPublicKey. - If the signature doesn't match, it's considered a MITM attack. Client initiates integrity breach sequence.
- Client calculates
SharedSecretby combiningClientEcdhPrivate+ReceivedServerPublib. - Client and server use HKDF-SHA256 to get a symmetric 32-byte
SessionKey.
Now we're ready to call RPC methods!
Request packet:
- q (0x71): seq
- t (0x74): current UTC UNIX timestamp
- r (0x72): request method name
- s (0x73): sessionToken (can be null if method is start-session)
- d (0x64): MessagePack structure of request payload
Response packet:
- q (0x71): seq
- S (0x53): status string (OK for successful request)
- e (0x65): error message (null for successful request)
- R (0x52): MessagePack structure of response payload
type SkylineRequest struct {
SeqID uint64 `msgpack:"q"`
Timestamp int64 `msgpack:"t"`
RequestMethod string `msgpack:"r"`
SessionToken string `msgpack:"s"`
Payload []byte `msgpack:"d"`
}
type SkylineResponse struct {
SeqID uint64 `msgpack:"q"`
ResponseStatus string `msgpack:"S"`
ErrorMessage string `msgpack:"e"`
Payload []byte `msgpack:"R"`
}The server tracks the seq field. If the client sent an unexpected seq value, this is considered an integrity breach.
Skyline RPC uses simple AES-256-GCM to encrypt the MessagePack data using the sessionKey. Go pseudocode:
func Encrypt(key []byte, data []byte) []byte {
block := aes.NewCipher(key)
aesGCM := cipher.NewGCM(block)
nonce := make([]byte, aesGCM.NonceSize())
io.ReadFull(rand.Reader, nonce)
ciphertext := aesGCM.Seal(nonce, nonce, data, nil)
return ciphertext
}
func Decrypt(key []byte, encryptedData []byte) []byte {
block := aes.NewCipher(key)
aesGCM := cipher.NewGCM(block)
nonceSize := aesGCM.NonceSize()
if len(encryptedData) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := encryptedData[:nonceSize], encryptedData[nonceSize:]
plaintext := aesGCM.Open(nil, nonce, ciphertext, nil)
return plaintext
}Lowest level Skyline RPC request. Must be called right after establishing encrypted connection to generate session token. The only request that allows sessionToken = null. Must have seq = 0.
{
"seq": 0,
"r": "start-session",
"s": null,
"d": {
"productId":"telepower",
"productVersion": "7.0.3",
"language": "russian",
"lastLoginReportedByClient": 1765529203,
"deviceSerialNumber": "ADTFBB4112800098",
"csProductUUID": "20240113-E0C2-646E-FDB1-E0C2646EFDB5",
"licenseKey": "WDGQX-WC2Y3-4R966-TK3H3-HXRB8"
}
}If version is outdated:
{
"seq": 0,
"S": "OUTDATED_VERSION",
"e": "Версия ПО устарела.",
"R": null
}Client must handle the status and update itself via content-disposition API.
If license key is already bound to another device:
{
"seq": 0,
"S": "KEY_BOUND_TO_ANOTHER_DEVICE",
"e": "Ключ привязан к другому устройству 25 октября 2024 кода. Для перепривязки ключа используйте личный кабинет.",
"R": null
}If last login time mismatches the one stored on the server (for instance, when the client recorded its last login time as May 23, but the server last got the request at May 22, this means that the user somehow logged into the software without contacting the server. This means the user tampered with the software):
{
"seq": 0,
"S": "INTEGRITY_BREACH",
"e": "Нарушена целостность программного обеспечения. Пожалуйста, обратитесь в техническую поддержку с кодом: LL",
"R": null
}If this is the first time logging in with this license key and it has just been activated:
{
"seq": 0,
"S": "OK",
"e": null,
"R": {
"isActivatedNow": true,
"activationDate": "2024-12-25",
"licensePlan": "telepower.licensePlan.release.monthly",
"expiryDate": "2025-01-25",
"sessionToken": "71ee0979-ea57b170"
}
}If this is not the first time user logged in:
{
"seq": 0,
"S": "OK",
"e": null,
"R": {
"isActivatedNow": false,
"activationDate": "2024-11-20",
"licensePlan": "telepower.licensePlan.release.quarterly",
"expiryDate": "2025-02-20",
"sessionToken": "0b4968ec-3f2d1d71"
}
}Request data: empty
Response data:
{
"engine.limits.inviteToChannel.dailyLimit": 500,
"engine.limits.sendMessage.dailyLimit": 150
}Request data:
{
"event": "app.navigation",
"data": {
"to": "/account-manager"
}
}Response data: empty
...and more RPC methods to come!