penguin/utils

my env utils

commit 173ed891428abd11609e9dc41cb6c9a6320bef70

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-27T10:48:52Z
subjectLinux can now use gpg agent for kstore secret unlock
commit 173ed891428abd11609e9dc41cb6c9a6320bef70
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-06-27T10:48:52Z

    Linux can now use gpg agent for kstore secret unlock
---
 bash/sources/15_gpg-agent |   3 +
 bash/sources/16_keystore  | 206 +++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 207 insertions(+), 2 deletions(-)

diff --git a/bash/sources/15_gpg-agent b/bash/sources/15_gpg-agent
index 181759d..6ff695a 100644
--- a/bash/sources/15_gpg-agent
+++ b/bash/sources/15_gpg-agent
@@ -21,6 +21,7 @@ function _rbash_enable_gpg_ssh_support {
 }
 
 _rbash_enable_gpg_ssh_support
+_rbash_gpg_enabled=
 
 if command -v gpg-connect-agent >/dev/null 2>&1; then
 	export GPG_TTY
@@ -30,6 +31,7 @@ if command -v gpg-connect-agent >/dev/null 2>&1; then
 
 	if [ -n "$GPG_TTY" ]; then
 		gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 || true
+		_rbash_gpg_enabled=yes
 	fi
 fi
 
@@ -44,5 +46,6 @@ then
 		export SSH_AUTH_SOCK="$_rbash_gpg_ssh_sock"
 	fi
 
+	_rbash_gpg_enabled=yes
 	unset _rbash_gpg_ssh_sock
 fi
diff --git a/bash/sources/16_keystore b/bash/sources/16_keystore
index d53014e..f96dba9 100644
--- a/bash/sources/16_keystore
+++ b/bash/sources/16_keystore
@@ -17,6 +17,8 @@ _AUTH_SECRET=
 _KSTORE_DEF_PROP=${_KSTORE_DEF_PROP:-val}
 _KSTORE_DEC_FAILED="$RBASH_HOME/.decypt-failed"
 _KSTORE_TABLE=store
+_KSTORE_GPG_RCPT="penguin@rbash.io"
+
 : "${_KSTORE_OPENSSL_ITER:=1000000}"
 
 function kstore {
@@ -184,8 +186,208 @@ function _kstoresecret-auto {
 	case $OSTYPE in
 		darwin*) _kstoresecret-macos "$@" ;;
 		cygwin) _kstoresecret-cygwin "$@" ;;
+		linux*) _kstoresecret-linux "$@" ;;
+		*)
+			echo "$OSTYPE is not supported yet" >&2
+			;;
+	esac
+}
+
+function _kstoresecret-linux-ensure-gpg-key {
+	local _GNUPGHOME _FPR
+
+	if ! command -v gpg >/dev/null 2>&1; then
+		echo "gpg is required" >&2
+		return 1
+	fi
+
+	_GNUPGHOME="${GNUPGHOME:-$HOME/.gnupg}"
+
+	mkdir -p "$_GNUPGHOME" || return 1
+	chmod 700 "$_GNUPGHOME" 2>/dev/null || true
+
+	if [ -n "${_KSTORE_GPG_RCPT:-}" ]; then
+		_FPR=$(
+			gpg --batch --with-colons --list-secret-keys "$_KSTORE_GPG_RCPT" 2>/dev/null |
+				awk -F: '$1 == "fpr" { print $10; exit }'
+		)
+
+		if [ -n "$_FPR" ]; then
+			export _KSTORE_GPG_RCPT="$_FPR"
+			return 0
+		fi
+	fi
+
+	_FPR=$(
+		gpg --batch --with-colons --list-secret-keys 2>/dev/null |
+			awk -F: '$1 == "fpr" { print $10; exit }'
+	)
+
+	if [ -n "$_FPR" ]; then
+		export _KSTORE_GPG_RCPT="$_FPR"
+		return 0
+	fi
+
+	return 1
+}
+
+function _kstoresecret-linux-generate-gpg-key {
+	local _SECRET _UID _TMP _FPR _CODE
+
+	_SECRET="$1"
+	_UID="${KSTORE_GPG_UID:-rbash kstore <kstore@rbash.local>}"
+
+	if [ -z "$_SECRET" ]; then
+		echo "empty local unlock secret" >&2
+		return 1
+	fi
+
+	_TMP=$(mktemp) || return 1
+	chmod 600 "$_TMP" 2>/dev/null || true
+
+	cat > "$_TMP" <<EOF
+Key-Type: eddsa
+Key-Curve: ed25519
+Key-Usage: sign
+Subkey-Type: ecdh
+Subkey-Curve: cv25519
+Subkey-Usage: encrypt
+Name-Real: rbash kstore
+Name-Email: kstore@rbash.local
+Expire-Date: 0
+Passphrase: $_SECRET
+%commit
+EOF
+
+	gpg --batch --pinentry-mode loopback --generate-key "$_TMP"
+	_CODE=$?
+
+	rm -f "$_TMP"
+
+	if [ "$_CODE" -ne 0 ]; then
+		return "$_CODE"
+	fi
+
+	_FPR=$(
+		gpg --batch --with-colons --list-secret-keys "$_UID" 2>/dev/null |
+			awk -F: '$1 == "fpr" { print $10; exit }'
+	)
+
+	if [ -z "$_FPR" ]; then
+		echo "failed to find generated GPG key fingerprint" >&2
+		return 1
+	fi
+
+	export _KSTORE_GPG_RCPT="$_FPR"
+}
+
+function _kstoresecret-linux {
+	local _SECRET_FILE _RECIPIENT _TMP _A _SECRET _SECRET2
+
+	_SECRET_FILE="${RBASH_HOME:-$HOME/.rbash}/keystore.secret.gpg"
+
+	if ! _kstoresecret-linux-ensure-gpg-key; then
+		read -rsp "Enter new local kstore unlock secret: " _SECRET
+		echo >&2
+		read -rsp "Confirm new local kstore unlock secret: " _SECRET2
+		echo >&2
+
+		if [ -z "$_SECRET" ]; then
+			echo "empty local unlock secret" >&2
+			unset _SECRET _SECRET2
+			return 1
+		fi
+
+		if [ "$_SECRET" != "$_SECRET2" ]; then
+			echo "local unlock secrets do not match" >&2
+			unset _SECRET _SECRET2
+			return 1
+		fi
+
+		_kstoresecret-linux-generate-gpg-key "$_SECRET" || {
+			unset _SECRET _SECRET2
+					return 1
+		}
+
+	unset _SECRET _SECRET2
+	fi
+
+	case "$1" in
+		get)
+			if [ ! -f "$_SECRET_FILE" ]; then
+				return 1
+			fi
+
+			_A=$(
+				gpg --quiet --decrypt "$_SECRET_FILE"
+			) || {
+				echo "failed to decrypt $_SECRET_FILE" >&2
+				return 1
+			}
+
+			if [ -z "$_A" ]; then
+				echo "decrypted _AUTH_SECRET is empty: $_SECRET_FILE" >&2
+				return 1
+			fi
+
+			_AUTH_SECRET="$_A"
+			unset _A
+			;;
+
+		set)
+			if [ -z "$_AUTH_SECRET" ]; then
+				echo "_AUTH_SECRET is not loaded; cannot create $_SECRET_FILE" >&2
+				return 1
+			fi
+
+			_RECIPIENT="$_KSTORE_GPG_RCPT"
+
+			mkdir -p "${RBASH_HOME:-$HOME/.rbash}" || return 1
+			chmod 700 "${RBASH_HOME:-$HOME/.rbash}" 2>/dev/null || true
+
+			_TMP=$(mktemp) || return 1
+
+			if printf "%s" "$_AUTH_SECRET" | gpg \
+				--quiet \
+				--batch \
+				--yes \
+				--encrypt \
+				--recipient "$_RECIPIENT" \
+				--output "$_TMP"
+			then
+				chmod 600 "$_TMP" 2>/dev/null || true
+			else
+				rm -f "$_TMP"
+				echo "failed to create $_SECRET_FILE" >&2
+				return 1
+			fi
+
+			_A=$(
+				gpg --quiet --decrypt "$_TMP"
+			) || {
+				rm -f "$_TMP"
+				echo "failed to verify $_SECRET_FILE" >&2
+				return 1
+			}
+
+			if [ "$_A" != "$_AUTH_SECRET" ]; then
+				rm -f "$_TMP"
+				unset _A
+				echo "verification mismatch for $_SECRET_FILE" >&2
+				return 1
+			fi
+
+			unset _A
+			mv "$_TMP" "$_SECRET_FILE"
+			;;
+
+		del)
+			rm -f "$_SECRET_FILE"
+			;;
+
 		*)
-			echo "$OSTYPE is Not supported yet" >&2
+			echo "Unknown action: $1" >&2
+			return 1
 			;;
 	esac
 }
@@ -260,7 +462,7 @@ function _kstoresecret-config {
 		fi
 
 		case $OSTYPE in
-			cygwin|darwin*)
+			cygwin|linux|darwin*)
 				read -p "Save this password to OS's keystore? (y/n): " _CONFIRM
 				if [ "$_CONFIRM" != "y" ]; then
 					return 0