Skip to content

Commit c9039ad

Browse files
authored
Merge pull request #84 from jagerman/perm-blinding-migration
User permission blinding migration
2 parents 985a7be + 7571eb0 commit c9039ad

19 files changed

+870
-174
lines changed

contrib/auth-example.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Example script for demonstrating X-SOGS-* authentication calculation.
22

3-
import nacl.bindings as salt
3+
import nacl.bindings as sodium
44
from nacl.signing import SigningKey
55
from hashlib import blake2b, sha512
66
from base64 import b64encode
@@ -29,10 +29,12 @@ def blinded_ed25519_signature(message_parts, s: SigningKey, ka: bytes, kA: bytes
2929
domain separation for different blinded pubkeys. (This doesn't affect verification at all).
3030
"""
3131
H_rh = sha512(s.encode()).digest()[32:]
32-
r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
33-
sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
34-
HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
35-
sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
32+
r = sodium.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
33+
sig_R = sodium.crypto_scalarmult_ed25519_base_noclamp(r)
34+
HRAM = sodium.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
35+
sig_s = sodium.crypto_core_ed25519_scalar_add(
36+
r, sodium.crypto_core_ed25519_scalar_mul(HRAM, ka)
37+
)
3638
return sig_R + sig_s
3739

3840

@@ -52,7 +54,7 @@ def get_signing_headers(
5254

5355
if blinded:
5456
# 64-byte blake2b hash then reduce to get the blinding factor:
55-
k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
57+
k = sodium.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
5658

5759
# Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
5860
# convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
@@ -61,8 +63,8 @@ def get_signing_headers(
6163
a = s.to_curve25519_private_key().encode()
6264

6365
# Our blinded keypair:
64-
ka = salt.crypto_core_ed25519_scalar_mul(k, a)
65-
kA = salt.crypto_scalarmult_ed25519_base_noclamp(ka)
66+
ka = sodium.crypto_core_ed25519_scalar_mul(k, a)
67+
kA = sodium.crypto_scalarmult_ed25519_base_noclamp(ka)
6668

6769
# Blinded session id:
6870
pubkey = '15' + kA.hex()

contrib/blinding.py

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from nacl.signing import SigningKey, VerifyKey
2-
import nacl.bindings as salt
2+
import nacl.bindings as sodium
33
from nacl.utils import random
44
import nacl.hash
55
from hashlib import blake2b
@@ -16,24 +16,26 @@
1616

1717
# This seems a bit weird, but: sodium's sk-to-curve uses the same private key scalar (a) for
1818
# both curves, so getting `a` for the curve25519 implicitly gives us the ed25519 `a` as well.
19-
a = salt.crypto_sign_ed25519_sk_to_curve25519(s.encode() + A)
19+
a = sodium.crypto_sign_ed25519_sk_to_curve25519(s.encode() + A)
2020

2121
A_xpk = s.to_curve25519_private_key().public_key.encode() # session id is 05 + this
2222

23-
assert salt.crypto_scalarmult_ed25519_base_noclamp(a) == A
24-
assert salt.crypto_scalarmult_base(a) == A_xpk
25-
assert salt.crypto_core_ed25519_is_valid_point(A)
23+
assert sodium.crypto_scalarmult_ed25519_base_noclamp(a) == A
24+
assert sodium.crypto_scalarmult_base(a) == A_xpk
25+
assert sodium.crypto_core_ed25519_is_valid_point(A)
2626

2727
server_pubkey = random(32)
28-
k = salt.crypto_core_ed25519_scalar_reduce(nacl.hash.generichash(server_pubkey, digest_size=64))
28+
k = sodium.crypto_core_ed25519_scalar_reduce(
29+
nacl.hash.generichash(server_pubkey, digest_size=64)
30+
)
2931

30-
ka = salt.crypto_core_ed25519_scalar_mul(k, a)
32+
ka = sodium.crypto_core_ed25519_scalar_mul(k, a)
3133
# kA will be my blinded pubkey visible from my posts, with '15' prepended, and is an *Ed*
3234
# pubkey, not an X pubkey.
33-
kA = salt.crypto_scalarmult_ed25519_noclamp(k, A)
35+
kA = sodium.crypto_scalarmult_ed25519_noclamp(k, A)
3436

35-
assert salt.crypto_scalarmult_ed25519_base_noclamp(ka) == kA
36-
assert salt.crypto_core_ed25519_is_valid_point(kA)
37+
assert sodium.crypto_scalarmult_ed25519_base_noclamp(ka) == kA
38+
assert sodium.crypto_core_ed25519_is_valid_point(kA)
3739

3840
#############################
3941
# Signing (e.g. for X-SOGS-*) with a blinded keypair ka/kA
@@ -45,13 +47,17 @@
4547
# which gives us a bog standard Ed25519 that can be verified using the kA pubkey with standard
4648
# verification code.
4749
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)
50+
H_rh = sodium.crypto_hash_sha512(s.encode())[32:]
51+
r = sodium.crypto_core_ed25519_scalar_reduce(
52+
sodium.crypto_hash_sha512(H_rh + kA + message_to_sign)
53+
)
54+
sig_R = sodium.crypto_scalarmult_ed25519_base_noclamp(r)
55+
HRAM = sodium.crypto_core_ed25519_scalar_reduce(
56+
sodium.crypto_hash_sha512(sig_R + kA + message_to_sign)
57+
)
58+
sig_s = sodium.crypto_core_ed25519_scalar_add(
59+
r, sodium.crypto_core_ed25519_scalar_mul(HRAM, ka)
5360
)
54-
sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
5561
full_sig = sig_R + sig_s
5662

5763
assert VerifyKey(kA).verify(message_to_sign, full_sig)
@@ -62,29 +68,29 @@
6268
# Our user A above wants to send a SOGS DM to another user B:
6369
s2 = SigningKey.generate()
6470
B = s2.verify_key.encode()
65-
b = salt.crypto_sign_ed25519_sk_to_curve25519(s2.encode() + B)
71+
b = sodium.crypto_sign_ed25519_sk_to_curve25519(s2.encode() + B)
6672

6773
# with blinded keys:
68-
kb = salt.crypto_core_ed25519_scalar_mul(k, b)
69-
kB = salt.crypto_scalarmult_ed25519_noclamp(k, B)
74+
kb = sodium.crypto_core_ed25519_scalar_mul(k, b)
75+
kB = sodium.crypto_scalarmult_ed25519_noclamp(k, B)
7076

7177
B_xpk = s2.to_curve25519_private_key().public_key.encode()
7278

73-
assert salt.crypto_scalarmult_ed25519_base_noclamp(kb) == kB
74-
assert salt.crypto_core_ed25519_is_valid_point(kB)
79+
assert sodium.crypto_scalarmult_ed25519_base_noclamp(kb) == kB
80+
assert sodium.crypto_core_ed25519_is_valid_point(kB)
7581

7682
#############################
7783
# Finding friends:
7884

7985
# For example (in reality this would come directly from the known session id):
80-
friend_xpk = salt.crypto_sign_ed25519_pk_to_curve25519(A)
86+
friend_xpk = sodium.crypto_sign_ed25519_pk_to_curve25519(A)
8187

8288
# From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is
8389
# the positive (which is what Signal's XEd25519 conversion always uses):
8490
pk1 = xed25519.pubkey(friend_xpk)
8591

8692
# Blind it:
87-
pk1 = salt.crypto_scalarmult_ed25519_noclamp(k, pk1)
93+
pk1 = sodium.crypto_scalarmult_ed25519_noclamp(k, pk1)
8894

8995
# For the negative, what we're going to get out of the above is simply the negative of pk1, so
9096
# flip the sign bit to get pk2:
@@ -102,7 +108,7 @@
102108
# around gives the same result as the shortcut:
103109
pk2_alt = xed25519.pubkey(friend_xpk)
104110
pk2_alt = pk2_alt[0:31] + bytes([pk2_alt[31] | 0b1000_0000])
105-
pk2_alt = salt.crypto_scalarmult_ed25519_noclamp(k, pk2_alt)
111+
pk2_alt = sodium.crypto_scalarmult_ed25519_noclamp(k, pk2_alt)
106112
assert pk2 == pk2_alt
107113

108114
# Now, if this is really my friend, his blinded key will equal one of these blinded keys:
@@ -128,15 +134,15 @@
128134
# BLAKE2b(b kA || kA || kB)
129135
#
130136
enc_key = blake2b(
131-
salt.crypto_scalarmult_ed25519_noclamp(a, kB) + kA + kB, digest_size=32
137+
sodium.crypto_scalarmult_ed25519_noclamp(a, kB) + kA + kB, digest_size=32
132138
).digest()
133139

134140
# Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey)
135141
plaintext = msg.encode() + A
136142

137143
# Encrypt using xchacha20-poly1305
138144
nonce = random(24)
139-
ciphertext = salt.crypto_aead_xchacha20poly1305_ietf_encrypt(
145+
ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
140146
plaintext, aad=None, nonce=nonce, key=enc_key
141147
)
142148

@@ -151,7 +157,7 @@
151157

152158
# Calculate the shared encryption key (see above)
153159
dec_key = blake2b(
154-
salt.crypto_scalarmult_ed25519_noclamp(b, kA) + kA + kB, digest_size=32
160+
sodium.crypto_scalarmult_ed25519_noclamp(b, kA) + kA + kB, digest_size=32
155161
).digest()
156162

157163
assert enc_key == dec_key
@@ -162,19 +168,21 @@
162168
assert v == 0x00 # Make sure our encryption version is okay
163169

164170
# Decrypt
165-
plaintext = salt.crypto_aead_xchacha20poly1305_ietf_decrypt(ct, aad=None, nonce=nc, key=dec_key)
171+
plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
172+
ct, aad=None, nonce=nc, key=dec_key
173+
)
166174

167175
assert len(plaintext) > 32
168176

169177
# Split up: the last 32 bytes are the sender's *unblinded* ed25519 key
170178
message, sender_edpk = plaintext[:-32], plaintext[-32:]
171179

172180
# Verify that the inner sender_edpk (A) yields the same outer kA we got with the message
173-
assert kA == salt.crypto_scalarmult_ed25519_noclamp(k, sender_edpk)
181+
assert kA == sodium.crypto_scalarmult_ed25519_noclamp(k, sender_edpk)
174182

175183
message = message.decode() # utf-8 bytes back to str
176184

177-
sender_session_id = '05' + salt.crypto_sign_ed25519_pk_to_curve25519(sender_edpk).hex()
185+
sender_session_id = '05' + sodium.crypto_sign_ed25519_pk_to_curve25519(sender_edpk).hex()
178186

179187
assert message == msg
180188
assert sender_edpk == A

contrib/pg-import.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"user_ban_futures",
6666
"user_request_nonces",
6767
"inbox",
68+
"needs_blinding",
6869
]
6970

7071
with pgsql.transaction():

sogs/__main__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ def print_room(room: Room):
265265

266266
if args.rooms == ['+']:
267267
for sid in args.add_moderators:
268-
u = User(session_id=sid)
268+
u = User(session_id=sid, try_blinding=True)
269269
u.set_moderator(admin=args.admin, visible=args.visible, added_by=sysadmin)
270270
print(
271271
"Added {} as {} global {}".format(
@@ -284,7 +284,7 @@ def print_room(room: Room):
284284
print(f"No such room: '{nsr.token}'", file=sys.stderr)
285285

286286
for sid in args.add_moderators:
287-
u = User(session_id=sid)
287+
u = User(session_id=sid, try_blinding=True)
288288
for room in rooms:
289289
room.set_moderator(u, admin=args.admin, visible=not args.hidden, added_by=sysadmin)
290290
print(
@@ -315,7 +315,7 @@ def print_room(room: Room):
315315

316316
if args.rooms == ['+']:
317317
for sid in args.delete_moderators:
318-
u = User(session_id=sid)
318+
u = User(session_id=sid, try_blinding=True)
319319
was_admin = u.global_admin
320320
if not u.global_admin and not u.global_moderator:
321321
print(f"{u.session_id} was not a global moderator")
@@ -332,7 +332,7 @@ def print_room(room: Room):
332332
print(f"No such room: '{nsr.token}'", file=sys.stderr)
333333

334334
for sid in args.delete_moderators:
335-
u = User(session_id=sid)
335+
u = User(session_id=sid, try_blinding=True)
336336
for room in rooms:
337337
room.remove_moderator(u, removed_by=sysadmin)
338338
print(f"Removed {u.session_id} as moderator/admin of {room.name} ({room.token})")

sogs/crypto.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
from nacl.public import PrivateKey
77
from nacl.signing import SigningKey, VerifyKey
88
from nacl.encoding import Base64Encoder, HexEncoder
9-
from nacl.bindings import crypto_scalarmult
9+
import nacl.bindings as sodium
1010

1111

1212
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
1313
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
1414

15-
from .utils import decode_hex_or_b64
1615
from .hashing import blake2b
1716

18-
import binascii
1917
import secrets
2018
import hmac
2119
import functools
@@ -93,15 +91,63 @@ def server_encrypt(pk, data):
9391
xed25519_verify = pyonionreq.xed25519.verify
9492
xed25519_pubkey = pyonionreq.xed25519.pubkey
9593

94+
# AKA "k" for blinding crypto:
95+
blinding_factor = sodium.crypto_core_ed25519_scalar_reduce(
96+
blake2b(server_pubkey_bytes, digest_size=64)
97+
)
98+
9699

97100
@functools.lru_cache(maxsize=1024)
98-
def compute_derived_key_bytes(pk_bytes):
99-
"""compute derived key as bytes with no prefix"""
100-
return crypto_scalarmult(server_pubkey_hash_bytes, pk_bytes)
101+
def compute_blinded_abs_key(x_pk: bytes, *, k: bytes = blinding_factor):
102+
"""
103+
Computes the *positive* blinded Ed25519 pubkey from an unprefixed session X25519 pubkey (i.e. 32
104+
bytes). The returned value will always have the sign bit (i.e. the most significant bit of the
105+
last byte) set to 0; the actual derived key associated with this session id could have either
106+
sign.
107+
108+
Input and result are in bytes, without the 0x05 or 0x15 prefix.
109+
110+
k allows you to compute for an alternative blinding factor, but should normally be omitted.
111+
"""
112+
A = xed25519_pubkey(x_pk)
113+
kA = sodium.crypto_scalarmult_ed25519_noclamp(k, A)
101114

115+
if kA[31] & 0x80:
116+
return kA[0:31] + bytes([kA[31] & 0x7F])
117+
return kA
118+
119+
120+
def compute_blinded_abs_id(session_id: str, *, k: bytes = blinding_factor):
121+
"""
122+
Computes the *positive* blinded id, as hex, from a prefixed, hex session id. This function is a
123+
wrapper around compute_derived_key_bytes that handles prefixes and hex conversions.
124+
125+
k allows you to compute for an alternative blinding factor, but should normally be omitted.
126+
"""
127+
return '15' + compute_blinded_abs_key(bytes.fromhex(session_id[2:]), k=k).hex()
128+
129+
130+
def blinded_abs(blinded_id: str):
131+
"""
132+
Takes a blinded hex pubkey (i.e. length 66, prefixed with 15) and returns the positive pubkey
133+
alternative: that is, if the pubkey is already positive, it is returned as-is; otherwise the
134+
returned value is a copy with the sign bit cleared.
135+
"""
136+
137+
# Sign bit is the MSB of the last byte, which will be at [31] of the private key, hence 64 is
138+
# the most significant nibble once we convert to hex and add 2 for the prefix:
139+
msn = int(blinded_id[64], 16)
140+
if msn & 0x8:
141+
return blinded_id[0:64] + str(msn & 0x7) + blinded_id[65:]
142+
return blinded_id
143+
144+
145+
def blinded_neg(blinded_id: str):
146+
"""
147+
Counterpart to blinded_abs that always returns the *negative* pubkey alternative.
148+
"""
102149

103-
def compute_derived_id(session_id, prefix='15'):
104-
"""compute derived session"""
105-
return prefix + binascii.hexlify(
106-
compute_derived_key_bytes(decode_hex_or_b64(session_id[2:], 32))
107-
).decode('ascii')
150+
msn = int(blinded_id[64], 16)
151+
if msn & 0x8:
152+
return blinded_id
153+
return blinded_id[0:64] + f"{msn | 0x8:x}" + blinded_id[65:]

sogs/db.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ def database_init(create=None, upgrade=True):
174174
# Make sure the system admin users exists
175175
create_admin_user(conn)
176176

177+
check_needs_blinding(conn)
178+
177179
return created or migrated
178180

179181

@@ -195,6 +197,36 @@ def create_admin_user(dbconn):
195197
)
196198

197199

200+
def check_needs_blinding(dbconn):
201+
if not config.REQUIRE_BLIND_KEYS:
202+
return
203+
204+
with transaction(dbconn):
205+
for uid, sid in query(
206+
"""
207+
SELECT id, session_id FROM users WHERE id IN (
208+
SELECT "user" FROM user_permission_overrides
209+
UNION
210+
SELECT "user" FROM user_permission_futures
211+
UNION
212+
SELECT "user" FROM user_ban_futures
213+
UNION
214+
SELECT id FROM users WHERE session_id LIKE '05%' AND (admin OR moderator OR banned)
215+
EXCEPT
216+
SELECT "user" FROM needs_blinding
217+
)
218+
""",
219+
dbconn=dbconn,
220+
):
221+
pos_derived = crypto.compute_blinded_abs_id(sid)
222+
query(
223+
'INSERT INTO needs_blinding (blinded_abs, "user") VALUES (:blinded, :uid)',
224+
blinded=pos_derived,
225+
uid=uid,
226+
dbconn=dbconn,
227+
)
228+
229+
198230
engine, engine_initial_pid, metadata = None, None, None
199231

200232

0 commit comments

Comments
 (0)