-
Notifications
You must be signed in to change notification settings - Fork 0
/
maxigpg
1293 lines (1138 loc) · 42.4 KB
/
maxigpg
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
#"=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~="
# ==================================================
_APP_SPECIFIC_NAME="MaxiGPG"
_APP_VERSION="0.3.8"
_APP_STATUS="beta"
_APP_INFO="${_APP_SPECIFIC_NAME} is a bash script CLI extension to manage
GPG encryption key with extra features.
GPG is copyright by Free Software Foundation, Inc."
_APP_VERSION_STATUS="${_APP_VERSION}-${_APP_STATUS}"
_AUTHOR="Author: Arafat Ali | Email: arafat@sofibox.com | (C) 2019-2022"
# ====================================================
# start TODO list
# Pending todo
# end TODO list
# requirements
# This function is used to converts seconds into days, hours, minutes and seconds.
# Usage: convert_seconds [seconds]
# Example: convert_seconds 1000
convert_seconds() {
local _seconds=$1 _days _hours _minutes _seconds
((_days = _seconds / 60 / 60 / 24))
((_hours = _seconds / 60 / 60 % 24))
((_minutes = _seconds / 60 % 60))
((_seconds = _seconds % 60))
printf "%dd %dh %dm %ds" "${_days}" "${_hours}" "${_minutes}" "${_seconds}"
}
# This function is use to generate random string
# Usage: random_string [length]
# Example: random_string 10
random_string() {
local _length=$1 _string
_string=$(tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w "${_length}" | head -n 1)
echo "${_string}"
}
# This function is used to terminate a current running script with exit code.
# Usage: _exit [exit_code]
# Example: _exit 1
_exit() {
exit "$1"
}
# This function is used to return value of a current running script.
# Example: _return [return_value]
# Example: _return 1
_return() {
return "$1"
}
# This function is used to confirm the next action.
# Usage: _ask [message] [default-response]
# Example: _ask "Do you want to continue?" "y"
_ask() {
local text default_response
text=$1
default_response=$2
if [ -z "${default_response}" ]; then
default_response="Y"
fi
read -r -p "${text} [default:${default_response}] [Y/n]: " response
# Simulate a default response so that we can press Enter key
if [[ -z "${response}" ]]; then
response="${default_response}"
fi
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
return 0
else
echo "[${SCRIPT_NAME}->${FUNCNAME[0]}->cancel]: Operation aborted!"
exit 0
fi
}
# This function is used to display the main help message from readme file.
# Usage: readme [file]
# Example: readme /docs/README.md
readme() {
local readme_file="${1}"
if [ -f "${readme_file}" ]; then
cat "${readme_file}"
echo ""
else
echo "Error, the readme file ${readme_file} does not exist."
exit 1
fi
}
# This script is used to update script from a remote repository based on a given path
# Usage: update_script [script_path]
# Example: update_script /opt/maxigpg_public/
script_update() {
local caller script_path current_configs new_configs
script_path="${1}"
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
echo "[${caller}]: Updating ${SCRIPT_NAME} to latest version ..."
navigate_to "${script_path}"
echo ""
echo "START git update information:"
git fetch --all
status_handler check --code $?
git reset --hard origin/main
status_handler check --code $?
echo "END git update information:"
echo ""
navigate_to "PREVIOUS"
echo ""
echo "[${caller}]: Updating ${SCRIPT_NAME} configuration file ..."
# This will add a new config variable into the config file if it does not exist
# It will not disturb variable that already has value. So you do not need to reconfigure the script
# If config contain invalid value like having no equal sign, it will also replace them with a new variable from sample config
current_configs=$(grep -E '^[A-Za-z0-9_].+=.+$' "${script_path}/${SCRIPT_NAME}.conf" | awk -F "=" '{print $1}')
new_configs=$(grep -E '^[A-Za-z0-9_].+=.+$' "${script_path}/${SCRIPT_NAME}.conf.sample" | awk -F "=" '{print $1}')
for new_config in ${new_configs}; do
if [[ ${current_configs} =~ ${new_config} ]]; then
:
else
echo "Adding new config: ${new_config} into ${script_path}/${SCRIPT_NAME}.conf"
echo "${new_config}=\"\"" >>"${script_path}/${SCRIPT_NAME}.conf"
status_handler check --code $?
fi
done
# Remove blank lines, comments and sort config file
grep -E '^[A-Za-z0-9_].+=.+$' "${script_path}/${SCRIPT_NAME}.conf" | sort >"${script_path}/${SCRIPT_NAME}.conf_tmp"
mv "${script_path}/${SCRIPT_NAME}.conf_tmp" "${script_path}/${SCRIPT_NAME}.conf"
echo "[${caller}]: Running ${SCRIPT_NAME} -V ..."
chmod +x "${script_path}/${SCRIPT_NAME}"
${SCRIPT_NAME} -V
status_handler check --code $?
}
# This function is used to check for script update. It will also prompt user to update the script if there is a new version available.
# Usage: check_update
# Example: check_update
check_update() {
local caller temp_file
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
echo "[${caller}]: Checking ${SCRIPT_NAME} for update..."
temp_file="${TEMP_PATH}/${SCRIPT_NAME}"
# The github raw hosting will not be updated immediately after I push the update to github. Need to wait about 5 minutes
curl -H 'Cache-Control: no-cache' -so "${temp_file}" "https://raw.githubusercontent.com/sofibox/${SCRIPT_NAME}_public/main/${SCRIPT_NAME}"
status_handler check --code $?
available_version="$(awk -F'=' '/^_APP_VERSION=/{ print $2 }' "${temp_file}" | sed 's/\"//g')"
this_version="${_APP_VERSION}"
echo ""
echo "Installed version is: v${this_version}"
echo "Online version is: v${available_version}"
echo ""
if [[ "ok" == "$(echo | awk "(${available_version} > ${this_version}) { print \"ok\"; }")" ]]; then
echo "[${caller}]: A new version of ${SCRIPT_NAME} is available."
_ask "[${caller}->input]: Do you want to update ${SCRIPT_NAME} to version ${available_version}?" "y"
script_update "${SCRIPT_INSTALL_PATH}"
elif [[ "ok" == "$(echo | awk "(${available_version} == ${this_version}) { print \"ok\"; }")" ]]; then
echo "[${caller}]: You are using the latest version of ${SCRIPT_NAME}."
else
echo "[${caller}]: You are using a newer version of ${SCRIPT_NAME} than the latest available."
fi
rm -f "${temp_file}"
}
# This is a placeholder for manual function
manual() {
# You can add your own manual here
:
}
# This function is used to convert an integer to ordinal number
# Usage: ordinal [number]
# Example: ordinal 1
# Output: 1st
to_ordinal() {
local integer
integer="$1"
case "${integer}" in
*1[0-9] | *[04-9]) echo "${integer}"th ;;
*1) echo "${integer}"st ;;
*2) echo "${integer}"nd ;;
*3) echo "${integer}"rd ;;
esac
}
# This function is used to navigate to a given path and save the previous path to a variable
# You can use PREVIOUS to go back to the previous path
# Usage: navigate_to [path]
# Example: navigate_to /opt
# Example 2: navigate_to PREVIOUS
navigate_to() {
local to_path="$1"
if [[ "${to_path^^}" == +(PRE|PREVIOUS|OLDPATH|BACK|OLDPWD) ]]; then
# OLD_PATH holds a previous path (see global variable of PREPATH in this script)
echo "[${SCRIPT_NAME}]: Navigating out into ${OLD_PATH} ..."
to_path="${OLD_PATH}"
else
echo "[${SCRIPT_NAME}]: Navigating into ${to_path} ..."
OLD_PATH="${PWD}"
fi
cd "${to_path}" || {
echo "[${SCRIPT_NAME}]: Error, failed to enter into the directory ${to_path}"
_exit 1
}
}
# This function is used to check if given package is installed and install the package if it is not installed
# It will not do anything if the required package exist
# Usage: required_packages [package(s)]
# Example: required_packages curl wget git
# TODO transform this into maxibuild script (might not use anymore because other script also depends on this)
required_packages() {
local required_cmds="$*" detect_by missing_count install_success install_failed retval
((missing_count = install_success = install_failed = 0))
# Loop through the received arguments. Do not quote the array, we want split elements:
for required_cmd in ${required_cmds[*]}; do
# Using command to obtain installation status
command -v "${required_cmd}" >/dev/null
cmd_status="$?"
[[ "${cmd_status}" -eq 1 ]] && detect_by="cmd_status"
if [[ "${cmd_status}" -eq 1 ]]; then
((missing_count++))
echo ""
echo "[${SCRIPT_NAME}]: Warning, missing ${required_cmd} component detected by ${detect_by}"
# This prevents error if we never run apt-get update when the apt cache is expired
# When the first missing package is triggered, then we display warning message,
# and for this first time, we trigger update command to refresh the expired apt cache
if [ "${missing_count}" -eq 1 ]; then
echo ""
echo "[${SCRIPT_NAME}]: Running apt-get update for the first time ..."
apt-get -qq -y update
echo ""
fi
echo "[${SCRIPT_NAME}]: Installing the $(to_ordinal "${missing_count}") missing component: ${required_cmd} ..."
echo ""
if [ "${required_cmd}" == "mail" ]; then
_ask "Do you want to install mailutils?"
apt-get -y install mailutils
retval=$?
elif [ "${required_cmd}" == "ipcalc" ]; then
_ask "Do you want to install ipcalc and its components?"
echo "[${SCRIPT_NAME}]: Installing required packages to build ipcalc"
sudo apt-get -y install git gcc meson
echo "[${SCRIPT_NAME}]: Removing existing ipcalc ..."
apt-get -y remove ipcalc
rm -rf "/usr/local/bin/ipcalc"
rm -rf "/opt/ipcalc"
echo ""
mkdir -p /opt/ipcalc
echo "[${SCRIPT_NAME}]: Installing ipcalc from source https://github.com/sofibox/ipcalc.git ..."
git clone https://github.com/sofibox/ipcalc.git "/opt/ipcalc"
navigate_to "/opt/ipcalc"
meson setup build --buildtype=release
ninja -C build
echo ""
ln -s /opt/ipcalc/build/ipcalc /usr/local/bin
ipcalc --version
retval=$?
elif [ "${required_cmd}" == "packagecommand3" ]; then
# Do another type of installation method for this package command name example install from source
:
else
# Do apt installation
apt-get -y install "${required_cmd}"
retval=$?
fi
if [[ "${retval}" -eq 0 ]]; then
((install_success++))
else
((install_failed++))
fi
fi
done
if [ "${install_failed}" -gt 0 ]; then
echo ""
echo "Total missing component: ${missing_count}"
echo "Total success installed: ${install_success}"
echo "Total failed installed: ${install_failed}"
echo ""
fi
}
# This function is used to check return status of a command
# Usage: status_handler check --code=[return code] --print-success --error-msg=[error message] --success-msg=[success message]
# Example:
# status_handler check --code="$?" --error-msg="Error, failed to do something" --success-msg="Success, we did something" --print-success
status_handler() {
declare caller action retval
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
action="$1"
shift
while [[ ${1} ]]; do
case "${1}" in
-h | --help)
HELP="true"
;;
-v | --verbose)
VERBOSE="true"
;;
-s | --scripting)
SCRIPTING="true"
;;
--without-prompt)
WITHOUT_PROMPT="true"
;;
-c | --code)
code="$2"
shift 2
;;
--code=*)
code="${1#*=}"
shift
;;
--error-msg)
error_msg="$2"
shift 2
;;
--error-msg=*)
error_msg="${1#*=}"
shift
;;
--success-msg)
success_msg="$2"
shift 2
;;
--success-msg=*)
success_msg="${1#*=}"
shift
;;
--print-success)
print_success=true
shift
;;
*)
echo "[${caller}]: Error, unknown argument: ${1}"
_exit 1
;;
esac
done
if [[ "${action}" == "get-status-code" || "${action}" == "get-status" || "${action}" == "check-status" || "${action}" == "get" || "${action}" == "check" ]]; then
# If the variable code is set, this will take precedence over the others curl_code and http_code if error occurred
if [ -n "${code}" ]; then
if [ "${code}" -eq 0 ]; then
if [[ "${print_success}" == true && "${SCRIPTING}" == false ]]; then
if [ -n "${success_msg}" ]; then
echo "[${caller}->status]: Success, ${success_msg}"
else
echo "[${caller}->status]: [ Success ]"
fi
fi
[[ "${print_success}" == true && "${SCRIPTING}" == true ]] && echo "success"
_return 0
# Write more return code here to handle errors
# elif [ "${code}" -eq 1 ]; then
else
if [[ ${SCRIPTING} == "false" ]]; then
if [ -n "${error_msg}" ]; then
echo "[${caller}->status]: Error(${code}), ${error_msg}"
else
echo "[${caller}->status]: [ Error(${code}) ]"
fi
else
echo "error"
fi
_exit "${code}"
fi
fi
elif
[ "${action}" == "other_commands" ]
then
:
else
echo "[${caller}]: Error, invalid action for status_code function"
_exit 1
fi
}
# This function is used to convert a different form of boolean representation into a single boolean true or false
# Usage: convert_boolean [boolean_string]
# Example: convert_boolean "yes" -> true
# Example 2: convert_boolean "no" -> false
# Example 3: convert_boolean "1" -> true
convert_boolean_alias() {
local option data
option="$1"
data="$2"
if [[ "${data^^}" == +(TRUE|YES|ENABLE|1) ]]; then
echo "true"
_return 0
elif [[ "${data^^}" == +(FALSE|NO|DISABLE|0) ]]; then
echo "false"
_return 0
else
echo "[${caller}]: Error, invalid value given for option ${option}. Use --help to see the valid options"
_exit 1
fi
}
# This function is used to convert gpg key trust level from integer to string
# Usage: convert_gpg_trust_level [trust_level]
# Example: convert_gpg_trust_level 5 -> I trust ultimately
convert_gpg_trust_level() {
local caller retval level
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
level="$1"
if [ "${level}" == "1" ]; then
echo "1 = I don't know or won't say"
elif [ "${level}" == "2" ]; then
echo "2 = I do NOT trust ultimately"
elif [ "${level}" == "3" ]; then
echo "3 = I trust marginally"
elif [ "${level}" == "4" ]; then
echo "4 = I trust fully"
elif [ "${level}" == "5" ]; then
echo "5 = I trust ultimately"
else
echo "[${caller}]: Error, invalid level given. Use --help to see the valid options"
_exit 1
fi
}
# This function is used to show information about a given process name
# Usage: show_process_info [process_name]
# Example: show_process_info "gpg-agent"
show_process_info() {
local name pid_count
name="$1"
pid_count=0
for pid in $(pidof "${name}"); do
((pid_count++))
echo "---------------------------"
echo "--- (${pid_count}) gpg-agent PID: ${pid} ---"
echo "Process ID details:"
ps -p "${pid}" -o pid,vsz=MEMORY -o user,group=GROUP -o comm,args=ARGS
echo "Process TREE details:"
pstree -sang "${pid}"
echo ""
done
echo "---------------------------"
}
# This function is used to show gpg-agent service status including its process information
# Usage: show_gpg_agent_status
# Example: show_gpg_agent_status
show_gpg_agent_status() {
local caller
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
systemctl --user --no-pager status gpg-agent
status_code check --code "$?" --print-success
echo ""
echo "[${caller}->PID output]:"
show_process_info "gpg-agent"
echo ""
}
# This function is used to stop gpg-agent
# Usage: stop_gpg_agent
# Example: stop_gpg_agent
stop_gpg_agent() {
local caller
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
echo "[${caller}]: Stopping gpg-agent ..."
systemctl --user stop gpg-agent
status_handler check --code "$?" --print-success
}
# This function is used initialize GPG configuration for the current user
# Usage: init_gpg_config
# Example: init_gpg_config
init_gpg_config() {
local caller
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
if [ "${WITHOUT_PROMPT}" == "false" ]; then
_ask "[${caller}->input]: Warning, this will delete all existing GPG keys and configuration. Do you want to continue?" n
fi
echo "[${caller}]: Stopping gpg-agent ..."
systemctl --user stop gpg-agent
status_handler check --code "$?" --print-success
echo ""
# Removing gpg agent .gnupg directory
echo "[${caller}]: Removing gpg-agent .gnupg directory ..."
rm -rf "${GPG_HOME_DIR}"
status_handler check --code "$?" --print-success
echo ""
# Creating gpg agent .gnupg directory
echo "[${caller}]: Creating gpg-agent .gnupg directory ..."
mkdir -p "${GPG_HOME_DIR}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Giving permission to gpg-agent .gnupg directory ..."
chmod -v 700 "${GPG_HOME_DIR}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Writing a new GPG config at ${GPG_AGENT_CONFIG_FILE} ..."
{
echo "allow-preset-passphrase"
echo "default-cache-ttl 600"
echo "max-cache-ttl 7200"
} >"${GPG_AGENT_CONFIG_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Giving permission to gpg-agent config file ..."
chmod -v 600 "${GPG_AGENT_CONFIG_FILE}"
status_handler check --code "$?" --print-success
echo ""
# We only create this service if user is root
if [ "${EUID}" -eq 0 ]; then
echo "[${caller}]: Writing a new gpg-agent socket at ${GPG_AGENT_SOCKET_FILE} ..."
touch "${GPG_AGENT_SOCKET_FILE}"
{
echo "[Unit]"
echo "Description=GnuPG cryptographic agent and passphrase cache"
echo "Documentation=man:gpg-agent(1)"
# Must put this partOf if I want to stop gpg-agent, this will also automatically stop the socket
echo "PartOf=gpg-agent.service"
echo "[Socket]"
echo "ListenStream=%t/gnupg/S.gpg-agent"
echo "FileDescriptorName=std"
echo "SocketMode=0600"
echo "DirectoryMode=0700"
echo ""
echo "[Install]"
echo "WantedBy=sockets.target"
} >"${GPG_AGENT_SOCKET_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Changing the owner of the gpg-agent socket to ${USER} ..."
chown "${USER}":"${USER}" "${GPG_AGENT_SOCKET_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Changing the permission of the gpg-agent socket to 600 ..."
chmod 600 "${GPG_AGENT_SOCKET_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Writing a new gpg-agent service at ${GPG_AGENT_SERVICE_FILE} ..."
touch "${GPG_AGENT_SERVICE_FILE}"
{
echo "[Unit]"
echo "Description=GnuPG cryptographic agent and passphrase cache"
echo "Documentation=man:gpg-agent(1)"
echo "Requires=gpg-agent.socket"
echo ""
echo "[Service]"
echo "ExecStart=/usr/bin/gpg-agent --options ${GPG_AGENT_CONFIG_FILE} --supervised"
echo "ExecReload=/usr/bin/gpgconf --reload gpg-agent"
} >"${GPG_AGENT_SERVICE_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Changing the owner of the gpg-agent service to ${USER} ..."
chown "${USER}":"${USER}" "${GPG_AGENT_SERVICE_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Changing the permission of the gpg-agent service to 600 ..."
chmod 600 "${GPG_AGENT_SERVICE_FILE}"
status_handler check --code "$?" --print-success
echo ""
fi
echo "[${caller}]: Reloading systemd daemon ..."
systemctl --user daemon-reload
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Enabling gpg-agent socket ..."
systemctl --user --global enable gpg-agent.socket
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Starting gpg-agent service ..."
systemctl --user start gpg-agent.service
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Restoring ${SCRIPT_NAME} config file at ${CONFIG_FILE} ..."
cp "${CONFIG_FILE_SAMPLE}" "${CONFIG_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Loading the new config file ..."
source "${CONFIG_FILE}"
status_handler check --code "$?" --print-success
}
# This function is used to test GPG key
# Usage: test_gpg_key --duration 10 --email arafat@sofibox.com --passphrase "123456"
test_gpg_key() {
local caller test_file retval
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
test_file="${SECURE_PATH}/$(random_string 8)"
[[ -z "${_PRIMARY_KEY_TYPE}" ]] && _PRIMARY_KEY_TYPE="RSA"
[[ -z "${_PRIMARY_KEY_SIZE}" ]] && _PRIMARY_KEY_SIZE="4096"
[[ -z "${_SUB_KEY_TYPE}" ]] && _SUB_KEY_TYPE="RSA"
[[ -z "${_SUB_KEY_SIZE}" ]] && _SUB_KEY_SIZE="4096"
[[ -z "${_EXPIRE_DATE}" ]] && _EXPIRE_DATE="0"
[[ -z "${_KEY_OWNER_NAME}" ]] && _KEY_OWNER_NAME=$(getent passwd "${USER}" | cut -d: -f5 | cut -d, -f1)
# If user full name is blank, then use the current user
if [ -z "${_KEY_OWNER_NAME}" ]; then
_KEY_OWNER_NAME="${USER}"
fi
[[ -z "${_KEY_COMMENT}" ]] && _KEY_COMMENT="GPG key for ${USER}"
[[ -z "${_KEY_PASSPHRASE}" ]] && _KEY_PASSPHRASE=""
[[ -z "${_KEY_TRUST_LEVEL}" ]] && _KEY_TRUST_LEVEL="1"
[[ -z "${_TEST_KEY}" ]] && _TEST_KEY=false
[[ -z "${_GPG_CACHE_DURATION}" ]] && _GPG_CACHE_DURATION="10"
if [[ -z "${_KEY_OWNER_EMAIL}" ]]; then
echo "[${caller}]: The key owner email is empty, please use --email option to set it."
exit 1
fi
if [[ -z "${_KEY_PASSPHRASE}" ]]; then
echo "[${caller}]: The key passphrase is empty, please use --passphrase option to set it."
exit 1
fi
echo "[${caller}]: Testing the GPG key by encrypting a dummy file ..."
echo "This is a test file" >"${test_file}"
# We put --trust-model always to avoid the prompt for the passphrase
gpg --trust-model always --encrypt --recipient "${_KEY_OWNER_EMAIL}" "${test_file}"
rm -f "${test_file}.asc" && gpg --trust-model always --verbose -e -a -r "${_KEY_OWNER_EMAIL}" "${test_file}"
status_handler check --code="$?" --print-success
echo ""
echo "[${caller}]: Setting up GPG cache duration ..."
set_gpg_cache_duration "${_GPG_CACHE_DURATION}"
echo ""
echo "[${caller}]: Testing the GPG key by decrypting the dummy file ..."
rm -f "${test_file}" && gpg --pinentry-mode=loopback --passphrase "${_KEY_PASSPHRASE}" --output "${test_file}" --decrypt "${test_file}.asc" >/dev/null
retval="$?"
echo "[${caller}]: Testing the GPG cache duration ..."
if [ "${retval}" -eq 0 ]; then
local test_elapsed
echo "[${caller}]: OK, GPG decryption test is successful."
echo ""
echo "[${caller}]: Notice, the passphrase is cached in GPG agent with PID $(pidof gpg-agent)."
echo ""
test_elapsed=1
while :; do
rm -f "${test_file}"
echo "[${caller}]: Testing GPG decryption cache with value less than ${_GPG_CACHE_DURATION} seconds ..."
echo 123 | gpg -q --pinentry-mode=loopback --passphrase-fd 0 --output "${test_file}" -d "${test_file}.asc" 2>/dev/null
retval=$?
if [ "${retval}" -ne 0 ]; then
echo ""
echo "[${caller}]: OK, passphrase cache was expired based on the value of ${_GPG_CACHE_DURATION} seconds."
break
else
if [ "${test_elapsed}" -eq ${_GPG_CACHE_DURATION} ]; then
echo ""
echo "[${caller}]: Error, passphrase cache was not expired based on the value of ${_GPG_CACHE_DURATION} seconds."
echo "[${caller}]: Removing the test file ..."
rm -f "${test_file}" "${test_file}.gpg" "${test_file}.asc"
_exit 1
fi
fi
sleep 1
((test_elapsed++))
done
#Any files that were encrypted using the newly created cert above can be unlocked automatically without passphrase
# The cache passphrase has duration of ${_GPG_CACHE_DURATION} second(s) based on config file ${GPG_AGENT_CONFIG_FILE}
# The new passphrase will be cached in gpg-agent on PID $(systemctl --user show --property MainPID --value gpg-agent)"
# Note I can use API to kill this gpg process (such as da_api_admin; the service variable). So that the password will not be used by other app
echo "[${caller}]: OK, GPG key is ready to use."
echo "[${caller}]: Removing the test file ..."
rm -f "${test_file}" "${test_file}.gpg" "${test_file}.asc"
else
echo "[${caller}]: Error, GPG decryption test is failed."
echo "[${caller}]: Removing the test file ..."
rm -f "${test_file}" "${test_file}.gpg" "${test_file}.asc"
_exit 1
fi
}
# This function is used to import a GPG key
# Usage: import_gpg_key --key-file <key_file> --passphrase <passphrase> --email <email>
# Example: import_gpg_key --key-file "${HOME}/.gnupg/keys/secret.asc" --passphrase "my_passphrase"
# If the key is encrypted, it will unlock it with the given passphrase
# To completely trust the key, use the option --trust-key
# To test the imported key, use the option --test-key
# You also need to pass the key owner email address using the option --email
import_gpg_key() {
local caller
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
[[ -z "${_KEY_PASSPHRASE}" ]] && _KEY_PASSPHRASE=""
[[ -z "${_KEY_TRUST_LEVEL}" ]] && _KEY_TRUST_LEVEL="1"
[[ -z "${_TEST_KEY}" ]] && _TEST_KEY=false
# Check if given file is exist
if [ ! -f "${_GPG_KEY_FILE}" ]; then
echo "[${caller}]: Error, GPG key file ${_GPG_KEY_FILE} is not exist. Please use --key-file option to specify the valid key file."
_exit 1
fi
# Check if the key owner email is given
if [ -z "${_KEY_OWNER_EMAIL}" ]; then
echo "[${caller}]: Error, GPG key owner email is not given. Please use --email option to specify the valid key owner email."
_exit 1
fi
echo "[${caller}]: Importing a GPG key from ${_GPG_KEY_FILE} ..."
if [ -n "${_KEY_PASSPHRASE}" ]; then
echo "[${caller}]: Unlocking GPG key ${_GPG_KEY_FILE}"
rm -f "${_GPG_KEY_FILE}_decrypted" && gpg --pinentry-mode=loopback --passphrase "${_KEY_PASSPHRASE}" -d -o "${_GPG_KEY_FILE}_decrypted" "${_GPG_KEY_FILE}"
status_handler check --code "$?" --print-success
echo ""
_GPG_KEY_FILE="${_GPG_KEY_FILE}_decrypted"
else
echo "[${caller}]: Error, please use --passphrase options to specify the passphrase of the key file."
_exit 1
fi
echo "[${caller}]: Importing GPG key ${_GPG_KEY_FILE} ..."
gpg --pinentry-mode=loopback --passphrase "${_KEY_PASSPHRASE}" --import "${_GPG_KEY_FILE}"
status_handler check --code "$?" --print-success
echo ""
# Remove the decrypted key file
echo "[${caller}]: Removing the decrypted key file ..."
rm -f "${_GPG_KEY_FILE}_decrypted"
if [ "${_TRUST_GPG_KEY}" == "true" ]; then
echo "[${caller}]: Setting up key trust level to level ${_KEY_TRUST_LEVEL} [ $(convert_gpg_trust_level "${_KEY_TRUST_LEVEL}") ] ..."
echo ""
echo -e "${_KEY_TRUST_LEVEL}\ny" | gpg --command-fd 0 --expert --edit-key "${_KEY_OWNER_EMAIL}" trust
status_handler check --code="$?" --print-success
echo ""
fi
if [ "${_TEST_KEY}" == "true" ]; then
echo "[${caller}]: Testing GPG key ..."
echo ""
test_gpg_key --duration 10 --email "${_KEY_OWNER_EMAIL}" --passphrase "${_KEY_PASSPHRASE}"
fi
}
# This function is used to set gpg cache duration in config file
# Usage: set_gpg_cache_duration --duration <duration>
set_gpg_cache_duration() {
local caller retval
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
if [ -z "${_GPG_CACHE_DURATION}" ]; then
echo "[${caller}]: Error, please use --duration option to specify the duration of the passphrase cache."
_exit 1
fi
echo "[${caller}]: Configuring GPG config to cache passphrase for ${_GPG_CACHE_DURATION} seconds ..."
{
echo "allow-preset-passphrase"
echo "default-cache-ttl ${_GPG_CACHE_DURATION}"
echo "max-cache-ttl ${_GPG_CACHE_DURATION}"
} >"${GPG_AGENT_CONFIG_FILE}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Stopping GPG agent ..."
systemctl --user stop gpg-agent
status_handler check --code "$?" --print-success
echo "[${caller}]: Starting GPG agent ..."
systemctl --user start gpg-agent
status_handler check --code "$?" --print-success
}
# This function is used to create a new GPG key from a template file
# Usage: create_gpg_key --key-type <key_type> --key-size <key_size> --expire-date <expire_date> --key-owner-name <key_owner_name>
# --key-owner-email <key_owner_email> --key-comment <key_comment> --key-passphrase <key_passphrase> --key-trust-level <key_trust_level>
# Example: create-key --key-type "RSA" --key-size "4096" --expire-date "0" --key-owner-name "John Doe" --key-owner-email " --passphrase "123456" --key-trust-level "1"
# Required argument: --passphrase <key_passphrase>
create_gpg_key() {
local caller retval
local template_file success_code
template_file="${SECURE_PATH}/$(random_string 8)"
success_code=$(random_string 8)
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
[[ -z "${_PRIMARY_KEY_TYPE}" ]] && _PRIMARY_KEY_TYPE="RSA"
[[ -z "${_PRIMARY_KEY_SIZE}" ]] && _PRIMARY_KEY_SIZE="4096"
[[ -z "${_SUB_KEY_TYPE}" ]] && _SUB_KEY_TYPE="RSA"
[[ -z "${_SUB_KEY_SIZE}" ]] && _SUB_KEY_SIZE="4096"
[[ -z "${_EXPIRE_DATE}" ]] && _EXPIRE_DATE="0"
[[ -z "${_KEY_OWNER_NAME}" ]] && _KEY_OWNER_NAME=$(getent passwd "${USER}" | cut -d: -f5 | cut -d, -f1)
# If user full name is blank, then use the current user
if [ -z "${_KEY_OWNER_NAME}" ]; then
_KEY_OWNER_NAME="${USER}"
fi
[[ -z "${_KEY_OWNER_EMAIL}" ]] && _KEY_OWNER_EMAIL="${USER}@$(hostname)"
[[ -z "${_KEY_COMMENT}" ]] && _KEY_COMMENT="GPG key for ${USER}"
[[ -z "${_KEY_PASSPHRASE}" ]] && _KEY_PASSPHRASE=""
[[ -z "${_KEY_TRUST_LEVEL}" ]] && _KEY_TRUST_LEVEL="1"
[[ -z "${_TEST_KEY}" ]] && _TEST_KEY=false
if [ -z "${_KEY_PASSPHRASE}" ]; then
echo "[${caller}]: Error, GPG passphrase is not set. Please set it with --passphrase"
_exit 1
fi
if ! [ -d "${GPG_HOME_DIR}" ]; then
echo "[${caller}]: Warning, GPG home directory ${GPG_HOME_DIR} does not exist. Calling init_gpg_config ..."
init_gpg_config --without-prompt
status_handler check --code "$?" --print-success
echo ""
fi
echo "[${caller}]: Creating a new GPG key template file ..."
touch "${template_file}"
{
echo "%echo Generating a basic OpenPGP key ..."
echo "Key-Type: ${_PRIMARY_KEY_TYPE}"
echo "Key-Length: ${_PRIMARY_KEY_SIZE}"
echo "Subkey-Type: ${_SUB_KEY_TYPE}"
echo "Subkey-Length: ${_SUB_KEY_SIZE}"
echo "Name-Real: ${_KEY_OWNER_NAME}"
echo "Name-Comment: ${_KEY_COMMENT}"
echo "Name-Email: ${_KEY_OWNER_EMAIL}"
echo "Expire-Date: ${_EXPIRE_DATE}"
echo "Passphrase: ${_KEY_PASSPHRASE}"
echo "%commit"
echo "%echo done"
echo "%echo success code: ${success_code}"
} >"${template_file}"
cat "${template_file}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Generating a new GPG key based on the template file ..."
test_commit=$(gpg --verbose --batch --generate-key "${template_file}" <<<"${_KEY_PASSPHRASE}" 2>&1)
# If the key generation is successful, the output will contain the same success code from the template file
has_success=$(echo "${test_commit}" | grep "success code: ${success_code}")
echo "${test_commit}"
if [[ ${retval} -eq 0 && -n "${has_success}" ]]; then
echo "[${caller}]: Success, generated a new GPG key"
else
echo "[${caller}]: Error, failed to generate a new GPG key"
_exit 1
fi
echo "[${caller}]: Removing the GPG key template file ..."
rm -f "${template_file}"
status_handler check --code "$?" --print-success
echo ""
echo "[${caller}]: Setting up key trust level for ${_KEY_OWNER_EMAIL} to level ${_KEY_TRUST_LEVEL} [ $(convert_gpg_trust_level "${_KEY_TRUST_LEVEL}") ] ..."
echo ""
echo -e "${_KEY_TRUST_LEVEL}\ny" | gpg --command-fd 0 --expert --edit-key "${_KEY_OWNER_EMAIL}" trust
status_handler check --code="$?" --print-success
echo ""
if [[ "${_TEST_KEY}" == "true" ]]; then
local test_file
test_file="${SECURE_PATH}/$(random_string 8)"
echo "[${caller}]: Testing the GPG key by encrypting a dummy file ..."
echo "This is a test file" >"${test_file}"
# We put --trust-model always to avoid the prompt for the passphrase
gpg --trust-model always --encrypt --recipient "${_KEY_OWNER_EMAIL}" "${test_file}"
rm -f "${test_file}.asc" && gpg --trust-model always --verbose -e -a -r "${_KEY_OWNER_EMAIL}" "${test_file}"
status_handler check --code="$?" --print-success
echo ""
echo "[${caller}]: Testing the GPG key by decrypting the dummy file ..."
rm -f "${test_file}" && gpg --pinentry-mode=loopback --passphrase "${_KEY_PASSPHRASE}" --output "${test_file}" --decrypt "${test_file}.asc" >/dev/null
status_handler check --code="$?" --print-success
echo ""
# Testing GPG cache duration
echo "[${caller}]: Testing GPG cache duration ..."
# TODO test GPG cache duration
echo "[${caller}]: Removing the dummy file ..."
# rm -f "${test_file}" "${test_file}.gpg"
fi
}
# This function is used to display whether the GPG key cached is valid or not
# Usage: check_gpg_key_validity
check_gpg_key_validity() {
local caller is_valid
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
is_valid=$(echo "1234" | gpg -q --pinentry-mode=loopback --status-fd 1 --sign --local-user "${_KEY_ID}" --passphrase-fd 0 2>&1 >/dev/null)
retval=$?
if [[ ${retval} -eq 0 ]]; then
echo "[${caller}]: Success, GPG passphrase is valid"
else
echo "[${caller}]: Error, GPG passphrase is not valid"
echo "${is_valid}"
fi
}
# This function is used to set GPG passphrase for the current user based on the GPG key ID
# If the passphrase is already set it will not ask for the new passphrase
# Usage: set_gpg_passphrase --key-id <key_id>
set_gpg_passphrase() {
local caller retval
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
if [ -z "${_KEY_ID}" ]; then
echo "[${caller}]: Error, GPG key ID is not set. Please set it with --key-id"
_exit 1
fi
# Clear previous passphrase
# echo "[${caller}]: Restarting gpg-agent to clear previous passphrase ..."
# systemctl --user restart gpg-agent.service
# status_handler check --code "$?" --print-success
# echo ""
echo "[${caller}]: Keying in passphrase for GPG key ID ${_KEY_ID} ..."
echo "1234" | gpg -q --pinentry-mode=loopback --status-fd 1 --sign --local-user "${_KEY_ID}" >/dev/null
retval=$?
if [[ ${retval} -eq 0 ]]; then
check_gpg_key_validity
else
echo "[${caller}]: Error, GPG cache is not valid"
_exit 1
fi
echo ""
}
# This function is used to clear GPG passphrase
# Usage: clear_gpg_passphrase
clear_gpg_passphrase() {
caller="${SCRIPT_NAME}->${FUNCNAME[0]}"
echo "[${caller}]: Clearing passphrase by restarting gpg-agent ..."
systemctl --user restart gpg-agent.service
status_handler check --code "$?" --print-success
}
############################
# MAIN FUNCTION START HERE #
############################
# This script was tested on Debian 11 (Bullseye)
declare SCRIPT_PATH SCRIPT_NAME SCRIPT_INSTALL_PATH SHORT_OPT_SPECS INDEX ACTION ARGNUM RETVAL
declare -A LONG_OPT_SPECS