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