root/src/bin/multiuser/multiuser_utils.cpp
/*
 * Copyright 2008, Ingo Weinhold, ingo_weinhold@gmx.de. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 */

#include "multiuser_utils.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

#include <AutoDeleterPosix.h>

#include <user_group.h>


status_t
read_password(const char* prompt, char* password, size_t bufferSize,
        bool useStdio)
{
        FILE* in = stdin;
        FILE* out = stdout;

        // open tty
        FileCloser tty;
        if (!useStdio) {
// TODO: Open tty with O_NOCTTY!
                tty.SetTo(fopen("/dev/tty", "w+"));
                if (!tty.IsSet()) {
                        fprintf(stderr, "Error: Failed to open tty: %s\n",
                                strerror(errno));
                        return errno;
                }

                in = tty.Get();
                out = tty.Get();
        }

        // disable echo
        int inFD = fileno(in);
        struct termios termAttrs;
        if (tcgetattr(inFD, &termAttrs) != 0) {
                fprintf(in, "Error: Failed to get tty attributes: %s\n",
                        strerror(errno));
                return errno;
        }

        tcflag_t localFlags = termAttrs.c_lflag;
        termAttrs.c_lflag &= ~ECHO;

        if (tcsetattr(inFD, TCSANOW, &termAttrs) != 0) {
                fprintf(in, "Error: Failed to set tty attributes: %s\n",
                        strerror(errno));
                return errno;
        }

        status_t error = B_OK;

        // prompt and read pwd
        fputs(prompt, out);
        fflush(out);

        if (fgets(password, bufferSize, in) == NULL) {
                fprintf(out, "\nError: Failed to read from tty: %s\n",
                        strerror(errno));
                error = errno != 0 ? errno : B_ERROR;
        } else
                fputc('\n', out);

        // chop off trailing newline
        if (error == B_OK) {
                size_t len = strlen(password);
                if (len > 0 && password[len - 1] == '\n')
                        password[len - 1] = '\0';
        }

        // restore the terminal attributes
        termAttrs.c_lflag = localFlags;
        tcsetattr(inFD, TCSANOW, &termAttrs);

        return error;
}


bool
verify_password(passwd* passwd, spwd* spwd, const char* plainPassword)
{
        if (passwd == NULL)
                return false;

        // check whether we need to check the shadow password
        const char* requiredPassword = passwd->pw_passwd;
        if (strcmp(requiredPassword, "x") == 0) {
                if (spwd == NULL) {
                        // Mmh, we're suppose to check the shadow password, but we don't
                        // have it. Bail out.
                        return false;
                }

                requiredPassword = spwd->sp_pwdp;
        }

        // If no password is required, we're done.
        if (requiredPassword == NULL || requiredPassword[0] == '\0') {
                if (plainPassword == NULL || plainPassword[0] == '\0')
                        return true;

                return false;
        }

        // crypt and check it
        char* encryptedPassword = crypt(plainPassword, requiredPassword);

        return (strcmp(encryptedPassword, requiredPassword) == 0);
}


/*!     Checks whether the user needs to authenticate with a password, and, if
        necessary, asks for it, and checks it.
        \a passwd must always be given, \a spwd only if there exists an entry
        for the user.
*/
status_t
authenticate_user(const char* prompt, passwd* passwd, spwd* spwd, int maxTries,
        bool useStdio)
{
        // check whether a password is need at all
        if (verify_password(passwd, spwd, ""))
                return B_OK;

        while (true) {
                // prompt the user for the password
                char plainPassword[MAX_SHADOW_PWD_PASSWORD_LEN];
                status_t error = read_password(prompt, plainPassword,
                        sizeof(plainPassword), useStdio);
                if (error != B_OK)
                        return error;

                // check it
                bool ok = verify_password(passwd, spwd, plainPassword);
                explicit_bzero(plainPassword, sizeof(plainPassword));
                if (ok)
                        return B_OK;

                fprintf(stderr, "Incorrect password.\n");
                if (--maxTries <= 0)
                        return B_PERMISSION_DENIED;
        }
}


status_t
authenticate_user(const char* prompt, const char* user, passwd** _passwd,
        spwd** _spwd, int maxTries, bool useStdio)
{
        struct passwd* passwd = getpwnam(user);
        struct spwd* spwd = getspnam(user);

        status_t error = authenticate_user(prompt, passwd, spwd, maxTries,
                useStdio);
        if (error == B_OK) {
                if (_passwd)
                        *_passwd = passwd;
                if (_spwd)
                        *_spwd = spwd;
        }

        return error;
}


status_t
setup_environment(struct passwd* passwd, bool preserveEnvironment, bool chngdir)
{
        const char* term = getenv("TERM");
        if (!preserveEnvironment) {
                static char *empty[1];
                environ = empty;
        }

        // always preserve $TERM
        if (term != NULL)
                setenv("TERM", term, false);
        if (passwd->pw_shell)
                setenv("SHELL", passwd->pw_shell, true);
        if (passwd->pw_dir)
                setenv("HOME", passwd->pw_dir, true);

        setenv("USER", passwd->pw_name, true);

        pid_t pid = getpid();
        // If stdin is not open, don't bother trying to TIOCSPGRP. (This is the
        // case when there is no PTY, e.g. for a noninteractive SSH session.)
        if (fcntl(STDIN_FILENO, F_GETFD) != -1) {
                if (ioctl(STDIN_FILENO, TIOCSPGRP, &pid) != 0)
                        return errno;
        }

        if (passwd->pw_gid && setgid(passwd->pw_gid) != 0)
                return errno;

        if (initgroups(passwd->pw_name, passwd->pw_gid) != 0)
                return errno;

        if (passwd->pw_uid && setuid(passwd->pw_uid) != 0)
                return errno;

        if (chngdir) {
                const char* home = getenv("HOME");
                if (home == NULL)
                        return B_ENTRY_NOT_FOUND;

                if (chdir(home) != 0)
                        return errno;
        }

        return B_OK;
}