*.deb extraction utility (also *.dsc by wrapping “dpkg-source -x”)
[shellsnippets/shellsnippets.git] / bash-ksh / generate-pgpkey-for-at-home
1 #!/bin/sh
2 # $Id: genkey-privatgebrauch.sh 3857 2013-11-04 12:26:56Z tglase $
3 #-
4 # Copyright © 2010, 2011, 2013
5 #       Thorsten Glaser <t.glaser@tarent.de>
6 # All rights reserved.
7 #
8 # Licenced under the GNU AGPLv3.
9 #-
10 # Generate a new GnuPG (PGP) key, for private use.
11
12 # check if we're called with mksh or bash; fix if not
13 if test -z "$shell_tried"; then
14         if test -z "$KSH_VERSION"; then
15                 if mksh -c true >/dev/null 2>&1; then
16                         shell_tried=1
17                         export shell_tried
18                         exec mksh "$0" "$@"
19                 fi
20                 if test -z "$BASH_VERSION"; then
21                         if bash -c true >/dev/null 2>&1; then
22                                 shell_tried=1
23                                 export shell_tried
24                                 exec bash "$0" "$@"
25                         fi
26                 fi
27         fi
28 fi
29 if test -z "$KSH_VERSION$BASH_VERSION"; then
30         echo >&2 "I've tried but couldn't find mksh or GNU bash."
31         echo >&2 "Please call me with one of these shells."
32         exit 1
33 fi
34 unset shell_tried
35
36 # set up some basic environment
37 export LC_ALL=C
38 unset LANG LANGUAGE
39 test -z "$BASH_VERSION" || shopt -s extglob
40 # we can now use Korn Shell extensions common to mksh and GNU bash
41 unset GPG_AGENT_INFO GPG_TTY
42 nl='
43 '
44 if ! tilde=$(cd && pwd) || [[ -z $tilde ]]; then
45         tilde=$HOME
46         if ! tilde=$(cd && pwd) || [[ -z $tilde ]]; then
47                 echo >&2 Eek, what\'s your home directory?
48                 exit 1
49         fi
50 fi
51
52 # initiate logging
53 cd "$(dirname "$0")"
54 log="$(basename "$0").log"
55 cat >>"$log" <<-EOF
56
57         New key generation started (Privatgebrauch)
58         $(date)
59         ===========================================
60 EOF
61 test -z "$KSH_VERSION" || echo ksh >>"$log"
62 test -z "$BASH_VERSION" || echo bash >>"$log"
63
64 # check for existence of prerequisite tools
65 for tool in gpg wget; do
66         $tool --version >/dev/null 2>&1 && continue
67         echo >&2 You must install $tool to continue.
68         exit 1
69 done
70
71 # subroutine for converting a string into an array
72 # taking into account Korn Shell vs GNU bash syntax
73 str2arr() {
74         local _a _b _s _vn=$1
75
76         eval _s=\$$_vn
77         if [[ -n $KSH_VERSION ]]; then
78                 _a="set -A $_vn -- "
79                 _b=
80         else
81                 _a="${_vn}=("
82                 _b=")"
83         fi
84         eval $_a$_s$_b
85 }
86
87 # subroutines for converting array elements into hex,
88 # printing with escapes honoured/ignored
89 # taking into account Korn Shell vs GNU bash syntax
90 if [[ -n $KSH_VERSION ]]; then
91         alias arr2hex='typeset -i16 '
92         alias eprint='print -n'
93         alias nprint='print -nr -- '
94 else
95         arr2hex() {
96                 local _vn=$1 _i _n _v
97
98                 _i=0
99                 eval _n='${#'$_vn'[*]}'
100                 while (( _i < _n )); do
101                         eval _v='${'$_vn'[_i]}'
102                         _v=$(printf '16#%x' $_v)
103                         eval $_vn'[_i++]=$_v'
104                 done
105         }
106         eprint() {
107                 printf "$@"
108         }
109         nprint() {
110                 printf '%s' "$*"
111         }
112 fi
113
114 ### BEGIN imported code {{{
115 # Copyright (c) 2008
116 #       Thorsten Glaser <tg@mirbsd.org>
117 #
118 # Provided that these terms and disclaimer and all copyright notices
119 # are retained or reproduced in an accompanying document, permission
120 # is granted to deal in this work without restriction, including un-
121 # limited rights to use, publicly perform, distribute, sell, modify,
122 # merge, give away, or sublicence.
123 #
124 # This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to
125 # the utmost extent permitted by applicable law, neither express nor
126 # implied; without malicious intent or gross negligence. In no event
127 # may a licensor, author or contributor be held liable for indirect,
128 # direct, other damage, loss, or other issues arising in any way out
129 # of dealing in the work, even if advised of the possibility of such
130 # damage or existence of a defect, except proven that it results out
131 # of said person's immediate fault when using the work as intended.
132
133 # read a password without echoing
134 askpass() {
135         set -o noglob
136         stty -echo
137         echo -n "$1 "
138         read resp
139         stty echo
140         set +o noglob
141         echo
142 }
143
144 # convert a string from UTF-8 or ISO-8859-1 to UTF-8
145 str2utf8() {
146         local _s="$*" _z _c _i _hv _wc _n
147
148         _c=$(nprint "$_s" | hexdump -ve '1/1 "16#%x "')
149         _c="$_c 0"
150         str2arr _c
151         _s=
152         _z=0
153         _i=0
154         while (( _c[_i] )); do
155                 (( _hv = _c[_i] ))
156                 if (( (_hv < 16#C2) || (_hv >= 16#F0) )); then
157                         _n=1
158                 elif (( _hv < 16#E0 )); then
159                         _n=2
160                 else
161                         _n=3
162                 fi
163                 if (( _n > 1 )); then
164                         (( (_c[_i + 1] & 16#C0) == 16#80 )) || _n=1
165                         (( _hv == 16#E0 )) && \
166                             (( _c[_i + 1] < 16#A0 )) && _n=1
167                 fi
168                 if (( _n > 2 )); then
169                         (( (_c[_i + 2] & 16#C0) == 16#80 )) || _n=1
170                         (( _hv == 16#EF && _c[_i + 1] == 16#EF && \
171                             _c[_i + 2] > 16#BD )) && _n=1
172                 fi
173                 case $_n in
174                 (1)
175                         if (( (_wc = _c[_i]) < 16#80 )); then
176                                 (( _s[_z++] = _wc ))
177                         else
178                                 (( _s[_z++] = 16#C0 | (_wc >> 6) ))
179                                 (( _s[_z++] = 16#80 | (_wc & 16#3F) ))
180                         fi
181                         ;;
182                 (2)
183                         (( _s[_z++] = _c[_i] ))
184                         (( _s[_z++] = _c[_i + 1] ))
185                         ;;
186                 (3)
187                         (( _s[_z++] = _c[_i] ))
188                         (( _s[_z++] = _c[_i + 1] ))
189                         (( _s[_z++] = _c[_i + 2] ))
190                         ;;
191                 esac
192                 (( _i += _n ))
193         done
194         arr2hex _s
195         eprint "$(echo ${_s[*]} | sed -e 's/16#/\\x/g' -e 's/ //g')"
196 }
197
198 ### END imported code }}}
199
200 # create a temporary directory in /dev/shm (Linux tmpfs) or /tmp (otherwise)
201 if [[ ! -d /dev/shm/. ]] || ! T=$(mktemp -d /dev/shm/genkey.XXXXXXXXXX); then
202         if ! T=$(mktemp -d /tmp/genkey.XXXXXXXXXX); then
203                 echo >&2 Cannot create temporary directory.
204                 exit 1
205         fi
206 fi
207
208 cleanup() {
209         trap - 0 1 2 3 13 15
210         # files to overwrite before removing
211         for wipefiles in resp wgetrc; do
212                 for x in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16; do
213                         echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
214                 done >"$T/$wipefiles"
215         done
216         sync
217         rm -rf "$T"
218         exit $1
219 }
220
221 # make sure the temporary files are removed if we are interrupted
222 trap "cleanup 1" 1 2 3 13 15
223 trap "cleanup 0" 0
224
225
226 # gpg2, as opposed to gnupg, doesn’t want to not use the agent
227 vsn=$(gpg --version 2>&1 | head -n 1)
228 isgpg2=0
229 if [[ $vsn = 'gpg (GnuPG) 1.4.'* ]]; then
230         : okay
231 elif [[ $vsn = 'gpg (GnuPG) 1.'* ]]; then
232         echo 'WARNING: Obsolete GnuPG 1.x version, you SHALL update!'
233 elif [[ $vsn = 'gpg (GnuPG) 2.0.'* ]]; then
234         isgpg2=1
235 else
236         echo "WARNING: Unknown GnuPG version '$vsn', tell tglase!"
237 fi
238
239
240 # request and record user/pass
241 echo -n "Vor- und Zuname: "
242 read i_name
243 [[ -n $i_name ]] || cleanup 0
244 echo -n "eMail-Adresse: "
245 read i_mail
246 echo -n "Kommentarfeld: "
247 read i_comm
248 echo "Generiere für: '$i_name${i_comm:+ "($i_comm)"}${i_mail:+ "<$i_mail>"}' (^C wenn falsch)"
249 askpass "Password:"
250 [[ -n $resp ]] || cleanup 0
251 pw1=$resp
252 askpass "Password (nochmal):"
253 [[ -n $resp ]] || cleanup 0
254 if [[ $pw1 != $resp ]]; then
255         echo Sind nicht gleich.
256         cleanup 1
257 fi
258
259 echo "userdata: $i_name ($i_comm) <$i_mail>" >>"$log"
260
261 # add entropy from CGIs to that pool (magic code ;) {{{
262 if [[ ! -s ${tilde}/.gnupg/random_seed ]]; then
263         # create and fill if it didn't exist
264         :>"${tilde}"/.gnupg/random_seed
265         chmod 0600 "${tilde}"/.gnupg/random_seed
266         dd if=/dev/urandom of="${tilde}"/.gnupg/random_seed bs=600 count=1 2>/dev/null
267 fi
268 entropy=$(wget -O - -T 10 --no-check-certificate \
269     https://call.mirbsd.org/lb.cgi?genkey.sh,1=$(hostname -f 2>/dev/null || hostname),seed=$RANDOM$RANDOM$RANDOM$RANDOM$RANDOM 2>/dev/null | \
270     hexdump -ve '1/1 "16#%x "')$x
271 poolfile=$(hexdump -ve '1/1 "16#%x "' <"${tilde}"/.gnupg/random_seed)
272 str2arr entropy
273 str2arr poolfile
274 (( n = ${#poolfile[*]} < ${#entropy[*]} ? ${#entropy[*]} : ${#poolfile[*]} ))
275 i=0
276 # XOR poolfile with new entropy (from CGIs)
277 while (( i < n )); do
278         (( poolfile[i % ${#poolfile[*]}] ^= entropy[i % ${#entropy[*]}] ))
279         let i++
280 done
281 # write back into the pool file
282 arr2hex poolfile
283 eprint "$(echo ${poolfile[*]} | sed -e 's/16#/\\x/g' -e 's/ //g')" | \
284     dd of="${tilde}"/.gnupg/random_seed conv=notrunc 2>/dev/null
285 # }}} end of magic code block ;)
286
287 # create response file for gpg
288 # NOTE: Key-Length can go up to 8192 (not more!) but please no lower
289 #       than 2048 (although more than 4096 may be incompatible *AND*
290 #       TERRIBLY SLOW)
291 cat >"$T/resp" <<-EOF
292         %echo Generating the key for $i_name ($i_mail) $i_comm
293         Key-Type: RSA
294         Key-Length: 3072
295         Key-Usage: auth,encrypt,sign
296         Passphrase: $(str2utf8 "$resp")
297         Name-Real: $(str2utf8 "$i_name")
298         ${i_comm:+Name-Comment: $(str2utf8 "$i_comm")}
299         ${i_mail:+Name-Email: $(str2utf8 "$i_mail")}
300         Expire-Date: 3y
301         Preferences: H8 H3 S8 S4 Z2 Z0 H9 H10 S9 S7
302         Keyserver: hkp://pgp.uni-mainz.de
303         %commit
304         %echo done
305 EOF
306
307 # really generate the key
308 echo
309 (gpg --no-use-agent --batch --gen-key "$T/resp"; echo $? >"$T/rc") 2>&1 | \
310     tee "$T/gen.out"
311 echo
312 (echo "create key {"; sed 's/^/ /' <"$T/gen.out"; echo "}") >>"$log"
313 # check for error exit
314 if (( $(<"$T/rc") > 0 )); then
315         echo >&2 Key generation failed.
316         cleanup 1
317 fi
318 # scan the gpg log for keyid of keypair just created
319 pkid=$(sed -n \
320     's/^gpg: key \([0-9A-F]*\) marked as ultimately trusted.*$/\1/p' \
321     "$T/gen.out")
322 if [[ $pkid != +([0-9A-F]) ]] || ! gpg -K $pkid; then
323         echo >&2 '┌─────────────────────────────────────────────────────────┐'
324         echo >&2 '│ Finding the key failed. YOU CAN USE THE KEY, BUT YOU    │'
325         echo >&2 '│ *MUST* CONTACT THE ADMINS with this error message.      │'
326         echo >&2 '│ Kann den neuen Schlüssel nicht finden. DU KANNST DIESES │'
327         echo >&2 '│ SCHLÜSSELPAAR BENUTZEN, ABER DU *MUẞT* DIE ADMINS mit   │'
328         echo >&2 '│ dieser Fehlernachricht KONTAKTIEREN.                    │'
329         echo >&2 '└─────────────────────────────────────────────────────────┘'
330         echo
331         echo >&2 Cannot find the key just generated.
332         cleanup 1
333 fi
334
335 # apply preference settings to our newly generated key
336 if [[ $isgpg2 = 0 ]]; then
337         usepw="$nl$(str2utf8 "$resp")"
338 else
339         usepw=""
340 fi
341 gpg --no-use-agent -q -u $pkid --command-fd 4 --edit-key $pkid \
342     >>"$T/edit.log" 2>&1 4<<-EOF
343         notation preferred-email-encoding@pgp.com=partitioned,pgpmime$usepw
344         trust
345         5
346         y
347         save
348 EOF
349 echo "=> $?" >>"$T/edit.log"
350
351         cat >&2 <<'EOF'
352 ┌─────────────────────────────────────────┐
353 │ Key generation finished with no errors. │
354 │ Schlüsselerzeugung fehlerfrei erledigt. │
355 └─────────────────────────────────────────┘
356
357 ╔═════════════════════════════════════════════════════════════════════╗
358 ║ You are responsible for backing up your PGP secret key BY YOURSELF! ║
359 ║ Du mußt SELBER für Sicherungskopiën des privaten Schlüssels sorgen! ║
360 ╚═════════════════════════════════════════════════════════════════════╝
361 EOF
362 (echo "finished:"; gpg -k $pkid | sed 's/^/|    /'; echo) >>"$log"
363 echo >&2
364 gpg -k $pkid || echo gpg ERROR -k: $?
365 cleanup 0