Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
loteoo committed Aug 6, 2023
0 parents commit ef4c123
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Alexandre Lotte

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Keychain Secrets manager

Straight-forward command line secrets manager powered by the Keychain tools already available on macOS systems.

It's a tiny and easy to use CLI that let's you securely store and retrieve encrypted secrets from a Keychain without any additional third parties involved.

It's built as a small wrapper around native `security` command, so it's fast, works offline and is fully interoperable with the Keychain Access app. This way you can also manage your secrets via the UI as well.

**This is for you if**:

- You're on macOS.
- You want to store and retrieve secrets via the command-line using simple commands.
- You like leveraging existing OS functionnality.
- You don't like the idea of relying on an HTTP connection, a third party service and a credit card subscription to manage secrets.

## Installation

Use the install script for an simple interactive installation, that will, in short:

1. Download the script file from github.
2. Place it into the chosen executable path and make sure it's executable.
3. Create a Keychain to store secrets in and register it in Keychain Access app.

```sh
curl -sSLf https://raw.githubusercontent.com/loteoo/ks/main/install | bash
```

## Manual installation

Clone this repo somewhere on your machine, then create a symlink in your executable bin folder to the script.

```sh
# This directory should be in your executable PATH
# /
ln -s ~/path/to/repo/ks ~/bin/ks
# \
# This should point to the actual secret file
```

To setup completions run something like this:

```sh
echo "source <(ks completion)" >> ~/.zshrc
```

## Usage

```
$ ks help
Keychain Secrets manager
Usage:
ks add <key> <value> Add an encrypted secret
ks show <key> Decrypt and reveal a secret
ks ls List secret keys
ks rm <key> Remove a secret
ks help
```
105 changes: 105 additions & 0 deletions install
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
dimmed=$'\e[2m'
bold=$(tput bold)
cyan=$'\e[96m'
normal=$(tput sgr0)

SCRIPT_NAME="ks"
SCRIPT_DOWNLOAD_URL="https://raw.githubusercontent.com/loteoo/ks/main/ks"
REQUIRES_RESTART="false"
SYSTEM_BIN_PATH="/usr/local/bin"
LOCAL_BIN_PATH="$HOME/.local/bin"
KEYCHAIN="${KS_DEFAULT_KEYCHAIN:-Secrets}"
KEYCHAIN_FILE="$KEYCHAIN.keychain"

info() {
# shellcheck disable=SC2145
echo "${dimmed}$@${normal}" 1>&2
}

success() {
# shellcheck disable=SC2145
echo "${cyan}$@${normal}"
}

throw() {
echo "$@" 1>&2
exit 1
}

yn() {
read -r -n 1 -p "$1 [y/n]: " yn
echo
if [[ "$yn" != [Yy]* ]]; then
return 1
fi
}

echo "Select install location:
1) For $USER user only. Script will be placed under: $LOCAL_BIN_PATH.
2) For whole system. Script will be placed under: $SYSTEM_BIN_PATH. Requires sudo."
read -r -n 1 -p "Pick one: " num
echo
case "$num" in
1) BIN_PATH="$LOCAL_BIN_PATH";;
2) BIN_PATH="$SYSTEM_BIN_PATH";;
*) throw "Invalid choice.";;
esac

# Create bin directory if needed.
if [[ ! -d "$BIN_PATH" ]]; then
if ! yn "Need to create $BIN_PATH. Continue?"; then
throw "Installation aborted."
fi
mkdir "$BIN_PATH"
success "==> Created $BIN_PATH folder."
else
info "✓ Directory exists."
fi

# Add to bin directory to $PATH if needed.
if ! echo "$PATH" | tr ':' $'\n' | grep -q "$BIN_PATH"; then
if ! yn "$BIN_PATH has not been added to \$PATH. Add to path? (requires sudo privileges)"; then
throw "Installation aborted."
fi
sudo bash -c "echo '$BIN_PATH' >> /etc/paths"
REQUIRES_RESTART="true"
success "==> $BIN_PATH was added to \$PATH."
else
info "✓ Directory is in \$PATH."
fi

SCRIPT_PATH="$BIN_PATH/$SCRIPT_NAME"

if [[ -f "$SCRIPT_PATH" ]]; then
if ! yn "File $SCRIPT_PATH already exists. Replace?"; then
throw "Installation aborted."
fi
fi

info "Downloading $SCRIPT_DOWNLOAD_URL..."
curl -sSLf "$SCRIPT_DOWNLOAD_URL" -o "$SCRIPT_PATH"
success "==> Script added under $SCRIPT_PATH"

if [[ ! -x "$SCRIPT_PATH" ]]; then
chmod +x "$SCRIPT_PATH"
success "==> Script made executable."
else
info "✓ Script is executable."
fi

if ! security show-keychain-info "$KEYCHAIN_FILE" > /dev/null 2>&1; then
$SCRIPT_NAME init
else
info "✓ Keychain \"$KEYCHAIN\" exists."
fi

if [[ "$REQUIRES_RESTART" == "true" ]]; then
info "⚠️ Restart your terminal to make $SCRIPT_NAME available."
fi

info "Give $SCRIPT_NAME it a star on Github if you like it! 🙏"
success "${bold}Installation completed! 🎉${normal}"
eval "$SCRIPT_NAME help"
138 changes: 138 additions & 0 deletions ks
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash

add() {
key="${1:?'Please provide the name of the secret to add.'}"
value="${2:?'Please provide the value to encrypt.'}"
security add-generic-password \
-a "$USER" \
-D secret \
-s "$key" \
-w "$value" \
"$KEYCHAIN_FILE" \
2> /dev/null \
|| throw "Secret \"$key\" already exists."
success "Secret \"$key\" added."
}

show() {
key="${1:?'Please provide the name of the secret to show.'}"
security find-generic-password \
-a "$USER" \
-s "$key" \
-w \
"$KEYCHAIN_FILE" \
2> /dev/null \
|| throw "Secret \"$key\" was not found in keychain."
}

rm() {
key="${1:?'Please provide the name of the secret to remove.'}"
security delete-generic-password \
-a "$USER" \
-s "$key" \
"$KEYCHAIN_FILE" \
> /dev/null 2>&1 \
|| throw "Secret \"$key\" was not found in keychain."
success "Secret \"$key\" deleted."
}

ls() {
security dump-keychain "$KEYCHAIN_FILE" \
| grep 0x00000007 \
| awk -F= '{print $2}' \
| tr -d \" \
|| throw "Keychain is empty."
}

init() {
maybe_error="$(cat <(security show-keychain-info "$KEYCHAIN_FILE" 2>&1))"
not_found_error="The specified keychain could not be found"
if [[ "$maybe_error" == *"SecKeychainCopySettings"* ]]; then
if [[ "$maybe_error" == *"$not_found_error"* ]]; then
if ! yn "Keychain \"$KEYCHAIN\" does not exist. Create it?"; then
throw "Aborted."
fi
if [[ "$KEYCHAIN" =~ [^a-zA-Z0-9_-] ]]; then
throw "Invalid keychain name. Alphanumeric with dashes and underscores only."
fi
echo "Choose a password for the keychain. You can change it later via the Keychain Access app."
read -rsp "Password: " pass
echo
security create-keychain -p "$pass" "$KEYCHAIN_FILE"
success "Keychain \"$KEYCHAIN\" created."
if yn "Register in Keychain Access app?"; then
eval "security list-keychains -s $(security default-keychain) $(security list-keychains | xargs) $HOME/Library/Keychains/$KEYCHAIN_FILE"
success "Keychain \"$KEYCHAIN\" added to Keychain Access app."
fi
else
throw "Could not access the \"$KEYCHAIN\" keychain."
fi
fi
}

# Meta stuff...
# ===================
set -euo pipefail
IFS=$'\n\t'
normal=$(tput sgr0)
cyan=$'\e[96m'

KEYCHAIN="${KS_DEFAULT_KEYCHAIN:-Secrets}"

help() {
cat << EOT
Keychain Secrets manager
Usage:
ks [-k keychain] <action> [...opts]
Commands:
add <key> <value> Add an encrypted secret
show <key> Decrypt and reveal a secret
rm <key> Remove a secret
ls List secret keys
init Create the specified Keychain
help Show this help text
EOT
}

completion() {
echo "complete -W \"add show rm ls help\" ks"
}

success() {
# shellcheck disable=SC2145
echo "${cyan}$@${normal}"
}

throw() {
echo "$@" 1>&2
exit 1
}

yn() {
read -r -n 1 -p "$1 [y/n]: " yn
echo
if [[ "$yn" != [Yy]* ]]; then
return 1
fi
}

while getopts "k:" arg; do
case $arg in
k) KEYCHAIN="$OPTARG";;
*) throw "$(help)";;
esac
done
shift $((OPTIND - 1))

KEYCHAIN_FILE="$KEYCHAIN.keychain"

if [[ "$(type -t "${1:-}")" == "function" ]]; then
if [[ "add show rm ls" = *"$1"* ]]; then
init
fi
$1 "${@:2}"
else
throw "$(help)"
fi

0 comments on commit ef4c123

Please sign in to comment.