Skip to content

miraclebots-dev/skyline

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Skyline licensing server WebSocket RPC

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

Trust anchor

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.

Handshake

  1. 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 SharedSecret by combining ServerEcdhPrivate with ClientEcdhPublic
  • 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>"
    }
}
  1. 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 SharedSecret by combining ClientEcdhPrivate + ReceivedServerPublib.
  • Client and server use HKDF-SHA256 to get a symmetric 32-byte SessionKey.

Now we're ready to call RPC methods!

Protocol

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.

Packet encryption

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
}

start-session

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"
    }
}

get-feature-toggles

Request data: empty
Response data:

{
    "engine.limits.inviteToChannel.dailyLimit": 500,
    "engine.limits.sendMessage.dailyLimit": 150
}

send-telemetry-event

Request data:

{
    "event": "app.navigation",
    "data": {
        "to": "/account-manager"
    }
}

Response data: empty

...and more RPC methods to come!

About

Licensing & telemetry server software. Supports multiple product activation

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors