#!/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 KDF 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 if ! isnumeric "$ROUNDS"; then throw-badval 1 "$ROUNDS" '-r|--rounds' fi # 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