diff options
-rw-r--r-- | Makefile | 25 | ||||
-rw-r--r-- | README.md | 72 | ||||
-rwxr-xr-x | askpass | 58 | ||||
-rwxr-xr-x | check | 266 | ||||
-rw-r--r-- | crypt.c | 163 | ||||
-rwxr-xr-x | hooks/cryptsetup-nuke-password | 33 |
6 files changed, 617 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e1d8c0d --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CFLAGS += -Wall -O2 +LDLIBS += -lcrypt + +EXECUTABLES = crypt + +all: $(EXECUTABLES) + +check: + ./check + +clean: + rm -f *.o + rm -f $(EXECUTABLES) + +install: $(EXECUTABLES) + mkdir -p $(DESTDIR)/lib/cryptsetup + cp askpass $(DESTDIR)/lib/cryptsetup/ + + mkdir -p $(DESTDIR)/usr/share/initramfs-tools/hooks/ + cp hooks/* $(DESTDIR)/usr/share/initramfs-tools/hooks/ + + mkdir -p $(DESTDIR)/usr/lib/cryptsetup-nuke-password + cp crypt $(DESTDIR)/usr/lib/cryptsetup-nuke-password/ + +.PHONY: check clean install diff --git a/README.md b/README.md new file mode 100644 index 0000000..b452530 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# cryptsetup-nuke-password + +Installing this package lets you configure a special "nuke password" that +can be used to destroy the encryption keys required to unlock the encrypted +partitions. This password can be entered in the usual early-boot prompt +asking the passphrase to unlock the encrypted partition(s). + +This provides a relatively stealth way to make your data unreadable in +case you fear that your computer is going to be seized. + +## How can I configure the nuke password + +After having installed the package, just run “dpkg-reconfigure +cryptsetup-nuke-password”. Behind the scene, this creates +`/etc/cryptsetup-nuke-password/password_hash` with +the output of `echo your-password | +/usr/lib/cryptsetup-nuke-password/crypt --generate` and rebuilds +the initramfs (`update-initramfs -u`). + +## How does it work? + +The packages diverts /lib/cryptsetup/askpass by our own script that will +call the original tool to get the password but that will do its own +treatment before outputting the same password to stdout... + +To identify the partition being unlocked, we rely on the environment +variables exported by +/usr/share/initramfs-tools/scripts/local-top/cryptroot + +To destroy the encryption keys, we call `cryptsetup erase <device>`. + +## How to configure the nuke password non-interactively + +You need to preseed the password in the debconf database and then +run dpkg-reconfigure in a way where it is not allowed to ask question: +``` +$ sudo debconf-set-selections <<END +cryptsetup-nuke-password cryptsetup-nuke-password/password string Th3Pa$$w0rd +cryptsetup-nuke-password cryptsetup-nuke-password/password-again string Th3Pa$$w0rd +END +$ sudo dpkg-reconfigure -f noninteractive cryptsetup-nuke-password +``` + +## Backing up the encryption key + +If you expect to make use of this nuke password, then you should consider +backing up the luks header. That way, if your computer is seized, and then +later given back to you, you will be able to restore the luks headers and +get back access to your data. + +Here's the command to use (replace `<device>` with the path of the device +file representing your luks encrypted partition, and `<your-backup-file>` +with the path to the backup file to create): +``` +$ sudo cryptsetup luksHeaderBackup <device> --header-backup-file <your-backup-file> +``` + +To later restore the header, you will have to do: +``` +$ sudo cryptsetup luksHeaderRestore <device> --header-backup-file <your-backup-file> +``` + +## Limitations + +Due to the way this feature is implemented, it can't be used if you +configure cryptsetup to use something else than an interactive password +to unlock the encryption keys. + +It is also not usable on a live image (unless you configured the nuke +password with debconf preseeding at the time you generated the live image) +since it needs to regenerate the initrd to embed itself and the nuke +password. @@ -0,0 +1,58 @@ +#!/bin/sh + +DIVERTED_ASKPASS=${DIVERTED_ASKPASS:-/lib/cryptsetup/askpass.cryptsetup} +NUKE_PASSWORD_HASH_PATH=${NUKE_PASSWORD_HASH_PATH:-/etc/cryptsetup-nuke-password/password_hash} +CRYPT_HELPER=${CRYPT_HELPER:-/usr/lib/cryptsetup-nuke-password/crypt} + +sanity_checks() { + local cryptsetup="$(which cryptsetup 2>/dev/null)" + if [ -z "$cryptsetup" ]; then + echo "$0: WARNING: cryptsetup not found in PATH" >&2 + return 1 + fi + if [ ! -e "$CRYPTTAB_SOURCE" ]; then + echo "$0: WARNING: \$CRYPTTAB_SOURCE (value: $CRYPTTAB_SOURCE) does not exist" >&2 + return 1 + fi + if [ ! -x "$CRYPT_HELPER" ]; then + echo "$0: WARNING: $CRYPT_HELPER is not executable" >&2 + return 1 + fi + return 0 +} + +hash_is_matching() { + local pass="$1" + local pass_hash + + if [ ! -r $NUKE_PASSWORD_HASH_PATH ]; then + # No hash, no match + return 1 + fi + pass_hash=$(cat $NUKE_PASSWORD_HASH_PATH) + if echo -n "$pass" | $CRYPT_HELPER --check "$pass_hash"; then + # User typed the nuke password! + return 0 + else + return 1 + fi +} + +nuke_cryptsetup_partition() { + local partition="$1" + cryptsetup --batch-mode erase "$partition" +} + +if [ ! -x "$DIVERTED_ASKPASS" ]; then + echo "ERROR: $DIVERTED_ASKPASS is not available/executable" >&2 + exit 1 +fi + +PASSWORD=$($DIVERTED_ASKPASS "$1") + +if sanity_checks && hash_is_matching "$PASSWORD"; then + nuke_cryptsetup_partition "$CRYPTTAB_SOURCE" +fi + +# Forward the password +echo -n "$PASSWORD" @@ -0,0 +1,266 @@ +#!/bin/sh + +CRYPT=${CRYPT:-./crypt} +ASKPASS=${ASKPASS:-./askpass} + +oneTimeSetUp() { + export OUTDIR="$SHUNIT_TMPDIR/out" + export BINDIR="$SHUNIT_TMPDIR/bin" + export PATH="$BINDIR:$PATH" + + # Directory for output data (dropped after each test) + mkdir -p $OUTDIR + + # Test helper scripts + mkdir -p $BINDIR + cat >$BINDIR/askpass <<END +#!/bin/sh +echo -n "\${FAKE_ASKPASS_ANSWER:-foobar}" +END + cat >$BINDIR/cryptsetup <<END +#!/bin/sh +echo "\$@" >>$OUTDIR/cryptsetup +END + chmod 755 $BINDIR/* +} + +setUp() { + # Overrides for askpass + export DIVERTED_ASKPASS="$BINDIR/askpass" + export CRYPT_HELPER="$CRYPT" + export NUKE_PASSWORD_HASH_PATH="$SHUNIT_TMPDIR/password_hash" + export CRYPTTAB_SOURCE="$SHUNIT_TMPDIR/device" + touch $CRYPTTAB_SOURCE + + # Clean up some environment variables that might be set by tests + unset FAKE_ASKPASS_ANSWER +} + +tearDown() { + if [ -d "$OUTDIR" ]; then + rm -f $OUTDIR/* + fi + rm -f $NUKE_PASSWORD_HASH_PATH +} + +testCryptNoArgsOutput() { + output=$($CRYPT 2>&1 </dev/null) + if ! echo "$output" | grep -q "Usage:"; then + fail 'Does not print Usage output without argument' + fi +} +testCryptNoArgsExitCode() { + $CRYPT >/dev/null 2>&1 </dev/null + assertEquals 'Bad exit code when ran without argument' 1 $? +} + +testCryptBadArgOutput() { + output=$($CRYPT --doesnotexist 2>&1 </dev/null) + if ! echo "$output" | grep -q "Usage:"; then + fail 'Does not print Usage output with invalid argument' + fi +} +testCryptBadArgExitCode() { + $CRYPT --doesnotexist >/dev/null 2>&1 </dev/null + assertEquals 'Bad exit code when ran with invalid argument' 1 $? +} + +testCryptHelpOutput() { + output=$($CRYPT --help 2>&1 </dev/null) + if ! echo "$output" | grep -q "Usage:"; then + fail 'Does not print Usage output with --help' + fi +} +testCryptHelpExitCode() { + $CRYPT --help >/dev/null 2>&1 </dev/null + assertEquals 'Bad exit code when ran with --help' 0 $? +} + +testCryptGenerateEmptyPassword() { + output_stdout=$($CRYPT --generate 'h/' 2>/dev/null </dev/null) + output_stderr=$($CRYPT --generate 'h/' 2>&1 >/dev/null </dev/null) + exit_code=$? + assertNull "'crypt --generate </dev/null' unexpectedly generated something" "$output_stdout" + if ! echo "$output_stderr" | grep -q "ERROR:"; then + fail "'crypt --generate </dev/null' did not print any error message" + fi + assertEquals "'crypt --generate </dev/null' has a bad exit code" 1 $exit_code +} + +do_testCryptGenerate() { + output=$(echo "foobar" | $CRYPT --generate $1) + exit_code=$? + assertNotNull "'echo foobar | crypt --generate $1' ($2) provided no output" "$output" + assertEquals "'echo foobar | crypt --generate $1' ($2) returned bad exit code" 0 $exit_code +} + +testCryptGenerateBasic() { + do_testCryptGenerate 'h/' 'DES' +} +testCryptGenerateMD5() { + do_testCryptGenerate '$1$h/$' 'MD5' +} +testCryptGenerateSHA256() { + do_testCryptGenerate '$5$h/$' 'SHA-256' +} +testCryptGenerateSHA512() { + do_testCryptGenerate '$6$h/$' 'SHA-512' +} + +testCryptGenerateBadSalt() { + output_stdout=$(echo foobar | $CRYPT --generate '$999$foobar$' 2>/dev/null) + output_stderr=$(echo foobar | $CRYPT --generate '$999$foobar$' 2>&1 >/dev/null) + exit_code=$? + assertNull "'echo foobar | crypt --generate <bad-salt>' unexpectedly generated something" "$output_stdout" + if ! echo "$output_stderr" | grep -q "ERROR:"; then + fail "'echo foobar | crypt --generate <bad-salt>' did not print any error message" + fi + assertEquals "'echo foobar | crypt --generate <bad-salt>' has a bad exit code" 1 $exit_code +} + +testCryptGenerateNoSaltSupplied() { + output_stdout=$(echo foobar | $CRYPT --generate 2>/dev/null) + output_stderr=$(echo foobar | $CRYPT --generate 2>&1 >/dev/null) + exit_code=$? + assertNull "'echo foobar | crypt --generate' generated noise on stderr" "$output_stderr" + if ! echo "$output_stdout" | grep -q '^\$6\$'; then + fail "'echo foobar | crypt --generate' did not generate a SHA-512 based hash ($output_stdout)" + fi + assertEquals "'echo foobar | crypt --generate' has a bad exit code" 0 $exit_code +} + +testCryptGenerateEmptySaltSupplied() { + output_stdout=$(echo foobar | $CRYPT --generate '' 2>/dev/null) + output_stderr=$(echo foobar | $CRYPT --generate '' 2>&1 >/dev/null) + exit_code=$? + assertNull "'echo foobar | crypt --generate ''' generated noise on stderr" "$output_stderr" + if ! echo "$output_stdout" | grep -q '^\$6\$'; then + fail "'echo foobar | crypt --generate ''' did not generate a SHA-512 based hash ($output_stdout)" + fi + assertEquals "'echo foobar | crypt --generate ''' has a bad exit code" 0 $exit_code +} + +testCryptGenerateNoSaltRandomness() { + output1=$(echo foobar | $CRYPT --generate 2>/dev/null) + output2=$(echo foobar | $CRYPT --generate 2>/dev/null) + salt1="$(echo $output1 | cut -d$ -f1-3)"'$' + salt2="$(echo $output2 | cut -d$ -f1-3)"'$' + assertNotEquals "Two consecutive runs of 'echo foobar | $CRYPT --generate' generated the same salt" "$output1" "$output2" + output3=$(echo foobar | $CRYPT --generate "$salt1") + assertEquals "'echo foobar | $CRYPT --generate <salt>' did not recreate the original hash" "$output1" "$output3" +} + +testCryptCheckEmptyPassword() { + output_stdout=$($CRYPT --check 'h/GdiFWQsXxA.' 2>/dev/null </dev/null) + output_stderr=$($CRYPT --check 'h/GdiFWQsXxA.' 2>&1 >/dev/null </dev/null) + exit_code=$? + assertNull "'crypt --check <hash> </dev/null' unexpectedly generated something" "$output_stdout" + if ! echo "$output_stderr" | grep -q "ERROR:"; then + fail "'crypt --check <hash> </dev/null' did not print any error message" + fi + assertEquals "'crypt --check <hash> </dev/null' has a bad exit code" 1 $exit_code +} + +testCryptCheckNoHashSupplied() { + output_stdout=$(echo "foobar" | $CRYPT --check 2>/dev/null) + output_stderr=$(echo "foobar" | $CRYPT --check 2>&1 >/dev/null) + if ! echo "$output_stderr" | grep -q "ERROR:"; then + fail "'echo foobar | crypt --check' did not print any error message" + fi + assertNull "'echo foobar | crypt --check' unexpectly returned something on stdout" "$output_stdout" +} + +testCryptCheckGoodPassword() { + output=$(echo "foobar" | $CRYPT --check '$6$dkcZzIkv$Ju7XCIc4igWvht3bOu266vvRam6IdnIFxoyonDt.6JZl8NfCaukACeIRYVW7WQtrUtqN2TrWSgEFnXumuTiN41' 2>&1) + exit_code=$? + assertNull "'echo foobar | $CRYPT --check <good-hash>' printed unexpected output" "$output" + assertEquals "'echo foobar | $CRYPT --check <good-hash>' did not exit with" 0 $exit_code +} + +testCryptCheckBadPassword() { + output=$(echo "foobar-bad" | $CRYPT --check '$6$dkcZzIkv$Ju7XCIc4igWvht3bOu266vvRam6IdnIFxoyonDt.6JZl8NfCaukACeIRYVW7WQtrUtqN2TrWSgEFnXumuTiN41' 2>&1) + exit_code=$? + assertNull "'echo foobar | $CRYPT --check <bad-hash>' printed unexpected output" "$output" + assertEquals "'echo foobar | $CRYPT --check <bad-hash>' did not exit with" 1 $exit_code +} + +testCryptGenerateCheckRoundtrip() { + for salt in '' 'h/' '$1$abcd$' '$5$12345678$' '$6$deadbeef$' + do + password="haX0rd3ad" + + password_hash=$(echo $password | $CRYPT --generate "$salt") + exit_code=$? + assertEquals "'echo $password | $CRYPT --generate $salt' did not exit with" 0 $exit_code + + echo $password | $CRYPT --check "$password_hash" + exit_code=$? + assertEquals "'echo $password | $CRYPT --check $password_hash' did not exit with" 0 $exit_code + done +} + +testAskPassWarnsAboutMissingCrypttabSource() { + export CRYPTTAB_SOURCE=/does/not/exist + + $ASKPASS >$OUTDIR/log 2>&1 + + if ! grep -q 'WARNING: $CRYPTTAB_SOURCE' $OUTDIR/log; then + fail "askpass should complain of missing CRYPTTAB_SOURCE" + fi +} + +testAskPassWarnsAboutMissingCryptHelper() { + export CRYPT_HELPER=/does/not/exist + + $ASKPASS >$OUTDIR/log 2>&1 + + if ! grep -q "WARNING: $CRYPT_HELPER" $OUTDIR/log; then + fail "askpass should complain of missing \$CRYPT_HELPER" + fi +} + +testAskPassCallsCryptsetupErase() { + # Setup the password and its matching hash + export FAKE_ASKPASS_ANSWER="foobar" + echo '$6$dkcZzIkv$Ju7XCIc4igWvht3bOu266vvRam6IdnIFxoyonDt.6JZl8NfCaukACeIRYVW7WQtrUtqN2TrWSgEFnXumuTiN41' >$NUKE_PASSWORD_HASH_PATH + + $ASKPASS >$OUTDIR/log 2>&1 + + touch $OUTDIR/cryptsetup + if ! grep -q "erase $CRYPTTAB_SOURCE" $OUTDIR/cryptsetup; then + echo "Output of askpass:" + cat $OUTDIR/log + echo "" + echo "Cryptsetup log:" + cat $OUTDIR/cryptsetup + fail "cryptsetup erase has not been called by askpass" + fi +} + +testAskPassWithoutPasswordHash() { + # No password_hash is created + + $ASKPASS >$OUTDIR/log 2>&1 + + assertFalse 'cryptsetup was unexpectly run' "[ -e $OUTDIR/cryptsetup ]" +} + +testAskPassWithNonMatchingPasswordHash() { + # Setup the password and a non-matching hash + export FAKE_ASKPASS_ANSWER="this-is-not-the-good-password" + echo '$6$dkcZzIkv$Ju7XCIc4igWvht3bOu266vvRam6IdnIFxoyonDt.6JZl8NfCaukACeIRYVW7WQtrUtqN2TrWSgEFnXumuTiN41' >$NUKE_PASSWORD_HASH_PATH + + $ASKPASS >$OUTDIR/log 2>&1 + + assertFalse 'cryptsetup was unexpectly run' "[ -e $OUTDIR/cryptsetup ]" +} + +testAskPassReturnsPassword() { + export FAKE_ASKPASS_ANSWER="my-password" + + OUT=$($ASKPASS 2>/dev/null) + + assertEquals "askpass did not print the password" "$FAKE_ASKPASS_ANSWER" "$OUT" +} + +. shunit2 @@ -0,0 +1,163 @@ +/* + * Copyright 2019 Offensive Security + * Copyright 2019 Raphaël Hertzog <raphael@offensive-security.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <sys/random.h> +#include <crypt.h> + +void +usage(const char* name, int exitcode) +{ + fprintf(stderr, "Usage: %s <command>\n", name); + fprintf(stderr, "Valid commands:\n"); + fprintf(stderr, " --help: display this output\n"); + fprintf(stderr, " --generate <salt>: read password from stdin and print hashed version\n"); + fprintf(stderr, " --check <hash>: read password from stdin and check it against supplied hash\n"); + fprintf(stderr, " Exits with error 0 when password matched, 1 otherwise.\n"); + exit(exitcode); +} + +void +read_password(char *buffer, int bufsize) +{ + int size = -1, total = 0, i = 0; + + while ((size != 0) && (total < bufsize - 1)) { + size = read(0, buffer+total, bufsize - total); + if (size != -1) { + total += size; + } + } + buffer[total+1] = '\0'; + for(i = 0; i < total; i++) { + if ((buffer[i] == '\n') || (buffer[i] == '\r')) { + buffer[i] = '\0'; + break; + } + } + if (strlen(buffer) == 0) { + fprintf(stderr, "ERROR: no password supplied"); + exit(1); + } +} + +char* +gen_salt() +{ + static const char alphanum[] = "123456789abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + static char salt[16]; + int i, index; + unsigned int seed; + ssize_t ret; + + ret = getrandom(&seed, sizeof(seed), 0); + if (ret != sizeof(seed)) { + fprintf(stderr, "ERROR: failed to get random data\n"); + exit(2); + } + srandom(seed); + + salt[0] = '$'; + salt[1] = '6'; + salt[2] = '$'; + for (i = 3; i < 11; i++) { + index = random() % (sizeof(alphanum) - 1); + salt[i] = alphanum[index]; + } + salt[11] = '$'; + salt[12] = '\0'; + return salt; +} + +char* +extract_salt(char* hash) +{ + static char salt[24]; + + if (hash[0] != '$') { + /* Traditional crypt hash with 2 characters */ + salt[0] = hash[0]; + salt[1] = hash[1]; + salt[2] = '\0'; + } else { + /* Hash delimited by $ characters */ + char *p, *end; + int i; + + p = hash; + end = strrchr(hash, '$'); + for(i = 0; p <= end && i < sizeof(salt) - 1; i++) { + salt[i] = *p; + p++; + } + salt[i] = '\0'; + } + return salt; +} + +int +main (int argc, char* argv[]) +{ + static char buffer[256]; + char *crypted, *salt; + + if (argc == 1) { + usage(argv[0], 1); + } + + if (strcmp(argv[1], "--help") == 0) { + usage(argv[0], 0); + } else if (strcmp(argv[1], "--generate") == 0) { + read_password(buffer, sizeof(buffer)); + salt = (argc > 2 && strlen(argv[2])) ? argv[2] : gen_salt(); + crypted = crypt(buffer, salt); + if (crypted == NULL || ( + strlen(crypted) < 13 && + strncmp(crypted, "*", 1) == 0 && + strcmp(crypted, salt) != 0 + )) { + fprintf(stderr, "ERROR: crypt() failed to return anything with salt '%s'", salt); + exit(1); + } + printf("%s\n", crypted); + } else if (strcmp(argv[1], "--check") == 0) { + read_password(buffer, sizeof(buffer)); + if (argc < 3) { + fprintf(stderr, "ERROR: missing <hash> argument after --check"); + exit(1); + } + salt = extract_salt(argv[2]); + crypted = crypt(buffer, salt); + if (crypted == NULL) + exit(1); + if (strcmp(crypted, argv[2]) == 0) + exit(0); + else + exit(1); + } else { + usage(argv[0], 1); + } + + return 0; +} + diff --git a/hooks/cryptsetup-nuke-password b/hooks/cryptsetup-nuke-password new file mode 100755 index 0000000..d350966 --- /dev/null +++ b/hooks/cryptsetup-nuke-password @@ -0,0 +1,33 @@ +#!/bin/sh + +set -e + +PREREQ="cryptroot" + +prereqs() +{ + echo "$PREREQ" +} + +case "$1" in + prereqs) + prereqs + exit 0 + ;; +esac + +. /usr/share/initramfs-tools/hook-functions + +# Copy the file that we diverted away and that does the real work +if [ -e /lib/cryptsetup/askpass.cryptsetup ]; then + copy_exec /lib/cryptsetup/askpass.cryptsetup +fi + +# Copy the password hash to recognize the nuke password +if [ -e /etc/cryptsetup-nuke-password/password_hash ]; then + copy_file conffile /etc/cryptsetup-nuke-password/password_hash +fi + +copy_exec /usr/lib/cryptsetup-nuke-password/crypt + +exit 0 |