ajhahn.de
← FlashOS
Flash 189 lines
// passwd — interactive password change.
//
// With no argument it changes the calling user's own password (uid ->
// login name via /etc/passwd); with an argument (`passwd <user>`) it
// targets that record — which only root may do for records other than
// its own (sys_passwd enforces this, the tool just passes it through).
// Prompts follow the Unix shape: the current password is skipped when
// the caller is root (root resets without proof), the new password is
// asked twice and must match. All password prompts run with kernel echo
// off.
//
// The KDF and the splice-safe shadow rewrite live in the kernel
// (sys_passwd, slot 46) — this tool only collects strings and reports
// the verdict. Without a writable FAT32 shadow (/mnt/shadow — absent on
// QEMU virt and on a freshly formatted card) the kernel answers -1 and
// the tool says so.
//
// Same coreutil recipe as login / dmesg (flibc _start shim, single
// PT_LOAD, no heap allocator — only fixed stack buffers).

use flibc
use syscall_defs as defs
use pwfile

link "flibc_start"
link "flibc_mem"

const PASSWD_PATH cstr = "/etc/passwd"

fn emit(s []u8) void {
    _ = flibc.sys.write_fd(1, s.ptr, s.len)
}

fn emitErr(s []u8) void {
    _ = flibc.sys.write_fd(2, s.ptr, s.len)
}

// Read one line from fd 0 (raw, one byte at a time) into `buf`, stopping
// at CR / LF or EOF. Returns the byte count, excluding the terminator.
// Echo of typed bytes is the kernel's job (the console echo flag) — this
// loop never echoes, which is exactly right for password input.
fn readLine(buf []mut u8) usize {
    var n usize = 0
    while n < buf.len {
        var ch [1]u8 = undefined
        r := flibc.sys.read(0, &ch, 1)
        if r <= 0 {
            break
        }
        if ch[0] == '\n' || ch[0] == '\r' {
            break
        }
        buf[n] = ch[0]
        n += 1
    }
    return n
}

fn strLen(s cstr) usize {
    var n usize = 0
    while s[n] != 0 {
        n += 1
    }
    return n
}

fn bytesEqual(a []u8, b []u8) bool {
    if a.len != b.len {
        return false
    }
    // (paired `for x, y in a, b` is not expressible; an index loop over the
    // now-equal lengths compares the same byte pairs.)
    var i usize = 0
    while i < a.len {
        if a[i] != b[i] {
            return false
        }
        i += 1
    }
    return true
}

export fn main(argc usize, argv argv) noreturn {
    var user_buf [64]u8 = undefined
    var old_buf [128]u8 = undefined
    var new_buf [128]u8 = undefined
    var retype_buf [128]u8 = undefined
    var pw_buf [512]u8 = undefined

    is_root := flibc.sys.geteuid() == 0

    // Resolve the target user: argv[1], or the caller's own login name.
    var user_len usize = 0
    if argc >= 2 {
        arg := argv[1].?
        alen := strLen(arg)
        if alen == 0 || alen > user_buf.len {
            emitErr("passwd: bad user name\n")
            flibc.exit()
        }
        var i usize = 0
        while i < alen {
            user_buf[i] = arg[i]
            i += 1
        }
        user_len = alen
    } else {
        uid_raw := flibc.sys.getuid()
        if uid_raw < 0 {
            emitErr("passwd: cannot read uid\n")
            flibc.exit()
        }
        fd := flibc.sys.open(PASSWD_PATH)
        if fd < 0 {
            emitErr("passwd: cannot open /etc/passwd\n")
            flibc.exit()
        }
        var pn usize = 0
        while pn < pw_buf.len {
            r := flibc.sys.read(fd, pw_buf[pn..].ptr, pw_buf.len - pn)
            if r <= 0 {
                break
            }
            pn += #intCast(r)
        }
        _ = flibc.sys.close(fd)
        // (orelse with a multi-statement block handler is not expressible; an
        // explicit null check yields the same divergent exit.)
        maybe_entry := pwfile.lookupByUid(pw_buf[0..pn], #intCast(uid_raw))
        if maybe_entry == null {
            emitErr("passwd: no passwd entry for this uid\n")
            flibc.exit()
        }
        entry := maybe_entry.?
        if entry.user.len > user_buf.len {
            emitErr("passwd: bad user name\n")
            flibc.exit()
        }
        var i usize = 0
        while i < entry.user.len {
            user_buf[i] = entry.user[i]
            i += 1
        }
        user_len = entry.user.len
    }

    emit("Changing password for ")
    emit(user_buf[0..user_len])
    emit("\n")

    // Current password — skipped for root (sys_passwd does not require
    // it from euid 0; that is the forgotten-password recovery path).
    var old_len usize = 0
    if !is_root {
        _ = flibc.sys.set_console_mode(0)
        emit("Current password: ")
        old_len = readLine(&old_buf)
        emit("\n")
    }

    // New password, asked twice, echo off.
    _ = flibc.sys.set_console_mode(0)
    emit("New password: ")
    new_len := readLine(&new_buf)
    emit("\n")
    emit("Retype new password: ")
    retype_len := readLine(&retype_buf)
    emit("\n")

    if new_len == 0 {
        emitErr("passwd: empty password not allowed\n")
        flibc.exit()
    }
    if !bytesEqual(new_buf[0..new_len], retype_buf[0..retype_len]) {
        emitErr("passwd: passwords do not match\n")
        flibc.exit()
    }

    ret := flibc.sys.passwd(&user_buf, user_len, &old_buf, old_len, &new_buf, new_len)
    if ret == 0 {
        emit("passwd: password updated\n")
    } else if ret == -defs.EACCES {
        emitErr("passwd: authentication failure\n")
    } else {
        emitErr("passwd: cannot write shadow (read-only or missing)\n")
    }
    flibc.exit()
}