Track 10 · Lesson 3 of 6
Build a hash and checksum tool
Build hashtool.sh on your phone, one small step at a time: hash a file or standard input, choose the algorithm with --algo, save the digests with --write, and check a checksum file with --check. The whole script is at the end.
In this lesson you build hashtool.sh, a checksum tool for your phone. You start with a single sha256sum command and grow it, one small step at a time, until it can hash a file or standard input, choose its algorithm with --algo, save the digests with --write, and check a checksum file with --check. Each step shows the whole file as it stands and the real output it prints, so you can run it after every step. The finished program sits in The whole script at the end: copy that one block into a file and you have the working tool. If you would rather use the tool than build it, the hash and checksum tool does the same job in the browser. Everything here runs offline in Termux as an ordinary app, with no root.
What you'll be able to do
- Hash one file and read the digest it prints.
- Put the work in a script that takes a filename argument.
- Choose SHA-256, SHA-1, MD5 or
opensslwith a--algooption. - Hash several files in one run and print one line per file.
- Write a checksum file with
--writeand check it later with--check. - Read standard input when no file is given.
- Explain why a digest from the same place as the file proves nothing, and what a signature adds.
Step 1: Hash one file with sha256sum
Make two small files to work with. printf writes exactly the text you give it, so the digests below will match yours. Then hash the first file with sha256sum, which is already on the phone.
printf 'alpha\n' > a.txt
printf 'bravo\n' > b.txt
sha256sum a.txt
b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060 a.txt
The line has two parts. First the digest: sixty-four hexadecimal characters, always that length no matter how large the file is. Then two spaces and the filename. The digest is a function of the file contents and nothing else, so hash the same file again and you get the same sixty-four characters, and change one byte and every one of them changes.
Step 2: Wrap it in a script that takes a filename
Typing sha256sum each time is fine until you want your own defaults, so put the work in a file. Create hashtool.sh with a shebang line and one command that hashes the file named as the first argument.
#!/bin/sh
sha256sum "$1"
Run it by naming the file after the script.
sh hashtool.sh a.txt
b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060 a.txt
$1 is the first argument. Keep the quotes around it: "$1" passes the name as one word, so a filename with a space stays a single name. The #!/bin/sh line tells the system to run the file with sh, which is the shell every phone has.
Step 3: Choose the algorithm with --algo
Now let the tool choose its algorithm. Add a loop that reads the arguments: --algo takes the next word as the algorithm name, and anything else is kept as the file. A case then maps the name to the command that does the work.
#!/bin/sh
algo=sha256
file=
while [ $# -gt 0 ]; do
case "$1" in
--algo) algo=$2; shift 2 ;;
*) file=$1; shift ;;
esac
done
case "$algo" in
sha256) hash_cmd=sha256sum ;;
sha1) hash_cmd=sha1sum ;;
md5) hash_cmd=md5sum ;;
openssl) hash_cmd="openssl dgst -sha256" ;;
*)
echo "hashtool: unknown algorithm: $algo" >&2
exit 2 ;;
esac
$hash_cmd "$file"
The default is sha256, so a bare filename still works, and --algo switches tools.
sh hashtool.sh a.txt
sh hashtool.sh --algo md5 a.txt
sh hashtool.sh --algo openssl a.txt
b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060 a.txt
9f9f90dbe3e5ee1218c86b8839db1995 a.txt
SHA2-256(a.txt)= b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060
The third line shows the same SHA-256 digest as the first, but written differently: openssl dgst prints a label and an equals sign before the digest, so its line is not yet in the two-space shape the sum tools use. You even that out in the next step. An unknown name falls through to the case default and stops with a clear message rather than a shell error. On MD5 and SHA-1: they are quick and fine for spotting accidental corruption, but practical collisions have been built for both, so do not use them against an attacker who might choose the file for you.
Step 4: Hash several files in one run
Hash as many files as you like at once. Collect every name into files, then loop over them. The new hash_line function prints one line per file, and it rewrites the openssl output into the same two-space shape as the sum tools, so every line looks alike.
#!/bin/sh
algo=sha256
files=
while [ $# -gt 0 ]; do
case "$1" in
--algo) algo=$2; shift 2 ;;
*) files="$files $1"; shift ;;
esac
done
case "$algo" in
sha256) hash_cmd=sha256sum ;;
sha1) hash_cmd=sha1sum ;;
md5) hash_cmd=md5sum ;;
openssl) hash_cmd="openssl dgst -sha256" ;;
*) echo "hashtool: unknown algorithm: $algo" >&2; exit 2 ;;
esac
hash_line() {
if [ "$algo" = openssl ]; then
digest=$(openssl dgst -sha256 "$1" | sed 's/^.*= //')
printf '%s %s\n' "$digest" "$1"
else
$hash_cmd "$1"
fi
}
for f in $files; do
hash_line "$f"
done
sh hashtool.sh a.txt b.txt
sh hashtool.sh --algo openssl a.txt
b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060 a.txt
5da8f23decf397b13f4f55b6fb8a61936238bfe08ed9d901132974f1beccc45c b.txt
b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060 a.txt
Two spaces between the digest and the name is the format the check mode reads, so making every line uniform now pays off in Step 6. The openssl line, which used to carry a label, now looks exactly like the sum tool lines.
Step 5: Save the digests with --write
Add --write to save the digests instead of printing them. When write_to is set, redirect standard output to that file with exec >, and everything printed after that lands in the file.
#!/bin/sh
algo=sha256
write_to=
files=
while [ $# -gt 0 ]; do
case "$1" in
--algo) algo=$2; shift 2 ;;
--write) write_to=$2; shift 2 ;;
*) files="$files $1"; shift ;;
esac
done
case "$algo" in
sha256) hash_cmd=sha256sum ;;
sha1) hash_cmd=sha1sum ;;
md5) hash_cmd=md5sum ;;
openssl) hash_cmd="openssl dgst -sha256" ;;
*) echo "hashtool: unknown algorithm: $algo" >&2; exit 2 ;;
esac
hash_line() {
if [ "$algo" = openssl ]; then
digest=$(openssl dgst -sha256 "$1" | sed 's/^.*= //')
printf '%s %s\n' "$digest" "$1"
else
$hash_cmd "$1"
fi
}
if [ -n "$write_to" ]; then
exec > "$write_to"
fi
for f in $files; do
hash_line "$f"
done
sh hashtool.sh --write SHA256SUMS a.txt b.txt
cat SHA256SUMS
b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060 a.txt
5da8f23decf397b13f4f55b6fb8a61936238bfe08ed9d901132974f1beccc45c b.txt
The first command prints nothing, because its output went into the file; cat then shows what was saved. SHA256SUMS is the name projects often use for this file. It is plain text: a digest, two spaces, a filename, one line per file. Keep it somewhere safe, because it is your record of what those files were when you last trusted them.
Step 6: Check a checksum file with --check
Add --check to verify a checksum file. Each sum tool has a -c flag that reads the file, hashes the files it names, and compares the results. openssl has no such flag, so the check uses the sum tool for the same digest, which is why the case now sets sum_cmd alongside hash_cmd.
#!/bin/sh
algo=sha256
write_to=
check_file=
files=
while [ $# -gt 0 ]; do
case "$1" in
--algo) algo=$2; shift 2 ;;
--write) write_to=$2; shift 2 ;;
--check) check_file=$2; shift 2 ;;
*) files="$files $1"; shift ;;
esac
done
case "$algo" in
sha256) hash_cmd=sha256sum; sum_cmd=sha256sum ;;
sha1) hash_cmd=sha1sum; sum_cmd=sha1sum ;;
md5) hash_cmd=md5sum; sum_cmd=md5sum ;;
openssl) hash_cmd="openssl dgst -sha256"; sum_cmd=sha256sum ;;
*) echo "hashtool: unknown algorithm: $algo" >&2; exit 2 ;;
esac
hash_line() {
if [ "$algo" = openssl ]; then
digest=$(openssl dgst -sha256 "$1" | sed 's/^.*= //')
printf '%s %s\n' "$digest" "$1"
else
$hash_cmd "$1"
fi
}
if [ -n "$check_file" ]; then
$sum_cmd -c "$check_file"
exit $?
fi
if [ -n "$write_to" ]; then
exec > "$write_to"
fi
for f in $files; do
hash_line "$f"
done
sh hashtool.sh --check SHA256SUMS
a.txt: OK
b.txt: OK
Every line ends in OK when the file still matches what you recorded. Change one file and check again, and that line reads FAILED while the others stay OK. The command also exits with status 1 when any line fails, so a script can test $? afterwards and act on it.
OK says only that the file is byte for byte what it was when the line was written. It says nothing about who wrote the file. If the file and the checksum both came from the same place, then whoever changed one could just as easily change the other to match, and the check would still print OK. A signature, made with the publisher's private key, is what ties a file to an author. A checksum on its own cannot.
Step 7: Read standard input when no file is given
When you give no filename, read from standard input, so the tool drops into a pipeline. The sum tools label that input with a single dash.
#!/bin/sh
algo=sha256
write_to=
check_file=
files=
while [ $# -gt 0 ]; do
case "$1" in
--algo) algo=$2; shift 2 ;;
--write) write_to=$2; shift 2 ;;
--check) check_file=$2; shift 2 ;;
*) files="$files $1"; shift ;;
esac
done
case "$algo" in
sha256) hash_cmd=sha256sum; sum_cmd=sha256sum ;;
sha1) hash_cmd=sha1sum; sum_cmd=sha1sum ;;
md5) hash_cmd=md5sum; sum_cmd=md5sum ;;
openssl) hash_cmd="openssl dgst -sha256"; sum_cmd=sha256sum ;;
*) echo "hashtool: unknown algorithm: $algo" >&2; exit 2 ;;
esac
hash_line() {
if [ "$algo" = openssl ]; then
digest=$(openssl dgst -sha256 "$1" | sed 's/^.*= //')
printf '%s %s\n' "$digest" "$1"
else
$hash_cmd "$1"
fi
}
if [ -n "$check_file" ]; then
$sum_cmd -c "$check_file"
exit $?
fi
if [ -n "$write_to" ]; then
exec > "$write_to"
fi
if [ -z "$files" ]; then
hash_line -
else
for f in $files; do
hash_line "$f"
done
fi
printf 'hello\n' | sh hashtool.sh
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 -
The - stands for standard input. That one line hashes those six bytes with no file in sight, which is what you want when the data is arriving from another command.
Step 8: A usage message and error handling
Finish the tool with a usage message and honest errors. The loop now checks that --algo, --write and --check each carry a value, rejects an unknown option, and the hash loop checks that every named file exists. -h prints the usage text, and -- stops option reading so a file whose name begins with a dash can still be hashed.
#!/bin/sh
# hashtool.sh - hash files or standard input, write a checksum file, or check one.
usage() {
cat <<'EOF'
hashtool.sh - hash files or standard input, and check checksums
Usage:
hashtool.sh [options] [FILE]
With no FILE, reads standard input.
Options:
-a, --algo NAME sha256 (default), sha1, md5, or openssl
-w, --write FILE write the checksums to FILE instead of the screen
-c, --check FILE check the digests listed in FILE
-h, --help show this message
Examples:
hashtool.sh a.txt
hashtool.sh --algo md5 a.txt
hashtool.sh --write SHA256SUMS a.txt b.txt
hashtool.sh --check SHA256SUMS
printf 'hello\n' | hashtool.sh
EOF
}
algo=sha256
write_to=
check_file=
files=
while [ $# -gt 0 ]; do
case "$1" in
-a|--algo)
if [ $# -lt 2 ]; then echo "hashtool: --algo needs a value" >&2; exit 2; fi
algo=$2; shift 2 ;;
-w|--write)
if [ $# -lt 2 ]; then echo "hashtool: --write needs a value" >&2; exit 2; fi
write_to=$2; shift 2 ;;
-c|--check)
if [ $# -lt 2 ]; then echo "hashtool: --check needs a value" >&2; exit 2; fi
check_file=$2; shift 2 ;;
-h|--help)
usage; exit 0 ;;
--)
shift; break ;;
-*)
echo "hashtool: unknown option: $1" >&2
echo "try: hashtool.sh --help" >&2
exit 2 ;;
*)
files="$files $1"; shift ;;
esac
done
case "$algo" in
sha256) hash_cmd=sha256sum; sum_cmd=sha256sum ;;
sha1) hash_cmd=sha1sum; sum_cmd=sha1sum ;;
md5) hash_cmd=md5sum; sum_cmd=md5sum ;;
openssl) hash_cmd=; sum_cmd=sha256sum ;;
*)
echo "hashtool: unknown algorithm: $algo" >&2
echo "use one of: sha256, sha1, md5, openssl" >&2
exit 2 ;;
esac
# One digest line, in the two-space format the check mode expects.
# $1 is a path, or "-" for standard input.
hash_line() {
if [ "$algo" = openssl ]; then
if [ "$1" = "-" ]; then
digest=$(openssl dgst -sha256 | sed 's/^.*= //')
else
digest=$(openssl dgst -sha256 "$1" | sed 's/^.*= //')
fi
printf '%s %s\n' "$digest" "$1"
else
$hash_cmd "$1"
fi
}
# Check mode: let the matching sum tool read the file and print OK or FAILED.
if [ -n "$check_file" ]; then
if [ ! -f "$check_file" ]; then
echo "hashtool: no such checksum file: $check_file" >&2
exit 2
fi
$sum_cmd -c "$check_file"
exit $?
fi
# Write mode: send everything that follows to the checksum file.
if [ -n "$write_to" ]; then
if ! ( : > "$write_to" ) 2>/dev/null; then
echo "hashtool: cannot write to $write_to" >&2
exit 2
fi
exec > "$write_to"
fi
# Hash mode: the listed files, or standard input when none are given.
if [ -z "$files" ]; then
hash_line -
else
for f in $files; do
if [ ! -f "$f" ]; then
echo "hashtool: no such file: $f" >&2
exit 2
fi
hash_line "$f"
done
fi
sh hashtool.sh --help
sh hashtool.sh --bogus
sh hashtool.sh nope.txt
sh hashtool.sh --algo sha999 a.txt
hashtool.sh - hash files or standard input, and check checksums
Usage:
hashtool.sh [options] [FILE]
With no FILE, reads standard input.
Options:
-a, --algo NAME sha256 (default), sha1, md5, or openssl
-w, --write FILE write the checksums to FILE instead of the screen
-c, --check FILE check the digests listed in FILE
-h, --help show this message
Examples:
hashtool.sh a.txt
hashtool.sh --algo md5 a.txt
hashtool.sh --write SHA256SUMS a.txt b.txt
hashtool.sh --check SHA256SUMS
printf 'hello\n' | hashtool.sh
hashtool: unknown option: --bogus
try: hashtool.sh --help
hashtool: no such file: nope.txt
hashtool: unknown algorithm: sha999
use one of: sha256, sha1, md5, openssl
A bad option, a missing value or a missing file prints one short line to standard error and exits with status 2, never a shell error or a stack trace. The usage text names every option, so anyone who finds the script can work out what it does.
The whole script
Here is the finished file in one piece. Save it as hashtool.sh, or copy this block into a file of that name, and it runs exactly as it stands.
#!/bin/sh
# hashtool.sh - hash files or standard input, write a checksum file, or check one.
usage() {
cat <<'EOF'
hashtool.sh - hash files or standard input, and check checksums
Usage:
hashtool.sh [options] [FILE]
With no FILE, reads standard input.
Options:
-a, --algo NAME sha256 (default), sha1, md5, or openssl
-w, --write FILE write the checksums to FILE instead of the screen
-c, --check FILE check the digests listed in FILE
-h, --help show this message
Examples:
hashtool.sh a.txt
hashtool.sh --algo md5 a.txt
hashtool.sh --write SHA256SUMS a.txt b.txt
hashtool.sh --check SHA256SUMS
printf 'hello\n' | hashtool.sh
EOF
}
algo=sha256
write_to=
check_file=
files=
while [ $# -gt 0 ]; do
case "$1" in
-a|--algo)
if [ $# -lt 2 ]; then echo "hashtool: --algo needs a value" >&2; exit 2; fi
algo=$2; shift 2 ;;
-w|--write)
if [ $# -lt 2 ]; then echo "hashtool: --write needs a value" >&2; exit 2; fi
write_to=$2; shift 2 ;;
-c|--check)
if [ $# -lt 2 ]; then echo "hashtool: --check needs a value" >&2; exit 2; fi
check_file=$2; shift 2 ;;
-h|--help)
usage; exit 0 ;;
--)
shift; break ;;
-*)
echo "hashtool: unknown option: $1" >&2
echo "try: hashtool.sh --help" >&2
exit 2 ;;
*)
files="$files $1"; shift ;;
esac
done
case "$algo" in
sha256) hash_cmd=sha256sum; sum_cmd=sha256sum ;;
sha1) hash_cmd=sha1sum; sum_cmd=sha1sum ;;
md5) hash_cmd=md5sum; sum_cmd=md5sum ;;
openssl) hash_cmd=; sum_cmd=sha256sum ;;
*)
echo "hashtool: unknown algorithm: $algo" >&2
echo "use one of: sha256, sha1, md5, openssl" >&2
exit 2 ;;
esac
# One digest line, in the two-space format the check mode expects.
# $1 is a path, or "-" for standard input.
hash_line() {
if [ "$algo" = openssl ]; then
if [ "$1" = "-" ]; then
digest=$(openssl dgst -sha256 | sed 's/^.*= //')
else
digest=$(openssl dgst -sha256 "$1" | sed 's/^.*= //')
fi
printf '%s %s\n' "$digest" "$1"
else
$hash_cmd "$1"
fi
}
# Check mode: let the matching sum tool read the file and print OK or FAILED.
if [ -n "$check_file" ]; then
if [ ! -f "$check_file" ]; then
echo "hashtool: no such checksum file: $check_file" >&2
exit 2
fi
$sum_cmd -c "$check_file"
exit $?
fi
# Write mode: send everything that follows to the checksum file.
if [ -n "$write_to" ]; then
if ! ( : > "$write_to" ) 2>/dev/null; then
echo "hashtool: cannot write to $write_to" >&2
exit 2
fi
exec > "$write_to"
fi
# Hash mode: the listed files, or standard input when none are given.
if [ -z "$files" ]; then
hash_line -
else
for f in $files; do
if [ ! -f "$f" ]; then
echo "hashtool: no such file: $f" >&2
exit 2
fi
hash_line "$f"
done
fi
Using it
Hash a single file, choose an algorithm, and read from a pipe.
sh hashtool.sh a.txt
sh hashtool.sh --algo sha1 b.txt
printf 'hello\n' | sh hashtool.sh
b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060 a.txt
bb596efe9e3023a502013767a0559a94a5eea4bc b.txt
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 -
Record the digests, check them while the files are untouched, then edit one and check again.
sh hashtool.sh --write SHA256SUMS a.txt b.txt
sh hashtool.sh --check SHA256SUMS
printf 'ALPHA\n' > a.txt
sh hashtool.sh --check SHA256SUMS
a.txt: OK
b.txt: OK
a.txt: FAILED
b.txt: OK
The first check reports both files unchanged. After a.txt is rewritten, the second check marks it FAILED and leaves b.txt as OK. One capital letter was enough to change every character of the digest.
Quick reference
| Command | What it does |
|---|---|
sh hashtool.sh FILE | Hashes a file with SHA-256 |
sh hashtool.sh --algo md5 FILE | Hashes with MD5 instead |
sh hashtool.sh --algo openssl FILE | Hashes with openssl dgst |
sh hashtool.sh A B | Hashes several files, one line each |
sh hashtool.sh --write SHA256SUMS A B | Saves the digests to a checksum file |
sh hashtool.sh --check SHA256SUMS | Checks each file and prints OK or FAILED |
printf 'text\n' | sh hashtool.sh | Hashes standard input |
sh hashtool.sh --help | Prints the usage message |
Next steps
- File integrity: noticing a change turns this workflow into a manifest you check on a schedule.
- Signing and verifying explains what a signature adds on top of a plain digest.
- Encoding and hashes covers base64 and the first look at what a hash is.
Test yourself on this
4 questions and 1 task where you run a real command, with the reasoning explained either way.