Skip to content

Instantly share code, notes, and snippets.

@craig-m-unsw
Forked from RichardBronosky/mkpasswd.py
Last active October 24, 2023 09:52
Show Gist options
  • Save craig-m-unsw/3668f1efcfdb865556b41065a96019c3 to your computer and use it in GitHub Desktop.
Save craig-m-unsw/3668f1efcfdb865556b41065a96019c3 to your computer and use it in GitHub Desktop.
Platform independent way of generating Linux compatible crypt(3) sha512 hashes ($6$ style). For systems (like macOS/OSX) where you can't `mkpasswd -m sha-512`.

Create Linux password hashes from Python

Platform independent way of generating Linux compatible crypt(3) sha512 hashes ($6$ style). For use in cloud-init, preseed files, or altering /etc/shadow. Use on systems where you can't mkpasswd -m sha-512 (which gets installed from sudo apt install whois weirdly on Ubuntu/Deb systems).

setup

We can install Passlib in a virtual environment anywhere we can run Python from with a small script.

So Install Python by downloading or using a package manager (eg sudo apt-get install python3 python3-virtualenv -y).

git clone https://gist.github.com/craig-m-unsw/3668f1efcfdb865556b41065a96019c3 linux-pw-hash
cd linux-pw-hash

For *nix OS:

On MacOS / Linux / WSL create a setup.sh with:

#!/bin/bash
python3 -m venv passlib
source passlib/bin/activate
pip install --require-virtualenv --require-hashes -r requirements.txt
echo foobar123 | python3 mkpasswd.py

Then run it:

chmod +x setup.sh
./setup.sh

Windows:

we can use setup.ps1 to create the venv:

<# 
Name:
    setup.ps1
Desc:
    setup a venv for passlib
Use:
    .\setup.ps1
#>
python -m venv passlib
passlib\scripts\activate.ps1
python -m pip install --upgrade pip
pip install --require-virtualenv --require-hashes -r requirements.txt
echo foobar123 | python .\mkpasswd.py

Or type the commmands above.

Use

You do not need to provide a salt, the PassLib documentation states:

Nearly all of the hash classes passlib.hash which use a salt will automatically generate a salt, and include it as part of the hash that’s returned.

This is why the output is different each time. Ways we can run our script:

python3 mkpasswd.py 
Password: 
$6$rounds=656000$slVpnt4XjeGW98XH$ZcEQxxcNJRoD0HpCWqofQ2XR.wU0o6evxc1pJ3R9zf98X7emqpfJ68yakwiAT0tkGP2oW1niVpBx.vHcN7G78/
echo password | python3 mkpasswd.py
$6$rounds=656000$bh.dCEeuCT9TXeVr$0z.4Wk6/.2vtWrmyLTXhbBdFaPFMe/mOUcs36viVbzOgNueCeG0/TWHW7xPRFQIWOa1WwPd3CpbJfmL.Jsmjm.
PROCESS_TIME=1 python3 mkpasswd.py
Password: 
$6$rounds=656000$cCoaCQjvTHDOAW16$RHPP3aRXRhp0BxRZvEVKbH1voWPr7vc88TTVN3WpDRetRhfFi51JiMW90yG8nlNKef288YOwXhI4F0d2hnVFc.
656000 rounds in 0.6795030000000001 seconds of cpu time
ROUNDS=1280000 PROCESS_TIME=1 python3 mkpasswd.py
Password: 
$6$rounds=1280000$n7zF1lw7Cif0S4kt$vsczSPD4TF1skIQ3a0EF1YsZcJ19tcv4smOsEAlhOPceVZe9f1Djs7t55Ke55OZAcm7ZP3RLEFh9JTb7btP9Z1
1280000 rounds in 1.3317999999999999 seconds of cpu time

Inside a Docker container without network access. First turn on DCT before downloading the image:

export DOCKER_CONTENT_TRUST=1
docker build --tag mkpasswd .
docker container run --network none -it mkpasswd

If you want to verify your password and hash will match we can run verifypw.py script:

python3 verifypw.py
Enter pass:
Enter hash: $6$rounds=1280000$n7zF1lw7Cif0S4kt$vsczSPD4TF1skIQ3a0EF1YsZcJ19tcv4smOsEAlhOPceVZe9f1Djs7t55Ke55OZAcm7ZP3RLEFh9JTb7btP9Z1
[*] try pass: password was correct
[*] try WRONG pass: password and hash do not match

Notes / docs

security

From the cloud-init docs on using password hashes we find this:

#               Please note: while the use of a hashed password is better than
#               plain text, the use of this feature is not ideal. Also,
#               using a high number of salting rounds will help, but it should
#               not be relied upon.
#
#               To highlight this risk, running John the Ripper against the
#               example hash above, with a readily available wordlist, revealed
#               the true password in 12 seconds on a i7-2620QM.
#
#               In other words, this feature is a potential security risk and is
#               provided for your convenience only. If you do not fully trust the
#               medium over which your cloud-config will be transmitted, then you
#               should use SSH authentication only.

So a passowrd hash must still be securely stored, unlike the public half of an SSH Key.

supply chain

If you want to think about software supply chain security, we have:

  • Your operating system vendor (Apple, Microsoft, A Linux distro)
  • Docker and Docker hub
  • Python project
  • Pip / Pypi
  • passlib project authors and hosting
  • Github
  • these six easily auditable text files:
    • 00_README.md
    • Dockerfile
    • requirements.txt
    • mkpasswd.py
    • verifypw.py
    • add-user.sh

links

#!/bin/bash
# create an account on a linux system without using local tools like adduser or useradd.
set -v
usr_add_name="newusr"
usr_add_uid="1330"
usr_add_gid="1330"
# foobar321
usr_add_hash='$6$rounds=1280000$9J.9r8OcixeN6Q/N$.Lo3cb//E9M28hEsqhx.LtJvALliwG1/W3yoGIESrrNZFr.VS2dBeOcpU2h3.nlntFE4oNVVQGTYnRvCde7vM/'
usr_pw_date=$(date +%s)
sudo true || { echo "error no sudo"; exit 1; }
getent passwd ${usr_add_uid}
if [ $? -eq 0 ]; then
echo "user ${usr_add_name} exists with uid ${usr_add_uid}";
exit 1;
else
echo "adding new user ${usr_add_name}";
fi
set -e
# add new account deets to /etc/{passwd,group,shadow,gshadow} files
echo "${usr_add_name}:x:${usr_add_uid}:${usr_add_gid}::/home/${usr_add_name}:/bin/bash" | sudo tee -a /etc/passwd
echo "${usr_add_name}:x:${usr_add_gid}:" | sudo tee -a /etc/group
echo "${usr_add_name}:${usr_add_hash}:${usr_pw_date}:0:99999:7:::" | sudo tee -a /etc/shadow
echo "${usr_add_name}:!::" | sudo tee -a /etc/gshadow
# home dir and skel files
sudo mkdir -pv /home/${usr_add_name}
sudo chmod 700 /home/${usr_add_name}
sudo chown ${usr_add_name}:${usr_add_name} /home/${usr_add_name}/
sudo cp -v -- /etc/skel/.[a-zA-Z]* /home/${usr_add_name}/
sudo chown -v -R ${usr_add_name}:${usr_add_name} /home/${usr_add_name}/
sudo ls -la -- /home | grep ${usr_add_name}
sudo ls -la -- /home/${usr_add_name} | grep ${usr_add_name}
echo "done"
FROM python:3.11-slim-bullseye
ARG USER_ID=1005
ARG GROUP_ID=1006
ARG USERNAME=pyapp
RUN groupadd --gid $GROUP_ID $USERNAME \
&& useradd --uid $USER_ID --gid $GROUP_ID -m $USERNAME;
WORKDIR /home/pyapp
COPY requirements.txt requirements.txt
COPY mkpasswd.py mkpasswd.py
USER pyapp
RUN mkdir -pv ~/.local/bin/
ENV PATH "$PATH:/home/$USERNAME/.local/bin"
RUN pip install --upgrade --user pip
RUN pip install --user --require-hashes -r requirements.txt
ENV PROCESS_TIME=1
ENV ROUNDS=1280000
CMD [ "python3", "mkpasswd.py" ]
#!/usr/bin/env python3
import os
import sys
import time
from getpass import getpass
from passlib.hash import sha512_crypt
if not (sys.version_info.major == 3 and sys.version_info.minor >= 11):
print("Passlib 1.8 requires Python >= 3.5")
print("You are using Python {}.{}.".format(sys.version_info.major, sys.version_info.minor))
sys.exit(1)
rounds = os.environ.get('ROUNDS')
if not rounds:
rounds = sha512_crypt.default_rounds
passwd = input() if not sys.stdin.isatty() else getpass()
proc = sha512_crypt.using(rounds=rounds)
start = time.process_time()
out = proc.hash(passwd)
end = time.process_time()
print(out)
if os.environ.get('PROCESS_TIME'):
print('{} rounds in {} seconds of cpu time'.format(rounds, end-start))
# not often updating https://passlib.readthedocs.io/en/stable/history/
passlib==1.7.4 \
--hash=sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1 \
--hash=sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04
#!/usr/bin/env python3
import os
import sys
import time
from getpass import getpass
from passlib.hash import sha512_crypt
if not (sys.version_info.major == 3 and sys.version_info.minor >= 5):
print("Passlib 1.8 requires Python >= 3.5")
print("You are using Python {}.{}.".format(sys.version_info.major, sys.version_info.minor))
sys.exit(1)
mypassword = getpass("Enter pass: ")
mypassword_hash = input("Enter hash: ")
def my_hash_check(input_pass, input_hash):
if sha512_crypt.verify(input_pass, input_hash):
return("password was correct")
else:
return("password and hash do not match")
pw_try_1 = my_hash_check(mypassword, mypassword_hash)
print("[*] try right: " + str(pw_try_1))
pw_try_2 = my_hash_check(mypassword[::-1], mypassword_hash)
print("[*] try WRONG: " + str(pw_try_2))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment