diff --git a/.gitignore b/.gitignore index f434778e91df3..f4e62ae530e26 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.dSYM *.elf *.exe +etctmp/ *.gcno *.gcda *.hex diff --git a/Documentation/components/tools/index.rst b/Documentation/components/tools/index.rst index ee6e5cd65c93b..22813813f2b6f 100644 --- a/Documentation/components/tools/index.rst +++ b/Documentation/components/tools/index.rst @@ -11,3 +11,98 @@ and host C programs that are important parts of the NuttX build system: :glob: ./* + +.. _mkpasswd_autogen: + +mkpasswd — Build-time ``/etc/passwd`` Generation +------------------------------------------------- + +``tools/mkpasswd`` is a C host tool (compiled from ``tools/mkpasswd.c``) that +generates a single ``/etc/passwd`` entry at build time. It is invoked +automatically by the ROMFS build step when +``CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y`` is set. + +Why build-time generation? +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Shipping a hard-coded default password in firmware is a well-known security +weakness (CWE-798). By generating the ``/etc/passwd`` entry from a +user-supplied plaintext password at build time, each firmware image carries +unique credentials. The build will fail if the password is left empty, +preventing accidental deployments with no credentials. + +For improved baseline security, the configured password must be at least +8 characters long. + +How it works +~~~~~~~~~~~~ + +1. The host tool reads the plaintext password from + ``CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD``. +2. The password is hashed using the Tiny Encryption Algorithm (TEA) — the + same implementation used at runtime in + ``libs/libc/misc/lib_tea_encrypt.c`` — with custom base64 encoding + matching ``apps/fsutils/passwd/passwd_encrypt.c``. +3. The resulting hashed entry is written to + ``etctmp//passwd`` and then embedded into the ROMFS image. +4. The **plaintext password is never stored in the firmware image**. + +Kconfig options +~~~~~~~~~~~~~~~ + +Enable the feature and configure credentials via ``make menuconfig``: + +.. code:: kconfig + + CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y + CONFIG_NSH_CONSOLE_LOGIN=y # required to enforce login prompt + CONFIG_BOARD_ETC_ROMFS_PASSWD_USER="admin" # default: admin + CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD="" # required, min length 8 + CONFIG_BOARD_ETC_ROMFS_PASSWD_UID=0 + CONFIG_BOARD_ETC_ROMFS_PASSWD_GID=0 + CONFIG_BOARD_ETC_ROMFS_PASSWD_HOME="/" + +The TEA encryption keys can be changed from their defaults via +``CONFIG_FSUTILS_PASSWD_KEY1..4``. + +``/etc/passwd`` file format +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: text + + user:x:uid:gid:home + +Where: + +* ``user`` — user name +* ``x`` — TEA-hashed, base64-encoded password +* ``uid`` — numeric user ID +* ``gid`` — numeric group ID +* ``home`` — login directory + +Verifying the generated entry +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After enabling ``CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE`` and setting a +password, rebuild and verify: + +1. **Configure and build**: + + .. code:: console + + $ make menuconfig # enable BOARD_ETC_ROMFS_PASSWD_ENABLE and set password + $ make + +2. **Inspect the generated passwd line** (written to the board build tree): + + .. code:: console + + $ cat boards////src/etctmp/etc/passwd + admin:s1IZjGjjmo/x8u5m5uY2jB:0:0:/ + +3. **Verify the plaintext is absent from firmware**: + + .. code:: console + + $ grep boards////src/etctmp.c + # must print nothing diff --git a/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst b/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst index 38c646e15f7b3..b99f6c911fe02 100644 --- a/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst +++ b/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst @@ -491,21 +491,21 @@ mounted at /etc and will look like this at run-time: nsh> ``/etc/init.d/rc.sysinit`` is system init script; ``/etc/init.d/rcS`` is the -start-up script; ``/etc/passwd`` is a the password file. It supports a single -user: +start-up script; ``/etc/passwd`` is the password file. -.. code:: text +The ``/etc/passwd`` file is auto-generated at build time when +``CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE`` is set. Enable the option and set +credentials via ``make menuconfig``: - USERNAME: admin - PASSWORD: Administrator +* ``CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y`` +* ``CONFIG_BOARD_ETC_ROMFS_PASSWD_USER`` (default: ``admin``) +* ``CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD`` (required, build fails if empty) - nsh> cat /etc/passwd - admin:8Tv+Hbmr3pLVb5HHZgd26D:0:0:/ +The password is hashed with TEA at build time by the host tool +``tools/mkpasswd``; the plaintext is **not** stored in the firmware. -The encrypted passwords in the provided passwd file are only valid if the TEA -key is set to: 012345678 9abcdef0 012345678 9abcdef0. Changes to either the key -or the password word will require regeneration of the ``nsh_romfimg.h`` header -file. +For the full description of the mechanism, TEA key configuration, file format, +and verification steps, see :ref:`mkpasswd_autogen`. The format of the password file is: diff --git a/Documentation/platforms/risc-v/esp32c3-legacy/boards/esp32c3-legacy-devkit/ROMFS.txt b/Documentation/platforms/risc-v/esp32c3-legacy/boards/esp32c3-legacy-devkit/ROMFS.txt index 5cc8e382a9c46..cc004cebaabfa 100644 --- a/Documentation/platforms/risc-v/esp32c3-legacy/boards/esp32c3-legacy-devkit/ROMFS.txt +++ b/Documentation/platforms/risc-v/esp32c3-legacy/boards/esp32c3-legacy-devkit/ROMFS.txt @@ -23,18 +23,22 @@ README nsh> /etc/init.d/rc.sysinit is system init script; /etc/init.d/rcS is the start-up - script; /etc/passwd is a the password file. It supports a single user: + script; /etc/passwd is the password file. - USERNAME: admin - PASSWORD: Administrator + The /etc/passwd file is auto-generated at build time when + CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE is set. To configure the admin user and + password, run 'make menuconfig' and set: - nsh> cat /etc/passwd - admin:8Tv+Hbmr3pLVb5HHZgd26D:0:0:/ + CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y + CONFIG_BOARD_ETC_ROMFS_PASSWD_USER (default: admin) + CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD (required, build fails if empty) - The encrypted passwords in the provided passwd file are only valid if the - TEA key is set to: 012345678 9abcdef0 012345678 9abcdef0. Changes to either - the key or the password word will require regeneration of the nsh_romfimg.h - header file. + The password is hashed with TEA at build time by the host tool + tools/mkpasswd; the plaintext is NOT stored in the firmware image. + + For the full description of the mechanism, TEA key configuration, file + format, and verification steps, see Documentation/components/tools/index.rst + (mkpasswd section). The format of the password file is: diff --git a/Documentation/platforms/sim/sim/boards/sim/index.rst b/Documentation/platforms/sim/sim/boards/sim/index.rst index e35a0e0582df2..5b0d92184a27a 100644 --- a/Documentation/platforms/sim/sim/boards/sim/index.rst +++ b/Documentation/platforms/sim/sim/boards/sim/index.rst @@ -2008,24 +2008,23 @@ mounted at ``/etc`` and will look like this at run-time: nsh> ``/etc/init.d/rc.sysinit`` is system init script; ``/etc/init.d/rcS`` is the -start-up script; ``/etc/passwd`` is a the password file. It supports a single -user: +start-up script; ``/etc/passwd`` is the password file. -.. code:: text - - USERNAME: admin - PASSWORD: Administrator - -.. code:: console +The ``/etc/passwd`` file is auto-generated at build time when +``CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE`` is set. Enable the option and set +credentials via ``make menuconfig``: - nsh> cat /etc/passwd - admin:8Tv+Hbmr3pLVb5HHZgd26D:0:0:/ +* ``CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y`` +* ``CONFIG_NSH_CONSOLE_LOGIN=y`` (required, otherwise login is not enforced) +* ``CONFIG_BOARD_ETC_ROMFS_PASSWD_USER`` (default: ``admin``) +* ``CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD`` (required, build fails if empty or shorter than 8 characters) -The encrypted passwords in the provided passwd file are only valid if the -TEA key is set to: 012345678 9abcdef0 012345678 9abcdef0. +The password is hashed with TEA at build time by the host tool +``tools/mkpasswd``; the plaintext is **not** stored in the firmware. -Changes to either the key or the password word will require regeneration of the -``nsh_romfimg.h`` header file. +For the full description of the build-time password generation mechanism, +TEA key configuration, file format, and verification steps, see +:ref:`mkpasswd_autogen`. The format of the password file is: @@ -2041,6 +2040,21 @@ Where: * gid: Group ID (0 for now) * home: Login directory (/ for now) +For configuration, verification steps, and TEA key details, see +:ref:`mkpasswd_autogen`. + +Login test inside the simulator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: console + + $ ./nuttx + NuttShell (NSH) NuttX- + nsh login: admin + Password: + User Logged-in! + nsh> + ``/etc/group`` is a group file. It is not currently used. .. code:: console diff --git a/boards/Board.mk b/boards/Board.mk index f355bc1fe4056..7198aef8e3145 100644 --- a/boards/Board.mk +++ b/boards/Board.mk @@ -35,6 +35,45 @@ $(ETCSRC): $(foreach raw,$(RCRAWS), $(if $(wildcard $(BOARD_DIR)$(DELIM)src$(DEL $(shell rm -rf $(ETCDIR)$(DELIM)$(raw)) \ $(shell mkdir -p $(dir $(ETCDIR)$(DELIM)$(raw))) \ $(shell cp -rfp $(if $(wildcard $(BOARD_DIR)$(DELIM)src$(DELIM)$(raw)), $(BOARD_DIR)$(DELIM)src$(DELIM)$(raw), $(if $(wildcard $(BOARD_COMMON_DIR)$(DELIM)$(raw)), $(BOARD_COMMON_DIR)$(DELIM)$(raw), $(BOARD_DIR)$(DELIM)src$(DELIM)$(raw))) $(ETCDIR)$(DELIM)$(raw))) +ifeq ($(CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE),y) +ifeq ($(CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD),) + $(error CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD must be set when BOARD_ETC_ROMFS_PASSWD_ENABLE is enabled. Run 'make menuconfig' to set a password.) +endif +ifeq ($(CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD),"") + $(error CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD must be set when BOARD_ETC_ROMFS_PASSWD_ENABLE is enabled. Run 'make menuconfig' to set a password.) +endif + $(Q) passwd=$(CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD); \ + passwd=$${passwd#\"}; \ + passwd=$${passwd%\"}; \ + if [ $${#passwd} -lt 8 ]; then \ + echo "ERROR: CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD must be at least 8 characters."; \ + exit 1; \ + fi + $(Q) if [ ! -f $(TOPDIR)$(DELIM)tools$(DELIM)mkpasswd$(HOSTEXEEXT) ]; then \ + $(MAKE) -C $(TOPDIR)$(DELIM)tools -f Makefile.host mkpasswd$(HOSTEXEEXT); \ + fi + $(Q) mkdir -p $(ETCDIR)$(DELIM)$(CONFIG_ETC_ROMFSMOUNTPT) + $(Q) user=$(CONFIG_BOARD_ETC_ROMFS_PASSWD_USER); \ + user=$${user#\"}; \ + user=$${user%\"}; \ + home=$(CONFIG_BOARD_ETC_ROMFS_PASSWD_HOME); \ + home=$${home#\"}; \ + home=$${home%\"}; \ + passwd=$(CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD); \ + passwd=$${passwd#\"}; \ + passwd=$${passwd%\"}; \ + $(TOPDIR)$(DELIM)tools$(DELIM)mkpasswd$(HOSTEXEEXT) \ + --user "$${user}" \ + --password "$${passwd}" \ + --uid $(CONFIG_BOARD_ETC_ROMFS_PASSWD_UID) \ + --gid $(CONFIG_BOARD_ETC_ROMFS_PASSWD_GID) \ + --home "$${home}" \ + $(if $(CONFIG_FSUTILS_PASSWD_KEY1),--key1 $(CONFIG_FSUTILS_PASSWD_KEY1)) \ + $(if $(CONFIG_FSUTILS_PASSWD_KEY2),--key2 $(CONFIG_FSUTILS_PASSWD_KEY2)) \ + $(if $(CONFIG_FSUTILS_PASSWD_KEY3),--key3 $(CONFIG_FSUTILS_PASSWD_KEY3)) \ + $(if $(CONFIG_FSUTILS_PASSWD_KEY4),--key4 $(CONFIG_FSUTILS_PASSWD_KEY4)) \ + -o $(ETCDIR)$(DELIM)$(CONFIG_ETC_ROMFSMOUNTPT)$(DELIM)passwd +endif $(Q) genromfs -f romfs.img -d $(ETCDIR)$(DELIM)$(CONFIG_ETC_ROMFSMOUNTPT) -V "NSHInitVol" $(Q) echo "#include " > $@ $(Q) xxd -i romfs.img | sed -e "s/^unsigned char/const unsigned char aligned_data(4)/g" >> $@ diff --git a/boards/Kconfig b/boards/Kconfig index 0ae1906040db2..7b07ac5cb6bf6 100644 --- a/boards/Kconfig +++ b/boards/Kconfig @@ -5423,3 +5423,53 @@ config BOARD_MEMORY_RANGE end: end address of memory range flags: Executable 0x1, Writable 0x2, Readable 0x4 example:{0x1000,0x2000,0x4},{0x2000,0x3000,0x6},{0x3000,0x4000,0x7} ... {0x0,0x0,0x0} + +config BOARD_ETC_ROMFS_PASSWD_ENABLE + bool "Auto-generate /etc/passwd at build time" + default n + depends on ETC_ROMFS + ---help--- + Generate the /etc/passwd file at build time from a user-supplied + password. This avoids shipping a hard-coded default password + (CWE-798). When enabled, the build will fail if no password + is configured, forcing each build to set its own credentials. + + The password is hashed at build time by the host tool + tools/mkpasswd (compiled from tools/mkpasswd.c) using the Tiny + Encryption Algorithm (TEA) — the same algorithm used at runtime + in libs/libc/misc/lib_tea_encrypt.c. The plaintext password is + never stored in the firmware image. + + See Documentation/components/passwd_autogen.rst for details. + +if BOARD_ETC_ROMFS_PASSWD_ENABLE + +config BOARD_ETC_ROMFS_PASSWD_USER + string "Admin username" + default "admin" + ---help--- + The username for the auto-generated /etc/passwd entry. + +config BOARD_ETC_ROMFS_PASSWD_PASSWORD + string "Admin password (required)" + default "" + ---help--- + The plaintext password for the auto-generated /etc/passwd entry. + This value is hashed with TEA at build time; the plaintext is NOT + stored in the firmware image. The build will fail if this is left + empty or shorter than 8 characters. Set this via + 'make menuconfig'. + +config BOARD_ETC_ROMFS_PASSWD_UID + int "Admin user ID" + default 0 + +config BOARD_ETC_ROMFS_PASSWD_GID + int "Admin group ID" + default 0 + +config BOARD_ETC_ROMFS_PASSWD_HOME + string "Admin home directory" + default "/" + +endif # BOARD_ETC_ROMFS_PASSWD_ENABLE diff --git a/boards/risc-v/esp32c3-legacy/esp32c3-legacy-devkit/src/Make.defs b/boards/risc-v/esp32c3-legacy/esp32c3-legacy-devkit/src/Make.defs index 4e78fdf8242c6..4040c7f93933f 100644 --- a/boards/risc-v/esp32c3-legacy/esp32c3-legacy-devkit/src/Make.defs +++ b/boards/risc-v/esp32c3-legacy/esp32c3-legacy-devkit/src/Make.defs @@ -46,7 +46,7 @@ endif ifeq ($(CONFIG_ETC_ROMFS),y) RCSRCS = etc/init.d/rc.sysinit etc/init.d/rcS - RCRAWS = etc/group etc/passwd + RCRAWS = etc/group endif DEPPATH += --dep-path board diff --git a/boards/risc-v/esp32c3-legacy/esp32c3-legacy-devkit/src/etc/passwd b/boards/risc-v/esp32c3-legacy/esp32c3-legacy-devkit/src/etc/passwd deleted file mode 100644 index bc1f3a68e46b0..0000000000000 --- a/boards/risc-v/esp32c3-legacy/esp32c3-legacy-devkit/src/etc/passwd +++ /dev/null @@ -1 +0,0 @@ -admin:8Tv+Hbmr3pLVb5HHZgd26D:0:0:/ diff --git a/boards/sim/sim/sim/configs/login/defconfig b/boards/sim/sim/sim/configs/login/defconfig index b59060be17449..e6c08c4d43fef 100644 --- a/boards/sim/sim/sim/configs/login/defconfig +++ b/boards/sim/sim/sim/configs/login/defconfig @@ -11,6 +11,9 @@ CONFIG_ARCH_BOARD="sim" CONFIG_ARCH_BOARD_SIM=y CONFIG_ARCH_CHIP="sim" CONFIG_ARCH_SIM=y +CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y +CONFIG_BOARD_ETC_ROMFS_PASSWD_USER="admin" +CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD="Administrator" CONFIG_BOARDCTL_APP_SYMTAB=y CONFIG_BOARDCTL_POWEROFF=y CONFIG_BOARD_LOOPSPERMSEC=0 diff --git a/boards/sim/sim/sim/src/CMakeLists.txt b/boards/sim/sim/sim/src/CMakeLists.txt index de5a6695baa98..cbf3cc7168c79 100644 --- a/boards/sim/sim/sim/src/CMakeLists.txt +++ b/boards/sim/sim/sim/src/CMakeLists.txt @@ -77,7 +77,6 @@ if(CONFIG_ETC_ROMFS) etc/init.d/rc.sysinit RCRAWS etc/group - etc/passwd PATH ${CMAKE_CURRENT_BINARY_DIR}/etc) diff --git a/boards/sim/sim/sim/src/Makefile b/boards/sim/sim/sim/src/Makefile index 4d5c5e83152a7..6fe51cd401e24 100644 --- a/boards/sim/sim/sim/src/Makefile +++ b/boards/sim/sim/sim/src/Makefile @@ -56,7 +56,7 @@ endif ifeq ($(CONFIG_ETC_ROMFS),y) RCSRCS = etc/init.d/rc.sysinit etc/init.d/rcS - RCRAWS = etc/group etc/passwd + RCRAWS = etc/group endif ifeq ($(CONFIG_ARCH_BUTTONS),y) diff --git a/boards/sim/sim/sim/src/etc/passwd b/boards/sim/sim/sim/src/etc/passwd deleted file mode 100644 index bc1f3a68e46b0..0000000000000 --- a/boards/sim/sim/sim/src/etc/passwd +++ /dev/null @@ -1 +0,0 @@ -admin:8Tv+Hbmr3pLVb5HHZgd26D:0:0:/ diff --git a/cmake/nuttx_add_romfs.cmake b/cmake/nuttx_add_romfs.cmake index 8124fd3f3075c..df204d3570838 100644 --- a/cmake/nuttx_add_romfs.cmake +++ b/cmake/nuttx_add_romfs.cmake @@ -282,6 +282,83 @@ function(process_all_directory_romfs) list(PREPEND RCSRCS ${board_rcsrcs} ${dyn_rcsrcs}) list(PREPEND RCRAWS ${board_rcraws} ${dyn_rcraws}) + # Auto-generate /etc/passwd at build time if configured + if(CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE) + if("${CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD}" STREQUAL "") + message( + FATAL_ERROR + "CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD must be set when" + " BOARD_ETC_ROMFS_PASSWD_ENABLE is enabled." + " Run 'make menuconfig' to set a password.") + endif() + + string(LENGTH "${CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD}" PASSWD_LEN) + if(PASSWD_LEN LESS 8) + message( + FATAL_ERROR + "CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD must be at least 8 characters." + ) + endif() + + # Determine host executable suffix (.exe on Windows, empty elsewhere) + if(CMAKE_HOST_WIN32) + set(HOST_EXE_SUFFIX .exe) + else() + set(HOST_EXE_SUFFIX "") + endif() + + # Locate a host C compiler to build the mkpasswd tool + find_program(HOST_CC NAMES cc gcc clang REQUIRED) + + # Build mkpasswd.c as a host binary in the CMake build directory and keep + # the source tree clean. + set(MKPASSWD_SRC ${NUTTX_DIR}/tools/mkpasswd.c) + set(MKPASSWD_BIN ${CMAKE_BINARY_DIR}/tools/mkpasswd${HOST_EXE_SUFFIX}) + + if(NOT TARGET build_host_mkpasswd) + add_custom_command( + OUTPUT ${MKPASSWD_BIN} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/tools + COMMAND ${HOST_CC} -o ${MKPASSWD_BIN} ${MKPASSWD_SRC} + DEPENDS ${MKPASSWD_SRC} + COMMENT "Building host tool: mkpasswd") + add_custom_target(build_host_mkpasswd DEPENDS ${MKPASSWD_BIN}) + endif() + + # Pass TEA key overrides when the user has changed them from defaults + set(MKPASSWD_KEY_ARGS "") + if(CONFIG_FSUTILS_PASSWD_KEY1) + list(APPEND MKPASSWD_KEY_ARGS --key1 ${CONFIG_FSUTILS_PASSWD_KEY1}) + endif() + if(CONFIG_FSUTILS_PASSWD_KEY2) + list(APPEND MKPASSWD_KEY_ARGS --key2 ${CONFIG_FSUTILS_PASSWD_KEY2}) + endif() + if(CONFIG_FSUTILS_PASSWD_KEY3) + list(APPEND MKPASSWD_KEY_ARGS --key3 ${CONFIG_FSUTILS_PASSWD_KEY3}) + endif() + if(CONFIG_FSUTILS_PASSWD_KEY4) + list(APPEND MKPASSWD_KEY_ARGS --key4 ${CONFIG_FSUTILS_PASSWD_KEY4}) + endif() + + set(GENPASSWD_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/etc/passwd) + add_custom_command( + OUTPUT ${GENPASSWD_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/etc + COMMAND + ${MKPASSWD_BIN} --user "${CONFIG_BOARD_ETC_ROMFS_PASSWD_USER}" + --password "${CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD}" --uid + ${CONFIG_BOARD_ETC_ROMFS_PASSWD_UID} --gid + ${CONFIG_BOARD_ETC_ROMFS_PASSWD_GID} --home + "${CONFIG_BOARD_ETC_ROMFS_PASSWD_HOME}" ${MKPASSWD_KEY_ARGS} -o + ${GENPASSWD_OUTPUT} + DEPENDS ${MKPASSWD_BIN} + COMMENT "Generating /etc/passwd from Kconfig values") + add_custom_target(generate_passwd DEPENDS ${GENPASSWD_OUTPUT}) + add_dependencies(generate_passwd build_host_mkpasswd) + list(APPEND RCRAWS ${GENPASSWD_OUTPUT}) + list(APPEND dyn_deps generate_passwd) + endif() + # init dynamic dependencies get_property( diff --git a/tools/.gitignore b/tools/.gitignore index 59c05a7e27c70..957b27157d54f 100644 --- a/tools/.gitignore +++ b/tools/.gitignore @@ -14,6 +14,7 @@ /mksymtab /mksyscall /mkversion +/mkpasswd /nxstyle /rmcr /incdir diff --git a/tools/Makefile.host b/tools/Makefile.host index 8ec6572c1956c..410dca776ad2f 100644 --- a/tools/Makefile.host +++ b/tools/Makefile.host @@ -35,8 +35,9 @@ endif all: b16$(HOSTEXEEXT) bdf-converter$(HOSTEXEEXT) cmpconfig$(HOSTEXEEXT) \ configure$(HOSTEXEEXT) mkconfig$(HOSTEXEEXT) mkdeps$(HOSTEXEEXT) \ mksymtab$(HOSTEXEEXT) mksyscall$(HOSTEXEEXT) mkversion$(HOSTEXEEXT) \ - cnvwindeps$(HOSTEXEEXT) nxstyle$(HOSTEXEEXT) initialconfig$(HOSTEXEEXT) \ - gencromfs$(HOSTEXEEXT) convert-comments$(HOSTEXEEXT) lowhex$(HOSTEXEEXT) \ + mkpasswd$(HOSTEXEEXT) cnvwindeps$(HOSTEXEEXT) nxstyle$(HOSTEXEEXT) \ + initialconfig$(HOSTEXEEXT) gencromfs$(HOSTEXEEXT) \ + convert-comments$(HOSTEXEEXT) lowhex$(HOSTEXEEXT) \ detab$(HOSTEXEEXT) rmcr$(HOSTEXEEXT) incdir$(HOSTEXEEXT) \ jlink-nuttx$(HOSTDYNEXT) default: mkconfig$(HOSTEXEEXT) mksyscall$(HOSTEXEEXT) mkdeps$(HOSTEXEEXT) \ @@ -44,8 +45,8 @@ default: mkconfig$(HOSTEXEEXT) mksyscall$(HOSTEXEEXT) mkdeps$(HOSTEXEEXT) \ ifdef HOSTEXEEXT .PHONY: b16 bdf-converter cmpconfig clean configure kconfig2html mkconfig \ - mkdeps mksymtab mksyscall mkversion cnvwindeps nxstyle initialconfig \ - gencromfs convert-comments lowhex detab rmcr incdir + mkdeps mksymtab mksyscall mkversion mkpasswd cnvwindeps nxstyle \ + initialconfig gencromfs convert-comments lowhex detab rmcr incdir endif ifdef HOSTDYNEXT .PHONY: jlink-nuttx @@ -106,6 +107,15 @@ ifdef HOSTEXEEXT mkversion: mkversion$(HOSTEXEEXT) endif +# mkpasswd - Generate a NuttX /etc/passwd entry with TEA-encrypted password + +mkpasswd$(HOSTEXEEXT): mkpasswd.c + $(Q) $(HOSTCC) $(HOSTCFLAGS) -o mkpasswd$(HOSTEXEEXT) mkpasswd.c + +ifdef HOSTEXEEXT +mkpasswd: mkpasswd$(HOSTEXEEXT) +endif + # mksyscall - Convert a CSV file into syscall stubs and proxies mksyscall$(HOSTEXEEXT): mksyscall.c csvparser.c @@ -268,6 +278,8 @@ clean: $(call DELFILE, mksyscall.exe) $(call DELFILE, mkversion) $(call DELFILE, mkversion.exe) + $(call DELFILE, mkpasswd) + $(call DELFILE, mkpasswd.exe) $(call DELFILE, nxstyle) $(call DELFILE, nxstyle.exe) $(call DELFILE, rmcr) diff --git a/tools/mkpasswd.c b/tools/mkpasswd.c new file mode 100644 index 0000000000000..099045419cfa3 --- /dev/null +++ b/tools/mkpasswd.c @@ -0,0 +1,587 @@ +/**************************************************************************** + * tools/mkpasswd.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Description: + * Host build tool that generates a NuttX /etc/passwd entry with a + * TEA-encrypted password hash. This is a pure C replacement for the + * former tools/mkpasswd.py, removing the Python dependency from the build. + * + * The encryption algorithm and base64 encoding are identical to those + * used at runtime by: + * libs/libc/misc/lib_tea_encrypt.c + * apps/fsutils/passwd/passwd_encrypt.c + * + * Usage: + * mkpasswd --user --password [options] [-o ] + * + * Options: + * --user Username (required) + * --password Plaintext password (required, not stored in output) + * --uid User ID (default: 0) + * --gid Group ID (default: 0) + * --home Home directory (default: /) + * --key1 TEA key word 1 (default: 0x12345678) + * --key2 TEA key word 2 (default: 0x9abcdef0) + * --key3 TEA key word 3 (default: 0x12345678) + * --key4 TEA key word 4 (default: 0x9abcdef0) + * -o Output file (default: stdout) + * + * Output format (matches NuttX passwd file format): + * username:encrypted_hash:uid:gid:home + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +/* Expose strdup(), mkdir() and other POSIX.1-2008 extensions when + * compiling with strict C99 mode (-std=c99). Has no effect on C11/GNU + * builds or MSVC. + */ + +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 200809L +#endif + +#include +#include +#include +#include +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* TEA key schedule constant (derived from the golden ratio) */ + +#define TEA_KEY_SCHEDULE_CONSTANT 0x9e3779b9u + +/* Password size limits - must match apps/fsutils/passwd/passwd.h */ + +#define MAX_ENCRYPTED 48 /* Max size of encrypted password (ASCII) */ +#define MAX_PASSWORD (3 * MAX_ENCRYPTED / 4) /* Max plaintext length */ +#define MIN_PASSWORD 8 /* Minimum plaintext length for security */ + +/* Default TEA key values - must match CONFIG_FSUTILS_PASSWD_KEY1-4 defaults + * in apps/fsutils/passwd/Kconfig so that the generated hash verifies + * correctly at runtime when the user has not changed the key config. + */ + +#define DEFAULT_KEY1 0x12345678u +#define DEFAULT_KEY2 0x9abcdef0u +#define DEFAULT_KEY3 0x12345678u +#define DEFAULT_KEY4 0x9abcdef0u + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* 8-byte block interpreted as bytes, 16-bit halves, or 32-bit words */ + +union block_u +{ + char b[8]; + uint16_t h[4]; + uint32_t l[2]; +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: tea_encrypt + * + * Description: + * Encrypt two 32-bit words in-place using the Tiny Encryption Algorithm. + * This is an exact copy of the algorithm in + * libs/libc/misc/lib_tea_encrypt.c (public-domain TEA by Wheeler & + * Needham), inlined here so that the host tool has no NuttX dependencies. + * + * Input Parameters: + * value - Two-element array [v0, v1] to encrypt (modified in-place) + * key - Four-element 128-bit key array + * + ****************************************************************************/ + +static void tea_encrypt(uint32_t *value, const uint32_t *key) +{ + uint32_t v0 = value[0]; + uint32_t v1 = value[1]; + uint32_t sum = 0; + int i; + + for (i = 0; i < 32; i++) + { + sum += TEA_KEY_SCHEDULE_CONSTANT; + v0 += ((v1 << 4) + key[0]) ^ (v1 + sum) ^ ((v1 >> 5) + key[1]); + v1 += ((v0 << 4) + key[2]) ^ (v0 + sum) ^ ((v0 >> 5) + key[3]); + } + + value[0] = v0; + value[1] = v1; +} + +/**************************************************************************** + * Name: passwd_base64 + * + * Description: + * Encode the low 6 bits of a byte as a custom base64 character. + * Alphabet: A-Z (0-25), a-z (26-51), 0-9 (52-61), + (62), / (63). + * The colon ':' is deliberately absent so it never collides with the + * passwd field separator. + * + * This matches passwd_base64() in apps/fsutils/passwd/passwd_encrypt.c. + * + ****************************************************************************/ + +static char passwd_base64(uint8_t binary) +{ + binary &= 63; + + if (binary < 26) + { + return (char)('A' + binary); + } + + binary -= 26; + if (binary < 26) + { + return (char)('a' + binary); + } + + binary -= 26; + if (binary < 10) + { + return (char)('0' + binary); + } + + binary -= 10; + if (binary == 0) + { + return '+'; + } + + return '/'; +} + +/**************************************************************************** + * Name: passwd_encrypt + * + * Description: + * Encrypt a plaintext password string and store the result as a + * NUL-terminated base64 string in `encrypted`. + * + * Algorithm (identical to apps/fsutils/passwd/passwd_encrypt.c): + * 1. Process the password in 8-byte gulps, padding short gulps with + * ASCII spaces. + * 2. TEA-encrypt each 8-byte gulp as two uint32_t words. + * 3. Interpret the result as four uint16_t half-words. + * 4. Stream-encode those half-words 6 bits at a time using the custom + * base64 alphabet above. + * + * Input Parameters: + * password - NUL-terminated plaintext password + * key - Four-element 128-bit TEA key + * encrypted - Output buffer (at least MAX_ENCRYPTED + 1 bytes) + * + * Returned Value: + * 0 on success, -1 on error (password too long). + * + ****************************************************************************/ + +static int passwd_encrypt(const char *password, + const uint32_t *key, + char encrypted[MAX_ENCRYPTED + 1]) +{ + union block_u value; + const char *src; + char *dest; + uint32_t tmp; + uint8_t remainder; + int remaining; + int gulpsize; + int nbits; + int i; + + remaining = (int)strlen(password); + if (remaining > MAX_PASSWORD) + { + fprintf(stderr, "mkpasswd: password too long (max %d characters)\n", + MAX_PASSWORD); + return -1; + } + + src = password; + dest = encrypted; + *dest = '\0'; + remainder = 0; + nbits = 0; + + for (; remaining > 0; remaining -= gulpsize) + { + /* Copy up to 8 bytes into the block, padding the rest with spaces */ + + gulpsize = 8; + if (gulpsize > remaining) + { + gulpsize = remaining; + } + + for (i = 0; i < gulpsize; i++) + { + value.b[i] = *src++; + } + + for (; i < 8; i++) + { + value.b[i] = ' '; + } + + /* TEA-encrypt the block in-place */ + + tea_encrypt(value.l, key); + + /* Stream-encode the four 16-bit half-words into base64 */ + + tmp = remainder; + + for (i = 0; i < 4; i++) + { + tmp = ((uint32_t)value.h[i] << nbits) | tmp; + nbits += 16; + + while (nbits >= 6) + { + *dest++ = passwd_base64((uint8_t)(tmp & 0x3f)); + tmp >>= 6; + nbits -= 6; + } + } + + remainder = (uint8_t)tmp; + *dest = '\0'; + } + + /* Flush any remaining bits */ + + if (nbits > 0) + { + *dest++ = passwd_base64(remainder); + *dest = '\0'; + } + + return 0; +} + +/**************************************************************************** + * Name: parse_uint32_hex + * + * Description: + * Parse a hex string (with or without leading "0x"/"0X") into a uint32_t. + * Returns 0 on success, -1 on parse error. + * + ****************************************************************************/ + +static int parse_uint32_hex(const char *str, uint32_t *out) +{ + char *endptr; + unsigned long val; + + if (str == NULL || *str == '\0') + { + return -1; + } + + errno = 0; + val = strtoul(str, &endptr, 0); /* base 0: auto-detect 0x prefix */ + if (errno != 0 || *endptr != '\0') + { + return -1; + } + + *out = (uint32_t)val; + return 0; +} + +/**************************************************************************** + * Name: mkdir_p + * + * Description: + * Create all directory components in `path`, like "mkdir -p". + * Returns 0 on success, -1 on error. + * + ****************************************************************************/ + +static int mkdir_p(const char *path) +{ + char *tmp; + char *p; + size_t len; + + tmp = strdup(path); + if (tmp == NULL) + { + return -1; + } + + len = strlen(tmp); + + /* Strip trailing slash */ + + if (len > 0 && tmp[len - 1] == '/') + { + tmp[len - 1] = '\0'; + } + + for (p = tmp + 1; *p != '\0'; p++) + { + if (*p == '/') + { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + + mkdir(tmp, 0755); + free(tmp); + return 0; +} + +/**************************************************************************** + * Name: show_usage + ****************************************************************************/ + +static void show_usage(const char *progname) +{ + fprintf(stderr, + "Usage: %s --user --password [options] [-o ]\n" + "\n" + "Options:\n" + " --user Username (required)\n" + " --password Plaintext password (required)\n" + " --uid User ID (default: 0)\n" + " --gid Group ID (default: 0)\n" + " --home Home directory (default: /)\n" + " --key1 TEA key word 1 (default: 0x%08x)\n" + " --key2 TEA key word 2 (default: 0x%08x)\n" + " --key3 TEA key word 3 (default: 0x%08x)\n" + " --key4 TEA key word 4 (default: 0x%08x)\n" + " -o Output file (default: stdout)\n" + "\n" + "Output format: username:encrypted_hash:uid:gid:home\n", + progname, + DEFAULT_KEY1, DEFAULT_KEY2, DEFAULT_KEY3, DEFAULT_KEY4); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int main(int argc, char **argv) +{ + const char *user = NULL; + const char *password = NULL; + const char *home = "/"; + const char *outpath = NULL; + int uid = 0; + int gid = 0; + uint32_t key[4] = + { + DEFAULT_KEY1, DEFAULT_KEY2, DEFAULT_KEY3, DEFAULT_KEY4 + }; + + char encrypted[MAX_ENCRYPTED + 1]; + FILE *out; + int i; + int ret; + + /* Simple long-option parser (avoids getopt_long portability concerns) */ + + for (i = 1; i < argc; i++) + { + if (strcmp(argv[i], "--user") == 0 && i + 1 < argc) + { + user = argv[++i]; + } + else if (strcmp(argv[i], "--password") == 0 && i + 1 < argc) + { + password = argv[++i]; + } + else if (strcmp(argv[i], "--uid") == 0 && i + 1 < argc) + { + uid = atoi(argv[++i]); + } + else if (strcmp(argv[i], "--gid") == 0 && i + 1 < argc) + { + gid = atoi(argv[++i]); + } + else if (strcmp(argv[i], "--home") == 0 && i + 1 < argc) + { + home = argv[++i]; + } + else if (strcmp(argv[i], "--key1") == 0 && i + 1 < argc) + { + if (parse_uint32_hex(argv[++i], &key[0]) < 0) + { + fprintf(stderr, "mkpasswd: invalid --key1 value: %s\n", + argv[i]); + return 1; + } + } + else if (strcmp(argv[i], "--key2") == 0 && i + 1 < argc) + { + if (parse_uint32_hex(argv[++i], &key[1]) < 0) + { + fprintf(stderr, "mkpasswd: invalid --key2 value: %s\n", + argv[i]); + return 1; + } + } + else if (strcmp(argv[i], "--key3") == 0 && i + 1 < argc) + { + if (parse_uint32_hex(argv[++i], &key[2]) < 0) + { + fprintf(stderr, "mkpasswd: invalid --key3 value: %s\n", + argv[i]); + return 1; + } + } + else if (strcmp(argv[i], "--key4") == 0 && i + 1 < argc) + { + if (parse_uint32_hex(argv[++i], &key[3]) < 0) + { + fprintf(stderr, "mkpasswd: invalid --key4 value: %s\n", + argv[i]); + return 1; + } + } + else if ((strcmp(argv[i], "-o") == 0 || + strcmp(argv[i], "--output") == 0) && i + 1 < argc) + { + outpath = argv[++i]; + } + else if (strcmp(argv[i], "--help") == 0 || + strcmp(argv[i], "-h") == 0) + { + show_usage(argv[0]); + return 0; + } + else + { + fprintf(stderr, "mkpasswd: unknown option: %s\n", argv[i]); + show_usage(argv[0]); + return 1; + } + } + + /* Validate required arguments */ + + if (user == NULL) + { + fprintf(stderr, "mkpasswd: --user is required\n"); + show_usage(argv[0]); + return 1; + } + + if (password == NULL) + { + fprintf(stderr, "mkpasswd: --password is required\n"); + show_usage(argv[0]); + return 1; + } + + if (password[0] == '\0') + { + fprintf(stderr, "mkpasswd: --password must not be empty\n"); + return 1; + } + + if (strlen(password) < MIN_PASSWORD) + { + fprintf(stderr, + "mkpasswd: --password must be at least %d characters\n", + MIN_PASSWORD); + return 1; + } + + /* Encrypt the password using TEA + custom base64. + * Only the hash is written to the output file; the plaintext is never + * stored in firmware. + */ + + ret = passwd_encrypt(password, key, encrypted); + if (ret < 0) + { + return 1; + } + + /* Open the output stream */ + + if (outpath != NULL) + { + /* Create parent directory if it does not exist */ + + char *dir = strdup(outpath); + char *last = strrchr(dir, '/'); + + if (last != NULL && last != dir) + { + *last = '\0'; + mkdir_p(dir); + } + + free(dir); + + out = fopen(outpath, "w"); + if (out == NULL) + { + fprintf(stderr, "mkpasswd: cannot open output file '%s': %s\n", + outpath, strerror(errno)); + return 1; + } + } + else + { + out = stdout; + } + + /* Write the passwd entry. + * Format: username:encrypted_hash:uid:gid:home + * This matches the format expected by apps/fsutils/passwd/passwd_find.c + * and the existing NuttX /etc/passwd files. + */ + + fprintf(out, "%s:%s:%d:%d:%s\n", user, encrypted, uid, gid, home); + + if (outpath != NULL) + { + fclose(out); + } + + return 0; +}