commit dbe4ce98153f6d8c3b76b6fb5b36eaf9e5119b11
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-06-26T13:17:20Z |
| subject | Added 18_kotp |
commit dbe4ce98153f6d8c3b76b6fb5b36eaf9e5119b11
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-06-26T13:17:20Z
Added 18_kotp
---
bash/sources/18_kotp | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 223 insertions(+)
diff --git a/bash/sources/18_kotp b/bash/sources/18_kotp
new file mode 100644
index 0000000..f01f500
--- /dev/null
+++ b/bash/sources/18_kotp
@@ -0,0 +1,223 @@
+#!/bin/bash
+
+declare -f kstore > /dev/null
+if [ $? -ne 0 ]; then
+ echo "kotp depends on kstore" >&2
+ return 1
+fi
+
+which oathtool > /dev/null 2>&1
+if [ $? -ne 0 ]; then
+ echo "oathtool is required" >&2
+ return 1
+fi
+
+_KOTP_PREFIX="${_KOTP_PREFIX:-otp}"
+_KOTP_PROP="${_KOTP_PROP:-secret}"
+
+function kotp {
+ case "$1" in
+ set) shift; _kotp-set "$@" ;;
+ gen|get) shift; _kotp-gen "$@" ;;
+ spread) shift; _kotp-spread "$@" ;;
+ list) shift; _kotp-list "$@" ;;
+ del|delete|rm) shift; _kotp-del "$@" ;;
+ *)
+ __func_head "set NAME [SECRET|-|file] set/update otp key"
+ __func_help "gen NAME [TIME] generate otp at TIME; defaults to now"
+ __func_help "spread NAME [COUNT] [TIME] generate surrounding otp codes; COUNT defaults to 1"
+ __func_help "list [TERM] list stored otp keys"
+ __func_help "del NAME delete otp key"
+ return 1
+ ;;
+ esac
+ return $?
+}
+
+function _kotp-key {
+ echo "${_KOTP_PREFIX}:$1"
+}
+
+function _kotp-clean-secret {
+ local s
+
+ if [ $# -gt 0 ]; then
+ s="$1"
+ else
+ s="$(cat)"
+ fi
+
+ case "$s" in
+ otpauth://*)
+ s="$(printf '%s' "$s" \
+ | sed -n 's/.*[?&]secret=\([^&]*\).*/\1/p')"
+ ;;
+ esac
+
+ printf '%s' "$s" \
+ | tr -d '[:space:]-' \
+ | tr '[:lower:]' '[:upper:]'
+}
+
+function _kotp-read-value {
+ if [ "$1" = "-" ]; then
+ cat
+ elif [ -n "$1" ] && [ -f "$1" ]; then
+ cat "$1"
+ else
+ printf '%s' "$1"
+ fi
+}
+
+function _kotp-set {
+ if [ -z "$1" ]; then
+ __func_head "NAME [SECRET|-|file]"
+ return 1
+ fi
+
+ local name src secret key
+ name="$1"
+ src="$2"
+
+ if [ -z "$src" ]; then
+ read -rsp "OTP secret: " src
+ echo
+ fi
+
+ secret="$(_kotp-read-value "$src" | _kotp-clean-secret)"
+ if [ -z "$secret" ]; then
+ echo "empty otp secret" >&2
+ return 1
+ fi
+
+ key="$(_kotp-key "$name")"
+
+ # Validate before storing. Most TOTP secrets are base32.
+ oathtool --totp --base32 "$secret" > /dev/null 2>&1
+ if [ $? -ne 0 ]; then
+ echo "invalid otp secret, or oathtool cannot parse it as base32" >&2
+ return 1
+ fi
+
+ kstore add "$key" "$secret" "$_KOTP_PROP" > /dev/null 2>&1
+ if [ $? -ne 0 ]; then
+ kstore update "$key" "$secret" "$_KOTP_PROP" || return 1
+ fi
+
+ echo "stored: $name"
+}
+
+function _kotp-secret {
+ if [ -z "$1" ]; then
+ __func_head "NAME"
+ return 1
+ fi
+
+ kstore get "$(_kotp-key "$1")" "$_KOTP_PROP"
+}
+
+function _kotp-gen {
+ if [ -z "$1" ]; then
+ __func_head "NAME [TIME]"
+ return 1
+ fi
+
+ local name when secret
+ name="$1"
+ when="$2"
+
+ secret="$(_kotp-secret "$name")" || return 1
+
+ if [ -n "$when" ]; then
+ oathtool --totp --base32 --now "$when" "$secret"
+ else
+ oathtool --totp --base32 "$secret"
+ fi
+}
+
+function _kotp-spread {
+ if [ -z "$1" ]; then
+ __func_head "NAME [COUNT] [TIME]"
+ return 1
+ fi
+
+ local name count when secret i offset label
+ name="$1"
+ count="${2:-1}"
+ when="$3"
+
+ if ! [[ "$count" =~ ^[0-9]+$ ]]; then
+ echo "COUNT must be a number" >&2
+ return 1
+ fi
+
+ secret="$(_kotp-secret "$name")" || return 1
+
+ for (( i=-count; i<=count; i++ )); do
+ offset=$(( i * 30 ))
+
+ if [ "$i" -eq 0 ]; then
+ label="now"
+ elif [ "$i" -lt 0 ]; then
+ label="${offset}s"
+ else
+ label="+${offset}s"
+ fi
+
+ if [ -n "$when" ]; then
+ printf '%8s ' "$label"
+ oathtool --totp --base32 --now "$when $offset seconds" "$secret"
+ else
+ printf '%8s ' "$label"
+ oathtool --totp --base32 --now "$offset seconds" "$secret"
+ fi
+ done
+}
+
+function _kotp-list {
+ local term="$1"
+
+ if [ -n "$term" ]; then
+ kstore search "${_KOTP_PREFIX}:$term" "$_KOTP_PROP"
+ else
+ kstore search "${_KOTP_PREFIX}:" "$_KOTP_PROP"
+ fi
+}
+
+function _kotp-del {
+ if [ -z "$1" ]; then
+ __func_head "NAME"
+ return 1
+ fi
+
+ kstore del "$(_kotp-key "$1")" "$_KOTP_PROP"
+}
+
+function _kotp {
+ local cur scope
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ scope="${COMP_WORDS[COMP_CWORD-1]}"
+
+ COMPREPLY=()
+
+ case "$scope" in
+ kotp)
+ COMPREPLY=( $(compgen -W "set gen get spread list del" -- "$cur") )
+ return
+ ;;
+ gen|get|spread|del|delete|rm)
+ COMPREPLY=( $(compgen -W "$(
+ kstore search "${_KOTP_PREFIX}:" "$_KOTP_PROP" 2>/dev/null \
+ | awk 'NR > 2 {print $1}' \
+ | sed "s/^${_KOTP_PREFIX}://"
+ )" -- "$cur") )
+ return
+ ;;
+ set)
+ COMPREPLY=( $(compgen -f -- "$cur") )
+ return
+ ;;
+ esac
+}
+
+complete -F _kotp kotp