Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion projects/LaunchWizard/main/SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SRCS = Glob("src/*.c*")
SRCS += append_srcs_dir(ADir("ui"))
INCLUDE = [ADir("."), ADir("ui")]
PRIVATE_INCLUDE = []
REQUIREMENTS = ["cp0_lvgl", "lvgl_component", "pthread", "Sigslot", "eventpp", "Miniaudio", "RadioLib"]
REQUIREMENTS = ["cp0_lvgl", "lvgl_component", "pthread", "crypt", "Sigslot", "eventpp", "Miniaudio", "RadioLib"]
STATIC_LIB = []
DYNAMIC_LIB = []
DEFINITIONS = []
Expand Down
3 changes: 2 additions & 1 deletion projects/LaunchWizard/main/ui/first_boot_policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ bool should_run_wizard(const FirstBootState &state)
if (state.rearm_marker)
return true;
if (state.factory_marker)
return true;
return state.factory_username &&
(!state.user_has_password || state.factory_credentials);
return state.legacy_piwiz_active;
}

Expand Down
20 changes: 16 additions & 4 deletions projects/LaunchWizard/main/ui/first_boot_policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,26 @@ struct FirstBootState {
bool rearm_marker = false;
// /var/lib/LaunchWizard/run-oobe: baked into every factory image by pi-gen.
bool factory_marker = false;
// The UID 1000 user is still named after the factory default ("pi"). A
// rename can only come from provisioning (Imager/userconf), so a changed
// name alone means the device was configured.
bool factory_username = false;
// The UID 1000 user's shadow entry holds a real "$..." hash, meaning the
// account was configured (Raspberry Pi Imager, a finished OOBE, ...).
bool user_has_password = false;
// The stored hash still verifies against the factory default password that
// pi-gen bakes into the image ("raspberry"), so an existing password does
// NOT mean the user configured the device.
bool factory_credentials = false;
// Legacy piwiz/lightdm first-boot autologin is still armed.
bool legacy_piwiz_active = false;
};

// The wizard runs when either OOBE marker is present (a user-requested re-run
// or a factory first boot), or when the legacy piwiz/lightdm first-boot
// autologin is still armed. LaunchWizard.service is only disabled after the
// wizard finishes configuration (apply_all).
// A user-requested re-run always shows the wizard. The factory marker shows it
// only while the account is exactly in factory state: default username with
// either no password at all or the baked default password. A changed username
// or a user-chosen password both mean the device was provisioned (Raspberry Pi
// Imager), so first boot skips straight to the launcher.
bool should_run_wizard(const FirstBootState &state);

// The keyboard guide runs exactly once per device, before and independently of
Expand Down
59 changes: 56 additions & 3 deletions projects/LaunchWizard/main/ui/wizard_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,18 @@ extern char **environ;
#endif
#endif

#if !LAUNCH_WIZARD_DRY_RUN
#include <crypt.h>
#endif

namespace launch_wizard {

constexpr uid_t kDefaultUserUid = 1000;
constexpr const char *kFirstBootWizardUser = "rpi-first-boot-wizard";
// Factory account baked into every image by pi-gen (build.yml FIRST_USER_NAME /
// FIRST_USER_PASS). Keep in sync with the pi-gen workflow configuration.
constexpr const char *kFactoryDefaultUser = "pi";
constexpr const char *kFactoryDefaultPassword = "raspberry";
#if LAUNCH_WIZARD_DRY_RUN
constexpr const char *kAccountJournalDir = "/tmp/LaunchWizard-dry-run";
constexpr const char *kAccountJournalPath =
Expand Down Expand Up @@ -282,6 +290,45 @@ bool first_user_has_password()
#endif
}

// True while the UID 1000 user is still named after the pi-gen factory
// default. userconf/Imager renames the user in passwd, so a different name
// alone proves the device was provisioned.
bool first_user_has_factory_name()
{
#if LAUNCH_WIZARD_DRY_RUN
return true;
#else
struct passwd *pw = getpwuid(kDefaultUserUid);
return pw && pw->pw_name && strcmp(pw->pw_name, kFactoryDefaultUser) == 0;
#endif
}

// True while the UID 1000 account still carries the factory credentials that
// pi-gen bakes into the image (FIRST_USER_NAME=pi / FIRST_USER_PASS=raspberry).
// Verification follows crypt(5): hash the candidate with the stored hash as
// the setting string; a byte-identical result means the password matches. Any
// crypt failure yields NULL or a "*" failure token that never compares equal,
// so errors safely count as "not factory".
bool first_user_has_factory_credentials()
{
#if LAUNCH_WIZARD_DRY_RUN
return false;
#else
struct passwd *pw = getpwuid(kDefaultUserUid);
if (!pw || !pw->pw_name || strcmp(pw->pw_name, kFactoryDefaultUser) != 0)
return false;
struct spwd *sp = getspnam(pw->pw_name);
if (!sp || !sp->sp_pwdp || sp->sp_pwdp[0] != '$')
return false;
// struct crypt_data is ~32 KiB; keep it off the stack. should_run() is
// called once from the single-threaded startup path.
static struct crypt_data data;
memset(&data, 0, sizeof(data));
const char *hash = crypt_r(kFactoryDefaultPassword, sp->sp_pwdp, &data);
return hash && hash[0] == '$' && strcmp(hash, sp->sp_pwdp) == 0;
#endif
}

std::vector<std::string> initial_user_groups()
{
#if LAUNCH_WIZARD_DRY_RUN
Expand Down Expand Up @@ -1002,10 +1049,16 @@ bool launch_wizard::WizardService::should_run()
// apply_all() clears it on completion, so the wizard still runs exactly
// once.
state.rearm_marker = access(kRearmOobeMarker, F_OK) == 0;
// pi-gen bakes the factory marker into every image. Any marker means the
// OOBE must run; only apply_all() disables LaunchWizard.service after the
// user completes configuration.
// pi-gen bakes the factory marker into every image. It must only trigger
// the OOBE while the account is exactly in factory state: still named
// "pi", with no password or the baked "raspberry" default. A renamed user
// or a user-chosen password means Raspberry Pi Imager provisioned the
// device, so first boot skips straight to the launcher
// (finish_configured_system() then removes the marker).
state.factory_marker = access(kFactoryOobeMarker, F_OK) == 0;
state.factory_username = first_user_has_factory_name();
state.user_has_password = first_user_has_password();
state.factory_credentials = first_user_has_factory_credentials();
state.legacy_piwiz_active =
lightdm_autologin_user() == kFirstBootWizardUser && piwiz_autostart_enabled();
return should_run_wizard(state);
Expand Down
29 changes: 26 additions & 3 deletions projects/LaunchWizard/tests/test_first_boot_policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,31 @@ bool test_first_boot_policy()
bool passed = true;
launch_wizard::FirstBootState state;

// Factory first boot: marker present -> wizard, regardless of whether the
// account already has a password. Only apply_all() disables the service.
// Factory first boot: marker present, default username, no password ->
// wizard.
state.factory_marker = true;
passed &= expect_wizard(true, state, "factory marker");
state.factory_username = true;
passed &= expect_wizard(true, state, "factory unconfigured");

// Factory image with the baked pi/raspberry password: the hash exists but
// still verifies as the factory default, so the wizard must run.
state.user_has_password = true;
state.factory_credentials = true;
passed &= expect_wizard(true, state, "factory baked default password");

// Imager-provisioned device: factory marker still present, but the user
// chose their own password -> skip straight to the launcher (bug #227).
state.factory_credentials = false;
passed &= expect_wizard(false, state, "factory imager-provisioned");

// A renamed user always means the device was provisioned, even when no
// password was set (e.g. Imager with SSH keys only).
state.factory_username = false;
state.user_has_password = false;
passed &= expect_wizard(false, state, "renamed user without password");
state.user_has_password = true;
state.factory_credentials = true;
passed &= expect_wizard(false, state, "renamed user keeps factory password");

// Settings "Run Setup Wizard" re-arm always wins, even when configured.
state.rearm_marker = true;
Expand All @@ -37,6 +58,8 @@ bool test_first_boot_policy()
passed &= expect_wizard(false, state, "no markers");
state.legacy_piwiz_active = true;
passed &= expect_wizard(true, state, "legacy piwiz");
state.user_has_password = true;
passed &= expect_wizard(true, state, "legacy piwiz ignores password");

// Keyboard guide: needs both the marker and the installed binary; a
// missing binary keeps the marker for a later boot.
Expand Down
Loading