Skip to content

Commit 4d1b717

Browse files
jagermanmajestrate
authored andcommitted
Add blinding example script
1 parent e3375dd commit 4d1b717

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

contrib/blinding.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from nacl.signing import SigningKey, VerifyKey
2+
from nacl.public import PublicKey
3+
import nacl.bindings as salt
4+
from nacl.utils import random
5+
import nacl.hash
6+
from hashlib import blake2b
7+
8+
from pyonionreq import xed25519
9+
10+
worked, trials = 0, 10000
11+
12+
pos_pk, neg_pk = 0, 0
13+
14+
for i in range(trials):
15+
s = SigningKey.generate()
16+
A = s.verify_key.encode()
17+
18+
# This seems a bit weird, but: sodium's sk-to-curve uses the same private key scalar (a) for
19+
# both curves, so getting `a` for the curve25519 implicitly gives us the ed25519 `a` as well.
20+
a = salt.crypto_sign_ed25519_sk_to_curve25519(s.encode() + A)
21+
22+
A_xpk = s.to_curve25519_private_key().public_key.encode() # session id is 05 + this
23+
24+
assert salt.crypto_scalarmult_ed25519_base_noclamp(a) == A
25+
assert salt.crypto_scalarmult_base(a) == A_xpk
26+
assert salt.crypto_core_ed25519_is_valid_point(A)
27+
28+
server_pubkey = random(32)
29+
k = salt.crypto_core_ed25519_scalar_reduce(nacl.hash.generichash(server_pubkey, digest_size=64))
30+
31+
ka = salt.crypto_core_ed25519_scalar_mul(k, a)
32+
# kA will be my blinded pubkey visible from my posts, with '15' prepended, and is an *Ed*
33+
# pubkey, not an X pubkey.
34+
kA = salt.crypto_scalarmult_ed25519_noclamp(k, A)
35+
36+
assert salt.crypto_scalarmult_ed25519_base_noclamp(ka) == kA
37+
assert salt.crypto_core_ed25519_is_valid_point(kA)
38+
39+
##### Signing (e.g. for X-SOGS-*) with a blinded keypair ka/kA
40+
41+
# This generation is *almost* just bog standard Ed25519 but we have one change: when generating
42+
# r we add kA into the hash r = H(H_rh || kA || M), rather than r = H(H_rh || M), so that there
43+
# is domain separation for r for different blinded keys. (H_rh here = right half of hash of the
44+
# secret key bytes.) After that we do the standard Ed25519 `r + H(R || kA || M)a` calculation,
45+
# which gives us a bog standard Ed25519 that can be verified using the kA pubkey with standard
46+
# verification code.
47+
message_to_sign = b'omg happy days'
48+
H_rh = salt.crypto_hash_sha512(s.encode())[32:]
49+
r = salt.crypto_core_ed25519_scalar_reduce(salt.crypto_hash_sha512(H_rh + kA + message_to_sign))
50+
sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
51+
HRAM = salt.crypto_core_ed25519_scalar_reduce(
52+
salt.crypto_hash_sha512(sig_R + kA + message_to_sign)
53+
)
54+
sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
55+
full_sig = sig_R + sig_s
56+
57+
assert VerifyKey(kA).verify(message_to_sign, full_sig)
58+
59+
##### Sending a DM
60+
61+
# Our user A above wants to send a SOGS DM to another user B:
62+
s2 = SigningKey.generate()
63+
B = s2.verify_key.encode()
64+
b = salt.crypto_sign_ed25519_sk_to_curve25519(s2.encode() + B)
65+
66+
# with blinded keys:
67+
kb = salt.crypto_core_ed25519_scalar_mul(k, b)
68+
kB = salt.crypto_scalarmult_ed25519_noclamp(k, B)
69+
70+
B_xpk = s2.to_curve25519_private_key().public_key.encode()
71+
72+
assert salt.crypto_scalarmult_ed25519_base_noclamp(kb) == kB
73+
assert salt.crypto_core_ed25519_is_valid_point(kB)
74+
75+
##### Finding friends:
76+
# For example (in reality this would come directly from the known session id):
77+
friend_xpk = salt.crypto_sign_ed25519_pk_to_curve25519(A)
78+
79+
# From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is
80+
# the positive (which is what Signal's XEd25519 conversion always uses):
81+
pk1 = xed25519.pubkey(friend_xpk)
82+
83+
# Blind it:
84+
pk1 = salt.crypto_scalarmult_ed25519_noclamp(k, pk1)
85+
86+
# For the negative, what we're going to get out of the above is simply the negative of pk1, so
87+
# flip the sign bit to get pk2:
88+
pk2 = pk1[0:31] + bytes([pk1[31] ^ 0b1000_0000])
89+
90+
# Optimization for Session:
91+
# - because the two blinded alternatives here differ by only the sign bit you can just always
92+
# force the sign bit to be 0 when looking up a blinded -> real session id. That is, when you
93+
# store it, you compute pk1 as above, then do a `pk1[31] &= 0x7f` to clear the sign bit, and
94+
# when looking up a blinded id to see if it's a friend, you also do the same bit clearing
95+
# before looking it up (which saves having to store two keys for each contact/server
96+
# combination).
97+
98+
# This calculation is completely unnecessary and is only here to verify that going the long way
99+
# around gives the same result as the shortcut:
100+
pk2_alt = xed25519.pubkey(friend_xpk)
101+
pk2_alt = pk2_alt[0:31] + bytes([pk2_alt[31] | 0b1000_0000])
102+
pk2_alt = salt.crypto_scalarmult_ed25519_noclamp(k, pk2_alt)
103+
assert pk2 == pk2_alt
104+
105+
# Now, if this is really my friend, his blinded key will equal one of these blinded keys:
106+
if kA == pk1:
107+
pos_pk += 1
108+
elif kA == pk2:
109+
neg_pk += 1
110+
else:
111+
# not my friend
112+
print("failed; got neither ±A")
113+
continue
114+
115+
##### Encrypting a SOGS DM
116+
msg = 'hello 🎂'
117+
118+
# Step one: calculate a shared secret, sending from A to B. We're going to calculate:
119+
#
120+
# BLAKE2b(a kB || kA || kB)
121+
#
122+
# from the sender, and the receiver can calculate this same value as:
123+
#
124+
# BLAKE2b(b kA || kA || kB)
125+
#
126+
enc_key = blake2b(
127+
salt.crypto_scalarmult_ed25519_noclamp(a, kB) + kA + kB, digest_size=32
128+
).digest()
129+
130+
# Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey)
131+
plaintext = msg.encode() + A
132+
133+
# Encrypt using xchacha20-poly1305
134+
nonce = random(24)
135+
ciphertext = salt.crypto_aead_xchacha20poly1305_ietf_encrypt(
136+
plaintext, aad=None, nonce=nonce, key=enc_key
137+
)
138+
139+
data = b'\x00' + ciphertext + nonce
140+
141+
##### Decrypting a SOGS DM
142+
# Opening the box on the recipient end.
143+
144+
# I receive alongside the message from sogs (i.e. this is the blinded session id minus the '15')
145+
# kA=...
146+
147+
# Calculate the shared encryption key (see above)
148+
dec_key = blake2b(
149+
salt.crypto_scalarmult_ed25519_noclamp(b, kA) + kA + kB, digest_size=32
150+
).digest()
151+
152+
assert enc_key == dec_key
153+
154+
assert len(data) > 25
155+
v, ct, nc = data[0], data[1:-24], data[-24:]
156+
157+
assert v == 0x00 # Make sure our encryption version is okay
158+
159+
# Decrypt
160+
plaintext = salt.crypto_aead_xchacha20poly1305_ietf_decrypt(ct, aad=None, nonce=nc, key=dec_key)
161+
162+
assert len(plaintext) > 32
163+
164+
# Split up: the last 32 bytes are the sender's *unblinded* ed25519 key
165+
message, sender_edpk = plaintext[:-32], plaintext[-32:]
166+
167+
# Verify that the inner sender_edpk (A) yields the same outer kA we got with the message
168+
assert kA == salt.crypto_scalarmult_ed25519_noclamp(k, sender_edpk)
169+
170+
message = message.decode() # utf-8 bytes back to str
171+
172+
sender_session_id = '05' + salt.crypto_sign_ed25519_pk_to_curve25519(sender_edpk).hex()
173+
174+
assert message == msg
175+
assert sender_edpk == A
176+
assert sender_session_id == '05' + A_xpk.hex()
177+
178+
worked += 1
179+
180+
181+
print(f"{worked} successes / {trials} trials")
182+
print(f"{pos_pk} positive Ed keys, {neg_pk} negative")

0 commit comments

Comments
 (0)