mirror of
https://github.com/NixOS/mobile-nixos.git
synced 2024-12-17 13:10:29 +03:00
Merge pull request #234 from samueldr-wip/feature/stage-1-passphrase
stage-1: Add interactive LUKS decrypting
This commit is contained in:
commit
63d49f51ad
@ -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
|
||||
|
||||
|
@ -1,44 +1,120 @@
|
||||
# Progress-reporting plumbing
|
||||
module Progress
|
||||
SOCKET = "/run/mobile-nixos-init.socket"
|
||||
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(" -> #{SOCKET}")
|
||||
@pub = ZMQ::Pub.new("ipc://#{SOCKET}")
|
||||
|
||||
$logger.debug(" -> messages: #{SOCKET_PREFIX}")
|
||||
@messages_socket = ZMQ::Pub.new("ipc://#{SOCKET_PREFIX}-messages")
|
||||
|
||||
$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 @pub
|
||||
# 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}")
|
||||
@pub.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
|
||||
|
||||
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()
|
||||
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
|
||||
|
@ -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|
|
||||
|
@ -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
|
||||
|
@ -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.")
|
||||
|
64
boot/init/tasks/luks.rb
Normal file
64
boot/init/tasks/luks.rb
Normal file
@ -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
|
@ -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("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)
|
||||
|
||||
|
61
boot/splash/lib/keyboard.rb
Normal file
61
boot/splash/lib/keyboard.rb
Normal file
@ -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
|
@ -60,6 +60,7 @@ class ProgressBar < LVGUI::Widget
|
||||
end
|
||||
|
||||
def progress=(val)
|
||||
val = 100 if val > 100
|
||||
@changed = true
|
||||
@progress_amount = val
|
||||
refresh_progress()
|
||||
|
90
boot/splash/lib/text_area.rb
Normal file
90
boot/splash/lib/text_area.rb
Normal file
@ -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
|
@ -13,22 +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
|
||||
@ -36,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
|
||||
@ -48,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
|
||||
@ -64,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
|
||||
|
||||
@ -81,14 +87,27 @@ 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()
|
||||
@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)
|
||||
@ -101,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
|
||||
@ -109,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!
|
||||
|
@ -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,14 +42,33 @@ 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!
|
||||
else
|
||||
$stderr.puts "[splash] Unexpected command #{msg}..."
|
||||
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 #{command.to_json}..."
|
||||
end
|
||||
end
|
||||
|
||||
# Update the UI...
|
||||
|
||||
# First updating the current progress
|
||||
@ -60,7 +82,6 @@ LVGUI.main_loop do
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
$stderr.puts "[splash] Broke out of the rendering loop. That's not supposed to happen."
|
||||
exit(1)
|
||||
|
13
examples/testing/README.md
Normal file
13
examples/testing/README.md
Normal file
@ -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!
|
45
examples/testing/qemu-cryptsetup/README.md
Normal file
45
examples/testing/qemu-cryptsetup/README.md
Normal file
@ -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`.
|
68
examples/testing/qemu-cryptsetup/configuration.nix
Normal file
68
examples/testing/qemu-cryptsetup/configuration.nix
Normal file
@ -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;
|
||||
};
|
||||
}
|
11
examples/testing/qemu-cryptsetup/default.nix
Normal file
11
examples/testing/qemu-cryptsetup/default.nix
Normal file
@ -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
|
@ -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;
|
||||
|
27
modules/luks.nix
Normal file
27
modules/luks.nix
Normal file
@ -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"
|
||||
];}
|
||||
];
|
||||
};
|
||||
}
|
@ -30,6 +30,7 @@
|
||||
./initrd-vendor.nix
|
||||
./initrd.nix
|
||||
./internal.nix
|
||||
./luks.nix
|
||||
./mobile-device.nix
|
||||
./nixpkgs.nix
|
||||
./quirks
|
||||
|
@ -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);
|
||||
|
@ -18,8 +18,8 @@ mrbgems.mkGem {
|
||||
src = fetchFromGitHub {
|
||||
repo = "mruby-lvgui";
|
||||
owner = "mobile-nixos";
|
||||
rev = "ab7cf5b1b2e318a4bf5fc973507eb842dce80214";
|
||||
sha256 = "09h9f3xlbvxdwpmzpf7whq0gphiv68842shy03ld712fw25393jx";
|
||||
rev = "f1bb1dd9b2c5aa3d3df4fcc41ca706f426d182a8";
|
||||
sha256 = "0ybjkzg743d21rn3q0vi0fa9zwp3ym9zw2q5ym24wc7gxdspjcjs";
|
||||
};
|
||||
|
||||
gemBuildInputs = [
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user