summaryrefslogtreecommitdiffstats
path: root/debian/initramfs/cryptroot-unlock
blob: dbc2ad046c4024f72ed59fa9feef8289e9868012 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#!/bin/busybox ash

# Remotely unlock encrypted volumes.
#
# Copyright © 2015-2018 Guilhem Moulin <guilhem@debian.org>
#
# 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 program 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 <http://www.gnu.org/licenses/>.

set -ue
PATH=/sbin:/bin

TIMEOUT=10
PASSFIFO=/lib/cryptsetup/passfifo
ASKPASS=/lib/cryptsetup/askpass
UNLOCK_ALL=n

[ -f /lib/cryptsetup/functions ] || return 0
. /lib/cryptsetup/functions
TABFILE="/cryptroot/crypttab"
unset -v IFS

if [ ! -f "$TABFILE" ] || [ "$TABFILE" -ot "/proc/1" ]; then
	# Too early, init-top/cryptroot hasn't finished yet
	echo "Try again later" >&2
	exit 1
fi

# Print the list of PIDs the executed command of which is $exe.
pgrep_exe() {
	local exe pid
	exe="$(readlink -f -- "$1" 2>/dev/null)" && [ -f "$exe" ] || return 0
	ps -eo pid= | while read pid; do
		[ "$(readlink -f "/proc/$pid/exe")" != "$exe" ] || printf '%d\n' "$pid"
	done
}

# Return 0 if $pid has a file descriptor pointing to $name, and 1
# otherwise.
in_fds() {
	local pid="$1" name fd
	name="$(readlink -f -- "$2" 2>/dev/null)" && [ -e "$name" ] || return 1
	for fd in $(find "/proc/$pid/fd" -type l); do
		[ "$(readlink -f "$fd")" != "$name" ] || return 0
	done
	return 1
}

# Print the PID of the askpass process with a file descriptor opened to
# /lib/cryptsetup/passfifo.
get_askpass_pid() {
	local pid
	for pid in $(pgrep_exe "$ASKPASS"); do
		if in_fds "$pid" "$PASSFIFO"; then
			echo "$pid"
			return 0
		fi
	done
	return 1
}

# Print the number of configured crypt devices that have not been unlocked yet.
count_locked_devices() {
	local COUNT=0
	crypttab_foreach_entry count_locked_devices_callback
	printf '%d\n' "$COUNT"
}
count_locked_devices_callback() {
	dm_blkdevname "$CRYPTTAB_NAME" >/dev/null || COUNT=$(( $COUNT + 1 ))
}

# Wait for askpass, then set $PID (resp. $BIRTH) to the PID (resp.
# birth date) of the cryptsetup process with same $CRYPTTAB_NAME.
wait_for_prompt() {
	local pid timer num_locked_devices=-1 n

	# wait for the fifo
	while :; do
		n=$(count_locked_devices)
		if [ $n -eq 0 ]; then
			# all configured devices have been unlocked, we're done
			exit 0
		elif [ $num_locked_devices -lt 0 ] || [ $n -lt $num_locked_devices ]; then
			# reset $timer if a device was unlocked (for instance using
			# a keyscript) while we were waiting
			timer=$(( 10 * $TIMEOUT ))
		fi
		num_locked_devices=$n

		if pid=$(get_askpass_pid) && [ -p "$PASSFIFO" ]; then
			break
		fi

		usleep 100000
		timer=$(( $timer - 1 ))
		if [ $timer -le 0 ]; then
			echo "Error: Timeout reached while waiting for askpass." >&2
			exit 1
		fi
	done

	# find the cryptsetup process with same $CRYPTTAB_NAME
	local o v
	for o in NAME TRIED OPTION_tries; do
		if v="$(grep -z -m1 "^CRYPTTAB_$o=" "/proc/$pid/environ")"; then
			eval "CRYPTTAB_$o"="\${v#CRYPTTAB_$o=}"
		else
			eval unset -v "CRYPTTAB_$o"
		fi
	done
	if [ -z "${CRYPTTAB_NAME:+x}" ] || [ -z "${CRYPTTAB_TRIED:+x}" ]; then
		return 1
	fi
	if ( ! crypttab_find_entry --quiet "$CRYPTTAB_NAME" ); then
		# use a subshell to avoid polluting our enironment
		echo "Error: Refusing to process unknown device $CRYPTTAB_NAME" >&2
		exit 1
	fi

	for pid in $(pgrep_exe "/sbin/cryptsetup"); do
		if grep -Fxqz "CRYPTTAB_NAME=$CRYPTTAB_NAME" "/proc/$pid/environ"; then
			PID=$pid
			BIRTH=$(stat -c"%Z" "/proc/$PID" 2>/dev/null) || break
			return 0
		fi
	done

	PID=
	BIRTH=
	return 1
}

# Wait until $PID no longer exists or has a birth date greater that
# $BIRTH (ie was reallocated).  Then return with exit value 0 if
# /dev/mapper/$CRYPTTAB_NAME exists, and with exit value 1 if the
# maximum number of tries exceeded.  Otherwise (if the unlocking
# failed), return with value 1.
wait_for_answer() {
	local timer=$(( 10 * $TIMEOUT )) b
	while [ -d "/proc/$PID" ] && b=$(stat -c"%Z" "/proc/$PID" 2>/dev/null) && [ $b -le $BIRTH ]; do
		usleep 100000
		timer=$(( $timer - 1 ))
		if [ $timer -le 0 ]; then
			echo "Error: Timeout reached while waiting for PID $PID." >&2
			exit 1
		fi
	done

	if dm_blkdevname "$CRYPTTAB_NAME" >/dev/null; then
		echo "cryptsetup: $CRYPTTAB_NAME set up successfully" >&2
		[ "$UNLOCK_ALL" = y ] && return 0 || exit 0
	elif [ $(( ${CRYPTTAB_TRIED:-0} + 1 )) -ge ${CRYPTTAB_OPTION_tries:-3} ] &&
			[ ${CRYPTTAB_OPTION_tries:-3} -gt 0 ]; then
		echo "cryptsetup: maximum number of tries exceeded for $CRYPTTAB_NAME" >&2
		exit 1
	else
		echo "cryptsetup: cryptsetup failed, bad password or options?" >&2
		return 1
	fi
}


if [ -t 0 ] && [ -x "$ASKPASS" ]; then
	# interactive mode on a TTY: keep trying until all configured devices have
	# been unlocked or the maximum number of tries exceeded
	UNLOCK_ALL=y
	while :; do
		# note: if the script is not killed before pivot_root it should
		# exit on its own once $TIMEOUT is reached
		if ! wait_for_prompt; then
			usleep 100000
			continue
		fi
		read -rs -p "Please unlock disk $CRYPTTAB_NAME: "; echo
		printf '%s' "$REPLY" >"$PASSFIFO"
		wait_for_answer || true
	done
else
	# non-interactive mode: slurp the passphrase from stdin and exit
	wait_for_prompt || exit 1
	echo "Please unlock disk $CRYPTTAB_NAME"
	cat >"$PASSFIFO"
	wait_for_answer || exit 1
fi

# vim: set filetype=sh :