refactor: replace shell-based password handling with secure stdin piping for LUKS and GRUB operations

This commit is contained in:
2026-06-03 13:39:34 +03:00
parent 8cdaf0dfa3
commit d7065dcb59
8 changed files with 239 additions and 46 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "yali-rs"
version = "4.0.0"
edition = "2021"
edition = "2024"
[dependencies]
eframe = "0.27"
+2 -1
View File
@@ -281,7 +281,7 @@ admin_title = "Root Password"
admin_description = "Set a password for the root (administrator) account. Leave empty to disable direct root login."
admin_password_label = "Root password:"
admin_password_confirm_label = "Confirm root password:"
admin_password_short = "Root password must be at least 4 characters."
admin_password_short = "Root password must be at least 8 characters."
# ── Date/Time ──────────────────────────────────────────────
datetime = "Date & Time"
@@ -532,3 +532,4 @@ lvm_lv_size = "Size (MB, 0=remaining):"
lvm_lv_action = "Action"
oem_change_password_warning = "OEM mode: You must change the default password before continuing."
+2 -1
View File
@@ -293,7 +293,7 @@ admin_title = "Root Parolası"
admin_description = "Root (yönetici) hesabı için bir parola belirleyin. Boş bırakılırsa doğrudan root girişi devre dışı kalır."
admin_password_label = "Root parolası:"
admin_password_confirm_label = "Root parolası (tekrar):"
admin_password_short = "Root parolası en az 4 karakter olmalıdır."
admin_password_short = "Root parolası en az 8 karakter olmalıdır."
# ── Tarih & Saat ────────────────────────────────────────────
datetime = "Tarih & Saat"
@@ -539,3 +539,4 @@ xfce_thunar_description = "Toplu yeniden adlandırma ve eklentiler ile hızlı v
xfce_settings_title = "XFCE Ayarları"
xfce_settings_description = "Masaüstü özelleştirmesi için kapsamlı ayar yöneticisi."
oem_change_password_warning = "OEM modu: Devam etmeden önce varsayılan şifreyi değiştirmelisiniz."
+32
View File
@@ -66,6 +66,38 @@ pub fn load(path: &std::path::Path) -> Result<AnswerFile, String> {
pub fn apply_to_state(af: &AnswerFile, state: &mut crate::installer::GlobalState) {
use crate::installer::{DiskInfo, PartitionPlan, PartitionTableType};
// --- Giriş doğrulama ---
// Disk yolu: yalnızca /dev/ ile başlayan, alfanumerik + /._- içeren değerler
let disk = &af.install.disk;
if !disk.starts_with("/dev/")
|| disk.contains("..")
|| !disk.chars().all(|c| c.is_alphanumeric() || "/_.-".contains(c))
{
eprintln!("autoinstall: geçersiz disk yolu reddedildi: '{}'", disk);
return;
}
// Timezone doğrulama
let tz = &af.install.timezone;
if tz.is_empty()
|| tz.contains("..")
|| !tz.chars().all(|c| c.is_alphanumeric() || "/._-+".contains(c))
{
eprintln!("autoinstall: geçersiz timezone reddedildi: '{}'", tz);
return;
}
// Locale doğrulama
let locale = &af.install.locale;
if locale.is_empty()
|| locale.contains("..")
|| locale.contains('/')
|| !locale.chars().all(|c| c.is_alphanumeric() || "._-@".contains(c))
{
eprintln!("autoinstall: geçersiz locale reddedildi: '{}'", locale);
return;
}
// Temel alanlar
state.timezone = af.install.timezone.clone();
state.keyboard_layout = af.install.keyboard.clone();
+4
View File
@@ -232,6 +232,9 @@ pub struct GlobalState {
#[allow(dead_code)]
pub luks_password: String,
pub oem_mode: bool,
/// OEM modunda: kullanıcı varsayılan şifreyi değiştirdi mi?
/// true olmadan kurulum ilerleyemez.
pub oem_password_changed: bool,
pub selected_package_groups: Vec<String>,
/// branding.toml [slides.icons] tablosundan yüklenen ikon → dosya URI eşlemesi.
/// Slideshow bu map üzerinden çalışma zamanında görsel yollarını çözümler.
@@ -290,6 +293,7 @@ impl Default for GlobalState {
encrypt_root: false,
luks_password: String::new(),
oem_mode: false,
oem_password_changed: false,
selected_package_groups: Vec::new(),
display_manager: "sddm".to_string(),
autologin: true,
+175 -40
View File
@@ -220,12 +220,32 @@ impl Job for UnmountBindJob {
"sys",
"proc",
];
let mut errors: Vec<String> = Vec::new();
for subdir in &subdirs {
let target = format!("{}/{}", mt, subdir);
ui.log(format!("umount {}", target));
// Hata görmezden gel; bazıları zaten unmount olmuş olabilir
let _ = tokio::process::Command::new("umount")
.arg("-lf").arg(&target).status().await;
match tokio::process::Command::new("umount")
.arg("-lf").arg(&target).status().await
{
Ok(status) if status.success() => {}
Ok(status) => {
// "not mounted" veya "no such file" hataları beklenen durumlar —
// bunları uyarı olarak logla ama kritik hata sayma.
ui.log(format!("⚠ umount uyarısı (kritik değil, kod {}): {}", status, target));
}
Err(e) => {
// Komut çalıştırılamadı — bu gerçek bir sorun.
let msg = format!("umount komutu başlatılamadı ({}): {}", target, e);
ui.log(format!("{}", msg));
errors.push(msg);
}
}
}
if !errors.is_empty() {
return Err(format!(
"Bind mount temizleme hatası — bazı sanal dosya sistemleri bağlı kalmış olabilir:\n{}",
errors.join("\n")
));
}
Ok(())
}
@@ -255,11 +275,14 @@ impl Job for GenerateInitramfsJob {
let _ = tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" lvm \"\n").await;
// mkinitcpio: add lvm2 hook
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
if let Ok(content) = tokio::fs::read_to_string(&mkinitcpio_conf).await {
if content.contains("HOOKS=") && !content.contains("lvm2") {
let new_content = content.replace("HOOKS=\"", "HOOKS=\"lvm2 ");
let new_content = content
.replace("HOOKS=\"", "HOOKS=\"lvm2 ")
.replace("HOOKS=(", "HOOKS=(lvm2 ");
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
}
}
@@ -279,12 +302,15 @@ impl Job for GenerateInitramfsJob {
.await.map_err(|e| format!("dracut luks conf yazılamadı: {}", e))?;
// mkinitcpio: add encrypt hook
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
let content = tokio::fs::read_to_string(&mkinitcpio_conf)
.await.unwrap_or_default();
if content.contains("HOOKS=") && !content.contains("encrypt") {
let new_content = content.replace("HOOKS=\"", "HOOKS=\"encrypt ");
let new_content = content
.replace("HOOKS=\"", "HOOKS=\"encrypt ")
.replace("HOOKS=(", "HOOKS=(encrypt ");
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
}
}
@@ -331,18 +357,49 @@ impl Job for InstallGrubJob {
let grub_d = format!("{}/etc/grub.d", self.mount);
let _ = tokio::fs::create_dir_all(&grub_d).await;
// GRUB şifresi (varsa) — bcrypt hash ile /etc/grub.d/40_custom yazılır
// GRUB şifresi (varsa) — grub-mkpasswd-pbkdf2 ile hash üretilir,
// düz metin asla diske yazılmaz.
if !self.password.is_empty() {
ui.log("GRUB şifresi yapılandırılıyor…");
ui.log("GRUB şifresi PBKDF2 ile hashleniyor…");
// grub-mkpasswd-pbkdf2 şifreyi stdin'den iki kez okur (şifre + doğrulama)
use tokio::io::AsyncWriteExt;
let pw_input = format!("{}\n{}\n", self.password, self.password);
let mut child = tokio::process::Command::new("grub-mkpasswd-pbkdf2")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| format!("grub-mkpasswd-pbkdf2 başlatılamadı: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(pw_input.as_bytes()).await
.map_err(|e| format!("grub-mkpasswd-pbkdf2 stdin hatası: {}", e))?;
}
let output = child.wait_with_output().await
.map_err(|e| format!("grub-mkpasswd-pbkdf2 bekleme hatası: {}", e))?;
// Çıktıdan "grub.pbkdf2.sha512...." token'ını ayrıştır
let stdout = String::from_utf8_lossy(&output.stdout);
let hash = stdout
.lines()
.find_map(|l| {
let pos = l.find("grub.pbkdf2")?;
Some(l[pos..].trim().to_string())
})
.ok_or("grub-mkpasswd-pbkdf2 çıktısında hash bulunamadı")?;
let custom_grub = format!(
"set superusers=\"root\"\npassword_pbkdf2 root {} \n",
self.password
"#!/bin/sh\nset superusers=\"root\"\npassword_pbkdf2 root {}\n",
hash
);
let pw_path = format!("{}/etc/grub.d/40_custom", self.mount);
tokio::fs::write(&pw_path, custom_grub)
.await
.map_err(|e| format!("GRUB şifre dosyası yazılamadı: {}", e))?;
run_cmd("chmod", &["+x", &pw_path]).await?;
ui.log("GRUB şifresi PBKDF2 hash olarak yazıldı.");
}
// GRUB timeout ayarı
@@ -552,6 +609,79 @@ async fn run_chroot(mount: &str, cmd: &[&str]) -> Result<(), String> {
}
}
/// LUKS bölümünü güvenli şekilde formatlar.
///
/// Şifre, `sh -c` + string interpolasyon yerine doğrudan `stdin` pipe'ı
/// üzerinden iletilir; böylece shell injection saldırıları önlenir.
async fn run_luks_format(device: &str, password: &str) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
return Ok(());
}
use tokio::io::AsyncWriteExt;
let mut child = tokio::process::Command::new("cryptsetup")
.args([
"luksFormat", "-q", "--type", "luks2",
"--key-file", "-",
device,
])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("cryptsetup luksFormat başlatılamadı: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(password.as_bytes()).await
.map_err(|e| format!("cryptsetup stdin yazma hatası: {}", e))?;
}
let output = child.wait_with_output().await
.map_err(|e| format!("cryptsetup bekleme hatası: {}", e))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
/// LUKS bölümünü güvenli şekilde açar (open).
///
/// Şifre, shell interpolasyon yerine `stdin` pipe'ı üzerinden iletilir.
async fn run_luks_open(device: &str, name: &str, password: &str) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
return Ok(());
}
use tokio::io::AsyncWriteExt;
let mut child = tokio::process::Command::new("cryptsetup")
.args(["open", "--key-file", "-", device, name])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("cryptsetup open başlatılamadı: {}", e))?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(password.as_bytes()).await
.map_err(|e| format!("cryptsetup open stdin yazma hatası: {}", e))?;
}
let output = child.wait_with_output().await
.map_err(|e| format!("cryptsetup open bekleme hatası: {}", e))?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
/// Bölümlendirme öncesi diski hazırlar:
/// mevcut bağlamaları ve swap'ı ayırır, ardından kernel'e yeni
/// bölüm tablosunu duyurarak aygıt düğümlerinin hazır olmasını bekler.
@@ -791,25 +921,17 @@ impl Job for PartitionJob {
run_cmd("swapon", &[&swap_part]).await?;
// LUKS şifreleme (kök bölümü)
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
let root_device = if self.plan.encrypt_root && !self.plan.luks_password.is_empty() {
let luks_name = "cryptroot".to_string();
let luks_name = "cryptroot";
ui.log(format!("cryptsetup luksFormat {}", root_part));
let _ = run_cmd("cryptsetup", &[
"luksFormat", "-q", "--type", "luks2", &root_part,
"--key-file", "-",
]).await;
// Burada batch modda çalıştığımız için şifreyi pipe ile veremeyiz;
// bunun yerine echo -n şifre | cryptsetup ile çalışır
let password_input = format!("echo -n '{}' | cryptsetup luksFormat -q --type luks2 {} --key-file=-", self.plan.luks_password, root_part);
let _ = tokio::process::Command::new("sh")
.args(["-c", &password_input])
.output().await;
run_luks_format(&root_part, &self.plan.luks_password).await
.map_err(|e| format!("LUKS format hatası: {}", e))?;
ui.log(format!("cryptsetup open {} {}", root_part, luks_name));
let open_cmd = format!("echo -n '{}' | cryptsetup open {} {}", self.plan.luks_password, root_part, luks_name);
let _ = tokio::process::Command::new("sh")
.args(["-c", &open_cmd])
.output().await;
run_luks_open(&root_part, luks_name, &self.plan.luks_password).await
.map_err(|e| format!("LUKS open hatası: {}", e))?;
format!("/dev/mapper/{}", luks_name)
} else {
@@ -1289,6 +1411,17 @@ impl Job for ConfigureLocaleJob {
fn name(&self) -> &str { "job_locale" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// Locale değeri doğrulama: yalnızca alfanumerik, '.', '_', '-', '@'
// karakterlerine izin ver. Yol manipülasyonu ve injection önlenir.
let locale_valid = !self.locale.is_empty()
&& !self.locale.contains("..")
&& !self.locale.contains('/')
&& !self.locale.contains('\n')
&& self.locale.chars().all(|c| c.is_alphanumeric() || "._-@".contains(c));
if !locale_valid {
return Err(format!("Geçersiz locale değeri: '{}'", self.locale));
}
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
let _ = tokio::fs::create_dir_all(format!("{}/etc/env.d", self.mount)).await;
@@ -1331,9 +1464,9 @@ impl Job for ConfigureLocaleJob {
.await
.map_err(|e| format!("/etc/locale-gen yazılamadı: {}", e))?;
// locale-gen çalıştır
// locale-gen çalıştır — kurulum sonrası locale desteği için zorunlu
ui.log("locale-gen çalıştırılıyor…");
//run_chroot(&self.mount, &["locale-gen"]).await?;
run_chroot(&self.mount, &["locale-gen"]).await?;
Ok(())
}
@@ -1350,6 +1483,16 @@ impl Job for ConfigureTimezoneJob {
fn name(&self) -> &str { "job_timezone" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// Path traversal koruması: timezone değeri yalnızca alfanumerik,
// '/', '_', '-', '+' karakterlerine izin verir; ".." asla geçemez.
let tz = &self.timezone;
let tz_valid = !tz.is_empty()
&& !tz.contains("..")
&& tz.chars().all(|c| c.is_alphanumeric() || "/._-+".contains(c));
if !tz_valid {
return Err(format!("Geçersiz timezone değeri: '{}'", tz));
}
// Sembolik link: /etc/localtime → /usr/share/zoneinfo/<tz>
let link_target = format!("/usr/share/zoneinfo/{}", self.timezone);
let link_path = format!("{}/etc/localtime", self.mount);
@@ -1775,6 +1918,8 @@ impl Job for CustomPartitionJob {
let path = &part.device;
// LUKS şifreleme (kök veya isteğe bağlı bölümler)
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
let fs_device = if part.encrypt && !part.luks_password.is_empty() {
let luks_name = if part.luks_name.is_empty() {
format!("crypt_{}", part.mountpoint.replace("/", "_"))
@@ -1784,22 +1929,12 @@ impl Job for CustomPartitionJob {
let luks_name = luks_name.trim_start_matches('_').to_string();
ui.log(format!("cryptsetup luksFormat {}", path));
let format_cmd = format!(
"echo -n '{}' | cryptsetup luksFormat -q --type luks2 {} --key-file=-",
part.luks_password, path
);
let _ = tokio::process::Command::new("sh")
.args(["-c", &format_cmd])
.output().await;
run_luks_format(path, &part.luks_password).await
.map_err(|e| format!("LUKS format hatası ({}): {}", path, e))?;
ui.log(format!("cryptsetup open {} {}", path, luks_name));
let open_cmd = format!(
"echo -n '{}' | cryptsetup open {} {}",
part.luks_password, path, luks_name
);
let _ = tokio::process::Command::new("sh")
.args(["-c", &open_cmd])
.output().await;
run_luks_open(path, &luks_name, &part.luks_password).await
.map_err(|e| format!("LUKS open hatası ({}): {}", path, e))?;
format!("/dev/mapper/{}", luks_name)
} else {
+3
View File
@@ -245,6 +245,9 @@ impl Default for YaliApp {
if !branding.oem.default_password.is_empty() {
app.state.password = branding.oem.default_password.clone();
app.state.password_confirm = branding.oem.default_password.clone();
// OEM varsayılan şifresi yüklendi; kullanıcı değiştirene kadar
// ilerleme engellenir.
app.state.oem_password_changed = false;
}
// OEM ön tanımlı disk planı varsa?
}
+20 -3
View File
@@ -193,12 +193,24 @@ impl InstallerStep for UsersStep {
// Şifre
ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text()));
// OEM modunda varsayılan şifre uyarısı göster
if state.oem_mode && !state.oem_password_changed {
ui.colored_label(
theme::c_error(),
t!("oem_change_password_warning"),
);
}
ui.horizontal(|ui| {
let before = state.password.clone();
if self.show_password {
ui.text_edit_singleline(&mut state.password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password).password(true));
}
// Kullanıcı şifreyi değiştirdiyse OEM bayrağını kaldır
if state.oem_mode && state.password != before {
state.oem_password_changed = true;
}
if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() {
self.show_password = !self.show_password;
}
@@ -280,7 +292,7 @@ impl InstallerStep for UsersStep {
if ui.small_button(if self.show_root_password { "🙈" } else { "👁" }).clicked() {
self.show_root_password = !self.show_root_password;
}
if !state.root_password.is_empty() && state.root_password.len() < 4 {
if !state.root_password.is_empty() && state.root_password.len() < 8 {
ui.colored_label(theme::c_error(), t!("admin_password_short"));
}
});
@@ -321,9 +333,14 @@ impl InstallerStep for UsersStep {
}
fn is_complete(&self, state: &GlobalState) -> bool {
// OEM modunda varsayılan şifre değiştirilmeden ilerlenmez.
if state.oem_mode && !state.oem_password_changed {
return false;
}
let user_ok = is_valid_username(&state.username)
&& is_valid_hostname(&state.hostname)
&& state.password.len() >= 4
&& state.password.len() >= 8
&& state.password == state.password_confirm;
if self.use_same_for_admin {
@@ -331,7 +348,7 @@ impl InstallerStep for UsersStep {
} else {
user_ok && (
state.root_password.is_empty()
|| (state.root_password.len() >= 4 && state.root_password == state.root_password_confirm)
|| (state.root_password.len() >= 8 && state.root_password == state.root_password_confirm)
)
}
}