This week’s script uses Leopard’s new xattr tool to
store MD5 checksum information alongside any file you wish. Later,
you can run the same script to ensure that the checksum has not
changed.
For example, I run it on my Applications directory like this:
find /Applications -print0 | sudo xargs -0 verify
The first time it’s run, verify will print a lot of
warnings about how none of the files currently have checksum
information. But the second time you run it, it silently verifies
that none of the files have changed. If there are changes, an error
is printed to standard error, and an exit code of 1 is returned.
It’s like a poor man’s Tripwire, but no
external databases or configuration files are necessary, since it
stores the checksum data in an extended attribute called
“checksum”. This means that if you later move the file (even to
another HFS+ volume), the verify script will still
verify the current contents against the initial checksum.
If the file has legitimately changed, you must first delete the old checksum:
xattr -d checksum FILE
I recommend using this script on archival data, where changes
are never expected. I created this script because I recently
discovered some data corruption in a set of ISO files, and found to
my dismay that ISO offers no built-in scheme to determine which
files had been corrupted and which hadn’t, forcing me to throw out
almost the entire lot. Now I use verify to ensure that
from this point on, the ISO never changes.
On a security note: This scheme offers no security, unlike systems like Tripwire. A hacker who has permissions to change the file will also have permissions to reset the extended attribute containing the checksum.
Here’s the bash script:
#!/bin/bash
# verify, version 1.0
# by John Wiegley <johnw@newartisans.com>
if [[ -z "$1" ]]; then
echo "usage: verify [OPTIONS] <FILE ...>"
echo "options:"
echo " -v be verbose about verification/checksum setting"
echo " -s only set checksums for files which don't have them"
echo " -f force setting the checksum even if file doesn't match"
exit 1
fi
ATTRNAME=checksum
verbose=false
setonly=false
force=false
while [[ "${1:0:1}" == "-" ]]; do
if [[ "$1" == "-v" ]]; then
verbose=true
shift 1
elif [[ "$1" == "-s" ]]; then
setonly=true
shift 1
elif [[ "$1" == "-f" ]]; then
force=true
shift 1
else
break
fi
done
error=false
for file in "$@"; do
name=$(basename "$file")
if [[ -f "$file" && "$name" != ".DS_Store" && \
"$name" != ".localized" ]]; then
CHKSUM=$(xattr -p $ATTRNAME "$file" 2> /dev/null)
if [[ -z "$CHKSUM" ]]; then
if [[ $verbose == true || $setonly == true ]]; then
echo "Note: No existing checksum for $file, setting..."
fi
CURSUM=$(md5 -q "$file")
xattr -w $ATTRNAME $CURSUM "$file"
CHKSUM=$CURSUM
elif [[ $setonly == false ]]; then
CURSUM=$(md5 -q "$file")
if [[ $CURSUM != $CHKSUM ]]; then
echo "Error: Checksum mismatch for $file: $CURSUM != $CHKSUM" \
> /dev/stderr
error=true
if [[ $force == true ]]; then
xattr -w $ATTRNAME $CURSUM "$file"
fi
elif [[ $verbose == true ]]; then
echo "Verified: $CHKSUM $file"
fi
fi
fi
done
[[ $error == true ]] && exit 1
exit 0