Rating:

Highlevel details
=================
1. Using the secp256k1 curve, the EllipticCurveKeyExchange class has a private key (random.randint to get an integer in the order of the curve) and a public key (which is a known point G multiplied by the private key).
2. The send_public method simply outputs the public key's coordinates.
3. The receive_public method receives JSON data representing a point and then saves the point as the recieved member. It will also extract a nonce integer from the JSON and will keep the nonce as the nonce member.
4. The get_shared_secret method validates the nonce is at least 65-bit wide, and then calculates the key: (received*private).x ^ nonce.
5. The send_fingerprint method publishes SHA256 of the shared key.
6. The check_fingerprint gets a fingerprint and validates that it's equal to own's key SHA256.
7. Finally, the encrypt_flag method does an AES-CBC cipher. It uses random 16 bytes as the IV (from os.urandom) and the first 16 bytes from the SHA1 digest of the shared key as the AES key.

Obviously, the goal is abusing the nonce and the received point to get a known key.
The shared secret is calculated as such:

shared_secret = (received * private).x ^ nonce


So, if we set received = G, we effectively set the shared secret to public.x ^ nonce (due to the multiplication of the point with private).
We can control difference nonce values for Alice and Bob to coordinate the secret.

Sending to Bob
==============
1. At this point we do not know Bob's public key.
2. Set recevied=G and Nonce to be 2**65 (just to be over 64 bits).
3. This makes the shared secret: bob.public.x ^ 2**65.

Sending to Alice
================
1. At this point we know everyone's public keys.
2. Set received=G, so now we must satisfy: alice.public.x ^ n = bob.public.x ^ 2**65, where n is the Nonce sent to Alice from Bob.
3. The solution is n = bob.public.x ^ alice.public.x ^ 2**65.

Solution code
=============
python
import json, hashlib

from Crypto.Cipher import AES
from Crypto.Util.number import getPrime, long_to_bytes

from fastecdsa.curve import secp256k1
from fastecdsa.point import Point

CURVE = secp256k1
ORDER = CURVE.q
G = CURVE.G

DUMMY_NONCE = 2**65

print('{"Px": %d, "Py": %d, "nonce": %d}' % (G.x, G.y, DUMMY_NONCE))

shared_secret = bob_pub['Px'] ^ DUMMY_NONCE
alice_nonce = bob_pub['Px'] ^ alice_pub['Px'] ^ DUMMY_NONCE
print('{"Px": %d, "Py": %d, "nonce": %d}' % (G.x, G.y, alice_nonce))

aes_key = hashlib.sha1(long_to_bytes(shared_secret)).digest()[:16]

I ended up with the flag: union{https://buttondown.email/cryptography-dispatches/archive/cryptography-dispatches-the-most-backdoor-looking/}