functionality for charsets added
This commit is contained in:
@@ -18,6 +18,7 @@ himi # generate → clipboard
|
|||||||
himi -o # generate → clipboard + stdout
|
himi -o # generate → clipboard + stdout
|
||||||
himi -o secure # use 'secure' template → clipboard + stdout
|
himi -o secure # use 'secure' template → clipboard + stdout
|
||||||
himi -n 5 -o easy # generate 5 'easy' secrets → clipboard + stdout
|
himi -n 5 -o easy # generate 5 'easy' secrets → clipboard + stdout
|
||||||
|
himi -q # stdout only, skip clipboard (or HIMI_NO_CLIP=1)
|
||||||
himi test '{word}-{num:2}' # test a custom template
|
himi test '{word}-{num:2}' # test a custom template
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -93,9 +94,29 @@ TEMPLATES=(
|
|||||||
| `{safe:N}` | N chars, ultra-safe (no ambiguous chars) | ~4.6 bits/char |
|
| `{safe:N}` | N chars, ultra-safe (no ambiguous chars) | ~4.6 bits/char |
|
||||||
| `{num:N}` | N digits | ~3.3 bits/char |
|
| `{num:N}` | N digits | ~3.3 bits/char |
|
||||||
| `{special:N}` | N from `!?@#%~=+` | ~3.0 bits/char |
|
| `{special:N}` | N from `!?@#%~=+` | ~3.0 bits/char |
|
||||||
|
| `{NAME:N-M}` | N to M chars from charset NAME | log2(Σ size^L) |
|
||||||
|
|
||||||
\* Word entropy depends on pool size. Values shown assume all built-in lists enabled (~14k words, ~13.8 bits/word).
|
\* Word entropy depends on pool size. Values shown assume all built-in lists enabled (~14k words, ~13.8 bits/word).
|
||||||
|
|
||||||
|
### Custom charsets
|
||||||
|
|
||||||
|
Add `"name|characters"` entries to `CHARSETS` in the config, then use `{name:N}` or `{name:N-M}` in any template:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CHARSETS=(
|
||||||
|
"hex|0123456789abcdef"
|
||||||
|
"b58|123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Names must match `[a-z][a-z0-9_]*` and override the built-in four (alpha, safe, num, special). Avoid duplicate characters; himi warns if it sees them, since they skew the distribution and overstate entropy.
|
||||||
|
|
||||||
|
Range tokens pick the length weighted by `size^L`, so every possible output is equally likely. `{num:1-2}` covers 0-99 plus 0-9 (110 outcomes, ~6.8 bits), not a 50/50 coin flip on length, which would tank min-entropy.
|
||||||
|
|
||||||
|
### Integrity checks
|
||||||
|
|
||||||
|
`himi rebuild` records a SHA-256 of the pool; generation refuses if `pool.txt` was modified outside of himi (accidental edits, sync corruption, casual tampering). himi also refuses to source a config file that isn't owned by you or is group/world-writable, since the config is executed shell. Neither check stops an attacker who can already write to your home directory (nothing can), but they catch everything short of that.
|
||||||
|
|
||||||
## Entropy rules of thumb
|
## Entropy rules of thumb
|
||||||
|
|
||||||
| Bits | Strength | Use case |
|
| Bits | Strength | Use case |
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# secret config — source'd by the secret command
|
# secret config — source'd by the secret command
|
||||||
# Location: ~/.config/himi/config.sh
|
# Location: ~/.config/himi/config.sh
|
||||||
|
|
||||||
|
# Schema version — himi nags if this is older than what it expects.
|
||||||
|
# After merging new options into an existing config, bump this to match.
|
||||||
|
CONFIG_VERSION=2
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Character sets for random portions
|
# Character sets for random portions
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -11,6 +15,17 @@ SAFE_CHARSET='ACDEFGHJKMNPQRTWXY234679'
|
|||||||
NUM_CHARSET='0123456789'
|
NUM_CHARSET='0123456789'
|
||||||
SPECIAL_CHARSET='!?@#%~=+'
|
SPECIAL_CHARSET='!?@#%~=+'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Custom charsets (optional)
|
||||||
|
# Format: "name|characters" — use as {name:N} or {name:N-M} in templates.
|
||||||
|
# Names: lowercase, matching [a-z][a-z0-9_]*. Entries here override the
|
||||||
|
# built-in four. Avoid duplicate characters (skews the distribution).
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
CHARSETS=(
|
||||||
|
# "hex|0123456789abcdef"
|
||||||
|
# "b58|123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||||
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Word filtering (applied when building pool.txt)
|
# Word filtering (applied when building pool.txt)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -27,10 +42,9 @@ WORD_MAX_LENGTH=9
|
|||||||
# {Word} — random word (Title Case)
|
# {Word} — random word (Title Case)
|
||||||
# {word!} — random lower or UPPER (+1 bit entropy)
|
# {word!} — random lower or UPPER (+1 bit entropy)
|
||||||
# {Word!} — random lower/Title/UPPER (+1.58 bits entropy)
|
# {Word!} — random lower/Title/UPPER (+1.58 bits entropy)
|
||||||
# {alpha:N} — N chars, Crockford Base32 (5.0 bits/char)
|
# {NAME:N} — N chars from charset NAME (alpha/safe/num/special/custom)
|
||||||
# {safe:N} — N chars, ultra-safe (4.6 bits/char)
|
# {NAME:N-M} — N to M chars, weighted so every output is equally likely
|
||||||
# {num:N} — N digits (3.3 bits/char)
|
# (entropy = log2 of total outcomes, e.g. {num:1-2} ≈ 6.8 bits)
|
||||||
# {special:N} — N characters from SPECIAL_CHARSET
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
TEMPLATES=(
|
TEMPLATES=(
|
||||||
"default|{word!}{num:2}-{word!}{num:2}-{alpha:3}{num:2}"
|
"default|{word!}{num:2}-{word!}{num:2}-{alpha:3}{num:2}"
|
||||||
|
|||||||
@@ -9,16 +9,25 @@ set -euo pipefail
|
|||||||
# himi Generate secret (default template) → clipboard
|
# himi Generate secret (default template) → clipboard
|
||||||
# himi <template> Generate secret (named template) → clipboard
|
# himi <template> Generate secret (named template) → clipboard
|
||||||
# himi -o [template] Output to stdout + clipboard
|
# himi -o [template] Output to stdout + clipboard
|
||||||
|
# himi -q [template] Output to stdout only (no clipboard)
|
||||||
# himi -n N Generate N secrets
|
# himi -n N Generate N secrets
|
||||||
# himi -e [template] Show entropy for template(s)
|
# himi entropy [name] Show entropy for template(s)
|
||||||
# himi -h Show help
|
# himi -h Show help
|
||||||
|
|
||||||
readonly VERSION="1.0.0"
|
readonly VERSION="1.1.0"
|
||||||
readonly CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
readonly CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
||||||
readonly CONFIG_FILE="${CONFIG_DIR}/config.sh"
|
readonly CONFIG_FILE="${CONFIG_DIR}/config.sh"
|
||||||
readonly LISTS_DIR="${CONFIG_DIR}/lists"
|
readonly LISTS_DIR="${CONFIG_DIR}/lists"
|
||||||
readonly POOL_DIR="${CONFIG_DIR}/pool"
|
readonly POOL_DIR="${CONFIG_DIR}/pool"
|
||||||
readonly POOL_FILE="${POOL_DIR}/pool.txt"
|
readonly POOL_FILE="${POOL_DIR}/pool.txt"
|
||||||
|
readonly POOL_SUM_FILE="${POOL_DIR}/.pool.sha256"
|
||||||
|
|
||||||
|
# Bump when config.sh gains new options; configs declaring an older
|
||||||
|
# CONFIG_VERSION (or none) get a nag until merged and re-stamped
|
||||||
|
readonly CONFIG_SCHEMA_VERSION=2
|
||||||
|
|
||||||
|
# Generic charset token: {name:N} or {name:N-M}
|
||||||
|
readonly TOKEN_RE='\{([a-z][a-z0-9_]*):([0-9]+)(-([0-9]+))?\}'
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -27,10 +36,14 @@ readonly POOL_FILE="${POOL_DIR}/pool.txt"
|
|||||||
ALPHA_CHARSET='0123456789ABCDEFGHJKMNPQRSTVWXYZ' # Crockford Base32 (32 chars, 5 bits)
|
ALPHA_CHARSET='0123456789ABCDEFGHJKMNPQRSTVWXYZ' # Crockford Base32 (32 chars, 5 bits)
|
||||||
SAFE_CHARSET='ACDEFGHJKMNPQRTWXY234679' # Ultra-safe (24 chars, 4.6 bits) - no B/8, 0/O, 1/I/L, 5/S
|
SAFE_CHARSET='ACDEFGHJKMNPQRTWXY234679' # Ultra-safe (24 chars, 4.6 bits) - no B/8, 0/O, 1/I/L, 5/S
|
||||||
NUM_CHARSET='0123456789'
|
NUM_CHARSET='0123456789'
|
||||||
SPECIAL_CHARSET='!?@#'
|
SPECIAL_CHARSET='!?@#%~=+'
|
||||||
WORD_MIN_LENGTH=4
|
WORD_MIN_LENGTH=4
|
||||||
WORD_MAX_LENGTH=8
|
WORD_MAX_LENGTH=8
|
||||||
|
|
||||||
|
# Custom charsets: "name|characters". Names are lowercase [a-z][a-z0-9_]*.
|
||||||
|
# Entries here override the built-in four (alpha, safe, num, special).
|
||||||
|
CHARSETS=()
|
||||||
|
|
||||||
TEMPLATES=(
|
TEMPLATES=(
|
||||||
"default|{word}-{word}-{alpha:5}"
|
"default|{word}-{word}-{alpha:5}"
|
||||||
"easy|{word}-{alpha:3}-{num:3}"
|
"easy|{word}-{alpha:3}-{num:3}"
|
||||||
@@ -91,15 +104,106 @@ shuffle_lines() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Refuse to trust files that aren't ours or that others can write to
|
||||||
|
# (ssh-config style check; the config file is sourced = code execution)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
require_safe_file() {
|
||||||
|
local f="$1"
|
||||||
|
[[ -e "$f" ]] || return 0
|
||||||
|
|
||||||
|
if [[ ! -O "$f" ]]; then
|
||||||
|
err "$f is not owned by you — refusing to use it"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local mode
|
||||||
|
case "$PLATFORM" in
|
||||||
|
macos) mode=$(stat -f '%Lp' "$f") ;;
|
||||||
|
*) mode=$(stat -c '%a' "$f") ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if (( (8#$mode & 8#022) != 0 )); then
|
||||||
|
err "$f is group/world-writable (mode $mode) — refusing to use it"
|
||||||
|
err "fix with: chmod go-w '$f'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Load user config if exists
|
# Load user config if exists
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
load_config() {
|
load_config() {
|
||||||
if [[ -f "$CONFIG_FILE" ]]; then
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
require_safe_file "$CONFIG_DIR" || exit 1
|
||||||
|
require_safe_file "$CONFIG_FILE" || exit 1
|
||||||
source "$CONFIG_FILE"
|
source "$CONFIG_FILE"
|
||||||
|
validate_charsets
|
||||||
|
check_config_version
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Nag (don't fail) when the user's config predates the current schema —
|
||||||
|
# new options like CHARSETS won't exist in it. Silenced by merging the new
|
||||||
|
# default config and setting CONFIG_VERSION to the current schema version.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
check_config_version() {
|
||||||
|
local have="${CONFIG_VERSION:-0}"
|
||||||
|
[[ "$have" =~ ^[0-9]+$ ]] || have=0
|
||||||
|
|
||||||
|
if (( have < CONFIG_SCHEMA_VERSION )); then
|
||||||
|
warn "config.sh is from an older himi (config v${have} < v${CONFIG_SCHEMA_VERSION}) — new options are missing"
|
||||||
|
warn "merge the repo's config.sh (or ${CONFIG_FILE}.new if the installer left one),"
|
||||||
|
warn "then set CONFIG_VERSION=${CONFIG_SCHEMA_VERSION} in it to silence this"
|
||||||
|
elif (( have > CONFIG_SCHEMA_VERSION )); then
|
||||||
|
warn "config.sh declares v${have} but this himi understands v${CONFIG_SCHEMA_VERSION} — update the binary?"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Sanity-check user-defined charsets (run once after config load)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
validate_charsets() {
|
||||||
|
local entry name set dupes
|
||||||
|
for entry in ${CHARSETS[@]+"${CHARSETS[@]}"}; do
|
||||||
|
IFS='|' read -r name set <<< "$entry"
|
||||||
|
if [[ ! "$name" =~ ^[a-z][a-z0-9_]*$ ]]; then
|
||||||
|
warn "charset '$name' ignored: names must match [a-z][a-z0-9_]*"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ -z "$set" ]]; then
|
||||||
|
warn "charset '$name' is empty"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
dupes=$(printf '%s' "$set" | grep -o . | sort | uniq -d | tr -d '\n')
|
||||||
|
if [[ -n "$dupes" ]]; then
|
||||||
|
warn "charset '$name' has duplicate chars ($dupes): distribution skewed, entropy overstated"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Look up a charset by name: user CHARSETS first, then built-ins
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
get_charset() {
|
||||||
|
local name="$1" entry cname cset
|
||||||
|
for entry in ${CHARSETS[@]+"${CHARSETS[@]}"}; do
|
||||||
|
IFS='|' read -r cname cset <<< "$entry"
|
||||||
|
if [[ "$cname" == "$name" ]]; then
|
||||||
|
printf '%s' "$cset"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
case "$name" in
|
||||||
|
alpha) printf '%s' "$ALPHA_CHARSET" ;;
|
||||||
|
safe) printf '%s' "$SAFE_CHARSET" ;;
|
||||||
|
num) printf '%s' "$NUM_CHARSET" ;;
|
||||||
|
special) printf '%s' "$SPECIAL_CHARSET" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Ensure directories exist
|
# Ensure directories exist
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -140,9 +244,15 @@ cmd_add() {
|
|||||||
|
|
||||||
init_dirs
|
init_dirs
|
||||||
|
|
||||||
# Derive name from filename (strip path and extension)
|
# Derive name from filename (strip path and extension, sanitize to [a-z0-9_-])
|
||||||
local name
|
local name
|
||||||
name=$(basename "$file" | sed 's/\.[^.]*$//' | tr 'A-Z' 'a-z')
|
name=$(basename "$file" | sed 's/\.[^.]*$//' | tr 'A-Z' 'a-z' | tr -cd 'a-z0-9_-')
|
||||||
|
|
||||||
|
if [[ -z "$name" ]]; then
|
||||||
|
err "cannot derive a valid list name from '$file'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
local listfile="${LISTS_DIR}/${name}.txt"
|
local listfile="${LISTS_DIR}/${name}.txt"
|
||||||
|
|
||||||
# Clean: lowercase, alpha only, sort, dedupe
|
# Clean: lowercase, alpha only, sort, dedupe
|
||||||
@@ -289,6 +399,39 @@ cmd_rebuild() {
|
|||||||
rebuild_pool
|
rebuild_pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Pool manifest: detect pool.txt changes made outside 'himi rebuild'
|
||||||
|
# (catches accidental edits, sync corruption, casual tampering; an attacker
|
||||||
|
# with write access to $HOME can defeat this, but they can also edit config.sh)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
sha256_file() {
|
||||||
|
if command -v sha256sum &>/dev/null; then
|
||||||
|
sha256sum "$1" | awk '{print $1}'
|
||||||
|
elif command -v shasum &>/dev/null; then
|
||||||
|
shasum -a 256 "$1" | awk '{print $1}'
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_pool_manifest() {
|
||||||
|
local sum
|
||||||
|
sum=$(sha256_file "$POOL_FILE") || return 0 # no hash tool: skip quietly
|
||||||
|
printf '%s\n' "$sum" > "$POOL_SUM_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_pool_manifest() {
|
||||||
|
[[ -f "$POOL_SUM_FILE" ]] || return 0 # pre-1.1 install: no manifest yet
|
||||||
|
local want have
|
||||||
|
want=$(<"$POOL_SUM_FILE")
|
||||||
|
have=$(sha256_file "$POOL_FILE") || return 0
|
||||||
|
if [[ "$want" != "$have" ]]; then
|
||||||
|
err "pool.txt changed outside 'himi rebuild' (checksum mismatch)"
|
||||||
|
warn "if this was you, run 'himi rebuild'; if not, inspect $POOL_DIR and $LISTS_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Rebuild pool from enabled lists (symlinks in pool/)
|
# Rebuild pool from enabled lists (symlinks in pool/)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -296,20 +439,25 @@ rebuild_pool() {
|
|||||||
local before=0
|
local before=0
|
||||||
[[ -f "$POOL_FILE" ]] && before=$(wc -l < "$POOL_FILE")
|
[[ -f "$POOL_FILE" ]] && before=$(wc -l < "$POOL_FILE")
|
||||||
|
|
||||||
# Only include symlinked files (enabled lists), exclude pool.txt itself
|
# Only include symlinked files (enabled lists), exclude pool.txt itself.
|
||||||
local enabled_lists
|
# find -exec is whitespace-safe (list names can contain '-' and '_' only,
|
||||||
enabled_lists=$(find "$POOL_DIR" -maxdepth 1 -type l -name '*.txt' 2>/dev/null)
|
# but belt and suspenders).
|
||||||
|
local enabled_count
|
||||||
|
enabled_count=$(find "$POOL_DIR" -maxdepth 1 -type l -name '*.txt' 2>/dev/null | wc -l)
|
||||||
|
|
||||||
if [[ -z "$enabled_lists" ]]; then
|
if (( enabled_count == 0 )); then
|
||||||
warn "no lists enabled"
|
warn "no lists enabled"
|
||||||
> "$POOL_FILE"
|
> "$POOL_FILE"
|
||||||
|
write_pool_manifest
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat $enabled_lists \
|
find "$POOL_DIR" -maxdepth 1 -type l -name '*.txt' -exec cat {} + \
|
||||||
| filter_words "$WORD_MIN_LENGTH" "$WORD_MAX_LENGTH" \
|
| filter_words "$WORD_MIN_LENGTH" "$WORD_MAX_LENGTH" \
|
||||||
> "$POOL_FILE"
|
> "$POOL_FILE"
|
||||||
|
|
||||||
|
write_pool_manifest
|
||||||
|
|
||||||
local after
|
local after
|
||||||
after=$(wc -l < "$POOL_FILE")
|
after=$(wc -l < "$POOL_FILE")
|
||||||
local diff=$((after - before))
|
local diff=$((after - before))
|
||||||
@@ -424,10 +572,20 @@ calc_log2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Get charset length
|
# Get charset length (characters, not bytes — multibyte-safe)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
charset_len() {
|
charset_len() {
|
||||||
printf "%s" "$1" | wc -c
|
local s="$1"
|
||||||
|
printf '%s' "${#s}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Count occurrences of a fixed token in a string (pure bash, no pipelines)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
count_token() {
|
||||||
|
local s="$1" t="$2" stripped
|
||||||
|
stripped="${s//"$t"/}"
|
||||||
|
printf '%d' $(( (${#s} - ${#stripped}) / ${#t} ))
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -446,8 +604,7 @@ calc_entropy() {
|
|||||||
# Count word variants (most specific first)
|
# Count word variants (most specific first)
|
||||||
# {word!} = pool + 1 bit (2 cases)
|
# {word!} = pool + 1 bit (2 cases)
|
||||||
local word_bang_count
|
local word_bang_count
|
||||||
word_bang_count=$(grep -o '{word!}' <<< "$template" | wc -l)
|
word_bang_count=$(count_token "$template" '{word!}')
|
||||||
word_bang_count="${word_bang_count:-0}"
|
|
||||||
if [[ "$word_bang_count" -gt 0 ]]; then
|
if [[ "$word_bang_count" -gt 0 ]]; then
|
||||||
entropy=$(awk -v e="$entropy" -v w="$word_bang_count" -v b="$word_bits" 'BEGIN { print e + w * (b + 1) }')
|
entropy=$(awk -v e="$entropy" -v w="$word_bang_count" -v b="$word_bits" 'BEGIN { print e + w * (b + 1) }')
|
||||||
template="${template//\{word!\}/}"
|
template="${template//\{word!\}/}"
|
||||||
@@ -455,8 +612,7 @@ calc_entropy() {
|
|||||||
|
|
||||||
# {Word!} = pool + log2(3) bits (3 cases)
|
# {Word!} = pool + log2(3) bits (3 cases)
|
||||||
local Word_bang_count
|
local Word_bang_count
|
||||||
Word_bang_count=$(grep -o '{Word!}' <<< "$template" | wc -l)
|
Word_bang_count=$(count_token "$template" '{Word!}')
|
||||||
Word_bang_count="${Word_bang_count:-0}"
|
|
||||||
if [[ "$Word_bang_count" -gt 0 ]]; then
|
if [[ "$Word_bang_count" -gt 0 ]]; then
|
||||||
entropy=$(awk -v e="$entropy" -v w="$Word_bang_count" -v b="$word_bits" 'BEGIN { print e + w * (b + log(3)/log(2)) }')
|
entropy=$(awk -v e="$entropy" -v w="$Word_bang_count" -v b="$word_bits" 'BEGIN { print e + w * (b + log(3)/log(2)) }')
|
||||||
template="${template//\{Word!\}/}"
|
template="${template//\{Word!\}/}"
|
||||||
@@ -464,64 +620,42 @@ calc_entropy() {
|
|||||||
|
|
||||||
# {WORD}, {Word}, {word} = pool bits only (no case entropy)
|
# {WORD}, {Word}, {word} = pool bits only (no case entropy)
|
||||||
local WORD_count Word_count word_count
|
local WORD_count Word_count word_count
|
||||||
WORD_count=$(grep -o '{WORD}' <<< "$template" | wc -l)
|
WORD_count=$(count_token "$template" '{WORD}')
|
||||||
WORD_count="${WORD_count:-0}"
|
|
||||||
template="${template//\{WORD\}/}"
|
template="${template//\{WORD\}/}"
|
||||||
|
|
||||||
Word_count=$(grep -o '{Word}' <<< "$template" | wc -l)
|
Word_count=$(count_token "$template" '{Word}')
|
||||||
Word_count="${Word_count:-0}"
|
|
||||||
template="${template//\{Word\}/}"
|
template="${template//\{Word\}/}"
|
||||||
|
|
||||||
word_count=$(grep -o '{word}' <<< "$template" | wc -l)
|
word_count=$(count_token "$template" '{word}')
|
||||||
word_count="${word_count:-0}"
|
|
||||||
|
|
||||||
local total_plain_words=$(( WORD_count + Word_count + word_count ))
|
local total_plain_words=$(( WORD_count + Word_count + word_count ))
|
||||||
if [[ "$total_plain_words" -gt 0 ]]; then
|
if [[ "$total_plain_words" -gt 0 ]]; then
|
||||||
entropy=$(awk -v e="$entropy" -v w="$total_plain_words" -v b="$word_bits" 'BEGIN { print e + (w * b) }')
|
entropy=$(awk -v e="$entropy" -v w="$total_plain_words" -v b="$word_bits" 'BEGIN { print e + (w * b) }')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Count {alpha:N} - extract N and calculate
|
# Generic charset tokens: {name:N} or {name:N-M}
|
||||||
while [[ "$template" =~ \{alpha:([0-9]+)\} ]]; do
|
# Entropy = log2( sum of charset_size^L for L in N..M )
|
||||||
local n="${BASH_REMATCH[1]}"
|
while [[ "$template" =~ $TOKEN_RE ]]; do
|
||||||
local charset_size
|
local token="${BASH_REMATCH[0]}"
|
||||||
charset_size=$(charset_len "$ALPHA_CHARSET")
|
local cname="${BASH_REMATCH[1]}"
|
||||||
local bits
|
local lmin="${BASH_REMATCH[2]}"
|
||||||
bits=$(awk -v n="$n" -v cs="$charset_size" 'BEGIN { printf "%.2f", n * (log(cs)/log(2)) }')
|
local lmax="${BASH_REMATCH[4]:-${BASH_REMATCH[2]}}"
|
||||||
entropy=$(awk -v e="$entropy" -v b="$bits" 'BEGIN { print e + b }')
|
lmin=$(( 10#$lmin ))
|
||||||
template="${template/\{alpha:$n\}/}"
|
lmax=$(( 10#$lmax ))
|
||||||
done
|
|
||||||
|
|
||||||
# Count {num:N}
|
local charset
|
||||||
while [[ "$template" =~ \{num:([0-9]+)\} ]]; do
|
if charset=$(get_charset "$cname") && (( lmin <= lmax )); then
|
||||||
local n="${BASH_REMATCH[1]}"
|
local cs_size bits
|
||||||
local charset_size
|
cs_size=$(charset_len "$charset")
|
||||||
charset_size=$(charset_len "$NUM_CHARSET")
|
bits=$(awk -v lmin="$lmin" -v lmax="$lmax" -v cs="$cs_size" 'BEGIN {
|
||||||
local bits
|
t = 0
|
||||||
bits=$(awk -v n="$n" -v cs="$charset_size" 'BEGIN { printf "%.2f", n * (log(cs)/log(2)) }')
|
for (l = lmin; l <= lmax; l++) t += cs ^ l
|
||||||
|
printf "%.4f", log(t) / log(2)
|
||||||
|
}')
|
||||||
entropy=$(awk -v e="$entropy" -v b="$bits" 'BEGIN { print e + b }')
|
entropy=$(awk -v e="$entropy" -v b="$bits" 'BEGIN { print e + b }')
|
||||||
template="${template/\{num:$n\}/}"
|
fi
|
||||||
done
|
# Unknown charsets and bad ranges contribute 0 bits; generation will warn.
|
||||||
|
template="${template/"$token"/}"
|
||||||
# Count {special:N}
|
|
||||||
while [[ "$template" =~ \{special:([0-9]+)\} ]]; do
|
|
||||||
local n="${BASH_REMATCH[1]}"
|
|
||||||
local charset_size
|
|
||||||
charset_size=$(charset_len "$SPECIAL_CHARSET")
|
|
||||||
local bits
|
|
||||||
bits=$(awk -v n="$n" -v cs="$charset_size" 'BEGIN { printf "%.2f", n * (log(cs)/log(2)) }')
|
|
||||||
entropy=$(awk -v e="$entropy" -v b="$bits" 'BEGIN { print e + b }')
|
|
||||||
template="${template/\{special:$n\}/}"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Count {safe:N}
|
|
||||||
while [[ "$template" =~ \{safe:([0-9]+)\} ]]; do
|
|
||||||
local n="${BASH_REMATCH[1]}"
|
|
||||||
local charset_size
|
|
||||||
charset_size=$(charset_len "$SAFE_CHARSET")
|
|
||||||
local bits
|
|
||||||
bits=$(awk -v n="$n" -v cs="$charset_size" 'BEGIN { printf "%.2f", n * (log(cs)/log(2)) }')
|
|
||||||
entropy=$(awk -v e="$entropy" -v b="$bits" 'BEGIN { print e + b }')
|
|
||||||
template="${template/\{safe:$n\}/}"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
printf "%.1f" "$entropy"
|
printf "%.1f" "$entropy"
|
||||||
@@ -535,13 +669,76 @@ pick_words() {
|
|||||||
shuffle_lines -n "$n" "$POOL_FILE"
|
shuffle_lines -n "$n" "$POOL_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Uniform random integer 0..n-1 from /dev/urandom
|
||||||
|
# (replaces bash $RANDOM, which is a weak PRNG; modulo bias for the n=2,3
|
||||||
|
# used here is ≤ 0.002% on 16 bits — negligible)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
rand_int() {
|
||||||
|
local n="$1" r
|
||||||
|
r=$(od -An -N2 -tu2 /dev/urandom | tr -d ' \n')
|
||||||
|
printf '%d' $(( r % n ))
|
||||||
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Generate random string from charset
|
# Generate random string from charset
|
||||||
|
# Indexes into the charset with rejection sampling: uniform, no modulo bias,
|
||||||
|
# no tr metacharacter pitfalls, no SIGPIPE games with pipefail.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
rand_chars() {
|
rand_chars() {
|
||||||
local charset="$1"
|
local charset="$1"
|
||||||
local count="$2"
|
local count="$2"
|
||||||
LC_ALL=C tr -dc "$charset" < /dev/urandom 2>/dev/null | head -c "$count"
|
local cs_len="${#charset}"
|
||||||
|
local out="" limit b idx
|
||||||
|
|
||||||
|
if (( cs_len < 1 || cs_len > 256 )); then
|
||||||
|
err "charset must have 1-256 characters (got $cs_len)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Largest multiple of cs_len that fits in a byte; reject bytes above it
|
||||||
|
limit=$(( 256 / cs_len * cs_len ))
|
||||||
|
|
||||||
|
while (( ${#out} < count )); do
|
||||||
|
for b in $(od -An -N64 -tu1 /dev/urandom); do
|
||||||
|
if (( b < limit )); then
|
||||||
|
idx=$(( b % cs_len ))
|
||||||
|
out+="${charset:idx:1}"
|
||||||
|
fi
|
||||||
|
(( ${#out} >= count )) && break
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s' "${out:0:count}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Pick a length in [lmin, lmax], weighted by charset_size^L so that every
|
||||||
|
# possible output string is equally likely (uniform length would skew toward
|
||||||
|
# short strings and tank the min-entropy)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
pick_length() {
|
||||||
|
local lmin="$1" lmax="$2" cs_len="$3"
|
||||||
|
|
||||||
|
if (( lmin == lmax )); then
|
||||||
|
printf '%s' "$lmin"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local r
|
||||||
|
r=$(od -An -N4 -tu4 /dev/urandom | tr -d ' \n')
|
||||||
|
|
||||||
|
awk -v r="$r" -v lmin="$lmin" -v lmax="$lmax" -v cs="$cs_len" 'BEGIN {
|
||||||
|
total = 0
|
||||||
|
for (l = lmin; l <= lmax; l++) total += cs ^ l
|
||||||
|
x = (r / 4294967296.0) * total
|
||||||
|
acc = 0
|
||||||
|
for (l = lmin; l <= lmax; l++) {
|
||||||
|
acc += cs ^ l
|
||||||
|
if (x < acc) { print l; exit }
|
||||||
|
}
|
||||||
|
print lmax
|
||||||
|
}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -568,71 +765,66 @@ expand_template() {
|
|||||||
# {word!} - random lower/UPPER (+1 bit)
|
# {word!} - random lower/UPPER (+1 bit)
|
||||||
while [[ "$result" =~ \{word!\} ]]; do
|
while [[ "$result" =~ \{word!\} ]]; do
|
||||||
word=$(pick_words 1)
|
word=$(pick_words 1)
|
||||||
if (( RANDOM % 2 )); then
|
if (( $(rand_int 2) )); then
|
||||||
word=$(printf '%s' "$word" | to_upper)
|
word=$(printf '%s' "$word" | to_upper)
|
||||||
fi
|
fi
|
||||||
result="${result/\{word!\}/$word}"
|
result="${result/\{word!\}/"$word"}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# {Word!} - random lower/Title/UPPER (+1.58 bits)
|
# {Word!} - random lower/Title/UPPER (+1.58 bits)
|
||||||
while [[ "$result" =~ \{Word!\} ]]; do
|
while [[ "$result" =~ \{Word!\} ]]; do
|
||||||
word=$(pick_words 1)
|
word=$(pick_words 1)
|
||||||
case $(( RANDOM % 3 )) in
|
case "$(rand_int 3)" in
|
||||||
1) word=$(printf '%s' "$word" | to_title) ;;
|
1) word=$(printf '%s' "$word" | to_title) ;;
|
||||||
2) word=$(printf '%s' "$word" | to_upper) ;;
|
2) word=$(printf '%s' "$word" | to_upper) ;;
|
||||||
esac
|
esac
|
||||||
result="${result/\{Word!\}/$word}"
|
result="${result/\{Word!\}/"$word"}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# {WORD} - all upper
|
# {WORD} - all upper
|
||||||
while [[ "$result" =~ \{WORD\} ]]; do
|
while [[ "$result" =~ \{WORD\} ]]; do
|
||||||
word=$(pick_words 1 | to_upper)
|
word=$(pick_words 1 | to_upper)
|
||||||
result="${result/\{WORD\}/$word}"
|
result="${result/\{WORD\}/"$word"}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# {Word} - title case
|
# {Word} - title case
|
||||||
while [[ "$result" =~ \{Word\} ]]; do
|
while [[ "$result" =~ \{Word\} ]]; do
|
||||||
word=$(pick_words 1 | to_title)
|
word=$(pick_words 1 | to_title)
|
||||||
result="${result/\{Word\}/$word}"
|
result="${result/\{Word\}/"$word"}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# {word} - all lower (default)
|
# {word} - all lower (default)
|
||||||
while [[ "$result" =~ \{word\} ]]; do
|
while [[ "$result" =~ \{word\} ]]; do
|
||||||
word=$(pick_words 1)
|
word=$(pick_words 1)
|
||||||
result="${result/\{word\}/$word}"
|
result="${result/\{word\}/"$word"}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Replace {alpha:N}
|
# Generic charset tokens: {name:N} or {name:N-M}
|
||||||
while [[ "$result" =~ \{alpha:([0-9]+)\} ]]; do
|
while [[ "$result" =~ $TOKEN_RE ]]; do
|
||||||
local n="${BASH_REMATCH[1]}"
|
local token="${BASH_REMATCH[0]}"
|
||||||
local chars
|
local cname="${BASH_REMATCH[1]}"
|
||||||
chars=$(rand_chars "$ALPHA_CHARSET" "$n")
|
local lmin="${BASH_REMATCH[2]}"
|
||||||
result="${result/\{alpha:$n\}/$chars}"
|
local lmax="${BASH_REMATCH[4]:-${BASH_REMATCH[2]}}"
|
||||||
|
lmin=$(( 10#$lmin ))
|
||||||
|
lmax=$(( 10#$lmax ))
|
||||||
|
|
||||||
|
local charset n chars
|
||||||
|
if ! charset=$(get_charset "$cname") || (( lmin > lmax )); then
|
||||||
|
# Unknown charset or bad range: neutralize the opening brace so the
|
||||||
|
# loop can move on; restored below so the leftover-token warning fires.
|
||||||
|
result="${result/"$token"/$'\x01'${token:1}}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
n=$(pick_length "$lmin" "$lmax" "$(charset_len "$charset")")
|
||||||
|
chars=$(rand_chars "$charset" "$n")
|
||||||
|
# Quoted replacement: keeps bash 5.2's patsub_replacement from
|
||||||
|
# interpreting '&' in user charsets as "the matched text"
|
||||||
|
result="${result/"$token"/"$chars"}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Replace {num:N}
|
# Restore any neutralized tokens so they're visible (and warnable)
|
||||||
while [[ "$result" =~ \{num:([0-9]+)\} ]]; do
|
result="${result//$'\x01'/\{}"
|
||||||
local n="${BASH_REMATCH[1]}"
|
|
||||||
local chars
|
|
||||||
chars=$(rand_chars "$NUM_CHARSET" "$n")
|
|
||||||
result="${result/\{num:$n\}/$chars}"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Replace {special:N}
|
|
||||||
while [[ "$result" =~ \{special:([0-9]+)\} ]]; do
|
|
||||||
local n="${BASH_REMATCH[1]}"
|
|
||||||
local chars
|
|
||||||
chars=$(rand_chars "$SPECIAL_CHARSET" "$n")
|
|
||||||
result="${result/\{special:$n\}/$chars}"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Replace {safe:N}
|
|
||||||
while [[ "$result" =~ \{safe:([0-9]+)\} ]]; do
|
|
||||||
local n="${BASH_REMATCH[1]}"
|
|
||||||
local chars
|
|
||||||
chars=$(rand_chars "$SAFE_CHARSET" "$n")
|
|
||||||
result="${result/\{safe:$n\}/$chars}"
|
|
||||||
done
|
|
||||||
|
|
||||||
printf "%s" "$result"
|
printf "%s" "$result"
|
||||||
}
|
}
|
||||||
@@ -655,6 +847,8 @@ get_template() {
|
|||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Copy to clipboard (cross-platform)
|
# Copy to clipboard (cross-platform)
|
||||||
|
# Consumes stdin in all cases; NEVER leaks the secret to stdout (the caller
|
||||||
|
# decides what to print — this used to double-print on headless boxes)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
to_clipboard() {
|
to_clipboard() {
|
||||||
case "$PLATFORM" in
|
case "$PLATFORM" in
|
||||||
@@ -669,14 +863,12 @@ to_clipboard() {
|
|||||||
elif command -v wl-copy &>/dev/null; then
|
elif command -v wl-copy &>/dev/null; then
|
||||||
wl-copy
|
wl-copy
|
||||||
else
|
else
|
||||||
cat # fallback: output to stdout
|
cat > /dev/null
|
||||||
warn "install xclip, xsel, or wl-copy for clipboard"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
cat # fallback: output to stdout
|
cat > /dev/null
|
||||||
warn "clipboard not supported on this platform"
|
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -758,11 +950,19 @@ cmd_test() {
|
|||||||
printf " ${BOLD}Entropy:${NC} ≈%s bits\n" "$entropy"
|
printf " ${BOLD}Entropy:${NC} ≈%s bits\n" "$entropy"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
printf " ${BOLD}Examples:${NC}\n"
|
printf " ${BOLD}Examples:${NC}\n"
|
||||||
|
local example leftovers=""
|
||||||
for ((i = 0; i < 5; i++)); do
|
for ((i = 0; i < 5; i++)); do
|
||||||
printf " %s\n" "$(expand_template "$pattern")"
|
example=$(expand_template "$pattern")
|
||||||
|
[[ "$example" == *'{'* && "$example" == *'}'* ]] && leftovers="true"
|
||||||
|
printf " %s\n" "$example"
|
||||||
done
|
done
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
|
||||||
|
if [[ -n "$leftovers" ]]; then
|
||||||
|
warn "pattern contains unexpanded {token}s — unknown charset or typo? (0 bits counted for them)"
|
||||||
|
printf "\n"
|
||||||
|
fi
|
||||||
|
|
||||||
# Show comparison with built-in templates
|
# Show comparison with built-in templates
|
||||||
printf " ${DIM}%-12s %s${NC}\n" "TEMPLATE" "ENTROPY"
|
printf " ${DIM}%-12s %s${NC}\n" "TEMPLATE" "ENTROPY"
|
||||||
printf " ${DIM}%-12s %s${NC}\n" "────────────" "───────"
|
printf " ${DIM}%-12s %s${NC}\n" "────────────" "───────"
|
||||||
@@ -784,12 +984,16 @@ cmd_generate() {
|
|||||||
local template_name="${1:-$DEFAULT_TEMPLATE}"
|
local template_name="${1:-$DEFAULT_TEMPLATE}"
|
||||||
local count="${2:-1}"
|
local count="${2:-1}"
|
||||||
local use_stdout="${3:-false}"
|
local use_stdout="${3:-false}"
|
||||||
|
local use_clipboard="${4:-true}"
|
||||||
|
|
||||||
if [[ ! -f "$POOL_FILE" ]] || [[ ! -s "$POOL_FILE" ]]; then
|
if [[ ! -f "$POOL_FILE" ]] || [[ ! -s "$POOL_FILE" ]]; then
|
||||||
err "pool empty — run 'himi rebuild' first"
|
err "pool empty — run 'himi rebuild' first"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Refuse if pool.txt was modified behind himi's back
|
||||||
|
verify_pool_manifest || return 1
|
||||||
|
|
||||||
# Sanity check: ensure pool has enough unique words for meaningful entropy
|
# Sanity check: ensure pool has enough unique words for meaningful entropy
|
||||||
local unique_words
|
local unique_words
|
||||||
unique_words=$(sort -u "$POOL_FILE" | wc -l)
|
unique_words=$(sort -u "$POOL_FILE" | wc -l)
|
||||||
@@ -808,13 +1012,22 @@ cmd_generate() {
|
|||||||
secrets+=$(expand_template "$pattern")
|
secrets+=$(expand_template "$pattern")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Always copy to clipboard
|
if [[ "$secrets" == *'{'* && "$secrets" == *'}'* ]]; then
|
||||||
if printf "%s" "$secrets" | to_clipboard; then
|
warn "output contains unexpanded {token}s — check the template ('himi test' helps)"
|
||||||
ok "copied to clipboard"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Also print to stdout if -o was passed
|
local copied="false"
|
||||||
if [[ "$use_stdout" == "true" ]]; then
|
if [[ "$use_clipboard" == "true" ]]; then
|
||||||
|
if printf "%s" "$secrets" | to_clipboard; then
|
||||||
|
ok "copied to clipboard"
|
||||||
|
copied="true"
|
||||||
|
else
|
||||||
|
warn "clipboard unavailable (install xclip, xsel, or wl-copy) — printing instead"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print if asked for stdout, or if the secret would otherwise be lost
|
||||||
|
if [[ "$use_stdout" == "true" || "$copied" == "false" ]]; then
|
||||||
printf "%s\n" "$secrets"
|
printf "%s\n" "$secrets"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -829,6 +1042,7 @@ himi v${VERSION} — memorable secret generator
|
|||||||
Usage:
|
Usage:
|
||||||
himi [template] Generate → clipboard (default: $DEFAULT_TEMPLATE)
|
himi [template] Generate → clipboard (default: $DEFAULT_TEMPLATE)
|
||||||
himi -o [template] Generate → clipboard + stdout
|
himi -o [template] Generate → clipboard + stdout
|
||||||
|
himi -q [template] Generate → stdout only (no clipboard; or HIMI_NO_CLIP=1)
|
||||||
himi -n N [template] Generate N secrets
|
himi -n N [template] Generate N secrets
|
||||||
|
|
||||||
himi add <file> Add and enable a word list
|
himi add <file> Add and enable a word list
|
||||||
@@ -858,10 +1072,24 @@ Tokens:
|
|||||||
{Word} Random word (Title Case)
|
{Word} Random word (Title Case)
|
||||||
{word!} Random word (lower or UPPER, +1 bit)
|
{word!} Random word (lower or UPPER, +1 bit)
|
||||||
{Word!} Random word (lower/Title/UPPER, +1.58 bits)
|
{Word!} Random word (lower/Title/UPPER, +1.58 bits)
|
||||||
{alpha:N} N chars, Crockford Base32 (5.0 bits/char)
|
{NAME:N} N chars from charset NAME
|
||||||
{safe:N} N chars, ultra-safe (4.6 bits/char)
|
{NAME:N-M} N to M chars from charset NAME (weighted so every
|
||||||
{num:N} N digits (3.3 bits/char)
|
output is equally likely; entropy = log2 of total outcomes)
|
||||||
{special:N} N from: $SPECIAL_CHARSET
|
|
||||||
|
Charsets:
|
||||||
|
EOF
|
||||||
|
printf " %-12s %s\n" "alpha" "$ALPHA_CHARSET"
|
||||||
|
printf " %-12s %s\n" "safe" "$SAFE_CHARSET"
|
||||||
|
printf " %-12s %s\n" "num" "$NUM_CHARSET"
|
||||||
|
printf " %-12s %s\n" "special" "$SPECIAL_CHARSET"
|
||||||
|
for entry in ${CHARSETS[@]+"${CHARSETS[@]}"}; do
|
||||||
|
IFS='|' read -r name pattern <<< "$entry"
|
||||||
|
printf " %-12s %s\n" "$name" "$pattern"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Custom charsets: add "name|chars" entries to CHARSETS in config
|
||||||
|
|
||||||
Config: $CONFIG_FILE
|
Config: $CONFIG_FILE
|
||||||
Pool: $POOL_FILE
|
Pool: $POOL_FILE
|
||||||
@@ -888,15 +1116,22 @@ main() {
|
|||||||
|
|
||||||
# 2. If it's not a command, parse generation flags and templates
|
# 2. If it's not a command, parse generation flags and templates
|
||||||
local use_stdout="false"
|
local use_stdout="false"
|
||||||
|
local use_clipboard="true"
|
||||||
local count=1
|
local count=1
|
||||||
local template_name=""
|
local template_name=""
|
||||||
|
|
||||||
|
[[ -n "${HIMI_NO_CLIP:-}" ]] && use_clipboard="false"
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-o|--stdout)
|
-o|--stdout)
|
||||||
use_stdout="true"
|
use_stdout="true"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
-q|--quiet)
|
||||||
|
use_clipboard="false"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-n|--number)
|
-n|--number)
|
||||||
if [[ -z "${2:-}" || ! "${2:-}" =~ ^[1-9][0-9]*$ ]]; then
|
if [[ -z "${2:-}" || ! "${2:-}" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
err "-n requires a positive integer (got: '${2:-}')"
|
err "-n requires a positive integer (got: '${2:-}')"
|
||||||
@@ -926,7 +1161,7 @@ main() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
[[ -z "$template_name" ]] && template_name="$DEFAULT_TEMPLATE"
|
[[ -z "$template_name" ]] && template_name="$DEFAULT_TEMPLATE"
|
||||||
cmd_generate "$template_name" "$count" "$use_stdout"
|
cmd_generate "$template_name" "$count" "$use_stdout" "$use_clipboard"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
+6
-2
@@ -7,7 +7,7 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BIN_DIR="${HOME}/.local/bin"
|
BIN_DIR="${HOME}/.local/bin"
|
||||||
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
||||||
BASH_COMPLETION_DIR="${XDG_DATA_HOME:-$HOME/.local/share/bash-completion/completions}"
|
BASH_COMPLETION_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions"
|
||||||
ZSH_COMPLETION_DIR="${HOME}/.zfunc"
|
ZSH_COMPLETION_DIR="${HOME}/.zfunc"
|
||||||
|
|
||||||
# Define markers for removal later
|
# Define markers for removal later
|
||||||
@@ -89,8 +89,12 @@ install_config() {
|
|||||||
if [[ ! -f "${CONFIG_DIR}/config.sh" ]]; then
|
if [[ ! -f "${CONFIG_DIR}/config.sh" ]]; then
|
||||||
cp "${SCRIPT_DIR}/config.sh" "${CONFIG_DIR}/"
|
cp "${SCRIPT_DIR}/config.sh" "${CONFIG_DIR}/"
|
||||||
ok "Installed default config to $CONFIG_DIR"
|
ok "Installed default config to $CONFIG_DIR"
|
||||||
|
elif ! cmp -s "${SCRIPT_DIR}/config.sh" "${CONFIG_DIR}/config.sh"; then
|
||||||
|
cp "${SCRIPT_DIR}/config.sh" "${CONFIG_DIR}/config.sh.new"
|
||||||
|
warn "Kept your existing config; new default saved as config.sh.new"
|
||||||
|
warn "Merge what you want, then set CONFIG_VERSION to match (himi will nag until you do)"
|
||||||
else
|
else
|
||||||
dim "Config already exists, skipping.\n"
|
dim "Config already up to date, skipping.\n"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
err "config.sh missing from installer directory"
|
err "config.sh missing from installer directory"
|
||||||
|
|||||||
Reference in New Issue
Block a user