#!/usr/bin/env bash # # (c) 2017-2018 Konstantin Gredeskoul # # MIT License, distributed as part of `sym` ruby gem. # • https://github.com/kigster/sym # #============================================================================== # # The purpose of this script is to transparently edit application secrets in # Rails apps or other projects. It simplifies the process of key import, as well # as the direct editing, as well as multi-file encryption/decryption routines. # # The idea is that you set some of the variables below to values specific to your # system and working with encrypted files will become very easy. # # SYMIT__FOLDER is a relative folder to your project root, under which you # might keep ALL of your encrypted files. Alternatively, if you keep encrypted # files sprinkled around your project, just leave it out, because it defaults # to "." — the current folder, and search anything beneath. # # Variables: # # # only search ./config folder # export SYMIT__FOLDER="config" # # # this will be the name of your key in OS-X KeyChain # export SYMIT__KEY="my-org.engineering.dev" # just a name # # # This is the extension given to the encrypted files. Ideally, leave it # # be as ".enc" # export SYMIT__EXTENSION=".enc" # # And then # # symit import key [ insecure ] # import a key and password-protect it (or not) # symit auto application.yml.enc # auto-decrypts it # symit auto application.yml # auto-encrypts it # symit decrypt application.yml # finds application.yml.enc and decrypts that. # # # ...and vola! You are editing the encrypted file with sym from the root of # your Rails application. Neat, no? # # Check if we are being sourced in, or run as a script: ( [[ -n ${ZSH_EVAL_CONTEXT} && ${ZSH_EVAL_CONTEXT} =~ :file$ ]] || \ [[ -n $BASH_VERSION && $0 != "$BASH_SOURCE" ]]) && _s_=1 || _s_=0 (( $_s_ )) && _is_sourced=1 (( $_s_ )) || _is_sourced=0 # Set all the defaults function __symit::init() { export SYMIT__EXTENSION=${SYMIT__EXTENSION:-'.enc'} export SYMIT__FOLDER=${SYMIT__FOLDER:-'.'} export SYMIT__KEY=${SYMIT__KEY} export SYMIT__MIN_VERSION='latest' } # Returns name of the current shell, eg 'bash' function __lib::shell::name() { echo $(basename $(printf $SHELL)) } # Returns 'yes' if current shell is BASH function __lib::shell::is_bash() { [[ $(__lib::shell::name) == "bash" ]] && echo yes } # Returns a number representing shell version, eg. # 3 or 4 for BASH v3 and v4 respectively. function __lib::bash::version_number() { echo $BASH_VERSION | awk 'BEGIN{FS="."}{print $1}' } # Enable all colors, but only if the STDOUT is a terminal function __lib::color::setup() { if [[ -t 1 ]]; then export txtblk='\e[0;30m' # Black - Regular export txtred='\e[0;31m' # Red export txtgrn='\e[0;32m' # Green export txtylw='\e[0;33m' # Yellow export txtblu='\e[0;34m' # Blue export txtpur='\e[0;35m' # Purple export txtcyn='\e[0;36m' # Cyan export txtwht='\e[0;37m' # White export bldblk='\e[1;30m' # Black - Bold export bldred='\e[1;31m' # Red export bldgrn='\e[1;32m' # Green export bldylw='\e[1;33m' # Yellow export bldblu='\e[1;34m' # Blue export bldpur='\e[1;35m' # Purple export bldcyn='\e[1;36m' # Cyan export bldwht='\e[1;37m' # White export unkblk='\e[4;30m' # Black - Underline export undred='\e[4;31m' # Red export undgrn='\e[4;32m' # Green export undylw='\e[4;33m' # Yellow export undblu='\e[4;34m' # Blue export undpur='\e[4;35m' # Purple export undcyn='\e[4;36m' # Cyan export undwht='\e[4;37m' # White export bakblk='\e[40m' # Black - Background export bakred='\e[41m' # Red export bakgrn='\e[42m' # Green export bakylw='\e[43m' # Yellow export bakblu='\e[44m' # Blue export bakpur='\e[45m' # Purple export bakcyn='\e[46m' # Cyan export bakwht='\e[47m' # White export clr='\e[0m' # Text Reset export txtrst='\e[0m' # Text Reset export rst='\e[0m' # Text Reset fi } # Unset all the colors, in case we a being piped into # something else. function __lib::color::reset() { export txtblk= export txtred= export txtgrn= export txtylw= export txtblu= export txtpur= export txtcyn= export txtwht= export bldblk= export bldred= export bldgrn= export bldylw= export bldblu= export bldpur= export bldcyn= export bldwht= export unkblk= export undred= export undgrn= export undylw= export undblu= export undpur= export undcyn= export undwht= export bakblk= export bakred= export bakgrn= export bakylw= export bakblu= export bakpur= export bakcyn= export bakwht= export clr= export txtrst= export rst= } # Enable or disable the colors based on whether the STDOUT # is a proper terminal, or a pipe. function __lib::stdout::configure() { if [[ -t 1 ]]; then __lib::color::setup else __lib::color::reset fi } __lib::stdout::configure # Check if we are being run as a script, and if so — bail. (( $_s_ )) || { printf "${bldred}This script is meant to be sourced into your environment,\n" printf "not run on a command line.${clr} \n\n" printf "Please add 'source $0' to your BASH initialization file,\n" printf "or run the following command:\n\n" printf " \$ ${bldgrn}sym -B ~/.bash_profile${clr}\n\n" printf "${bldblu}Thanks for using Sym!${clr}\n" exit 1 } # Horizontal line, width of the full terminal function __lib::color::hr() { local cols=${1:-${COLUMNS}} local char=${2:-"—"} local color=${3:-${txtylw}} printf "${color}" eval "printf \"%0.s${char}\" {1..${cols}}" printf "${clr}\n" } # Large header, all caps function __lib::color::h1() { local title=$(echo "$*" | tr 'a-z' 'A-Z') len=${#title} printf "${bldylw}${title}\n" __lib::color::hr ${len} '─' } # Smaller header function __lib::color::h2() { printf "${bldpur}$*${clr}\n" } # Shift cursor by N positions to the right function __lib::color::cursor-right-by() { position=$1 printf "\e[${position}C" } # Shift cursor by N positions to the left function __lib::color::cursor-left-by() { position=$1 printf "\e[${position}D" } # Shift cursor by N positions up function __lib::color::cursor-up-by() { position=$1 printf "\e[${position}A" } # Shift cursor by N positions down function __lib::color::cursor-down-by() { position=$1 printf "\e[${position}B" } # Convert a version string such as "1.50.17" to an integer # 101050017 for numeric comparison: function __lib::ver-to-i() { version=${1} echo ${version} | awk 'BEGIN{FS="."}{ printf "1%02d%03.3d%03.3d", $1, $2, $3}' } # Convert a result of __lib::ver-to-i() back to a regular version. function __lib::i-to-ver() { version=${1} /usr/bin/env ruby -e "ver='${version}'; printf %Q{%d.%d.%d}, ver[1..2].to_i, ver[3..5].to_i, ver[6..8].to_i" } # Prints Usage function __symit::usage() { echo __lib::color::h1 "symit" printf " ${bldylw}symit${bldgrn} is a powerful BASH helper, that enhances the CLI encryption tool called ${bldred}Sym${clr}, which is a Ruby Gem. Sym has an extensive CLI interface, but it only handles one encryption/decryption operation per invocation. With this script, you can auto decrypt all files in a given folder, you can import the key in a simpler way, and you can save into the environment sym configuration that will be used. It also streamlines editing of encrypted files in a given folder. Symit can be configured either with the ENV variables, or using the CLI flags.\n" printf " The recommended way to use ${bldred}symit${clr} is to set the following environment variables, which removes the need to pass these values via the flags. These variables default to the shown values if not set elsewhere: Perhaps the most critically important variable to set is ${txtylw}SYMIT__KEY${clr}: ${txtylw} export SYMIT__KEY='my-org.my-app.dev' eg: export SYMIT__KEY='github.web.development' ${clr} The ${txtcya}key${clr} can resolve to a file name, or a name of ENV variable, a keychain entry, or be the actual key (not recommended!). See the following link for more info: ${undblu}https://github.com/kigster/sym#resolving-the--k-argument${clr} Additional configuration is available through these variables: ${txtylw} export SYMIT__EXTENSION='${SYMIT__EXTENSION}' export SYMIT__FOLDER='${SYMIT__FOLDER}' export SYMIT__MIN_VERSION='latest' ${clr} The last variable defines the minimum Sym version desired. Set it to 'latest' to have symit auto-upgrade Sym every time it is invoked. ${clr}\n" __lib::color::h2 "Usage:" printf " ${bldgrn}symit [ action ] [ file-path/pattern ] [ flags ]${clr}\n\n" __lib::color::h2 "Actions:" printf " Action is the first word that defaults to ${bldylw}edit${clr}.\n\n" printf " ${bldcya}Valid actions are below, starting with the Key import or creation:${clr}\n\n" printf " ${bldylw}— generate ${clr}create a new secure key, and copies it to the\n" printf " clipboard (if supported), otherwise prints to STDOUT\n" printf " Key name (set via SYMIT__KEY or -k flag) is required,\n" printf " and is used as the KeyChain entry name for the new key.\n\n" printf " ${bldylw}— import [insecure]\n" printf " ${clr}imports the key from clipboard and adds password\n" printf " encryption unless 'insecure' is passed in. Same as above\n" printf " in relation with the key parameter.\n\n" printf " ${bldcya}The following actions require the file pattern/path argument:${clr}\n" printf " ${bldylw}— edit ${clr}Finds all files, and opens them in $EDITOR\n" printf " ${bldylw}— encrypt ${clr}Encrypts files matching file-path\n" printf " ${bldylw}— decrypt ${clr}Adds the extension to file pattern and decrypts\n" printf " ${bldylw}— auto ${clr}encrypts decrypted file, and vice versa\n" echo __lib::color::h2 "Flags:" printf " -f | --folder DIR ${clr}Top level folder to search.${clr}\n" printf " -k | --key KEY ${clr}Key identifier${clr}\n" printf " -x | --extension EXT ${clr}Default extension of encrypted files.${clr}\n" printf " -n | --dry-run ${clr}Print stuff, but dont do it${clr}\n" printf " -a | --all-files ${clr}If provided ALL FILES are operated on${clr}\n" printf " ${clr}Use with CAUTION!${clr}\n" printf " -v | --verbose ${clr}Print more stuff${clr}\n" printf " -q | --quiet ${clr}Print less stuff${clr}\n" printf " -h | --help ${clr}Show this help message${clr}\n" echo __lib::color::h2 'Encryption key identifier can be:' printf "${clr}" printf ' 1. name of the keychain item storing the keychain (secure) 2. name of the environment variable storing the Key (*) 3. name of the file storing the key (*) 4. the key itself (*)' echo printf "${bldred}" printf ' (*) 2-4 are insecure UNLESS the key is encrypted with a password.'; echo printf "${clr}\ Please refer to README about generating password protected keys:\n ${bldblu}${undblu}https://github.com/kigster/sym#generating-the-key--examples${clr}\n\n" echo __lib::color::h1 'Examples:' printf " To import a key securely, first copy the key to your clipboard,\n" printf " and then run the following command, pasting the key when asked:\n\n" printf " ❯ ${bldgrn}symit${bldblu} import key ${clr}\n\n" printf " To encrypt or decrypt ALL files in the 'config' directory:${clr}\n\n" printf " ❯ ${bldgrn}symit${bldblu} encrypt|decrypt -a -f config ${clr}\n\n" printf " To decrypt all *.yml.enc files in the 'config' directory:${clr}\n\n" printf " ❯ ${bldgrn}symit${bldblu} decrypt '*.yml' -f config ${clr}\n\n" printf " To edit an encrypted file ${txtblu}config/application.yml.enc${clr}\n\n" printf " ❯ ${bldgrn}symit${bldblu} application.yml${clr}\n\n" printf " To auto decrypt a file ${txtblu}config/settings/crypt/pass.yml.enc${clr}\n\n" printf " ❯ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml.enc${clr}\n\n" printf " To automatically decide to either encrypt or decrypt a file,\n" printf " based on the file extension use 'auto' command. The first line below\n" printf " encrypts the file, second decrypts it, because the file extension is .enc:${clr}\n\n" printf " ❯ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml${clr}\n" printf " ❯ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml.enc${clr}\n\n" printf " To encrypt a file ${txtblu}config/settings.yml${clr}\n" printf " ❯ ${bldgrn}symit${bldblu} encrypt config/settings.yml${clr}\n\n" } function __datum() { date +"%m/%d/%Y.%H:%M:%S" } function __warn() { __lib::color::cursor-left-by 1000 printf "${bldylw}$* ${bldylw}\n" } function __err() { __lib::color::cursor-left-by 1000 printf "${bldred}ERROR: ${txtred}$* ${bldylw}\n" } function __inf() { [[ ${cli__opts__quiet} ]] && return __lib::color::cursor-left-by 1000 printf "${txtblu}$*${clr}\n" } function __dbg() { [[ ${cli__opts__verbose} ]] || return __lib::color::cursor-left-by 1000 printf "${txtgrn}$*${clr}\n" } function __lib::command::print() { __inf "${bldylw}❯ ${bldcya}$*${clr}" } function __symit::sym::installed_version() { __lib::ver-to-i $(gem list | grep sym | awk '{print $2}' | sed 's/(//g;s/)//g') } function __symit::sym::latest_version() { __lib::ver-to-i $(gem query --remote -n '^sym$' | awk '{print $2}' | sed 's/(//g;s/)//g') } function __symit::install::update() { local desired_version=$1 shift local current_version=$2 shift local version_args=$* __inf "updating sym to version ${bldylw}$(__lib::i-to-ver ${desired_version})${clr}..." printf "${bldblu}" >&1 echo y | gem uninstall sym --force -x 2>/dev/null printf "${clr}" >&1 command="gem install sym ${version_args} " eval "${command}" >/dev/null code=$? printf "${clr}" >&2 if [[ ${code} != 0 ]]; then __err "gem install returned ${code}, with command ${bldylw}${command}" return 127 fi current_version=$(__symit::sym::installed_version) __inf "sym version ${bldylw}$(__lib::i-to-ver ${current_version}) was successfully installed." } function __symit::install::gem() { if [[ -n ${__symit_last_checked_at} ]]; then now=$(date +'%s') if [[ $(( $now - ${__symit_last_checked_at} )) -lt 3600 ]]; then return fi fi export __symit_last_checked_at=${now:-$(date +'%s')} __inf "Verifying current sym version, please wait..." current_version=$(__symit::sym::installed_version) if [[ -n ${SYMIT__MIN_VERSION} ]]; then if [[ ${SYMIT__MIN_VERSION} -eq 'latest' ]]; then desired_version=$(__symit::sym::latest_version) version_args='' else desired_version=$( __lib::ver-to-i ${SYMIT__MIN_VERSION}) version_args=" --version ${SYMIT__MIN_VERSION}" fi if [[ "${desired_version}" != "${current_version}" ]]; then __symit::install::update "${desired_version}" "${current_version}" "${version_args}" else __inf "${bldgrn}sym${clr} ${txtblu}is on the correct version ${bldylw}$(__lib::i-to-ver ${desired_version})${txtblu} already" fi else if [[ -z ${current_version} ]] ; then __dbg "installing latest version of ${bldylw}sym..." fi fi } function __symit::files() { eval $(__symit::files::cmd) } function __symit::files::cmd() { if [[ -n ${cli__opts__file} && -n ${cli__opts__extension} ]]; then local folder=${cli__opts__folder} local file="${cli__opts__file}" local ext="${cli__opts__extension}" if [[ ${file} =~ '/' ]]; then if [[ ${folder} == '.' ]]; then folder="$(dirname ${file})" else folder="${folder}/$(dirname ${file})" fi file="$(basename ${file})" fi if [[ "${cli__opts__action}" == "encrypt" ]] ; then printf "find ${folder} -name '${file}' -and -not -name '*${ext}'" elif [[ "${cli__opts__action}" == "auto" ]] ; then printf "find ${folder} -name '${file}'" else # edit, decrypt [[ ${file} =~ "${ext}" ]] || file="${file}${ext}" printf "find ${folder} -name '${file}'" fi fi } function __symit::command() { file=${1} if [[ -n "${cli__opts__key}" && -n "${cli__opts__extension}" ]]; then action="${cli__opts__action}" v="sym__actions__${action}" flags="${!v}" if [[ ${action} =~ "key" ]]; then [[ -n ${cli__opts__verbose} ]] && printf "processing key import action ${bldylw}${action}${clr}\n" >&2 printf "sym ${flags} ${cli__opts__key} " elif [[ ${action} =~ "generate" ]] ; then [[ -n ${cli__opts__verbose} ]] && printf "processing generate key action ${bldylw}${action}${clr}\n" >&2 if [[ -n $(which pbcopy) ]]; then out_key=/tmp/outkey command="sym ${flags} ${cli__opts__key} -q -o ${out_key}; cat ${out_key} | pbcopy; rm -f ${out_key}" printf "${command}" else printf "sym ${flags} ${cli__opts__key} " fi elif [[ -n ${file} ]] ; then ext="${cli__opts__extension}" [[ -z ${ext} ]] && ext='.enc' ext=$(echo ${ext} | sed -E 's/[\*\/,.]//g') if [[ ${action} =~ "encrypt" ]]; then printf "sym ${flags} ${file} -ck ${cli__opts__key} -o ${file}.${ext}" elif [[ ${action} =~ "decrypt" ]]; then new_name=$(echo ${file} | sed "s/\.${ext}//g") [[ "${new_name}" == "${file}" ]] && name="${file}.decrypted" printf "sym ${flags} ${file} -ck ${cli__opts__key} -o ${new_name}" else printf "sym ${flags} ${file} -ck ${cli__opts__key} " fi else printf "printf \"ERROR: not sure how to generate a correct command\\n\"" fi fi } function __symit::cleanup() { unset sym__actions unset cli__opts } function __symit::exit() { code=${1:-0} __symit::cleanup echo -n ${code} } function __symit::print_cli_args() { __dbg "action ${bldylw}: ${cli__opts__action}${clr}" __dbg "key ${bldylw}: ${cli__opts__key}${clr}" __dbg "file ${bldylw}: ${cli__opts__file}${clr}" __dbg "extension ${bldylw}: ${cli__opts__extension}${clr}" __dbg "folder ${bldylw}: ${cli__opts__folder}${clr}" __dbg "verbose ${bldylw}: ${cli__opts__verbose}${clr}" __dbg "dry_run ${bldylw}: ${cli__opts__dry_run}${clr}" } function __symit::args::needs_file() { if [[ "${cli__opts__action}" == 'edit' || \ "${cli__opts__action}" == 'auto' || \ "${cli__opts__action}" == 'encrypt' || \ "${cli__opts__action}" == 'decrypt' ]]; then printf 'yes' fi } function __symit::validate_args() { if [[ -n $(__symit::args::needs_file) && -z ${cli__opts__file} ]]; then __err "missing file argument, config/application.yml" return $(__symit::exit 2) fi if [[ -z "${cli__opts__key}" ]]; then __err "Key was not defined, pass it with ${bldblu}-k KEY_ID${bldred}" __err "or set it via ${bldgrn}\$SYMIT__KEY${bldred} variable." return $(__symit::exit 4) fi if [[ -z ${cli__opts__extension} ]]; then cli__opts__extension='.enc' fi } function __symit::run() { __symit::cleanup __symit::init cli__opts__verbose='' cli__opts__quiet='' cli__opts__key=${SYMIT__KEY} cli__opts__extension=${SYMIT__EXTENSION} cli__opts__folder=${SYMIT__FOLDER} cli__opts__dry_run='' cli__opts__action=edit cli__opts__file='' sym__actions__generate=' -cpgx ' sym__actions__edit=' -t ' sym__actions__encrypt='-e -f ' sym__actions__decrypt='-d -f ' sym__actions__auto=' -n ' sym__actions__key_secure=' -iqcpx ' sym__actions__key_insecure=' -iqcx ' sym__actions__install='install' if [[ -z $1 ]]; then __symit::usage return $(__symit::exit 0) fi while :; do case $1 in -h|-\?|--help) shift __symit::usage __symit::cleanup return $(__symit::exit 0) ;; -k|--key) shift if [[ -z $1 ]]; then __err "-k/--key requires an argument" && return $(__symit::exit 1) else cli__opts__key=$1 shift fi ;; -x|--extension) shift if [[ -z $1 ]]; then __err "-x/--extension requires an argument" && return $(__symit::exit 1) else cli__opts__extension=${1} shift fi ;; -f|--folder) shift if [[ -z $1 ]]; then __err "-f/--folder requires an argument" && return $(__symit::exit 1) else cli__opts__folder=${1} shift fi ;; -a|--all-files) shift cli__opts__file="'*'" ;; -n|--dry-run) shift cli__opts__dry_run="yes" ;; -v|--verbose) shift cli__opts__verbose="yes" ;; -q|--quiet) shift cli__opts__quiet="yes" ;; import|key) shift cli__opts__action="key_secure" ;; insecure) shift if [[ "${cli__opts__action}" == 'key_secure' ]] ; then cli__opts__action="key_insecure" fi ;; --) # End of all options. shift break ;; -?*) __err 'WARN: Unknown option: %s\n' "$1" >&2 return $(__symit::exit 127) shift ;; ?*) param=$1 v="sym__actions__${param}" if [[ ! ${param} =~ '.' && -n "${!v}" ]]; then __dbg "Action ${bldylw}${param}${clr} is a valid action." cli__opts__action=${param} else __dbg "Parameter ${bldylw}${param}${clr} is not a valid action," __dbg "therefore it must be a file pattern." cli__opts__file=${1} fi shift ;; *) # Default case: If no more options then break out of the loop. break shift esac done [[ -n "${cli__opts__verbose}" ]] && __symit::print_cli_args if [[ "${cli__opts__action}" == 'install' ]]; then if [[ -n ${cli__opts__dry_run} ]]; then __dbg "This command verifies that Sym is properly installed," __dbg "and if not found — installs it." return $(__symit::exit 0) else __symit::install::gem return $(__symit::exit 0) fi fi __symit::validate_args code=$? if [[ ${code} != 0 ]]; then return $(__symit::exit ${code}) fi __symit::install::gem changed_count=0 if [[ -n "${cli__opts__dry_run}" ]] ; then __lib::color::h1 "DRY RUN" for file in $(__symit::files); do printf " \$ ${bldblu}$(__symit::command ${file})${clr}\n" done else if [[ -n "${cli__opts__file}" ]]; then [[ -n ${cli__opts__verbose} ]] && __dbg $(__symit::files) declare -a file_list for file in $(__symit::files); do local cmd="$(__symit::command ${file})" __lib::command::print "${cmd}" eval "${cmd}" code=$?; [[ ${code} != 0 ]] && __err "command '${bldblu}${cmd}${bldred}' exited with code ${bldylw}${code}" changed_count=$(( ${changed_count} + 1)) done if [[ ${changed_count} == 0 ]]; then printf "${undylw}Bad news:${clr}\n\n" __warn " No files matched your specification. The following 'find' command" __warn " ran to find the file you requested. Please change the name, and " __warn " try again.\n" __warn " ${bldblu}$(__symit::files::cmd)${clr}\n\n" return $(__symit::exit 5) fi else # opts[file] cmd=$(__symit::command) __lib::command::print "${cmd}" eval "${cmd}" code=$?; [[ ${code} != 0 ]] && return $(__symit::exit ${code}) changed_count=$(( ${changed_count} + 1)) fi fi } function symit() { __lib::stdout::configure __symit::run $@ }