diff --git a/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst b/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst index 38c646e15f7b3..466c76cc13223 100644 --- a/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst +++ b/Documentation/platforms/renesas/rx65n/boards/rx65n-grrose/index.rst @@ -491,21 +491,22 @@ 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_ETC_ROMFS_GENPASSWD`` is enabled. To configure the admin user and +password, run ``make menuconfig`` and set: - USERNAME: admin - PASSWORD: Administrator +* ``CONFIG_ETC_ROMFS_GENPASSWD=y`` +* ``CONFIG_ETC_ROMFS_PASSWD_USER`` (default: ``admin``) +* ``CONFIG_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 using +``tools/mkpasswd.py``; 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. +The encrypted passwords are only valid if the TEA key matches the one +configured in ``CONFIG_FSUTILS_PASSWD_KEY1..4`` (default: +``012345678 9abcdef0 012345678 9abcdef0``). 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..adffc8b1345a3 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_ETC_ROMFS_GENPASSWD is enabled. To configure the admin user and + password, run 'make menuconfig' and set: - nsh> cat /etc/passwd - admin:8Tv+Hbmr3pLVb5HHZgd26D:0:0:/ + CONFIG_ETC_ROMFS_GENPASSWD=y + CONFIG_ETC_ROMFS_PASSWD_USER (default: admin) + CONFIG_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 using tools/mkpasswd.py; + the plaintext is NOT stored in the firmware image. + + The encrypted passwords are only valid if the TEA key matches the one + configured in CONFIG_FSUTILS_PASSWD_KEY1..4 (default: + 012345678 9abcdef0 012345678 9abcdef0). 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..be49a0a066b6a 100644 --- a/Documentation/platforms/sim/sim/boards/sim/index.rst +++ b/Documentation/platforms/sim/sim/boards/sim/index.rst @@ -2008,24 +2008,22 @@ 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_ETC_ROMFS_GENPASSWD`` is enabled. To configure the admin user and +password, run ``make menuconfig`` and set: - nsh> cat /etc/passwd - admin:8Tv+Hbmr3pLVb5HHZgd26D:0:0:/ +* ``CONFIG_ETC_ROMFS_GENPASSWD=y`` +* ``CONFIG_ETC_ROMFS_PASSWD_USER`` (default: ``admin``) +* ``CONFIG_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. +The password is hashed with TEA at build time using +``tools/mkpasswd.py``; 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. +The encrypted passwords are only valid if the TEA key matches the one +configured in ``CONFIG_FSUTILS_PASSWD_KEY1..4`` (default: +``012345678 9abcdef0 012345678 9abcdef0``). The format of the password file is: diff --git a/boards/Board.mk b/boards/Board.mk index f355bc1fe4056..df042035e9392 100644 --- a/boards/Board.mk +++ b/boards/Board.mk @@ -35,6 +35,19 @@ $(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_ETC_ROMFS_GENPASSWD),y) +ifeq ($(CONFIG_ETC_ROMFS_PASSWD_PASSWORD),) + $(error CONFIG_ETC_ROMFS_PASSWD_PASSWORD must be set when ETC_ROMFS_GENPASSWD is enabled. Run 'make menuconfig' to set a password.) +endif + $(Q) mkdir -p $(ETCDIR)$(DELIM)$(CONFIG_ETC_ROMFSMOUNTPT) + $(Q) python3 $(TOPDIR)$(DELIM)tools$(DELIM)mkpasswd.py \ + --user $(CONFIG_ETC_ROMFS_PASSWD_USER) \ + --password $(CONFIG_ETC_ROMFS_PASSWD_PASSWORD) \ + --uid $(CONFIG_ETC_ROMFS_PASSWD_UID) \ + --gid $(CONFIG_ETC_ROMFS_PASSWD_GID) \ + --home $(CONFIG_ETC_ROMFS_PASSWD_HOME) \ + -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/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/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..52f8aebc75475 100644 --- a/cmake/nuttx_add_romfs.cmake +++ b/cmake/nuttx_add_romfs.cmake @@ -282,6 +282,33 @@ 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_ETC_ROMFS_GENPASSWD) + if("${CONFIG_ETC_ROMFS_PASSWD_PASSWORD}" STREQUAL "") + message( + FATAL_ERROR + "CONFIG_ETC_ROMFS_PASSWD_PASSWORD must be set when" + " ETC_ROMFS_GENPASSWD is enabled." + " Run 'make menuconfig' to set a password.") + 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 + ${Python3_EXECUTABLE} ${NUTTX_DIR}/tools/mkpasswd.py --user + "${CONFIG_ETC_ROMFS_PASSWD_USER}" --password + "${CONFIG_ETC_ROMFS_PASSWD_PASSWORD}" --uid + ${CONFIG_ETC_ROMFS_PASSWD_UID} --gid ${CONFIG_ETC_ROMFS_PASSWD_GID} + --home "${CONFIG_ETC_ROMFS_PASSWD_HOME}" -o ${GENPASSWD_OUTPUT} + COMMENT "Generating /etc/passwd from Kconfig values") + add_custom_target(generate_passwd DEPENDS ${GENPASSWD_OUTPUT}) + list(APPEND RCRAWS ${GENPASSWD_OUTPUT}) + list(APPEND dyn_deps generate_passwd) + endif() + # init dynamic dependencies get_property( diff --git a/sched/Kconfig b/sched/Kconfig index 21d0b11d96452..d75ba0c583149 100644 --- a/sched/Kconfig +++ b/sched/Kconfig @@ -623,6 +623,46 @@ config ETC_FATMOUNTPT will mount a FAT FS under /tmp. This is the location where the FAT FS will be mounted. Default is "/tmp". +config ETC_ROMFS_GENPASSWD + bool "Auto-generate /etc/passwd at build time" + default n + ---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. + +if ETC_ROMFS_GENPASSWD + +config ETC_ROMFS_PASSWD_USER + string "Admin username" + default "admin" + ---help--- + The username for the auto-generated /etc/passwd entry. + +config 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. Set this via 'make menuconfig'. + +config ETC_ROMFS_PASSWD_UID + int "Admin user ID" + default 0 + +config ETC_ROMFS_PASSWD_GID + int "Admin group ID" + default 0 + +config ETC_ROMFS_PASSWD_HOME + string "Admin home directory" + default "/" + +endif # ETC_ROMFS_GENPASSWD + endif # ETC_ROMFS config RR_INTERVAL diff --git a/tools/mkpasswd.py b/tools/mkpasswd.py new file mode 100644 index 0000000000000..e6febc0d90fbf --- /dev/null +++ b/tools/mkpasswd.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +############################################################################ +# tools/mkpasswd.py +# +# 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. +# +############################################################################ + +"""Generate a NuttX /etc/passwd entry with a TEA-encrypted password hash. + +This script implements the same Tiny Encryption Algorithm (TEA) and base64 +encoding used by NuttX at runtime (libs/libc/misc/lib_tea_encrypt.c and +apps/fsutils/passwd) to produce a passwd line suitable for embedding in a +ROMFS image at build time. + +Usage: + python3 mkpasswd.py --user admin --password secret + python3 mkpasswd.py --user admin --password secret -o /path/to/passwd +""" + +import argparse +import os +import struct +import sys + +# The TEA key schedule constant (0x9e3779b9 is the golden ratio derived value) +TEA_KEY_SCHEDULE_CONSTANT = 0x9E3779B9 +MASK32 = 0xFFFFFFFF + + +def tea_encrypt(v0, v1, key): + """Encrypt two 32-bit values using TEA with the given 128-bit key. + + This matches the C implementation in libs/libc/misc/lib_tea_encrypt.c. + """ + sum_val = 0 + for _ in range(32): + sum_val = (sum_val + TEA_KEY_SCHEDULE_CONSTANT) & MASK32 + v0 = (v0 + ((((v1 << 4) & MASK32) + key[0]) ^ ((v1 + sum_val) & MASK32) ^ (((v1 >> 5) & MASK32) + key[1]))) & MASK32 + v1 = (v1 + ((((v0 << 4) & MASK32) + key[2]) ^ ((v0 + sum_val) & MASK32) ^ (((v0 >> 5) & MASK32) + key[3]))) & MASK32 + return v0, v1 + + +def passwd_base64(binary): + """Encode a 6-bit value as a base64 character. + + This matches the custom base64 encoding in apps/fsutils/passwd/passwd_encrypt.c. + The mapping is: A-Z (0-25), a-z (26-51), 0-9 (52-61), + (62), / (63). + """ + binary &= 63 + if binary < 26: + return chr(ord("A") + binary) + binary -= 26 + if binary < 26: + return chr(ord("a") + binary) + binary -= 26 + if binary < 10: + return chr(ord("0") + binary) + binary -= 10 + if binary == 0: + return "+" + return "/" + + +def hash_password(password, key): + """Hash a password string using TEA encryption, returning a custom base64 string. + + This replicates the exact algorithm from apps/fsutils/passwd/passwd_encrypt.c: + 1. Process password in 8-byte chunks, padding short chunks with spaces. + 2. TEA-encrypt each chunk as two uint32_t values (little-endian). + 3. Read the encrypted result as four uint16_t values (little-endian). + 4. Stream-encode the uint16_t values into custom base64 (6 bits at a time). + """ + pwd_bytes = password.encode("utf-8") + remaining = len(pwd_bytes) + src_offset = 0 + dest = [] + remainder = 0 + nbits = 0 + + while remaining > 0: + gulpsize = min(8, remaining) + # Copy bytes into 8-byte block, pad with spaces + block = pwd_bytes[src_offset : src_offset + gulpsize] + block += b" " * (8 - gulpsize) + src_offset += gulpsize + remaining -= gulpsize + + # TEA encrypt (interpret as two little-endian uint32_t) + v0, v1 = struct.unpack("= 6: + dest.append(passwd_base64(tmp & 0x3F)) + tmp >>= 6 + nbits -= 6 + remainder = tmp & 0xFF + + # Handle any remaining bits + if nbits > 0: + dest.append(passwd_base64(remainder)) + + return "".join(dest) + + +def parse_hex(value): + """Parse a hex string (with or without 0x prefix) as a 32-bit unsigned int.""" + return int(value, 0) & MASK32 + + +def main(): + parser = argparse.ArgumentParser( + description="Generate a NuttX /etc/passwd entry with TEA-encrypted password." + ) + parser.add_argument( + "--user", required=True, help="Username for the passwd entry." + ) + parser.add_argument( + "--password", required=True, help="Plaintext password to encrypt." + ) + parser.add_argument( + "--uid", type=int, default=0, help="User ID (default: 0)." + ) + parser.add_argument( + "--gid", type=int, default=0, help="Group ID (default: 0)." + ) + parser.add_argument( + "--home", default="/", help="Home directory (default: /)." + ) + parser.add_argument( + "--key1", + type=parse_hex, + default=0x12345678, + help="TEA key word 1 (default: 0x12345678).", + ) + parser.add_argument( + "--key2", + type=parse_hex, + default=0x9ABCDEF0, + help="TEA key word 2 (default: 0x9abcdef0).", + ) + parser.add_argument( + "--key3", + type=parse_hex, + default=0x12345678, + help="TEA key word 3 (default: 0x12345678).", + ) + parser.add_argument( + "--key4", + type=parse_hex, + default=0x9ABCDEF0, + help="TEA key word 4 (default: 0x9abcdef0).", + ) + parser.add_argument( + "-o", + "--output", + default=None, + help="Output file path. If omitted, prints to stdout.", + ) + + args = parser.parse_args() + + if not args.password: + print("ERROR: --password must not be empty.", file=sys.stderr) + sys.exit(1) + + key = [args.key1, args.key2, args.key3, args.key4] + encrypted = hash_password(args.password, key) + line = f"{args.user}:{encrypted}:{args.uid}:{args.gid}:{args.home}\n" + + if args.output: + os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True) + with open(args.output, "w") as f: + f.write(line) + else: + sys.stdout.write(line) + + +if __name__ == "__main__": + main()