From 97c9dfb98610b0b4cbfbeb9cb630eb48b91546d3 Mon Sep 17 00:00:00 2001 From: _cry64 Date: Wed, 21 Jan 2026 14:15:31 +1000 Subject: [PATCH] add subcmds/new/password --- ceru/subcmds/new/default.sh | 1 + ceru/subcmds/new/password | 260 ++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100755 ceru/subcmds/new/password diff --git a/ceru/subcmds/new/default.sh b/ceru/subcmds/new/default.sh index 4a5bc52..af34920 100755 --- a/ceru/subcmds/new/default.sh +++ b/ceru/subcmds/new/default.sh @@ -23,6 +23,7 @@ ${BOLD}${UNDERLINE}${RED}Options${RESET} ${BOLD}${UNDERLINE}${RED}Subcommands${RESET} ${BOLD}${CYAN}cache-key${RESET} Generate a new binary-cache signing keypair + ${BOLD}${CYAN}password${RESET} Generate a new hashed Unix user password ${BOLD}${CYAN}ssh-key${RESET} Generate a new SSH keypair ${BOLD}${CYAN}wg-key${RESET} Generate a new Wireguard keypair" diff --git a/ceru/subcmds/new/password b/ceru/subcmds/new/password new file mode 100755 index 0000000..ae640e7 --- /dev/null +++ b/ceru/subcmds/new/password @@ -0,0 +1,260 @@ +#!/usr/bin/env bash +# Copyright 2026 Emile Clark-Boman +# +# Licensed 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. + +set -euo pipefail + +USAGE="${BOLD}${UNDERLINE}${RED}Usage${RESET} + ${BOLD}${GREEN}$THIS new password [option...]${RESET} + +${BOLD}${UNDERLINE}${RED}Description${RESET} + Generates a new password hash in libxcrypt format with secure defaults. + For more advanced usage run the ${BOLD}${MAGENTA}\`mkpasswd\`${RESET} utility directly. + +${BOLD}${UNDERLINE}${RED}Options${RESET} + ${BOLD}${MAGENTA}-h, --help${RESET} Show this message (^_^) + ${BOLD}${MAGENTA}-o, --out${RESET} Private key file name to write to (the public key is named identically but ends with ${BOLD}${CYAN}.pub${RESET}) + ${BOLD}${MAGENTA}-j, --json${RESET} Output in JSON format + ${BOLD}${MAGENTA}-i, --stdin${RESET} Read the password to hash from stdin ${BOLD}$CYAN(single line only)${RESET} + ${BOLD}${MAGENTA}-t, --type${RESET} The hash algorithm to use: ${BOLD}${MAGENTA}yescrypt, scrypt, bcrypt ${CYAN}(default: yescrypt)${RESET} + ${BOLD}${MAGENTA}-r, --rounds${RESET} The number of key derivation function rounds to apply ${BOLD}${MAGENTA}(format: ^[0-9]+\$) ${CYAN}(defaults: yescrypt=11, scrypt=10, bcrypt=14)${RESET} + ${BOLD}${MAGENTA}-s, --salt${RESET} Specify the hash's salt directly ${BOLD}${RED}(not recommended)${RESET} ${BOLD}${CYAN}(default: libxcrypt secure default)${RESET}" + +# ==== Argument Values ==== +TYPE='yescrypt' +ROUNDS='' +SALTED=false +OUT='' +JSON=false +STDIN=false +EXTRA='' +# ==== Argument Values ==== + +# parse all args +while [[ $# -gt 0 ]]; do + ARG="$1" + case "$ARG" in + -h|--help) + throw-usage 0 + ;; + -o|--out) + shift + OUT="$1"; shift + ;; + -j|--json) + shift + JSON=true + ;; + -i|--stdin) + shift + STDIN=true + ;; + -t|--type) + shift + TYPE="$1"; shift + ;; + -r|--rounds) + shift + ROUNDS="$1"; shift + ;; + -s|--salt) + shift + if [[ "$SALTED" == false ]]; then + SALTED=true + EXTRA="$EXTRA --salt=\'$1\'" + fi + shift + ;; + -*) + throw-badflag 1 "$ARG" + ;; + *) + throw-badarg 1 "$ARG" + ;; + esac +done; unset -v ARG + +# NOTE: Available password hashing methods for /etc/passwd & /etc/shadow +# NOTE: Read the manual pages via `man 3 crypt` and `man 5 crypt` (if available) +# NOTE: Available online via https://man.archlinux.org/man/crypt.5 +# WARNING: Due to modern developments in cryptography most of these methods +# WARNING: are no longer recommended however some distrobutions still use them. +# WARNING: Cerulean intentionally restricts access to only secure algorithms. +# $ mkpasswd -m help +## Available methods: +## yescrypt Yescrypt +## gost-yescrypt GOST Yescrypt +## scrypt scrypt +## bcrypt bcrypt +## bcrypt-a bcrypt (obsolete $2a$ version) +## sha512crypt SHA-512 +## sha256crypt SHA-256 +## sunmd5 SunMD5 +## md5crypt MD5 +## bsdicrypt BSDI extended DES-based crypt(3) +## descrypt standard 56 bit DES-based crypt(3) +## nt NT-Hash +function perr-unsupportedhash { + local ALGO="$1" + echo -e "${BOLD}${CYAN}$THIS${RED} does not support the ${MAGENTA}$ALGO${RED} hashing algorithm${RESET}" >&2 +} +function perr-forbiddenhash { + local ALGO="$1" + echo -e "${BOLD}${CYAN}Cerulean${RED} intentionally forbids ${MAGENTA}$ALGO-based${RED} hashes.${RESET}" >&2 +} +function perr-recommendhash { + local ALGO="$1" + echo -e "${BOLD}${CYAN}Cerulean${WHITE} recommends the ${MAGENTA}$ALGO${WHITE} algorithm ${GREEN}(embrace modernity loser)${RESET}" >&2 +} + +# ensure $TYPE is a valid hash algorithm +case "$TYPE" in + # ========= PERMITTED HASH ALGORITHMS ========= + yescrypt) + if [[ -z "$ROUNDS" ]]; then + ROUNDS='11' + fi + ;; + scrypt) + if [[ -z "$ROUNDS" ]]; then + ROUNDS='10' + fi + ;; + bcrypt) + if [[ -z "$ROUNDS" ]]; then + ROUNDS='14' + fi + ;; + + # ========= FORBIDDEN HASH ALGORITHMS ========= + gost-yescrypt) + perr-unsupportedhash "$TYPE" + perr-recommendhash 'yescrypt' + echo -e "┏ +┃ Dear Comrade, +┃ It is with a heavy heart I must inform you that \"GOST Algorithms +┃ Considered Harmful\" - Edsger Wybe Dijkstra (probably). Alas +┃ GOST is considered broken... It is no longer 1970, please grow up :( +┃ Слава Родине! - Glory to the Motherland! +┗" >&2 + exit 1 + ;; + bcrypt-a) + perr-unsupportedhash "$TYPE" + perr-recommendhash 'bcrypt' + echo -e "┏ +┃ The alternative prefix \"\$2y$\" is equivalent to \"\$2b$\". +┃ It exists for historical reasons only. The alternative prefixes +┃ \"\$2a$\" and \"\$2x$\" provide bug-compatibility with +┃ crypt_blowfish 1.0.4 and earlier, which incorrectly processed +┃ characters with the 8th bit set. +┗" >&2 + exit 1 + ;; + sha512crypt|sha256crypt) + perr-unsupportedhash "$TYPE" + perr-forbiddenhash 'SHA' + echo -e "┏ +┃ SHA-based hashes are considered outdated and generally insecure +┃ due to their vulnerabilit to brute-force and collision attacks. +┃ Modern algorithms such as yescrypt, scrypt, and bcrypt are recommended. +┗" >&2 + exit 1 + ;; + sunmd5|md5crypt) + perr-unsupportedhash "$TYPE" + perr-forbiddenhash 'MD5' + echo -e "┏ +┃ Not as weak as the DES-based hashes, but MD5 is so cheap +┃ on modern hardware that it should not be used for new hashes. +┗" >&2 + exit 1 + ;; + bsdicrypt|descrypt) + perr-unsupportedhash "$TYPE" + perr-forbiddenhash 'DES' + echo -e "┏ +┃ The DES block cipher is cheap on modern hardware. Because there are only +┃ 4096 possible salts and 2**56 distinct passphrases, which it +┃ truncates to 8 characters, it is feasible to discover any passphrase +┃ hashed with this method. It should only be used if you absolutely have to +┃ generate hashes that will work on an old operating system that supports nothing else. +┗" >&2 + exit 1 + ;; + nt) + perr-unsupportedhash "$TYPE" + echo -e "${BOLD}Please ${RED}repent${WHITE} for your filthy sins ${RED}you disgusting human...${RESET}" >&2 + echo -e "┏ +┃ Available for cross-compatibility's sake on FreeBSD. Based on MD4. +┃ Has no salt or tunable cost parameter. It is so weak that +┃ almost any human-chosen passphrase hashed with this method is guessable. +┃ It should only be used if you absolutely have to generate hashes that +┃ will work on an old operating system that supports nothing else. +┗" >&2 + exit 1 + ;; + *) + echo -e "${BOLD}${RED}Unrecognised hash algorithm ${MAGENTA}\"$TYPE\"${RESET}" >&2 + echo -e "${BOLD}${GREEN}Supported algorithms: ${MAGENTA}yescrypt, scrypt, bcrypt${RESET}" >&2 + exit 1 + ;; +esac +unset -f perr-unsupportedhash perr-forbiddenhash perr-recommendhash + +# ensure $ROUNDS is a valid numeric +re='^[0-9]+$' +if ! [[ "$ROUNDS" =~ ^[0-9]+$ ]] ; then + throw-badval 1 "$ROUNDS" '-r|--rounds' +fi +unset -v re + +# Acquire password from stdin +if [[ "$STDIN" == true ]]; then + read -s PASS +else + read -sp "$(echo -e "${BOLD}${GREEN}Password:${RESET} ")" PASS + echo # \n + read -sp "$(echo -e "${BOLD}${GREEN}Retype Password:${RESET} ")" PASS2 + echo # \n + if [[ "$PASS" != "$PASS2" ]]; then + echo -e "${BOLD}${RED}Sorry, passwords do not match${RESET}" >&2 + exit 1 + fi + unset -v PASS2 +fi + +# Compute hash of password +RESULT=$(mkpasswd -sm "$TYPE" -R "$ROUNDS" $EXTRA <<<"$PASS") +unset -v PASS +# Format as JSON if necessary +if [[ "$JSON" == true ]]; then + RESULT="{ + \"type\": \"${TYPE}\", + \"rounds\": ${ROUNDS}, + \"hash\": \"${RESULT}\" +}" +fi + +# Display hash result +if [[ -n "$OUT" ]]; then + echo "$RESULT" > "$OUT" +elif [[ "$JSON" == true ]]; then + echo "$RESULT" +else + echo -e "${BOLD}${GREEN}Hash:${WHITE} $RESULT${RESET}" +fi +unset -v RESULT + +unset -v TYPE ROUNDS SALTED OUT JSON STDIN EXTRA