From 2e8a17854ecbbc19a796ccd138a6b376dc93f496 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Thu, 25 Dec 2025 16:10:16 +0100 Subject: [PATCH] Refactored keys screen --- sbapp/main.py | 145 ++---------------------- sbapp/ui/keys.py | 270 ++++++++++++++++++++++++++++++++++++++++++++ sbapp/ui/layouts.py | 121 -------------------- 3 files changed, 281 insertions(+), 255 deletions(-) create mode 100644 sbapp/ui/keys.py diff --git a/sbapp/main.py b/sbapp/main.py index 56a5172..7fe6601 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -436,6 +436,7 @@ else: from ui.utilities import Utilities from ui.voice import Voice from ui.guide import Guide + from ui.keys import Keys from ui.objectdetails import ObjectDetails from ui.announces import Announces from ui.messages import Messages, ts_format, messages_screen_kv @@ -469,6 +470,7 @@ else: from .ui.utilities import Utilities from .ui.voice import Voice from .ui.guide import Guide + from .ui.keys import Keys from .ui.objectdetails import ObjectDetails from .ui.messages import Messages, ts_format, messages_screen_kv from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem @@ -1446,6 +1448,7 @@ class SidebandApp(MDApp): self.announces_view = None self.guide_view = None + self.keys_view = None if RNS.vendor.platformutils.is_android(): ActivityInfo = autoclass('android.content.pm.ActivityInfo') @@ -5384,37 +5387,20 @@ class SidebandApp(MDApp): ###################################### def keys_action(self, sender=None, direction="left"): - if self.root.ids.screen_manager.has_screen("keys_screen"): - self.keys_open(direction=direction) + if self.root.ids.screen_manager.has_screen("keys_screen"): self.keys_open(direction=direction) else: self.loader_action(direction=direction) def final(dt): - self.keys_init() - def o(dt): - self.keys_open(no_transition=True) + self.init_keys_view() + def o(dt): self.keys_open(no_transition=True) Clock.schedule_once(o, ll_ot) Clock.schedule_once(final, ll_ft) - def keys_init(self): - if not self.root.ids.screen_manager.has_screen("keys_screen"): - self.keys_screen = Builder.load_string(layout_keys_screen) - self.keys_screen.app = self - self.root.ids.screen_manager.add_widget(self.keys_screen) - self.bind_clipboard_actions(self.keys_screen.ids) - - self.keys_screen.ids.keys_scrollview.effect_cls = ScrollEffect - info1 = "[size=18dp][b]Encryption Keys[/b][/size][size=5dp]\n \n[/size]Your primary encryption keys are stored in a Reticulum Identity within the Sideband app. If you want to backup this Identity for later use on this or another device, you can export it as a plain text blob, with the key data encoded in Base32 format. This will allow you to restore your address in Sideband or other LXMF clients at a later point.\n\n[b]Warning![/b] Anyone that gets access to the key data will be able to control your LXMF address, impersonate you, and read your messages. It is [b]extremely important[/b] that you keep the Identity data secure if you export it.\n\nBefore displaying or exporting your Identity data, make sure that no machine or person in your vicinity is able to see, copy or record your device screen or similar." - info2 = "[size=18dp][b]Backup & Restore[/b][/size][size=5dp]\n \n[/size]You can backup your entire Sideband profile for import on a computer or other device. The exported backup archive will be saved in the downloads folder of your device. Please note that the exported archive contains all your messages, data and encryption keys. Take extreme care to keep this archive secure." - - if not RNS.vendor.platformutils.get_platform() == "android": - self.widget_hide(self.keys_screen.ids.keys_share) - - self.keys_screen.ids.keys_info.text = info1 - self.keys_screen.ids.backup_info.text = info2 + def init_keys_view(self, sender=None): + if not self.keys_view: self.keys_view = Keys(self) def keys_open(self, sender=None, direction="left", no_transition=False): - if no_transition: - self.root.ids.screen_manager.transition = self.no_transition + if no_transition: self.root.ids.screen_manager.transition = self.no_transition else: self.root.ids.screen_manager.transition = self.slide_transition self.root.ids.screen_manager.transition.direction = direction @@ -5423,119 +5409,10 @@ class SidebandApp(MDApp): self.root.ids.screen_manager.current = "keys_screen" self.root.ids.nav_drawer.set_state("closed") self.sideband.setstate("app.displaying", self.root.ids.screen_manager.current) + if no_transition: self.root.ids.screen_manager.transition = self.slide_transition - if no_transition: - self.root.ids.screen_manager.transition = self.slide_transition + def close_keys_action(self, sender=None): self.open_conversations(direction="right") - def close_keys_action(self, sender=None): - self.open_conversations(direction="right") - - def _profile_backup_job(self): - import tarfile - from ui.helpers import file_ts_format - output_path = plyer.storagepath.get_downloads_dir() - time_str = time.strftime(file_ts_format, time.localtime(time.time())) - target_file = f"{output_path}/sideband_backup_{time_str}.tar.gz" - tar = tarfile.open(target_file, "w:gz") - tar.add(f"{self.sideband.app_dir}/app_storage", arcname="Sideband Backup") - tar.close() - - def job(dt): - self.keys_screen.ids.keys_backup.disabled = False - toast("Backup done") - ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) - dialog = MDDialog(text=f"Backup has been saved to {target_file}", - buttons=[ok_button]) - def dl_yes(s): dialog.dismiss() - - ok_button.bind(on_release=dl_yes) - dialog.open() - - Clock.schedule_once(job, 0.3) - - def profile_backup_action(self, sender=None): - self.keys_screen.ids.keys_backup.disabled = True - toast("Creating backup...") - threading.Thread(target=self._profile_backup_job, daemon=True).start() - - def identity_display_action(self, sender=None): - yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) - - dialog = MDDialog( - text="Your Identity key, in base32 format is as follows:\n\n[b]"+str(base64.b32encode(self.sideband.identity.get_private_key()).decode("utf-8"))+"[/b]", - buttons=[ yes_button ], - # elevation=0, - ) - def dl_yes(s): - dialog.dismiss() - - yes_button.bind(on_release=dl_yes) - dialog.open() - - def identity_copy_action(self, sender=None): - c_yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject) - c_no_button = MDRectangleFlatButton(text="No, go back",font_size=dp(18)) - c_dialog = MDDialog(text="[b]Caution![/b]\n\nYour Identity key will be copied to the system clipboard. Take extreme care that no untrusted app steals your key by reading the clipboard data. Clear the system clipboard immediately after pasting your key where you need it.\n\nAre you sure that you wish to proceed?", buttons=[ c_no_button, c_yes_button ]) - def c_dl_no(s): - c_dialog.dismiss() - def c_dl_yes(s): - c_dialog.dismiss() - yes_button = MDRectangleFlatButton(text="OK") - dialog = MDDialog(text="Your Identity key was copied to the system clipboard", buttons=[ yes_button ]) - def dl_yes(s): - dialog.dismiss() - yes_button.bind(on_release=dl_yes) - - Clipboard.copy(str(base64.b32encode(self.sideband.identity.get_private_key()).decode("utf-8"))) - dialog.open() - - c_yes_button.bind(on_release=c_dl_yes) - c_no_button.bind(on_release=c_dl_no) - - c_dialog.open() - - def identity_share_action(self, sender=None): - if RNS.vendor.platformutils.get_platform() == "android": - self.share_text(str(base64.b32encode(self.sideband.identity.get_private_key()).decode("utf-8"))) - - def identity_restore_action(self, sender=None): - c_yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.color_reject, text_color=self.color_reject) - c_no_button = MDRectangleFlatButton(text="No, go back",font_size=dp(18)) - c_dialog = MDDialog(text="[b]Caution![/b]\n\nYou are about to import a new Identity key into Sideband. The currently active key will be irreversibly destroyed, and you will loose your LXMF address if you have not already backed up your current Identity key.\n\nAre you sure that you wish to import the key?", buttons=[ c_no_button, c_yes_button ]) - def c_dl_no(s): - c_dialog.dismiss() - def c_dl_yes(s): - c_dialog.dismiss() - b32_text = self.keys_screen.ids.key_restore_text.text - - try: - key_bytes = base64.b32decode(b32_text) - new_id = RNS.Identity.from_bytes(key_bytes) - - if new_id != None: - new_id.to_file(self.sideband.identity_path) - - yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) - dialog = MDDialog(text="[b]The provided Identity key data was imported[/b]\n\nThe app will now exit. Please restart Sideband to use the new Identity.", buttons=[ yes_button ]) - def dl_yes(s): - dialog.dismiss() - self.quit_action(sender=self) - yes_button.bind(on_release=dl_yes) - dialog.open() - - except Exception as e: - yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) - dialog = MDDialog(text="[b]The provided Identity key data was not valid[/b]\n\nThe error reported by Reticulum was:\n\n[i]"+str(e)+"[/i]\n\nNo Identity was imported into Sideband.", buttons=[ yes_button ]) - def dl_yes(s): - dialog.dismiss() - yes_button.bind(on_release=dl_yes) - dialog.open() - - - c_yes_button.bind(on_release=c_dl_yes) - c_no_button.bind(on_release=c_dl_no) - - c_dialog.open() ### Plugins & Services screen ###################################### diff --git a/sbapp/ui/keys.py b/sbapp/ui/keys.py new file mode 100644 index 0000000..5b5e8aa --- /dev/null +++ b/sbapp/ui/keys.py @@ -0,0 +1,270 @@ +import time +import RNS + +import base64 +import threading +from kivy.metrics import dp,sp +from kivy.lang.builder import Builder +from kivy.core.clipboard import Clipboard +from kivymd.uix.button import MDRectangleFlatButton +from kivymd.uix.dialog import MDDialog +from kivymd.toast import toast +from kivy.effects.scroll import ScrollEffect + +if RNS.vendor.platformutils.get_platform() == "android": + from ui.helpers import dark_theme_text_color +else: + from .helpers import dark_theme_text_color + +class Keys(): + def __init__(self, app): + self.app = app + self.keys_screen = None + + if not self.app.root.ids.screen_manager.has_screen("keys_screen"): + self.keys_screen = Builder.load_string(layout_keys_screen) + self.keys_screen.app = self + self.app.root.ids.screen_manager.add_widget(self.keys_screen) + self.app.bind_clipboard_actions(self.keys_screen.ids) + + self.keys_screen.ids.keys_scrollview.effect_cls = ScrollEffect + info1 = "[size=18dp][b]Encryption Keys[/b][/size][size=5dp]\n \n[/size]Your primary encryption keys are stored in a Reticulum Identity within the Sideband app. If you want to backup this Identity for later use on this or another device, you can export it as a plain text blob, with the key data encoded in Base32 format. This will allow you to restore your address in Sideband or other LXMF clients at a later point.\n\n[b]Warning![/b] Anyone that gets access to the key data will be able to control your LXMF address, impersonate you, and read your messages. It is [b]extremely important[/b] that you keep the Identity data secure if you export it.\n\nBefore displaying or exporting your Identity data, make sure that no machine or person in your vicinity is able to see, copy or record your device screen or similar." + info2 = "[size=18dp][b]Backup & Restore[/b][/size][size=5dp]\n \n[/size]You can backup your entire Sideband profile for import on a computer or other device. The exported backup archive will be saved in the downloads folder of your device. Please note that the exported archive contains all your messages, data and encryption keys. Take extreme care to keep this archive secure." + + if not RNS.vendor.platformutils.get_platform() == "android": + self.app.widget_hide(self.keys_screen.ids.keys_share) + + self.keys_screen.ids.keys_info.text = info1 + self.keys_screen.ids.backup_info.text = info2 + + + def _profile_backup_job(self): + import tarfile + import plyer + from .helpers import file_ts_format + from kivy.clock import Clock + output_path = plyer.storagepath.get_downloads_dir() + time_str = time.strftime(file_ts_format, time.localtime(time.time())) + target_file = f"{output_path}/sideband_backup_{time_str}.tar.gz" + tar = tarfile.open(target_file, "w:gz") + tar.add(f"{self.app.sideband.app_dir}/app_storage", arcname="Sideband Backup") + tar.close() + + def job(dt): + self.keys_screen.ids.keys_backup.disabled = False + toast("Backup done") + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + dialog = MDDialog(text=f"Backup has been saved to {target_file}", + buttons=[ok_button]) + def dl_yes(s): dialog.dismiss() + + ok_button.bind(on_release=dl_yes) + dialog.open() + + Clock.schedule_once(job, 0.3) + + def profile_backup_action(self, sender=None): + self.keys_screen.ids.keys_backup.disabled = True + toast("Creating backup...") + threading.Thread(target=self._profile_backup_job, daemon=True).start() + + def identity_display_action(self, sender=None): + yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + + dialog = MDDialog( + text="Your Identity key, in base32 format is as follows:\n\n[b]"+str(base64.b32encode(self.app.sideband.identity.get_private_key()).decode("utf-8"))+"[/b]", + buttons=[ yes_button ], + # elevation=0, + ) + def dl_yes(s): + dialog.dismiss() + + yes_button.bind(on_release=dl_yes) + dialog.open() + + def identity_copy_action(self, sender=None): + c_yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.app.color_reject, text_color=self.app.color_reject) + c_no_button = MDRectangleFlatButton(text="No, go back",font_size=dp(18)) + c_dialog = MDDialog(text="[b]Caution![/b]\n\nYour Identity key will be copied to the system clipboard. Take extreme care that no untrusted app steals your key by reading the clipboard data. Clear the system clipboard immediately after pasting your key where you need it.\n\nAre you sure that you wish to proceed?", buttons=[ c_no_button, c_yes_button ]) + def c_dl_no(s): + c_dialog.dismiss() + def c_dl_yes(s): + c_dialog.dismiss() + yes_button = MDRectangleFlatButton(text="OK") + dialog = MDDialog(text="Your Identity key was copied to the system clipboard", buttons=[ yes_button ]) + def dl_yes(s): + dialog.dismiss() + yes_button.bind(on_release=dl_yes) + + Clipboard.copy(str(base64.b32encode(self.app.sideband.identity.get_private_key()).decode("utf-8"))) + dialog.open() + + c_yes_button.bind(on_release=c_dl_yes) + c_no_button.bind(on_release=c_dl_no) + + c_dialog.open() + + def identity_share_action(self, sender=None): + if RNS.vendor.platformutils.get_platform() == "android": + self.share_text(str(base64.b32encode(self.app.sideband.identity.get_private_key()).decode("utf-8"))) + + def identity_restore_action(self, sender=None): + c_yes_button = MDRectangleFlatButton(text="Yes",font_size=dp(18), theme_text_color="Custom", line_color=self.app.color_reject, text_color=self.app.color_reject) + c_no_button = MDRectangleFlatButton(text="No, go back",font_size=dp(18)) + c_dialog = MDDialog(text="[b]Caution![/b]\n\nYou are about to import a new Identity key into Sideband. The currently active key will be irreversibly destroyed, and you will loose your LXMF address if you have not already backed up your current Identity key.\n\nAre you sure that you wish to import the key?", buttons=[ c_no_button, c_yes_button ]) + def c_dl_no(s): + c_dialog.dismiss() + def c_dl_yes(s): + c_dialog.dismiss() + b32_text = self.keys_screen.ids.key_restore_text.text + + try: + key_bytes = base64.b32decode(b32_text) + new_id = RNS.Identity.from_bytes(key_bytes) + + if new_id != None: + new_id.to_file(self.app.sideband.identity_path) + + yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + dialog = MDDialog(text="[b]The provided Identity key data was imported[/b]\n\nThe app will now exit. Please restart Sideband to use the new Identity.", buttons=[ yes_button ]) + def dl_yes(s): + dialog.dismiss() + self.app.quit_action(sender=self) + yes_button.bind(on_release=dl_yes) + dialog.open() + + except Exception as e: + yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + dialog = MDDialog(text="[b]The provided Identity key data was not valid[/b]\n\nThe error reported by Reticulum was:\n\n[i]"+str(e)+"[/i]\n\nNo Identity was imported into Sideband.", buttons=[ yes_button ]) + def dl_yes(s): + dialog.dismiss() + yes_button.bind(on_release=dl_yes) + dialog.open() + + + c_yes_button.bind(on_release=c_dl_yes) + c_no_button.bind(on_release=c_dl_no) + + c_dialog.open() + + +layout_keys_screen = """ +MDScreen: + name: "keys_screen" + + BoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Backup & Keys" + anchor_title: "left" + elevation: 0 + left_action_items: + [['menu', lambda x: root.app.nav_drawer.set_state("open")]] + right_action_items: + [ + ['close', lambda x: root.app.close_keys_action(self)], + ] + + ScrollView: + id:keys_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(35), dp(35), dp(35), dp(35)] + + + MDLabel: + id: backup_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + MDRectangleFlatIconButton: + id: keys_backup + icon: "home-export-outline" + text: "Backup Sideband Profile" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.app.profile_backup_action(self) + + MDRectangleFlatIconButton: + id: keys_restore + icon: "home-import-outline" + text: "Restore Sideband Profile" + disabled: True + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.app.profile_backup_action(self) + + MDLabel: + id: keys_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + MDRectangleFlatIconButton: + id: keys_display + icon: "eye" + text: "Display Identity Key" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.app.identity_display_action(self) + + MDRectangleFlatIconButton: + id: keys_copy + icon: "file-key" + text: "Copy Key To Clipboard" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.app.identity_copy_action(self) + + MDRectangleFlatIconButton: + id: keys_share + icon: "upload-lock" + text: "Send Key To Other App" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.app.identity_share_action(self) + + MDBoxLayout: + orientation: "vertical" + # spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(0), dp(12), dp(0), dp(0)] + + MDTextField: + id: key_restore_text + hint_text: "Enter base32 key for import" + mode: "rectangle" + # size_hint: [1.0, None] + pos_hint: {"center_x": .5} + + MDRectangleFlatIconButton: + id: keys_restore + icon: "download-lock" + text: "Restore Identity From Key" + padding: [dp(0), dp(14), dp(0), dp(14)] + icon_size: dp(24) + font_size: dp(16) + size_hint: [1.0, None] + on_release: root.app.identity_restore_action(self) +""" \ No newline at end of file diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py index 46c67cd..6ef961e 100644 --- a/sbapp/ui/layouts.py +++ b/sbapp/ui/layouts.py @@ -1053,127 +1053,6 @@ MDScreen: id: map_layout """ -layout_keys_screen = """ -MDScreen: - name: "keys_screen" - - BoxLayout: - orientation: "vertical" - - MDTopAppBar: - title: "Backup & Keys" - anchor_title: "left" - elevation: 0 - left_action_items: - [['menu', lambda x: root.app.nav_drawer.set_state("open")]] - right_action_items: - [ - ['close', lambda x: root.app.close_keys_action(self)], - ] - - ScrollView: - id:keys_scrollview - - MDBoxLayout: - orientation: "vertical" - spacing: "24dp" - size_hint_y: None - height: self.minimum_height - padding: [dp(35), dp(35), dp(35), dp(35)] - - - MDLabel: - id: backup_info - markup: True - text: "" - size_hint_y: None - text_size: self.width, None - height: self.texture_size[1] - - MDRectangleFlatIconButton: - id: keys_backup - icon: "home-export-outline" - text: "Backup Sideband Profile" - padding: [dp(0), dp(14), dp(0), dp(14)] - icon_size: dp(24) - font_size: dp(16) - size_hint: [1.0, None] - on_release: root.app.profile_backup_action(self) - - MDRectangleFlatIconButton: - id: keys_restore - icon: "home-import-outline" - text: "Restore Sideband Profile" - disabled: True - padding: [dp(0), dp(14), dp(0), dp(14)] - icon_size: dp(24) - font_size: dp(16) - size_hint: [1.0, None] - on_release: root.app.profile_backup_action(self) - - MDLabel: - id: keys_info - markup: True - text: "" - size_hint_y: None - text_size: self.width, None - height: self.texture_size[1] - - MDRectangleFlatIconButton: - id: keys_display - icon: "eye" - text: "Display Identity Key" - padding: [dp(0), dp(14), dp(0), dp(14)] - icon_size: dp(24) - font_size: dp(16) - size_hint: [1.0, None] - on_release: root.app.identity_display_action(self) - - MDRectangleFlatIconButton: - id: keys_copy - icon: "file-key" - text: "Copy Key To Clipboard" - padding: [dp(0), dp(14), dp(0), dp(14)] - icon_size: dp(24) - font_size: dp(16) - size_hint: [1.0, None] - on_release: root.app.identity_copy_action(self) - - MDRectangleFlatIconButton: - id: keys_share - icon: "upload-lock" - text: "Send Key To Other App" - padding: [dp(0), dp(14), dp(0), dp(14)] - icon_size: dp(24) - font_size: dp(16) - size_hint: [1.0, None] - on_release: root.app.identity_share_action(self) - - MDBoxLayout: - orientation: "vertical" - # spacing: "24dp" - size_hint_y: None - height: self.minimum_height - padding: [dp(0), dp(12), dp(0), dp(0)] - - MDTextField: - id: key_restore_text - hint_text: "Enter base32 key for import" - mode: "rectangle" - # size_hint: [1.0, None] - pos_hint: {"center_x": .5} - - MDRectangleFlatIconButton: - id: keys_restore - icon: "download-lock" - text: "Restore Identity From Key" - padding: [dp(0), dp(14), dp(0), dp(14)] - icon_size: dp(24) - font_size: dp(16) - size_hint: [1.0, None] - on_release: root.app.identity_restore_action(self) -""" - layout_plugins_screen = """ MDScreen: name: "plugins_screen"