penguin/utils

my env utils

commit dbe4ce98153f6d8c3b76b6fb5b36eaf9e5119b11

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-26T13:17:20Z
subjectAdded 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