tsdeclarations icon indicating copy to clipboard operation
tsdeclarations copied to clipboard

How do I calculate SharedIV and SharedMAC on the server side?

Open bzp2010 opened this issue 1 year ago • 5 comments

Hi, great team.

Problem description

I'm trying to implement a client and server in Golang and trying to interoperate with the client libraries tsclientlib you provide. I had one problem with the last step of the High-level encryption handshake on the server side, I read the relevant description in ts3protocol.md and I found that it is described from the client side perspective, not the server side.

I have passed the clientek part of the low-level handshake and high-level handshake and got the client_public_key, I want to know how the SharedIV and SharedMAC are calculated on the server side, is it calculated using the server's ed25519 private key (the one that corresponds to the public key of the last block Ephemeral Block in the license) and the public key sent by the client via the clientek command for X25519 scalar multiplication and then after performing the hash and some other operations?

Sorry I don't know much about cryptography related to elliptic curves, but it looks like it won't be equal to the SharedIV and SharedMAC calculated by the client with the server public key and the local temporary private key. And I guess the server and client must calculate the same SharedIV and SharedMAC in this one calculation session to achieve encrypted communication.

I'm stuck here and would appreciate your help or pointing out where I'm going wrong, I'll attach some of the relevant code below.

How do I calculate shared secret?

func sharedSecret(serverPrivateKey ed25519.PrivateKey, clientPublicKey ed25519.PublicKey, alpha, beta []byte) ([]byte, string, error) {
    sharedData, err := curve25519.X25519(serverPrivateKey, clientPublicKey)
    if err != nil {
        return nil, "", err
    }
    sharedIV := sha512.Sum512(sharedData[:32])

    // Xor alpha
    for i := range alpha {
        sharedIV[i] ^= alpha[i]
    }
    // Xor beta
    for i := range beta {
        sharedIV[10+i] ^= beta[i]
    }

    macHash := sha1.Sum(sharedIV[:])
    sharedMAC := macHash[:8]
    return sharedIV[:], string(sharedMAC), nil
}
  • The server private key is stored and exported when the license block is calculated, it looks like the client uses its corresponding public key as the public key named server_ek.
  • The client public key is the base64 decoded content of the ek value passed by the client from the clientek command, and I see that it is exactly the required 32 bytes in length.
  • BTW, after comparing I determined that the alpha and beta values used on the client and server are the same.

Thank you!

bzp2010 avatar Jul 23 '22 01:07 bzp2010

Hi, on the client, the shared secret is calculated through next_public_key * client_private_key. Therefore, the shared secret on the server needs to be calculated symmetrically with next_private_key * client_public_key.

The client_public_key is the ephemeral key from the clientek command. next_private_key is obtained by the key calculation through all the license blocks, but with private keys instead of public keys. Of course that means the private key of some license block is needed to start the computation (or the private key of the root key, the computation can be started/resumed at any block).

I hope that helps.

Flakebi avatar Jul 23 '22 14:07 Flakebi

@bzp2010 Can I have a discussion with you in Telegram or ...?

Mandofskii avatar Sep 08 '22 20:09 Mandofskii

Hi @Flakebi

Thank you for your answer and the great work on this project.

After some tweaks, I have been able to solve for the same public key on the client side via License that matches the server side.

But I don't understand next_private_key you mentioned, you mentioned that I can resume the computation of private key from any License block, or from the private part of the root public key (obviously I don't know the content of this private key), when I am computing next public key, it uses:

next_key = public _key * clamp(sha512(block[1...]) [0..32]) + parent

Does that mean the private key of the current block is:

clamp(sha512(block[1...]) [0..32])

If not how will the private key calculation be resume from an intermediate block? (As I understand it, if I don't know the root private key, I can't compute any of the blocks that follow)

Thanks again!

bzp2010 avatar Sep 11 '22 03:09 bzp2010

As you say, the next public key of a block is computed through

next_public_key = public_key * clamp(sha512(block[1...]) [0..32]) + parent_public_key

To compute the public key, you need to know public_key, which is stored inside the license, and parent_public_key, which – for the first license block – is the root public key and you need to get from somewhere “outside”. If you know the parent_public_key of some license block, you can compute the next_public_key for the whole license.

The computation for the private key is exactly the same, just with replacing the public with private keys:

next_private_key = private_key * clamp(sha512(block[1...]) [0..32]) + parent_private_key

To compute the private key, you need to know the private_key, which you know if you generated the license block, and the parent_private_key, which you need to get from somewhere “outside”. If you know the parent_private_key of some license block and the private_key for all following blocks, you can compute the next_private_key for the whole license.

The “outside” here is TeamSpeak, they define the root key, which can be whatever they choose, and they also hand out the parent_private_keys for intermediate license blocks.

Why this computation works

Here’s a short proof of why the private key can be computed that way and that it ends up at the same public key.

From our elliptic curve we need the base point g (generator) and the addition (+) and multiplication (*) operations, where most of our “regular math rules” like associativity and distributivity apply.

A public key is a point on the curve and is computed by multiplying the base point g by a number, which is the private key (the private key is not a point on the curve, it’s a number!):

pub = g * priv

This is just repeatedly adding g to itself, priv times.

Now, to compute the next private key, our equation is

next_private_key = private_key * clamp(sha512(block[1...]) [0..32]) + parent_private_key

As private keys are “just” numbers, this is a computation in numbers, there’s no curve points involved.

Now, to get the public key from this private key, we just multiply g * next_private_key as said before:

next_public_key = g * next_private_key

Inserting the computation of the next key, we have

next_public_key = g * (private_key * clamp(sha512(block[1...]) [0..32]) + parent_private_key)

Through distributivity, we pull g into the addition:

next_public_key = g * private_key * clamp(sha512(block[1...]) [0..32]) + g * parent_private_key

Now we see that we have g * private_key and g * parent_private_key, which are both computations to compute the public key from a private key! That means we can replace these computations by their respective public key (also making use of associativity):

next_public_key = public_key * clamp(sha512(block[1...]) [0..32]) + g * parent_public_key

And we end up at the exact computation of our public key. ∎

Flakebi avatar Sep 11 '22 11:09 Flakebi

Oh, I know.

When I don't have the private key of the root key or any of the block private keys, I can't develop a specific server implementation for this. 🤔

Thank you for your support.

bzp2010 avatar Sep 11 '22 15:09 bzp2010