Skip to content

Commit 6c1a716

Browse files
committed
Add Vaccount auth flow to current arhitecture
- login with owner key `VA` - login with operational `VA` - login with ephemeral `VA` Fixed: - Bug with display name table row
1 parent ee910d7 commit 6c1a716

File tree

9 files changed

+653
-48
lines changed

9 files changed

+653
-48
lines changed

synapse/api/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
# the maximum length for a user id is 255 characters
2828
MAX_USERID_LENGTH = 255
29+
USERID_BYTES_LENGTH = 20
2930

3031
# The maximum length for a group id is 255 characters
3132
MAX_GROUPID_LENGTH = 255

synapse/handlers/register.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from prometheus_client import Counter
2222

2323
from synapse import types
24-
from synapse.api.constants import MAX_USERID_LENGTH, EventTypes, JoinRules, LoginType
24+
from synapse.api.constants import EventTypes, JoinRules, LoginType
2525
from synapse.api.errors import AuthError, Codes, ConsentNotGivenError, SynapseError
2626
from synapse.appservice import ApplicationService
2727
from synapse.config.server import is_threepid_reserved
@@ -33,7 +33,7 @@
3333
)
3434
from synapse.spam_checker_api import RegistrationBehaviour
3535
from synapse.storage.state import StateFilter
36-
from synapse.types import RoomAlias, UserID, create_requester
36+
from synapse.types import RoomAlias, UserID, create_requester, is_valid_mxid_len
3737

3838
from ._base import BaseHandler
3939

@@ -96,10 +96,15 @@ async def check_username(
9696
if types.contains_invalid_mxid_characters(localpart):
9797
raise SynapseError(
9898
400,
99-
"User ID can only contain characters a-z, 0-9, or '=_-./'",
99+
"User ID can only contain hexdigits",
100100
Codes.INVALID_USERNAME,
101101
)
102102

103+
if not is_valid_mxid_len(localpart):
104+
raise SynapseError(
105+
400, "User ID length must be 20 bytes", Codes.INVALID_USERNAME
106+
)
107+
103108
if not localpart:
104109
raise SynapseError(400, "User ID cannot be empty", Codes.INVALID_USERNAME)
105110

@@ -122,13 +127,6 @@ async def check_username(
122127

123128
self.check_user_id_not_appservice_exclusive(user_id)
124129

125-
if len(user_id) > MAX_USERID_LENGTH:
126-
raise SynapseError(
127-
400,
128-
"User ID may not be longer than %s characters" % (MAX_USERID_LENGTH,),
129-
Codes.INVALID_USERNAME,
130-
)
131-
132130
users = await self.store.get_users_by_id_case_insensitive(user_id)
133131
if users:
134132
if not guest_access_token:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Top-level module `Vaccount` auth Provider for `synapse`.
2+
This module provider login/registration based on signature by private key associated
3+
with `Vaccount` stored on `VelasChain` or ephemeral.
4+
"""
5+
6+
__version__ = "0.1.0"
7+
__version_info__ = tuple(
8+
int(i) for i in __version__.split(".") if i.isdigit()
9+
)
10+
11+
from synapse.handlers.vaccount_auth.auth_provider import VaccountAuthProvider
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Shared Secret Authenticator module for Matrix Synapse
4+
# Copyright (C) 2018 Slavi Pantaleev
5+
#
6+
# http://devture.com/
7+
#
8+
# This program is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU Affero General Public License as
10+
# published by the Free Software Foundation, either version 3 of the
11+
# License, or (at your option) any later version.
12+
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU Affero General Public License for more details.
17+
18+
# You should have received a copy of the GNU Affero General Public License
19+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
#
21+
from base58 import b58decode
22+
from unpaddedbase64 import encode_base64
23+
from hashlib import sha256
24+
25+
from twisted.internet import defer
26+
from solana.publickey import PublicKey
27+
from nacl.signing import VerifyKey
28+
from nacl.exceptions import BadSignatureError
29+
30+
import logging
31+
32+
from synapse.handlers.vaccount_auth.constants import SIGN_TIMESTAMP_TOLERANCE, OPERATIONAL_STATE
33+
from synapse.handlers.vaccount_auth.utils import VaccountInfo, get_vaccount_evm_address
34+
from synapse.api.errors import HttpResponseException
35+
from synapse.handlers.vaccount_auth.utils import is_valid_vaccount_address
36+
from synapse.module_api import ModuleApi
37+
from synapse.storage import Databases, DataStore
38+
39+
logger = logging.getLogger(__name__)
40+
41+
42+
class VaccountAuthProvider:
43+
"""
44+
Provide a login/registration by Vaccount flow.
45+
"""
46+
47+
def __init__(self, config, account_handler: ModuleApi):
48+
self.account_handler = account_handler
49+
self.store: DataStore = account_handler._hs.get_datastore()
50+
self.last_tonce = int(account_handler._hs.get_clock().time())
51+
52+
@staticmethod
53+
def get_supported_login_types():
54+
supported_login_types = {
55+
'm.login.vaccount': (
56+
'vaccount_address',
57+
'signature',
58+
'signer',
59+
'signed_timestamp',
60+
'signer_type',
61+
)
62+
}
63+
64+
return supported_login_types
65+
66+
async def check_auth(self, evm_vaccount_address, login_type, login_dict):
67+
"""Attempt to authenticate a user by Vaccount flow and register an account if none exists.
68+
69+
Args:
70+
evm_vaccount_address: ethereum based interpretation of the Vaccount address
71+
login_type: type of authentication
72+
login_dict: authentication parameters `supported_login_types
73+
74+
Returns:
75+
Canonical user ID if authentication was successful
76+
"""
77+
vaccount_address = login_dict.get('vaccount_address')
78+
signature = login_dict.get('signature')
79+
signer_key = login_dict.get('signer')
80+
signer_type = login_dict.get('signer_type')
81+
signed_timestamp = int(login_dict.get('signed_timestamp'))
82+
display_name = login_dict.get('displayname')
83+
84+
if not signature or not signer_key or not signed_timestamp or not vaccount_address or not signer_type:
85+
return False
86+
87+
if not self._is_valid_sign_timestamp(signed_timestamp):
88+
return False
89+
90+
if evm_vaccount_address.startswith("@") and ":" in evm_vaccount_address:
91+
# username is of the form @V4Bw2..:bar.com
92+
evm_vaccount_address = evm_vaccount_address.split(":", 1)[0][1:]
93+
94+
# if evm_vaccount_address.startswith("0x"):
95+
# evm_vaccount_address = evm_vaccount_address[2:]
96+
97+
msg = f'{vaccount_address}*{signed_timestamp}'
98+
signed_msg = sha256(msg.encode()).digest()
99+
100+
is_valid_signature = self._is_valid_signature(
101+
signature=bytes.fromhex(signature),
102+
signer_key=signer_key,
103+
signed_msg=signed_msg
104+
)
105+
106+
is_active_vaccount = await self._is_active_vaccount(
107+
vaccount_address=PublicKey(vaccount_address),
108+
signer=PublicKey(signer_key),
109+
signer_type=signer_type,
110+
)
111+
112+
expected_evm_address = get_vaccount_evm_address(PublicKey(vaccount_address))
113+
is_valid_evm_address = expected_evm_address == evm_vaccount_address
114+
115+
if not is_valid_signature or not is_active_vaccount or not is_valid_evm_address:
116+
return False
117+
118+
user_id = self.account_handler.get_qualified_user_id(username=evm_vaccount_address)
119+
120+
if await self.account_handler.check_user_exists(user_id):
121+
return user_id
122+
123+
else:
124+
user_id = await self.register_user(
125+
localpart=evm_vaccount_address,
126+
displayname=display_name,
127+
)
128+
129+
# signer_key = encode_base64(b58decode(signer_key))
130+
# vaccount_signing_key = {
131+
# 'keys': {
132+
# f'ed25519{signer_key}': signer_key,
133+
# },
134+
# }
135+
136+
# await self.store.set_e2e_cross_signing_key(
137+
# user_id,
138+
# "vaccount",
139+
# vaccount_signing_key,
140+
# )
141+
142+
# async with self.store._cross_signing_id_gen.get_next() as stream_id:
143+
# await self.store.db_pool.runInteraction(
144+
# "add_e2e_cross_signing_key",
145+
# self.store._set_e2e_cross_signing_key_txn,
146+
# user_id,
147+
# "vaccount",
148+
# vaccount_signing_key,
149+
# stream_id,
150+
# )
151+
# await self.store.set_e2e_cross_signing_key(
152+
# user_id, "master", vaccount_signing_key
153+
# )
154+
self.last_tonce = signed_timestamp
155+
156+
return user_id
157+
158+
@staticmethod
159+
def _is_valid_signature(signature, signer_key, signed_msg) -> bool:
160+
signer_key = b58decode(signer_key)
161+
try:
162+
VerifyKey(signer_key).verify(signed_msg, signature)
163+
164+
except BadSignatureError as e:
165+
logger.debug(f"Invalid signature provided for {signer_key}.")
166+
return False
167+
168+
return True
169+
170+
def _is_valid_sign_timestamp(self, signed_timestamp: int):
171+
"""Check if signed timestamp is valid
172+
Args:
173+
signed_tonce: signed timestamp
174+
Returns:
175+
True if timestamp is greater than last signed timestamp
176+
"""
177+
current_timestamp = int(self.account_handler._hs.get_clock().time())
178+
ts_window = current_timestamp - signed_timestamp
179+
if signed_timestamp >= self.last_tonce and ts_window <= SIGN_TIMESTAMP_TOLERANCE:
180+
return True
181+
182+
return False
183+
184+
async def register_user(self, localpart, displayname):
185+
"""Register a Synapse user, first checking if they exist.
186+
Args:
187+
localpart (str): Localpart of the user to register on this homeserver.
188+
displayname (str): Full name of the user.
189+
Returns:
190+
user_id (str): User ID of the newly registered user.
191+
"""
192+
# Get full user id from localpart
193+
user_id = self.account_handler.get_qualified_user_id(localpart)
194+
195+
if await self.account_handler.check_user_exists(user_id):
196+
# exists, authentication complete
197+
return user_id
198+
199+
user_id = await self.account_handler.register_user(
200+
localpart=localpart,
201+
displayname=displayname,
202+
)
203+
204+
logger.info(f"Registration was successful: {user_id}, timestamp: {self.last_tonce}")
205+
return user_id
206+
207+
async def _is_active_vaccount(self, vaccount_address: PublicKey, signer: PublicKey, signer_type: str) -> bool:
208+
"""
209+
"""
210+
vaccount_info = VaccountInfo(vaccount_address)
211+
212+
if vaccount_info.is_ephemeral():
213+
return is_valid_vaccount_address(signer, vaccount_address)
214+
215+
if signer_type == 'owner' and signer in vaccount_info.owners:
216+
return True
217+
218+
if signer_type == 'operational':
219+
for operational in vaccount_info.operational_storage:
220+
221+
if operational.pubkey == signer and operational.state == OPERATIONAL_STATE.enum.Initialized():
222+
return True
223+
224+
return False
225+
226+
async def _get_parsed_account_info(self, account_address):
227+
"""
228+
"""
229+
testnet = 'https://testnet.velas.com/rpc'
230+
payload = {
231+
"jsonrpc": "2.0",
232+
"id": 1,
233+
"method": "getAccountInfo",
234+
'params': [
235+
str(account_address),
236+
{
237+
'encoding': 'jsonParsed'
238+
}
239+
]
240+
}
241+
try:
242+
account_data = await self.account_handler.http_client.post_json_get_json(
243+
uri=testnet,
244+
post_json=payload,
245+
)
246+
except HttpResponseException as e:
247+
logger.info(f"HttpResponseException eeee*: {e}")
248+
return None
249+
250+
account_data = account_data.get('result').get('value').get('data').get('parsed').get('info')
251+
252+
return account_data
253+
254+
@staticmethod
255+
def parse_config(config):
256+
return config
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
Provide implementation of the constants for vaccount auth provider.
3+
"""
4+
from solana.publickey import PublicKey
5+
from borsh_construct import (
6+
Bool,
7+
CStruct,
8+
Enum,
9+
U8,
10+
U16,
11+
)
12+
13+
VACCOUNT_PROGRAM_ID = PublicKey("VAcccHVjpknkW5N5R9sfRppQxYJrJYVV7QJGKchkQj5")
14+
VACCOUNT_SEED = b'vaccount'
15+
VELAS_RPC_URI = 'https://api.devnet.velas.com'
16+
BASE_OPERATIONAL_LEN = 134
17+
18+
SIGN_TIMESTAMP_TOLERANCE = 120
19+
20+
VACCOUNT_INFO = CStruct(
21+
'version' / U8,
22+
'owners' / U8[96],
23+
'genesis_seed_key' / U8[32],
24+
'operational_storage_nonce' / U16,
25+
'token_storage_nonce' / U16,
26+
'programs_storage_nonce' / U16
27+
)
28+
29+
OPERATIONAL_STATE = Enum (
30+
'Initialized',
31+
'Frozen',
32+
enum_name="OperationalState"
33+
)
34+
35+
OPERATIONAL_INFO = CStruct(
36+
'pubkey' / U8[32],
37+
'state' / OPERATIONAL_STATE,
38+
'agent_type' / U8[32],
39+
'scopes' / U8[4],
40+
'tokens_indices' / U8[32],
41+
'external_programs_indices' / U8[32],
42+
'is_master_key' / Bool,
43+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Provide implementation of the custom `Vaccount` errors.
3+
"""

0 commit comments

Comments
 (0)