Skip to content
22 changes: 18 additions & 4 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,31 @@ def banned_user(db):


@pytest.fixture
def blind_user(db):
def blind15_user(db):
import user

return user.User(blinded=True)
return user.User(blinded15=True)


@pytest.fixture
def blind_user2(db):
def blind15_user2(db):
import user

return user.User(blinded=True)
return user.User(blinded15=True)


@pytest.fixture
def blind25_user(db):
import user

return user.User(blinded25=True)


@pytest.fixture
def blind25_user2(db):
import user

return user.User(blinded25=True)


@pytest.fixture
Expand Down
1 change: 0 additions & 1 deletion contrib/auth-example.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def get_signing_headers(
body,
blinded: bool = True,
):

assert len(server_pk) == 32
assert len(nonce) == 16

Expand Down
64 changes: 64 additions & 0 deletions contrib/blind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python3

import sys
import nacl.bindings as sodium
import nacl.hash
import nacl.signing
from nacl.encoding import RawEncoder
from pyonionreq import xed25519

if len(sys.argv) < 3:
print(
f"Usage: {sys.argv[0]} SERVERPUBKEY {{SESSIONID|\"RANDOM\"}} [SESSIONID ...] -- blinds IDs",
file=sys.stderr,
)
sys.exit(1)

server_pk = sys.argv[1]
sids = sys.argv[2:]

if len(server_pk) != 64 or not all(c in '0123456789ABCDEFabcdef' for c in server_pk):
print(f"Invalid argument: expected 64 hex digit server pk as first argument")
sys.exit(2)

server_pk = bytes.fromhex(server_pk)

print(nacl.hash.blake2b(server_pk, digest_size=64, encoder=RawEncoder))

k15 = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(server_pk, digest_size=64, encoder=RawEncoder)
)


for i in range(len(sids)):
if sids[i] == "RANDOM":
sids[i] = (
"05"
+ nacl.signing.SigningKey.generate()
.verify_key.to_curve25519_public_key()
.encode()
.hex()
)
if (
len(sids[i]) != 66
or not sids[i].startswith('05')
or not all(c in '0123456789ABCDEFabcdef' for c in sids[i])
):
print(f"Invalid session id: expected 66 hex digit id as first argument")

print(f"SOGS pubkey: {server_pk.hex()}")

for s in sids:
s = bytes.fromhex(s)

if s[0] == 0x05:
k25 = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(s[1:] + server_pk, digest_size=64, encoder=RawEncoder)
)

pk15 = sodium.crypto_scalarmult_ed25519_noclamp(k15, xed25519.pubkey(s[1:]))
pk25 = sodium.crypto_scalarmult_ed25519_noclamp(k25, xed25519.pubkey(s[1:]))

print(
f"{s.hex()} blinds to:\n - 15{pk15.hex()} or …{pk15[31] ^ 0x80:02x}\n - 25{pk25.hex()} or …{pk25[31] ^ 0x80:02x}"
)
111 changes: 111 additions & 0 deletions contrib/blind25-testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3

import sys
import nacl.bindings as sodium
import nacl.hash
import nacl.signing
from nacl.encoding import RawEncoder
from pyonionreq import xed25519

server_pk = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000001")

to_sign = "hello!"

for i in range(1000):
sk = nacl.signing.SigningKey.generate()
pk = sk.verify_key
xpk = pk.to_curve25519_public_key()
sid = "05" + xpk.encode().hex()

k25 = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(
bytes.fromhex(sid) + server_pk, digest_size=64, encoder=RawEncoder, key=b"SOGS_blind_v2"
)
)

# Comment notation:
# P = server pubkey
# a/A = ed25519 keypair
# b/B = x25519 keypair, converted from a/A
# S = session id = 0x05 || B
# T = |A|, that is, A with the sign bit cleared
# t = private scalar s.t. tG = T (which is ± the private scalar associated with A)
# k = blinding factor = H_64(S || P, key="SOGS_blind_v2")

# This is simulating what the blinding client (i.e. with full keys) can compute:

# k * A
pk25a = sodium.crypto_scalarmult_ed25519_noclamp(k25, pk.encode())
# -k * A
neg_k25 = sodium.crypto_core_ed25519_scalar_negate(k25)
pk25b = sodium.crypto_scalarmult_ed25519_noclamp(neg_k25, pk.encode())

# print(f"k: {k25.hex()}")
# print(f"-k: {neg_k25.hex()}")
#
# print(f"a: {pk25a.hex()}")
# print(f"b: {pk25b.hex()}")

assert pk25a != pk25b
assert pk25a[0:31] == pk25b[0:31]
assert pk25a[31] ^ 0x80 == pk25b[31]

# The one we want to use is what we would end up with *if* our Ed25519 had been positive (but of
# course there's a 50% chance it's negative).
ed_pk_is_positive = pk.encode()[31] & 0x80 == 0

pk25 = pk25a if ed_pk_is_positive else pk25b

###########
# Make sure we can get to pk25 from the session id
# We know sid and server_pk, so we can compute k25
T_pk25 = sodium.crypto_scalarmult_ed25519_noclamp(k25, xed25519.pubkey(xpk.encode()))
assert T_pk25 == pk25

# To sign something that validates with pk25 we have a bit more work

# First get our blinded, private scalar; we'll call it j

# We want to pick j such that it is always associated with |A|, that is, our positive pubkey,
# even if our pubkey is negative, so that someone with our session id can get our signing pubkey
# deterministically.

t = (
sk.to_curve25519_private_key().encode()
) # The value we get here is actually our private scalar, despite the name
if pk.encode()[31] & 0x80:
# If our actual pubkey is negative then negate j so that it is as if we are working from the
# positive version of our pubkey
t = sodium.crypto_core_ed25519_scalar_negate(t)

kt = sodium.crypto_core_ed25519_scalar_mul(k25, t)

kT = sodium.crypto_scalarmult_ed25519_base_noclamp(kt)
assert kT == pk25

# Now we more or less follow EdDSA, but with our blinded scalar instead of real scalar, and with
# a different hash function. (See comments in libsession-util config/groups/keys.cpp for more
# details).
hseed = nacl.hash.blake2b(
sk.encode()[0:31], key=b"SOGS25Seed", encoder=nacl.encoding.RawEncoder
)
r = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(
hseed + pk25 + to_sign.encode(), 64, key=b"SOGS25Sig", encoder=nacl.encoding.RawEncoder
)
)
R = sodium.crypto_scalarmult_ed25519_base_noclamp(r)

# S = r + H(R || A || M) a (with A=kT, a=kt)
hram = nacl.hash.sha512(R + kT + to_sign.encode(), encoder=nacl.encoding.RawEncoder)
S = sodium.crypto_core_ed25519_scalar_reduce(hram)
S = sodium.crypto_core_ed25519_scalar_mul(S, kt)
S = sodium.crypto_core_ed25519_scalar_add(S, r)

sig = R + S

###########################################
# Test bog standard Ed25519 signature verification:

vk = nacl.signing.VerifyKey(pk25)
vk.verify(to_sign.encode(), sig)
2 changes: 0 additions & 2 deletions contrib/pg-import.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@


with pgsql.transaction():

curin = old.cursor()
curout = pgsql.cursor()

Expand Down Expand Up @@ -131,7 +130,6 @@
curout.execute("ALTER TABLE rooms DROP CONSTRAINT room_image_fk")

def copy(table):

cols = [r['name'] for r in curin.execute(f"PRAGMA table_info({table})")]
if not cols:
raise RuntimeError(f"Expected table {table} does not exist in sqlite db")
Expand Down
2 changes: 1 addition & 1 deletion sogs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.3.8.dev0"
__version__ = "0.4.0.dev0"
76 changes: 29 additions & 47 deletions sogs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,6 @@ def parse_and_set_perm_flags(flags, perm_setting):
sys.exit(2)

elif update_room:

rooms = []
all_rooms = False
global_rooms = False
Expand Down Expand Up @@ -428,15 +427,15 @@ def parse_and_set_perm_flags(flags, perm_setting):

if args.add_moderators:
for a in args.add_moderators:
if not re.fullmatch(r'[01]5[A-Fa-f0-9]{64}', a):
if not re.fullmatch(r'[012]5[A-Fa-f0-9]{64}', a):
print(f"Error: '{a}' is not a valid session id", file=sys.stderr)
sys.exit(1)

sysadmin = SystemUser()

if global_rooms:
for sid in args.add_moderators:
u = User(session_id=sid, try_blinding=True)
u = User(session_id=sid)
u.set_moderator(admin=args.admin, visible=args.visible, added_by=sysadmin)
print(
"Added {} as {} global {}".format(
Expand All @@ -447,7 +446,7 @@ def parse_and_set_perm_flags(flags, perm_setting):
)
else:
for sid in args.add_moderators:
u = User(session_id=sid, try_blinding=True)
u = User(session_id=sid)
for room in rooms:
room.set_moderator(
u, admin=args.admin, visible=not args.hidden, added_by=sysadmin
Expand All @@ -464,57 +463,40 @@ def parse_and_set_perm_flags(flags, perm_setting):

if args.delete_moderators:
for a in args.delete_moderators:
if not re.fullmatch(r'[01]5[A-Fa-f0-9]{64}', a):
if not re.fullmatch(r'[012]5[A-Fa-f0-9]{64}', a):
print(f"Error: '{a}' is not a valid session id", file=sys.stderr)
sys.exit(1)

sysadmin = SystemUser()

if global_rooms:
for sid in args.delete_moderators:
u = User(session_id=sid, try_blinding=True)
was_admin = u.global_admin
if not u.global_admin and not u.global_moderator:
print(f"{u.session_id} was not a global moderator")
else:
u.remove_moderator(removed_by=sysadmin)
print(
f"Removed {u.session_id} as global {'admin' if was_admin else 'moderator'}"
)

if u.is_blinded and sid.startswith('05'):
try:
u2 = User(session_id=sid, try_blinding=False, autovivify=False)
if u2.global_admin or u2.global_moderator:
was_admin = u2.global_admin
u2.remove_moderator(removed_by=sysadmin)
print(
f"Removed {u2.session_id} as global "
f"{'admin' if was_admin else 'moderator'}"
)
except NoSuchUser:
pass
try:
u = User(session_id=sid, autovivify=False)
if u.global_admin or u.global_moderator:
was_admin = u.global_admin
u.remove_moderator(removed_by=sysadmin)
print(
f"Removed {u.session_id} "
f"(identified by {sid}) "
f"as global {'admin' if was_admin else 'moderator'}"
)
except NoSuchUser:
pass
else:
for sid in args.delete_moderators:
u = User(session_id=sid, try_blinding=True)
u2 = None
if u.is_blinded and sid.startswith('05'):
try:
u2 = User(session_id=sid, try_blinding=False, autovivify=False)
except NoSuchUser:
pass

for room in rooms:
room.remove_moderator(u, removed_by=sysadmin)
print(
f"Removed {u.session_id} as moderator/admin of {room.name} ({room.token})"
)
if u2 is not None:
room.remove_moderator(u2, removed_by=sysadmin)
try:
u = User(session_id=sid, autovivify=False)
for room in rooms:
room.remove_moderator(u, removed_by=sysadmin)
print(
f"Removed {u2.session_id} as moderator/admin of {room.name} "
f"({room.token})"
f"Removed {u.session_id} "
f"(identified by {sid}) "
f"as moderator/admin of {room.name} ({room.token})"
)
except NoSuchUser:
pass


if args.add_perms or args.clear_perms or args.remove_perms:
if global_rooms:
Expand All @@ -524,9 +506,10 @@ def parse_and_set_perm_flags(flags, perm_setting):
)
sys.exit(1)

vivify = args.add_perms or args.remove_perms
users = []
if args.users:
users = [User(session_id=sid, try_blinding=True) for sid in args.users]
users = [User(session_id=sid, autovivify=vivify) for sid in args.users]

# users not specified means set room defaults
if not len(users):
Expand Down Expand Up @@ -577,8 +560,7 @@ def parse_and_set_perm_flags(flags, perm_setting):
if args.name is not None:
if global_rooms or all_rooms:
print(
"Error: --rooms cannot be '+' or '*' (i.e. global/all) with --name",
file=sys.stderr,
"Error: --rooms cannot be '+' or '*' (i.e. global/all) with --name", file=sys.stderr
)
sys.exit(1)

Expand Down
6 changes: 5 additions & 1 deletion sogs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
ALPHABET_SILENT = True
FILTER_MODS = False
REQUIRE_BLIND_KEYS = True
REQUIRE_BLIND_V2 = False
TEMPLATE_PATH = 'templates'
STATIC_PATH = 'static'
UPLOAD_PATH = 'uploads'
Expand Down Expand Up @@ -147,7 +148,10 @@ def reply_to_format(v):
'active_prune_threshold': ('ROOM_ACTIVE_PRUNE_THRESHOLD', None, days_to_seconds),
},
'direct_messages': {'expiry': ('DM_EXPIRY', None, days_to_seconds)},
'users': {'require_blind_keys': bool_opt('REQUIRE_BLIND_KEYS')},
'users': {
'require_blind_keys': bool_opt('REQUIRE_BLIND_KEYS'),
'require_blind_v2': bool_opt('REQUIRE_BLIND_V2'),
},
'messages': {
'history_prune_threshold': ('MESSAGE_HISTORY_PRUNE_THRESHOLD', None, days_to_seconds),
'profanity_filter': bool_opt('PROFANITY_FILTER'),
Expand Down
Loading