From 1fb62bdf632096aaab7cfec190f39b0e0a7f801b Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 1 Nov 2020 00:13:17 -0400 Subject: [PATCH 01/16] generated-filesystems: Allow passing raw images This will be useful for some tests. --- modules/generated-filesystems.nix | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/generated-filesystems.nix b/modules/generated-filesystems.nix index 3e94cf95..4e9b3763 100644 --- a/modules/generated-filesystems.nix +++ b/modules/generated-filesystems.nix @@ -62,6 +62,14 @@ let limitations in CI situations. ''; }; + raw = lib.mkOption { + internal = true; + type = types.nullOr types.package; + default = null; + description = '' + Use an output directly rather than creating it from the options. + ''; + }; }; config = { }; @@ -79,7 +87,8 @@ in }; config = { - system.build.generatedFilesystems = lib.attrsets.mapAttrs (name: {type, id, label, ...} @ attrs: + system.build.generatedFilesystems = lib.attrsets.mapAttrs (name: {raw, type, id, label, ...} @ attrs: + if raw != null then raw else filesystemFunctions."${type}" (attrs // { name = label; partitionID = id; From 8a47b7913b2e75c2239288a27b05628e4fae8d54 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 1 Nov 2020 00:14:24 -0400 Subject: [PATCH 02/16] mkExtraUtils: Allow specifying specific binaries --- overlay/lib/extra-utils.nix | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/overlay/lib/extra-utils.nix b/overlay/lib/extra-utils.nix index ed308aca..6da51032 100644 --- a/overlay/lib/extra-utils.nix +++ b/overlay/lib/extra-utils.nix @@ -16,11 +16,17 @@ let install_package = set: let pkg = if set ? type && set.type == "derivation" then set else set.package; + binaries = if set ? binaries then set.binaries else [ "*" ]; in - '' - for BIN in ${pkg}/{s,}bin/*; do - copy_bin_and_libs $BIN + (concat (map (path: '' + for BIN in ${pkg}/{s,}bin/${path}; do + if [ -e "$BIN" ]; then + copy_bin_and_libs "$BIN" + fi done + '') binaries )) + + + '' ${if set ? extraCommand then set.extraCommand else ""} ''; install_packages = concat(map (install_package) packages); From a28cd3a293da6bb02deb485211bb576f35b9aba5 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 1 Nov 2020 00:13:58 -0400 Subject: [PATCH 03/16] modules/luks: Init This, with the upcoming luks task allows encrypted drives to work! --- modules/luks.nix | 27 +++++++++++++++++++++++++++ modules/module-list.nix | 1 + 2 files changed, 28 insertions(+) create mode 100644 modules/luks.nix diff --git a/modules/luks.nix b/modules/luks.nix new file mode 100644 index 00000000..d29167ce --- /dev/null +++ b/modules/luks.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +let + inherit (config.boot.initrd) luks; +in + +lib.mkIf (luks.devices != {} || luks.forceLuksSupportInInitrd) { + mobile.boot.stage-1 = { + bootConfig = { + luksDevices = luks.devices; + }; + kernel = { + modules = [ + "dm_mod" "dm_crypt" "cryptd" "input_leds" + ] ++ luks.cryptoModules + ; + }; + + extraUtils = [ + { package = pkgs.cryptsetup; } + # dmsetup is required for device mapper stuff to work in stage-1. + { package = lib.getBin pkgs.lvm2; binaries = [ + "lvm" "dmsetup" + ];} + ]; + }; +} diff --git a/modules/module-list.nix b/modules/module-list.nix index 72af46ba..344034a6 100644 --- a/modules/module-list.nix +++ b/modules/module-list.nix @@ -30,6 +30,7 @@ ./initrd-vendor.nix ./initrd.nix ./internal.nix + ./luks.nix ./mobile-device.nix ./nixpkgs.nix ./quirks From f66c31fd7e341c52b76108254c0c13c62e303b4f Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 30 Oct 2020 23:39:28 -0400 Subject: [PATCH 04/16] mrbgems: mruby-lvgui: Update with latest changes --- overlay/mruby-builder/mrbgems/mruby-lvgui/default.nix | 4 ++-- overlay/mruby-builder/mrbgems/mruby-lvgui/lvgui.nix | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/overlay/mruby-builder/mrbgems/mruby-lvgui/default.nix b/overlay/mruby-builder/mrbgems/mruby-lvgui/default.nix index 97276b1b..a549a841 100644 --- a/overlay/mruby-builder/mrbgems/mruby-lvgui/default.nix +++ b/overlay/mruby-builder/mrbgems/mruby-lvgui/default.nix @@ -18,8 +18,8 @@ mrbgems.mkGem { src = fetchFromGitHub { repo = "mruby-lvgui"; owner = "mobile-nixos"; - rev = "ab7cf5b1b2e318a4bf5fc973507eb842dce80214"; - sha256 = "09h9f3xlbvxdwpmzpf7whq0gphiv68842shy03ld712fw25393jx"; + rev = "f1bb1dd9b2c5aa3d3df4fcc41ca706f426d182a8"; + sha256 = "0ybjkzg743d21rn3q0vi0fa9zwp3ym9zw2q5ym24wc7gxdspjcjs"; }; gemBuildInputs = [ diff --git a/overlay/mruby-builder/mrbgems/mruby-lvgui/lvgui.nix b/overlay/mruby-builder/mrbgems/mruby-lvgui/lvgui.nix index 24664f5f..14c1b31f 100644 --- a/overlay/mruby-builder/mrbgems/mruby-lvgui/lvgui.nix +++ b/overlay/mruby-builder/mrbgems/mruby-lvgui/lvgui.nix @@ -24,13 +24,13 @@ let in stdenv.mkDerivation { pname = "lvgui"; - version = "2020-07-25"; + version = "2020-11-01"; src = fetchFromGitHub { repo = "lvgui"; owner = "mobile-nixos"; - rev = "0dc257d07271fad023a5e6e9ac42222d2397c5cf"; - sha256 = "1zb7naamqfzcsfi5809c93f1ygxp4w3aiw6172bpmnk1vxdchwsh"; + rev = "4f8af498a81bd669d42ce3b370fc66fe4ec681b5"; + sha256 = "00rik18c3c3l4glzh2azg90cwvp56s4wnski86rsn00bxslia5ma"; }; # Document `LVGL_ENV_SIMULATOR` in the built headers. From 71149eb01d2f7c624744a67089d6a6cd228123a0 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 30 Oct 2020 17:29:11 -0400 Subject: [PATCH 05/16] boot/splash: Ensure cover doesn't intercept events --- boot/splash/lib/ui.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/boot/splash/lib/ui.rb b/boot/splash/lib/ui.rb index e77334e6..58d8ab89 100644 --- a/boot/splash/lib/ui.rb +++ b/boot/splash/lib/ui.rb @@ -24,6 +24,7 @@ class UI add_logo add_progress_bar add_label + add_cover # last end @@ -86,9 +87,10 @@ class UI def add_cover() @cover = LVGL::LVObject.new(@screen) # Make it so we can use the opacity to fade in/out - @cover.set_opa_scale_enable(1) + @cover.set_opa_scale_enable(true) @cover.set_width(@screen.get_width()) @cover.set_height(@screen.get_height()) + @cover.set_click(false) @cover.get_style().dup.tap do |style| @cover.set_style(style) From 5755438dfa4f8eea807c589d4b9640bf900723f1 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 30 Oct 2020 17:30:05 -0400 Subject: [PATCH 06/16] boot/splash: Add wrappers around text area and keyboard They will, at some point, be promoted into LVGUI. For the time being they are local as they have only been verified to work in a useful manner for this limited use case. --- boot/splash/lib/keyboard.rb | 61 ++++++++++++++++++++++++ boot/splash/lib/text_area.rb | 90 ++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 boot/splash/lib/keyboard.rb create mode 100644 boot/splash/lib/text_area.rb diff --git a/boot/splash/lib/keyboard.rb b/boot/splash/lib/keyboard.rb new file mode 100644 index 00000000..ea81ed20 --- /dev/null +++ b/boot/splash/lib/keyboard.rb @@ -0,0 +1,61 @@ +# Wraps a raw +lv_keyboard+ in minimal helpers +# This is intended to be used as a singleton instance, where you can +# "re-parent" the keyboard as needed. +class Keyboard < LVGUI::Widget + include Singleton + + private + + def initialize() + @shown = false + # Attach the keyboard to the current active screen, by default. + super(LVGL::LVKeyboard.new(LVGL::LVDisplay.get_scr_act())) + set_cursor_manage(true) + + get_style(LVGL::KB_STYLE::BG).dup.tap do |style| + set_style(LVGL::KB_STYLE::BG, style) + padding = 4 + style.body_padding_top = padding + style.body_padding_left = padding + style.body_padding_right = padding + style.body_padding_bottom = padding + style.body_padding_inner = padding + end + set_y(get_parent.get_height()) + end + + public + + def set_height(value) + super(value) + _set_position() + end + + def show() + _animate_y(get_parent.get_height() - get_height()) + end + + def hide() + _animate_y(get_parent.get_height) + end + + def _set_position() + if @shown + _animate_y(get_parent.get_height() - get_height()) + else + _animate_y(get_parent.get_height()) + end + end + + def _animate_y(ending) + LVGL::LVAnim.new().tap do |anim| + anim.set_exec_cb(self, :lv_obj_set_y) + anim.set_time(300, 0) + anim.set_values(get_y(), ending) + anim.set_path_cb(LVGL::LVAnim::Path::EASE_OUT) + + # Launch the animation + anim.create() + end + end +end diff --git a/boot/splash/lib/text_area.rb b/boot/splash/lib/text_area.rb new file mode 100644 index 00000000..7eab7e83 --- /dev/null +++ b/boot/splash/lib/text_area.rb @@ -0,0 +1,90 @@ +# Wraps a raw +lv_ta+ in minimal helpers +class TextArea < LVGUI::Widget + attr_reader :hidden + + def initialize(parent) + super(LVGL::LVTextArea.new(parent)) + + @hidden = false + set_text("") + set_placeholder_text("") + set_pwd_mode(true) + set_one_line(true) + set_opa_scale_enable(true) + get_style(LVGL::TA_STYLE::BG).dup.tap do |style| + set_style(LVGL::TA_STYLE::BG, style) + style.body_main_color = 0xFF000000 + style.body_grad_color = 0xFF000000 + style.body_radius = 5 + style.body_border_color = 0xFFFFFFFF + style.body_border_width = 3 + style.body_border_opa = 255 + style.text_color = 0xFFFFFFFF + end + get_style(LVGL::TA_STYLE::PLACEHOLDER).dup.tap do |style| + set_style(LVGL::TA_STYLE::PLACEHOLDER, style) + style.text_color = 0xFFAAAAAA + end + set_cursor_type(get_cursor_type() | LVGL::CURSOR::HIDDEN) + + self.event_handler = -> (event) do + return if hidden() + case event + when LVGL::EVENT::CLICKED + Keyboard.instance.set_ta(self) + Keyboard.instance.show() + when LVGL::EVENT::INSERT + # Not exactly right, but right enough. + char = LVGL::FFI.lv_event_get_data().to_str(1) + # Assume there is only one input. + # Also assume Enter sends; that it is a single line. + if char == "\n" + Keyboard.instance.set_ta(nil) + Keyboard.instance.hide() + # Create a new string + # get_text() gives us a Fiddle::Pointer (leaky abstraction!!!) + value = "#{get_text()}" + @on_submit.call(value) if @on_submit + hide() + end + #else + # puts "Unhandled event for #{self}: #{LVGL::EVENT.from_value(event)}" + end + end + end + + def show() + @hidden = false + LVGL::LVAnim.new().tap do |anim| + anim.set_exec_cb(self, :lv_obj_set_opa_scale) + anim.set_time(FADE_LENGTH, 0) + anim.set_values(0, 255) + anim.set_path_cb(LVGL::LVAnim::Path::EASE_OUT) + + # Launch the animation + anim.create() + end + end + + def hide(skip_animation: false) + @hidden = true + if skip_animation + set_opa_scale(0) + return + end + + LVGL::LVAnim.new().tap do |anim| + anim.set_exec_cb(self, :lv_obj_set_opa_scale) + anim.set_time(FADE_LENGTH, 0) + anim.set_values(255, 0) + anim.set_path_cb(LVGL::LVAnim::Path::EASE_IN) + + # Launch the animation + anim.create() + end + end + + def on_submit=(cb) + @on_submit = cb + end +end From 1f93066e89d2ea3a7d2b361fc3d7b4aa6c8ad35c Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 30 Oct 2020 22:16:13 -0400 Subject: [PATCH 07/16] boot/splash: Ensure no funny business if given bogus progress bar values --- boot/splash/lib/progress_bar.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/boot/splash/lib/progress_bar.rb b/boot/splash/lib/progress_bar.rb index 334cc2eb..34fcbca7 100644 --- a/boot/splash/lib/progress_bar.rb +++ b/boot/splash/lib/progress_bar.rb @@ -60,6 +60,7 @@ class ProgressBar < LVGUI::Widget end def progress=(val) + val = 100 if val > 100 @changed = true @progress_amount = val refresh_progress() From 665d58a7cdbf8c8c872b7d6308394217c8a09bed Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 30 Oct 2020 23:32:41 -0400 Subject: [PATCH 08/16] boot/splash: Add textarea and keyboard, allowing text input The following commit will plug it into the messages queue. --- boot/splash/lib/ui.rb | 82 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/boot/splash/lib/ui.rb b/boot/splash/lib/ui.rb index 58d8ab89..f9d88eaf 100644 --- a/boot/splash/lib/ui.rb +++ b/boot/splash/lib/ui.rb @@ -13,23 +13,28 @@ end class UI attr_reader :screen attr_reader :progress_bar + attr_reader :ask_identifier # As this is not using BaseWindow, LVGUI::init isn't handled for us. LVGUI.init() def initialize() add_screen + add_page # Biggest of horizontal or vertical; a percent. @unit = ([@screen.get_width, @screen.get_height].max * 0.01).ceil add_logo add_progress_bar add_label + add_textarea + add_keyboard + add_cover # last end def add_label() - @label = LVGL::LVLabel.new(@screen) + @label = LVGL::LVLabel.new(@page) @label.get_style(LVGL::LABEL_STYLE::MAIN).dup.tap do |style| @label.set_style(LVGL::LABEL_STYLE::MAIN, style) style.text_color = 0xFFFFFFFF @@ -37,7 +42,7 @@ class UI @label.set_long_mode(LVGL::LABEL_LONG::BREAK) @label.set_align(LVGL::LABEL_ALIGN::CENTER) - @label.set_width(@screen.get_width * 0.9) + @label.set_width(@page.get_width * 0.9) @label.set_pos(*center(@label, 0, 5*@unit)) @label.set_text("") end @@ -49,15 +54,15 @@ class UI file = "./logo.svg" if File.exist?("./logo.svg") return unless file - if @screen.get_height > @screen.get_width + if @page.get_height > @page.get_width # 80% of the width - LVGL::Hacks::LVNanoSVG.resize_next_width((@screen.get_width * 0.8).to_i) + LVGL::Hacks::LVNanoSVG.resize_next_width((@page.get_width * 0.8).to_i) else # 15% of the height - LVGL::Hacks::LVNanoSVG.resize_next_height((@screen.get_height * 0.15).to_i) + LVGL::Hacks::LVNanoSVG.resize_next_height((@page.get_height * 0.15).to_i) end - @logo = LVGL::LVImage.new(@screen) + @logo = LVGL::LVImage.new(@page) @logo.set_src(file) # Position the logo @@ -65,9 +70,9 @@ class UI end def add_progress_bar() - @progress_bar = ProgressBar.new(@screen) + @progress_bar = ProgressBar.new(@page) @progress_bar.set_height(3 * @unit) - @progress_bar.set_width(@screen.get_width * 0.7) + @progress_bar.set_width(@page.get_width * 0.7) @progress_bar.set_pos(*center(@progress_bar)) end @@ -82,6 +87,18 @@ class UI end end + def add_page() + @page = LVGL::LVContainer.new(@screen) + @page.set_width(@screen.get_width) + @page.set_height(@screen.get_height) + @page.get_style(LVGL::CONT_STYLE::MAIN).dup.tap do |style| + @page.set_style(LVGL::CONT_STYLE::MAIN, style) + style.body_main_color = 0xFF000000 + style.body_grad_color = 0xFF000000 + style.body_border_width = 0 + end + end + # Used to handle fade-in/fade-out # This is because opacity handles multiple overlaid objects wrong. def add_cover() @@ -103,6 +120,22 @@ class UI end end + def add_textarea() + @ta = TextArea.new(@page) + @ta.set_width(@page.get_width * 0.9) + @ta.set_pos(*center(@ta, 0, @unit * 14)) + # Always present, but initially hidden + @ta.hide(skip_animation: true) + end + + def add_keyboard() + @keyboard = Keyboard.instance() + # The keyboard is not added to the page; the page holds the elements that + # may move to ensure they're not covered by the keyboard. + @keyboard.set_parent(@screen) + @keyboard.set_height(@screen.get_width * 0.55) + end + def set_progress(amount) progress_bar.progress = amount end @@ -111,6 +144,39 @@ class UI @label.set_text(text) end + # +cb+ is a proc that wille be +#call+'d with the text once submitted. + def ask_user(placeholder: "", identifier: , cb:) + return if identifier == @ask_identifier + + @ask_identifier = identifier + @ta.set_placeholder_text(placeholder) + @ta.show() + @keyboard.set_ta(@ta) + @keyboard.show() + + bottom_space = @screen.get_height() - (@ta.get_y() + @ta.get_height()) + delta = bottom_space - @keyboard.get_height() - 3*@unit + offset_page(delta) if delta < 0 + + @ta.on_submit = ->(value) do + @ta.set_text("") + offset_page(0) + cb.call(value) + end + end + + def offset_page(delta) + LVGL::LVAnim.new().tap do |anim| + anim.set_exec_cb(@page, :lv_obj_set_y) + anim.set_time(300, 0) + anim.set_values(@page.get_y(), delta) + anim.set_path_cb(LVGL::LVAnim::Path::EASE_OUT) + + # Launch the animation + anim.create() + end + end + # Fade-in animation # Note that this looks like inverted logic because it is! # We're actually fading-out the cover! From 7b477b9ca50687c4fe07c9b4e1277a67573ecd3a Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 30 Oct 2020 23:33:39 -0400 Subject: [PATCH 09/16] boot/splash: Allow textual questions to be asked For now, extremely assumed to be passphrase input. --- boot/splash/main.rb | 59 ++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/boot/splash/main.rb b/boot/splash/main.rb index 93fa36d1..5923a255 100644 --- a/boot/splash/main.rb +++ b/boot/splash/main.rb @@ -9,14 +9,17 @@ FADE_LENGTH = 400 PROGRESS_UPDATE_LENGTH = 500 VERBOSE = !!Args.get(:verbose, false) -SOCKET = File.expand_path(Args.get(:socket, "/run/mobile-nixos-init.socket")) +SOCKET = File.expand_path(Args.get(:socket, "/run/mobile-nixos-init")) # Create the UI ui = UI.new # Socket for status updates -puts "[splash] Listening on: ipc://#{SOCKET}" -$sub = ZMQ::Sub.new("ipc://#{SOCKET}", "") +puts "[splash] Listening on: ipc://#{SOCKET}-messages" +$messages = ZMQ::Sub.new("ipc://#{SOCKET}-messages", "") + +puts "[splash] Replying on: ipc://#{SOCKET}-replies" +$replies = ZMQ::Pub.new("ipc://#{SOCKET}-replies") # Initial fade-in ui.fade_in() @@ -28,7 +31,7 @@ LVGUI.main_loop do # Empty all messages from the queue before continuing. loop do begin - msg = JSON.parse($sub.recv(LibZMQ::DONTWAIT).to_str) + msg = JSON.parse($messages.recv(LibZMQ::DONTWAIT).to_str) rescue Errno::EWOULDBLOCK # No messages left? break out! break @@ -39,25 +42,43 @@ LVGUI.main_loop do p msg end - # We might have a special command, if we got a String rather than a Hash. - if msg.is_a? String then - if msg == "quit" + # We might have a special command; handle it. + if msg["command"] then + command = msg["command"] + + case command["name"] + when "quit" ui.quit! + when "ask" + ui.ask_user(placeholder: command["placeholder"], identifier: command["identifier"], cb: ->(value) do + msg = { + type: "reply", + identifier: command["identifier"], + value: value, + }.to_json + + if VERBOSE + print "[splash:send] " + p msg + end + + $replies.send(msg) + end) else - $stderr.puts "[splash] Unexpected command #{msg}..." + $stderr.puts "[splash] Unexpected command #{command.to_json}..." end + end + + # Update the UI... + + # First updating the current progress + ui.set_progress(msg["progress"]) + + # And updating the label as needed. + if msg["label"] + ui.set_label(msg["label"]) else - # Update the UI... - - # First updating the current progress - ui.set_progress(msg["progress"]) - - # And updating the label as needed. - if msg["label"] - ui.set_label(msg["label"]) - else - ui.set_label("") - end + ui.set_label("") end end end From e891e2f75220042f043ba75eaa3787caa64b558e Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 30 Oct 2020 23:34:44 -0400 Subject: [PATCH 10/16] boot/init: Minimal changes required to continue working These changes implement the different protocol changes. --- boot/init/lib/progress.rb | 14 +++++++++----- boot/init/tasks/splash.rb | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/boot/init/lib/progress.rb b/boot/init/lib/progress.rb index e2a35025..410bd04a 100644 --- a/boot/init/lib/progress.rb +++ b/boot/init/lib/progress.rb @@ -1,20 +1,24 @@ # Progress-reporting plumbing module Progress - SOCKET = "/run/mobile-nixos-init.socket" + SOCKET = "/run/mobile-nixos-init" def self.start() @progress = 0 $logger.debug("Starting progress IPC through ZeroMQ") - $logger.debug(" -> #{SOCKET}") - @pub = ZMQ::Pub.new("ipc://#{SOCKET}") + + $logger.debug(" -> messages: #{SOCKET}") + @messages = ZMQ::Pub.new("ipc://#{SOCKET}-messages") + + $logger.debug(" -> replies: #{SOCKET}") + @replies = ZMQ::Pub.new("ipc://#{SOCKET}-replies") end # Prefer not sending messages directly, rather use the helpers. def self.publish(msg) msg = msg.to_json - if @pub + if @messages $logger.debug("[send] #{msg}") - @pub.send(msg) + @messages.send(msg) else $logger.debug("[send] Couldn't send #{msg}") end diff --git a/boot/init/tasks/splash.rb b/boot/init/tasks/splash.rb index 55bb03fc..bec87d97 100644 --- a/boot/init/tasks/splash.rb +++ b/boot/init/tasks/splash.rb @@ -30,7 +30,7 @@ class Tasks::Splash < SingletonTask # Ensures the progress is shown Progress.publish({progress: 100, label: reason}) # Command it to quit - Progress.publish("quit") + Progress.publish({command: {name: "quit"}}) # If it has quit, break out! break if Process.wait(@pid, Process::WNOHANG) From c80476a0478171ffdffbf52740cb93fc1a25ecd6 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sat, 31 Oct 2020 16:51:10 -0400 Subject: [PATCH 11/16] boot/init: Rework Progress into a "state stash" This way we really only update the current state bit we want to affect. --- boot/init/lib/progress.rb | 95 +++++++++++++++++++++++++++------------ boot/init/lib/task.rb | 2 +- boot/init/tasks/splash.rb | 13 +++--- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/boot/init/lib/progress.rb b/boot/init/lib/progress.rb index 410bd04a..9ff2ae92 100644 --- a/boot/init/lib/progress.rb +++ b/boot/init/lib/progress.rb @@ -1,48 +1,85 @@ # Progress-reporting plumbing module Progress - SOCKET = "/run/mobile-nixos-init" + SOCKET_PREFIX = "/run/mobile-nixos-init" + # Starts the queue sockets. + # This is waiting for /run/ to be available. + # It needs to be possible to let some consumers (e.g. splash) alive from + # stage-1 and waiting for fresh messages from stage-2. + # A stage-2 process could ask that splash to "hand-off" to a stage-2 splash. def self.start() @progress = 0 $logger.debug("Starting progress IPC through ZeroMQ") - $logger.debug(" -> messages: #{SOCKET}") - @messages = ZMQ::Pub.new("ipc://#{SOCKET}-messages") + $logger.debug(" -> messages: #{SOCKET_PREFIX}") + @messages_socket = ZMQ::Pub.new("ipc://#{SOCKET_PREFIX}-messages") - $logger.debug(" -> replies: #{SOCKET}") - @replies = ZMQ::Pub.new("ipc://#{SOCKET}-replies") + $logger.debug(" -> replies: #{SOCKET_PREFIX}") + @replies_socket = ZMQ::Sub.new("ipc://#{SOCKET_PREFIX}-replies", "") end - # Prefer not sending messages directly, rather use the helpers. - def self.publish(msg) - msg = msg.to_json - if @messages + # Given values (in a Hash), it will update the state with them, and send the + # updated state to the messages queue. + # +nil+ values are compacted out of the state. + def self.update(values) + @state ||= {} + @state.merge!(values).compact! + send_state() + end + + # Get a specific value from the state. + # This should be done as little as possible. + def self.get(attr) + @state ||= {} + @state[attr] + end + + # See +#get+ + def self.[](name) + get(name) + end + + # Send the current state over the messages socket. + def self.send_state() + msg = @state.to_json + if @messages_socket $logger.debug("[send] #{msg}") - @messages.send(msg) + @messages_socket.send(msg) else - $logger.debug("[send] Couldn't send #{msg}") + $logger.debug("[send] Socket not open yet.") + $logger.debug("[send] Couldn't send: #{msg}") end end - # Sets the progress to a specific amount - def self.set(amount) - @progress = amount - - publish({ - progress: @progress, - }) - end - # Executes the given block, showing the message beforehand, and removing the # message once done. - def self.with_message(msg) - publish({ - progress: @progress, - label: msg, - }) - yield - publish({ - progress: @progress, - }) + def self.exec_with_message(label) + previous = get(:label) + update({label: label}) + ret = yield + update({label: previous}) + ret + end + + # Read one reply + # If none are available, returns nil + def self.read_reply() + begin + msg = @replies_socket.recv(LibZMQ::DONTWAIT).to_str + $logger.debug("[recv] #{msg}") + JSON.parse(msg) + rescue Errno::EWOULDBLOCK + # No message? + nil + end + end + + # Reads replies until there are none + def self.each_replies() + loop do + msg = read_reply + break unless msg + yield msg + end end end diff --git a/boot/init/lib/task.rb b/boot/init/lib/task.rb index a9ab7fc1..ba02b46b 100644 --- a/boot/init/lib/task.rb +++ b/boot/init/lib/task.rb @@ -43,7 +43,7 @@ module Tasks # Update the current progress count = @tasks.length.to_f - Progress.set((100 * (1 - (todo.length / count))).ceil) + Progress.update({progress: (100 * (1 - (todo.length / count))).ceil}) todo.each do |task| if task._try_run_task then diff --git a/boot/init/tasks/splash.rb b/boot/init/tasks/splash.rb index bec87d97..a17051b3 100644 --- a/boot/init/tasks/splash.rb +++ b/boot/init/tasks/splash.rb @@ -24,14 +24,17 @@ class Tasks::Splash < SingletonTask # Implementation details-y; ask for the splash applet to be exited. def quit(reason) + # Ensures the progress is shown + Progress.update({progress: 100, label: reason}) + + # Command it to quit + Progress.update({command: {name: "quit"}}) + # Ensures that if for any reason the splash didn't start in time for the # socket to listen to this message, that we'll be quitting it. loop do - # Ensures the progress is shown - Progress.publish({progress: 100, label: reason}) - # Command it to quit - Progress.publish({command: {name: "quit"}}) - + # Repeatedly send the current state (which has the quit command). + Progress.send_state() # If it has quit, break out! break if Process.wait(@pid, Process::WNOHANG) From eaf8fa9ddee9e0d56ab3940737e1f1de0db8adb0 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sat, 31 Oct 2020 16:51:50 -0400 Subject: [PATCH 12/16] boot/init: Long running tasks can export a label With the same tooling we will be able to ask for a throbber or some other kind of work indicator. --- boot/init/lib/system.rb | 24 ++++++++++++++++++++++++ boot/init/tasks/auto_resize.rb | 10 +++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/boot/init/lib/system.rb b/boot/init/lib/system.rb index 4e827cc3..144b8ca3 100644 --- a/boot/init/lib/system.rb +++ b/boot/init/lib/system.rb @@ -51,6 +51,30 @@ module System Kernel.spawn(*args) end + # Runs a long-running task in the background while we keep the progress + # reporting active. + def self.run_long_running(*args) + pretty_command = prettify_command(*args) + pid = System.spawn(*args) + ret = nil + + loop do + # Update progress + Progress.send_state() + # Look at the status + break if ret = Process.wait(pid, Process::WNOHANG) + # Don't loop too tightly + sleep(0.1) + end + + status = $?.exitstatus + if status == 127 + raise CommandNotFound.new("Command not found... `#{pretty_command}` (#{status})") + elsif !$?.success? + raise CommandError.new("Command failed... `#{pretty_command}` (#{status})") + end + end + # Discovers the location of given program name. def self.which(program_name) ENV["PATH"].split(":").each do |path| diff --git a/boot/init/tasks/auto_resize.rb b/boot/init/tasks/auto_resize.rb index 6564d7ea..faabad3d 100644 --- a/boot/init/tasks/auto_resize.rb +++ b/boot/init/tasks/auto_resize.rb @@ -11,7 +11,7 @@ class Tasks::AutoResize < Task def run() log("Resizing #{@device}...") if @type.match(/^ext[234]$/) - Progress.with_message("Verifying #{@device}...") do + Progress.exec_with_message("Verifying #{@device}...") do # TODO: Understand the actual underlying issue with e2fsck. # It seems `e2fsck` succeeds, according to the output, but has a >0 exit # status. Running it again in those situations is a no-op, which is weird @@ -19,14 +19,14 @@ class Tasks::AutoResize < Task # This is why we unconditionally run it once, then twice. # The second will hopefully abort the boot if it fails too. begin - System.run("e2fsck", "-fp", @device) + System.run_long_running("e2fsck", "-fp", @device) rescue System::CommandError $logger.info("Re-running e2fsc...") - System.run("e2fsck", "-fp", @device) + System.run_long_running("e2fsck", "-fp", @device) end end - Progress.with_message("Resizing #{@device}...") do - System.run("resize2fs", "-f", @device) + Progress.exec_with_message("Resizing #{@device}...") do + System.run_long_running("resize2fs", "-f", @device) end else $logger.warn("Cannot resize #{@type}... filesystem left untouched.") From f45b5b301c9373654fe4c785b5e13377cf50b38a Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sat, 31 Oct 2020 17:32:19 -0400 Subject: [PATCH 13/16] boot/init: Implement asking user --- boot/init/lib/progress.rb | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/boot/init/lib/progress.rb b/boot/init/lib/progress.rb index 9ff2ae92..96060c8b 100644 --- a/boot/init/lib/progress.rb +++ b/boot/init/lib/progress.rb @@ -61,6 +61,41 @@ module Progress ret end + def self.ask(placeholder, label: nil) + identifier = "0x#{Random.rand(0xFFFFF).to_s(16)}" + + previous_label = get(:label) + Progress.update({label: label}) if label + + update(command: { + name: "ask", + identifier: identifier, + placeholder: placeholder, + }) + + value = loop do + # Keep progress state updated for processes attaching late. + send_state() + value = + each_replies do |reply| + # A reply for the current question? + if reply and reply["type"] == "reply" and reply["identifier"] == identifier + break reply["value"] + else + nil + end + end + break value if value + + # Leave some breathing room to the CPU! + sleep(0.1) + end + + update({label: previous_label}) + + value + end + # Read one reply # If none are available, returns nil def self.read_reply() From 36aa305f27a3dfef22aa8874e68f52fd2944b067 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 1 Nov 2020 15:44:48 -0500 Subject: [PATCH 14/16] boot/init: Add LUKS device support --- boot/init/lib/mounting.rb | 4 +++ boot/init/tasks/luks.rb | 64 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 boot/init/tasks/luks.rb diff --git a/boot/init/lib/mounting.rb b/boot/init/lib/mounting.rb index 864a97a8..6b4e1db2 100644 --- a/boot/init/lib/mounting.rb +++ b/boot/init/lib/mounting.rb @@ -75,6 +75,10 @@ module Mounting [mount_point, task] end.to_h auto_depend_mount_points(mount_points) + + (Configuration["luksDevices"] or []).each do |mapper, info| + Tasks::Luks.new(info["device"], mapper) + end end end diff --git a/boot/init/tasks/luks.rb b/boot/init/tasks/luks.rb new file mode 100644 index 00000000..8f9d78cd --- /dev/null +++ b/boot/init/tasks/luks.rb @@ -0,0 +1,64 @@ +# Opens LUKS devices +class Tasks::Luks < Task + attr_reader :source + attr_reader :mapper + + TRIES = 10 + + class ExistingLuksTask < StandardError + end + + class CouldNotUnlock < StandardError + end + + def self.register(mapper, instance) + @registry ||= {} + unless @registry[mapper].nil? then + raise ExistingLuksTask.new("LUKS task for '#{mapper}' already exists.") + end + @registry[mapper] = instance + end + + def self.registry() + @registry + end + + def initialize(source, mapper) + @source = source + @mapper = mapper + + add_dependency(:Task, Tasks::UDev.instance) + add_dependency(:Files, source) + add_dependency(:Mount, "/run") + add_dependency(:Target, :Environment) + self.class.register(@mapper, self) + end + + def run() + FileUtils.mkdir_p("/run/cryptsetup") + + TRIES.times do + passphrase = Progress.ask("Passphrase for #{mapper}") + + begin + Progress.exec_with_message("Checking...") do + # TODO: implement with process redirection rather than shelling out + System.run("echo #{passphrase.shellescape} | exec cryptsetup luksOpen #{source.shellescape} #{mapper.shellescape}") + end + Progress.update({label: nil}) + + # If we're there, we're done! + return + rescue System::CommandError + Progress.update({label: "Wrong passphrase given..."}) + end + end + + # We failed multiple times. + raise CouldNotUnlock.new("Could not unlock #{source}; tried #{TRIES} times.") + end + + def name() + "#{super}(#{source}, #{mapper})" + end +end From 3f8c115ee435f25820b080bc07abea0ab4207a48 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 1 Nov 2020 15:46:28 -0500 Subject: [PATCH 15/16] examples/testing: Add subdirectory for feature testing systems --- examples/testing/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 examples/testing/README.md diff --git a/examples/testing/README.md b/examples/testing/README.md new file mode 100644 index 00000000..4d601e35 --- /dev/null +++ b/examples/testing/README.md @@ -0,0 +1,13 @@ +Testing examples +================ + +These examples are made specifically to test features. + +They are not to be used as a normal system, as they likely have some fatal +flaws, either usability-wise (e.g. not usable at all) or security-wise (e.g. +default passwords). + +More details are available inside each testing systems directories. + +Though, please refer to those systems as reference implementation for +validating changes around their features under test! From a1813efdfb75a556ad1ec372a5181ff61f0501c1 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 1 Nov 2020 16:00:43 -0500 Subject: [PATCH 16/16] testing/qemu-cryptesetup: Add a test system for LUKS --- examples/testing/qemu-cryptsetup/README.md | 45 ++++++++++++ .../testing/qemu-cryptsetup/configuration.nix | 68 +++++++++++++++++++ examples/testing/qemu-cryptsetup/default.nix | 11 +++ 3 files changed, 124 insertions(+) create mode 100644 examples/testing/qemu-cryptsetup/README.md create mode 100644 examples/testing/qemu-cryptsetup/configuration.nix create mode 100644 examples/testing/qemu-cryptsetup/default.nix diff --git a/examples/testing/qemu-cryptsetup/README.md b/examples/testing/qemu-cryptsetup/README.md new file mode 100644 index 00000000..85b86f5d --- /dev/null +++ b/examples/testing/qemu-cryptsetup/README.md @@ -0,0 +1,45 @@ +`qemu-cryptsetup` +================= + +What does this test? +-------------------- + +Using the `hello` system, pre-configured to use the `qemu` system type. + +This tests: + + - Encryption passphrase at boot + +**This test is manual.** + +The passphrase in use is: + +``` +1234 +``` + + +Why is this scary? +------------------ + + - Secrets in the store! + - Well-known insecure passphrase + + +How is success defined? +----------------------- + +The `hello` system applet is booted-to after the user supplies the encryption +passphrase during boot. + + +How is success defined? +----------------------- + +Assuming you are `cd`'d into the root of a Mobile NixOS checkout: + +``` +nix-build ./examples/testing/qemu-cryptsetup && ./result +``` + +As always, be mindful of your `NIX_PATH`. diff --git a/examples/testing/qemu-cryptsetup/configuration.nix b/examples/testing/qemu-cryptsetup/configuration.nix new file mode 100644 index 00000000..561c0f2c --- /dev/null +++ b/examples/testing/qemu-cryptsetup/configuration.nix @@ -0,0 +1,68 @@ +{ config, lib, pkgs, ... }: + +let + # This is not a secure or safe way to create an encrypted drive in a build. + # This is SOLELY for testing purposes. + passphrase = "1234"; + uuid = "12345678-1234-1234-1234-123456789abc"; # heh + + # We are re-using the raw filesystem from the hello system. + rootfsExt4 = ( + import ../../hello { device = config.mobile.device.name; } + ).build.rootfs; + + # This is not a facility from the disk images builder because **it is really + # insecure to use**. + # So, for now, we have an implementation details-y way of producing an + # encrypted rootfs. + encryptedRootfs = pkgs.vmTools.runInLinuxVM ( + pkgs.runCommand "encrypted-rootfs" { + buildInputs = [ pkgs.cryptsetup ]; + } '' + (PS4=" $ "; set -x + mkdir -p /run/cryptsetup + mkdir -p $out + cd $out + + slack=32 # MiB + + # Some slack space we'll append to the raw fs + # Used by `--reduce-device-size` read cryptsetup(8). + dd if=/dev/zero of=tmp.img bs=1024 count=$((slack*1024)) + + # Catting both to ensure it's writable, and to add some slack space at + # the end + cat ${rootfsExt4}/${rootfsExt4.label}.img tmp.img > encrypted.img + rm tmp.img + + echo ${builtins.toJSON passphrase} | cryptsetup \ + reencrypt \ + --encrypt ./encrypted.img \ + --reduce-device-size $((slack*1024*1024)) + + #echo YES | + cryptsetup luksUUID --uuid=${builtins.toJSON uuid} ./encrypted.img + ) + '' + ); +in + +{ + boot.initrd.luks.devices = { + LUKS-MOBILE-ROOTFS = { + device = "/dev/disk/by-uuid/${uuid}"; + }; + }; + + fileSystems = { + "/" = { + device = "/dev/mapper/LUKS-MOBILE-ROOTFS"; + fsType = "ext4"; + }; + }; + + # Instead of the (mkDefault) rootfs, provide our raw encrypted rootfs. + mobile.generatedFilesystems.rootfs = { + raw = encryptedRootfs; + }; +} diff --git a/examples/testing/qemu-cryptsetup/default.nix b/examples/testing/qemu-cryptsetup/default.nix new file mode 100644 index 00000000..436e432a --- /dev/null +++ b/examples/testing/qemu-cryptsetup/default.nix @@ -0,0 +1,11 @@ +let + device = "qemu-x86_64"; + system-build = import ../../../. { + inherit device; + configuration = [ { imports = [ + ../../hello/configuration.nix + ./configuration.nix + ]; } ]; + }; +in + system-build.build.default