Merge branch 'sas'
This commit is contained in:
commit
c16dc70768
4 changed files with 270 additions and 34 deletions
|
@ -23,14 +23,14 @@ from builtins import str
|
||||||
from future.moves.itertools import zip_longest
|
from future.moves.itertools import zip_longest
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from nio import EncryptionError
|
from nio import EncryptionError, LocalProtocolError
|
||||||
|
|
||||||
from . import globals as G
|
from . import globals as G
|
||||||
from .colors import Formatted
|
from .colors import Formatted
|
||||||
from .globals import SERVERS, W, UPLOADS, SCRIPT_NAME
|
from .globals import SERVERS, W, UPLOADS, SCRIPT_NAME
|
||||||
from .server import MatrixServer
|
from .server import MatrixServer
|
||||||
from .utf import utf8_decode
|
from .utf import utf8_decode
|
||||||
from .utils import key_from_value, tags_from_line_data
|
from .utils import key_from_value
|
||||||
from .uploads import UploadsBuffer, Upload
|
from .uploads import UploadsBuffer, Upload
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,6 +152,18 @@ class WeechatCommandParser(object):
|
||||||
import_parser.add_argument("file")
|
import_parser.add_argument("file")
|
||||||
import_parser.add_argument("passphrase")
|
import_parser.add_argument("passphrase")
|
||||||
|
|
||||||
|
sas_parser = subparsers.add_parser("verification")
|
||||||
|
sas_parser.add_argument(
|
||||||
|
"action",
|
||||||
|
choices=[
|
||||||
|
"start",
|
||||||
|
"accept",
|
||||||
|
"confirm",
|
||||||
|
"cancel",
|
||||||
|
])
|
||||||
|
sas_parser.add_argument("user_id")
|
||||||
|
sas_parser.add_argument("device_id")
|
||||||
|
|
||||||
return WeechatCommandParser._run_parser(parser, args)
|
return WeechatCommandParser._run_parser(parser, args)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -385,6 +397,7 @@ def hook_commands():
|
||||||
"blacklist <user-id> <device-id> ||"
|
"blacklist <user-id> <device-id> ||"
|
||||||
"unverify <user-id> <device-id> ||"
|
"unverify <user-id> <device-id> ||"
|
||||||
"verify <user-id> <device-id> ||"
|
"verify <user-id> <device-id> ||"
|
||||||
|
"verification start|accept|cancel|confirm <user-id> <device-id> ||"
|
||||||
"export <file-name> <passphrase> ||"
|
"export <file-name> <passphrase> ||"
|
||||||
"import <file-name> <passphrase>"
|
"import <file-name> <passphrase>"
|
||||||
),
|
),
|
||||||
|
@ -394,6 +407,7 @@ def hook_commands():
|
||||||
" unblacklist: unblacklist a device\n"
|
" unblacklist: unblacklist a device\n"
|
||||||
" unverify: unverify a device\n"
|
" unverify: unverify a device\n"
|
||||||
" verify: verify a device\n"
|
" verify: verify a device\n"
|
||||||
|
"verification: manage interactive device verification\n"
|
||||||
" export: export encryption keys\n"
|
" export: export encryption keys\n"
|
||||||
" import: import encryption keys\n\n"
|
" import: import encryption keys\n\n"
|
||||||
"Examples:"
|
"Examples:"
|
||||||
|
@ -406,6 +420,7 @@ def hook_commands():
|
||||||
'unblacklist %(olm_user_ids) %(olm_devices) ||'
|
'unblacklist %(olm_user_ids) %(olm_devices) ||'
|
||||||
'unverify %(olm_user_ids) %(olm_devices) ||'
|
'unverify %(olm_user_ids) %(olm_devices) ||'
|
||||||
'verify %(olm_user_ids) %(olm_devices) ||'
|
'verify %(olm_user_ids) %(olm_devices) ||'
|
||||||
|
'verification start|accept|cancel|confirm %(olm_user_ids) %(olm_devices) ||'
|
||||||
'export %(filename) ||'
|
'export %(filename) ||'
|
||||||
'import %(filename)'
|
'import %(filename)'
|
||||||
),
|
),
|
||||||
|
@ -471,13 +486,15 @@ def hook_commands():
|
||||||
hook_page_up()
|
hook_page_up()
|
||||||
|
|
||||||
|
|
||||||
def format_device(device_id, fp_key):
|
def format_device(device_id, fp_key, display_name):
|
||||||
fp_key = partition_key(fp_key)
|
fp_key = partition_key(fp_key)
|
||||||
message = (" - Device ID: {device_color}{device_id}{ncolor}\n"
|
message = (" - Device ID: {device_color}{device_id}{ncolor}\n"
|
||||||
|
" - Display name: {device_color}{display_name}{ncolor}\n"
|
||||||
" - Device key: {key_color}{fp_key}{ncolor}").format(
|
" - Device key: {key_color}{fp_key}{ncolor}").format(
|
||||||
device_color=W.color("chat_channel"),
|
device_color=W.color("chat_channel"),
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
ncolor=W.color("reset"),
|
ncolor=W.color("reset"),
|
||||||
|
display_name=display_name,
|
||||||
key_color=W.color("chat_server"),
|
key_color=W.color("chat_server"),
|
||||||
fp_key=fp_key)
|
fp_key=fp_key)
|
||||||
return message
|
return message
|
||||||
|
@ -511,7 +528,8 @@ def olm_info_command(server, args):
|
||||||
|
|
||||||
device_strings.append(format_device(
|
device_strings.append(format_device(
|
||||||
device.id,
|
device.id,
|
||||||
device.ed25519
|
device.ed25519,
|
||||||
|
device.display_name
|
||||||
))
|
))
|
||||||
|
|
||||||
if not device_strings:
|
if not device_strings:
|
||||||
|
@ -632,7 +650,8 @@ def olm_action_command(server, args, category, error_category, prefix, action):
|
||||||
for device in device_list:
|
for device in device_list:
|
||||||
device_strings.append(format_device(
|
device_strings.append(format_device(
|
||||||
device.id,
|
device.id,
|
||||||
device.ed25519
|
device.ed25519,
|
||||||
|
device.display_name
|
||||||
))
|
))
|
||||||
if not device_strings:
|
if not device_strings:
|
||||||
continue
|
continue
|
||||||
|
@ -710,6 +729,54 @@ def olm_import_command(server, args):
|
||||||
server.info("Succesfully imported keys")
|
server.info("Succesfully imported keys")
|
||||||
|
|
||||||
|
|
||||||
|
def olm_sas_command(server, args):
|
||||||
|
try:
|
||||||
|
device_store = server.client.device_store
|
||||||
|
except LocalProtocolError:
|
||||||
|
server.error("The device store is not loaded")
|
||||||
|
return W.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
device = device_store[args.user_id][args.device_id]
|
||||||
|
except KeyError:
|
||||||
|
server.error("Device {} of user {} not found".format(
|
||||||
|
args.user_id,
|
||||||
|
args.device_id
|
||||||
|
))
|
||||||
|
return W.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
if device.deleted:
|
||||||
|
server.error("Device {} of user {} is deleted.".format(
|
||||||
|
args.user_id,
|
||||||
|
args.device_id
|
||||||
|
))
|
||||||
|
return W.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
if args.action == "start":
|
||||||
|
server.start_verification(device)
|
||||||
|
elif args.action in ["accept", "confirm", "cancel"]:
|
||||||
|
sas = server.client.get_active_sas(args.user_id, args.device_id)
|
||||||
|
|
||||||
|
if not sas:
|
||||||
|
server.error("No active key verification found for "
|
||||||
|
"device {} of user {}.".format(
|
||||||
|
args.device_id,
|
||||||
|
args.user_id
|
||||||
|
))
|
||||||
|
return W.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.action == "accept":
|
||||||
|
server.accept_sas(sas)
|
||||||
|
elif args.action == "confirm":
|
||||||
|
server.confirm_sas(sas)
|
||||||
|
elif args.action == "cancel":
|
||||||
|
server.cancel_sas(sas)
|
||||||
|
|
||||||
|
except LocalProtocolError as e:
|
||||||
|
server.error(str(e))
|
||||||
|
|
||||||
|
|
||||||
@utf8_decode
|
@utf8_decode
|
||||||
def matrix_olm_command_cb(data, buffer, args):
|
def matrix_olm_command_cb(data, buffer, args):
|
||||||
def command(server, data, buffer, args):
|
def command(server, data, buffer, args):
|
||||||
|
@ -736,6 +803,8 @@ def matrix_olm_command_cb(data, buffer, args):
|
||||||
olm_blacklist_command(server, parsed_args)
|
olm_blacklist_command(server, parsed_args)
|
||||||
elif parsed_args.subcommand == "unblacklist":
|
elif parsed_args.subcommand == "unblacklist":
|
||||||
olm_unblacklist_command(server, parsed_args)
|
olm_unblacklist_command(server, parsed_args)
|
||||||
|
elif parsed_args.subcommand == "verification":
|
||||||
|
olm_sas_command(server, parsed_args)
|
||||||
else:
|
else:
|
||||||
message = ("{prefix}matrix: Command not implemented.".format(
|
message = ("{prefix}matrix: Command not implemented.".format(
|
||||||
prefix=W.prefix("error")))
|
prefix=W.prefix("error")))
|
||||||
|
|
|
@ -207,7 +207,7 @@ def matrix_olm_device_completion_cb(data, completion_item, buffer, completion):
|
||||||
if len(fields) < 2:
|
if len(fields) < 2:
|
||||||
return W.WEECHAT_RC_OK
|
return W.WEECHAT_RC_OK
|
||||||
|
|
||||||
user = fields[1]
|
user = fields[-1]
|
||||||
|
|
||||||
if user not in device_store.users:
|
if user not in device_store.users:
|
||||||
return W.WEECHAT_RC_OK
|
return W.WEECHAT_RC_OK
|
||||||
|
|
179
matrix/server.py
179
matrix/server.py
|
@ -66,7 +66,14 @@ from nio import (
|
||||||
LoginError,
|
LoginError,
|
||||||
JoinedMembersResponse,
|
JoinedMembersResponse,
|
||||||
JoinedMembersError,
|
JoinedMembersError,
|
||||||
RoomKeyEvent
|
RoomKeyEvent,
|
||||||
|
KeyVerificationStart,
|
||||||
|
KeyVerificationCancel,
|
||||||
|
KeyVerificationKey,
|
||||||
|
KeyVerificationMac,
|
||||||
|
KeyVerificationEvent,
|
||||||
|
ToDeviceResponse,
|
||||||
|
ToDeviceError
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import globals as G
|
from . import globals as G
|
||||||
|
@ -293,6 +300,7 @@ class MatrixServer(object):
|
||||||
self.keys_queried = False # type: bool
|
self.keys_queried = False # type: bool
|
||||||
self.keys_claimed = defaultdict(bool) # type: Dict[str, bool]
|
self.keys_claimed = defaultdict(bool) # type: Dict[str, bool]
|
||||||
self.group_session_shared = defaultdict(bool) # type: Dict[str, bool]
|
self.group_session_shared = defaultdict(bool) # type: Dict[str, bool]
|
||||||
|
self.to_device_sent = []
|
||||||
|
|
||||||
self.config = ServerConfig(self.name, config_ptr)
|
self.config = ServerConfig(self.name, config_ptr)
|
||||||
self._create_session_dir()
|
self._create_session_dir()
|
||||||
|
@ -364,6 +372,102 @@ class MatrixServer(object):
|
||||||
self.get_session_path(),
|
self.get_session_path(),
|
||||||
extra_path=extra_path
|
extra_path=extra_path
|
||||||
)
|
)
|
||||||
|
self.client.add_to_device_callback(
|
||||||
|
self.key_verification_cb,
|
||||||
|
KeyVerificationEvent
|
||||||
|
)
|
||||||
|
|
||||||
|
def key_verification_cb(self, event):
|
||||||
|
if isinstance(event, KeyVerificationStart):
|
||||||
|
self.info_highlight("{user} via {device} has started a key "
|
||||||
|
"verification process.\n"
|
||||||
|
"To accept use /olm verification "
|
||||||
|
"accept {user} {device}".format(
|
||||||
|
user=event.sender,
|
||||||
|
device=event.from_device
|
||||||
|
))
|
||||||
|
|
||||||
|
elif isinstance(event, KeyVerificationKey):
|
||||||
|
sas = self.client.key_verifications.get(event.transaction_id, None)
|
||||||
|
if not sas:
|
||||||
|
return
|
||||||
|
|
||||||
|
if sas.canceled:
|
||||||
|
return
|
||||||
|
|
||||||
|
device = sas.other_olm_device
|
||||||
|
emoji = sas.get_emoji()
|
||||||
|
|
||||||
|
emojis = [x[0] for x in emoji]
|
||||||
|
descriptions = [x[1] for x in emoji]
|
||||||
|
|
||||||
|
centered_width = 12
|
||||||
|
|
||||||
|
def center_emoji(emoji, width):
|
||||||
|
# Assume each emoji has width 2
|
||||||
|
emoji_width = 2
|
||||||
|
|
||||||
|
# These are emojis that need VARIATION-SELECTOR-16 (U+FE0F) so
|
||||||
|
# that they are rendered with coloured glyphs. For these, we
|
||||||
|
# need to add an extra space after them so that they are
|
||||||
|
# rendered properly in weechat.
|
||||||
|
variation_selector_emojis = [
|
||||||
|
'☁️',
|
||||||
|
'❤️',
|
||||||
|
'☂️',
|
||||||
|
'✏️',
|
||||||
|
'✂️',
|
||||||
|
'☎️',
|
||||||
|
'✈️'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Hack to make weechat behave properly when one of the above is
|
||||||
|
# printed.
|
||||||
|
if emoji in variation_selector_emojis:
|
||||||
|
emoji += " "
|
||||||
|
|
||||||
|
# This is a trick to account for the fact that emojis are wider
|
||||||
|
# than other monospace characters.
|
||||||
|
placeholder = '.' * emoji_width
|
||||||
|
|
||||||
|
return placeholder.center(width).replace(placeholder, emoji)
|
||||||
|
|
||||||
|
emoji_str = u"".join(center_emoji(e, centered_width)
|
||||||
|
for e in emojis)
|
||||||
|
desc = u"".join(d.center(centered_width) for d in descriptions)
|
||||||
|
short_string = u"\n".join([emoji_str, desc])
|
||||||
|
|
||||||
|
self.info_highlight(u"Short authentication string for "
|
||||||
|
u"{user} via {device}:\n{string}\n"
|
||||||
|
u"Confirm that the strings match with "
|
||||||
|
u"/olm verification confirm {user} "
|
||||||
|
u"{device}".format(
|
||||||
|
user=device.user_id,
|
||||||
|
device=device.id,
|
||||||
|
string=short_string
|
||||||
|
))
|
||||||
|
|
||||||
|
elif isinstance(event, KeyVerificationMac):
|
||||||
|
try:
|
||||||
|
sas = self.client.key_verifications[event.transaction_id]
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
|
||||||
|
device = sas.other_olm_device
|
||||||
|
|
||||||
|
if sas.verified:
|
||||||
|
self.info_highlight("Device {} of user {} succesfully "
|
||||||
|
"verified".format(
|
||||||
|
device.id,
|
||||||
|
device.user_id
|
||||||
|
))
|
||||||
|
|
||||||
|
elif isinstance(event, KeyVerificationCancel):
|
||||||
|
self.info_highlight("The interactive device verification with "
|
||||||
|
"user {} got canceled: {}.".format(
|
||||||
|
event.sender,
|
||||||
|
event.reason
|
||||||
|
))
|
||||||
|
|
||||||
def update_option(self, option, option_name):
|
def update_option(self, option, option_name):
|
||||||
if option_name == "address":
|
if option_name == "address":
|
||||||
|
@ -468,6 +572,14 @@ class MatrixServer(object):
|
||||||
# type: (MatrixServer) -> None
|
# type: (MatrixServer) -> None
|
||||||
self.send_buffer = b""
|
self.send_buffer = b""
|
||||||
|
|
||||||
|
def info_highlight(self, message):
|
||||||
|
buf = ""
|
||||||
|
if self.server_buffer:
|
||||||
|
buf = self.server_buffer
|
||||||
|
|
||||||
|
msg = "{}{}: {}".format(W.prefix("network"), SCRIPT_NAME, message)
|
||||||
|
W.prnt_date_tags(buf, 0, "notify_highlight", msg)
|
||||||
|
|
||||||
def info(self, message):
|
def info(self, message):
|
||||||
buf = ""
|
buf = ""
|
||||||
if self.server_buffer:
|
if self.server_buffer:
|
||||||
|
@ -561,6 +673,7 @@ class MatrixServer(object):
|
||||||
self.keys_queried = False
|
self.keys_queried = False
|
||||||
self.keys_claimed = defaultdict(bool)
|
self.keys_claimed = defaultdict(bool)
|
||||||
self.group_session_shared = defaultdict(bool)
|
self.group_session_shared = defaultdict(bool)
|
||||||
|
self.to_device_sent = []
|
||||||
|
|
||||||
if self.server_buffer:
|
if self.server_buffer:
|
||||||
message = ("{prefix}matrix: disconnected from server").format(
|
message = ("{prefix}matrix: disconnected from server").format(
|
||||||
|
@ -1121,7 +1234,7 @@ class MatrixServer(object):
|
||||||
else:
|
else:
|
||||||
inviter_msg = ""
|
inviter_msg = ""
|
||||||
|
|
||||||
self.info(
|
self.info_highlight(
|
||||||
"You have been invited to {} {}({}{}{}){}"
|
"You have been invited to {} {}({}{}{}){}"
|
||||||
"{}".format(
|
"{}".format(
|
||||||
room.display_name,
|
room.display_name,
|
||||||
|
@ -1134,7 +1247,9 @@ class MatrixServer(object):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.info("You have been invited to {}.".format(room_id))
|
self.info_highlight("You have been invited to {}.".format(
|
||||||
|
room_id
|
||||||
|
))
|
||||||
|
|
||||||
for room_id, info in response.rooms.leave.items():
|
for room_id, info in response.rooms.leave.items():
|
||||||
if room_id not in self.buffers:
|
if room_id not in self.buffers:
|
||||||
|
@ -1206,6 +1321,38 @@ class MatrixServer(object):
|
||||||
room_buffer.undecrypted_events.remove(undecrypted_event)
|
room_buffer.undecrypted_events.remove(undecrypted_event)
|
||||||
room_buffer.replace_undecrypted_line(event)
|
room_buffer.replace_undecrypted_line(event)
|
||||||
|
|
||||||
|
def start_verification(self, device):
|
||||||
|
_, request = self.client.start_key_verification(device)
|
||||||
|
self.send(request)
|
||||||
|
self.info("Starting an interactive device verification with "
|
||||||
|
"{} {}".format(device.user_id, device.id))
|
||||||
|
|
||||||
|
def accept_sas(self, sas):
|
||||||
|
_, request = self.client.accept_key_verification(sas.transaction_id)
|
||||||
|
self.send(request)
|
||||||
|
|
||||||
|
def cancel_sas(self, sas):
|
||||||
|
_, request = self.client.cancel_key_verification(sas.transaction_id)
|
||||||
|
self.send(request)
|
||||||
|
|
||||||
|
def to_device(self, message):
|
||||||
|
_, request = self.client.to_device(message)
|
||||||
|
self.send(request)
|
||||||
|
|
||||||
|
def confirm_sas(self, sas):
|
||||||
|
_, request = self.client.confirm_short_auth_string(sas.transaction_id)
|
||||||
|
self.send(request)
|
||||||
|
|
||||||
|
device = sas.other_olm_device
|
||||||
|
|
||||||
|
if sas.verified:
|
||||||
|
self.info("Device {} of user {} succesfully verified".format(
|
||||||
|
device.id,
|
||||||
|
device.user_id
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.info("Waiting for {} to confirm...".format(device.user_id))
|
||||||
|
|
||||||
def _handle_sync(self, response):
|
def _handle_sync(self, response):
|
||||||
# we got the same batch again, nothing to do
|
# we got the same batch again, nothing to do
|
||||||
if self.next_batch == response.next_batch:
|
if self.next_batch == response.next_batch:
|
||||||
|
@ -1215,9 +1362,7 @@ class MatrixServer(object):
|
||||||
self._handle_room_info(response)
|
self._handle_room_info(response)
|
||||||
|
|
||||||
for event in response.to_device_events:
|
for event in response.to_device_events:
|
||||||
if not isinstance(event, RoomKeyEvent):
|
if isinstance(event, RoomKeyEvent):
|
||||||
continue
|
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
"sender": event.sender,
|
"sender": event.sender,
|
||||||
"sender_key": event.sender_key,
|
"sender_key": event.sender_key,
|
||||||
|
@ -1302,6 +1447,12 @@ class MatrixServer(object):
|
||||||
self.group_session_shared[response.room_id] = False
|
self.group_session_shared[response.room_id] = False
|
||||||
self.share_group_session(response.room_id)
|
self.share_group_session(response.room_id)
|
||||||
|
|
||||||
|
elif isinstance(response, ToDeviceError):
|
||||||
|
try:
|
||||||
|
self.to_device_sent.remove(response.to_device_message)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
def handle_response(self, response):
|
def handle_response(self, response):
|
||||||
# type: (Response) -> None
|
# type: (Response) -> None
|
||||||
response_lag = response.elapsed
|
response_lag = response.elapsed
|
||||||
|
@ -1319,6 +1470,12 @@ class MatrixServer(object):
|
||||||
if isinstance(response, ErrorResponse):
|
if isinstance(response, ErrorResponse):
|
||||||
self.handle_error_response(response)
|
self.handle_error_response(response)
|
||||||
|
|
||||||
|
elif isinstance(response, ToDeviceResponse):
|
||||||
|
try:
|
||||||
|
self.to_device_sent.remove(response.to_device_message)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
elif isinstance(response, LoginResponse):
|
elif isinstance(response, LoginResponse):
|
||||||
self._handle_login(response)
|
self._handle_login(response)
|
||||||
|
|
||||||
|
@ -1646,6 +1803,16 @@ def matrix_timer_cb(server_name, remaining_calls):
|
||||||
server.disconnect()
|
server.disconnect()
|
||||||
return W.WEECHAT_RC_OK
|
return W.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
for i, message in enumerate(server.client.outgoing_to_device_messages):
|
||||||
|
if i >= 5:
|
||||||
|
break
|
||||||
|
|
||||||
|
if message in server.to_device_sent:
|
||||||
|
continue
|
||||||
|
|
||||||
|
server.to_device(message)
|
||||||
|
server.to_device_sent.append(message)
|
||||||
|
|
||||||
if server.sync_time and current_time > server.sync_time:
|
if server.sync_time and current_time > server.sync_time:
|
||||||
timeout = 0 if server.transport_type == TransportType.HTTP else 30000
|
timeout = 0 if server.transport_type == TransportType.HTTP else 30000
|
||||||
sync_filter = {
|
sync_filter = {
|
||||||
|
|
|
@ -6,4 +6,4 @@ atomicwrites
|
||||||
attrs
|
attrs
|
||||||
logbook
|
logbook
|
||||||
pygments
|
pygments
|
||||||
matrix-nio @ git+https://github.com/poljar/matrix-nio.git@master#egg=matrix-nio-0
|
matrix-nio>=0.3
|
||||||
|
|
Loading…
Add table
Reference in a new issue