tsdeclarations
tsdeclarations copied to clipboard
How do I calculate SharedIV and SharedMAC on the server side?
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!
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.
@bzp2010 Can I have a discussion with you in Telegram or ...?
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!
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_key
s 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. ∎
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.