migration
This commit is contained in:
@@ -1,3 +1,213 @@
|
|||||||
# himitsu
|
# himitsu
|
||||||
|
|
||||||
himitsu - secrets generation using custom word-lists, character sets, and templating
|
Memorable secret generator with configurable word lists and templates.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires GNU coreutils (macOS: brew install coreutils)
|
||||||
|
git clone <repo> && cd himitsu
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Generating secrets
|
||||||
|
```bash
|
||||||
|
himi # generate → clipboard
|
||||||
|
himi -o # generate → clipboard + stdout
|
||||||
|
himi -o secure # use 'secure' template → clipboard + stdout
|
||||||
|
himi -n 5 -o easy # generate 5 'easy' secrets → clipboard + stdout
|
||||||
|
himi test '{word}-{num:2}' # test a custom template
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use in scripts
|
||||||
|
```bash
|
||||||
|
# SSH key with generated passphrase (on clipboard too, ready to save)
|
||||||
|
ssh-keygen -t ed25519 -f mykey -P "$(himi -o)"
|
||||||
|
|
||||||
|
# Encrypt a file
|
||||||
|
openssl enc -aes-256-cbc -pbkdf2 -in secrets.tar -out secrets.enc -k "$(himi -o secure)"
|
||||||
|
|
||||||
|
# Generate and reuse in the same session
|
||||||
|
PASS="$(himi -o)"
|
||||||
|
htpasswd -b -c .htpasswd admin "$PASS"
|
||||||
|
|
||||||
|
# Wi-Fi password for a new AP
|
||||||
|
nmcli dev wifi hotspot ssid mynet password "$(himi -o wifi)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managing word lists and config
|
||||||
|
```bash
|
||||||
|
himi list # show lists and enabled status
|
||||||
|
himi add pokemon.txt # add + enable a list
|
||||||
|
himi disable pokemon # disable a list
|
||||||
|
himi enable pokemon # re-enable
|
||||||
|
himi rebuild # regenerate pool after manual changes
|
||||||
|
himi config # open config in $EDITOR (auto-rebuilds on change)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking entropy
|
||||||
|
```bash
|
||||||
|
himi entropy # show all templates with entropy
|
||||||
|
himi entropy secure # show specific template
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
TEMPLATE EXAMPLE ENTROPY
|
||||||
|
-------- ------- -------
|
||||||
|
default MAPLE42-river17-K7M93 ≈57.9 bits
|
||||||
|
easy crane-B4R-842 ≈38.8 bits
|
||||||
|
secure helm-river-A3K9P-fox-M2X8R! ≈94.4 bits
|
||||||
|
pin 483927 ≈19.9 bits
|
||||||
|
diceware crane-helm-tiger-river-maple ≈69.0 bits
|
||||||
|
wifi TIGER-maple4827 ≈42.9 bits
|
||||||
|
phone ACFG-KM3Y-XTWD ≈55.0 bits
|
||||||
|
```
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
Edit `~/.config/himi/config.sh` to customize:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TEMPLATES=(
|
||||||
|
"default|{word!}{num:2}-{word!}{num:2}-{alpha:3}{num:2}"
|
||||||
|
"easy|{word}-{alpha:3}-{num:3}"
|
||||||
|
"secure|{word}-{word}-{alpha:5}-{word}-{alpha:5}{special:1}"
|
||||||
|
"pin|{num:6}"
|
||||||
|
"diceware|{word}-{word}-{word}-{word}-{word}"
|
||||||
|
"wifi|{word!}-{word!}{num:4}"
|
||||||
|
"phone|{safe:4}-{safe:4}-{safe:4}"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tokens:**
|
||||||
|
| Token | Output | Entropy |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| `{word}` | `maple` | ~13.8 bits* |
|
||||||
|
| `{WORD}` | `MAPLE` | ~13.8 bits* |
|
||||||
|
| `{Word}` | `Maple` | ~13.8 bits* |
|
||||||
|
| `{word!}` | `maple` or `MAPLE` | ~14.8 bits* |
|
||||||
|
| `{Word!}` | `maple`/`Maple`/`MAPLE` | ~15.4 bits* |
|
||||||
|
| `{alpha:N}` | N chars, Crockford Base32 | ~5.0 bits/char |
|
||||||
|
| `{safe:N}` | N chars, ultra-safe (no ambiguous chars) | ~4.6 bits/char |
|
||||||
|
| `{num:N}` | N digits | ~3.3 bits/char |
|
||||||
|
| `{special:N}` | N from `!?@#%~=+` | ~3.0 bits/char |
|
||||||
|
|
||||||
|
\* Word entropy depends on pool size. Values shown assume all built-in lists enabled (~14k words, ~13.8 bits/word).
|
||||||
|
|
||||||
|
## Entropy rules of thumb
|
||||||
|
|
||||||
|
| Bits | Strength | Use case |
|
||||||
|
|------|----------|----------|
|
||||||
|
| ~40 | Casual | Low-value accounts, rate-limited logins |
|
||||||
|
| ~50 | Reasonable | Personal accounts |
|
||||||
|
| ~60 | Strong | Important accounts, recommended minimum |
|
||||||
|
| ~80 | Very strong | High-value targets, offline cracking resistance |
|
||||||
|
| ~128 | Paranoid | Nation-state threat models |
|
||||||
|
|
||||||
|
Online attacks (rate-limited) need far less entropy than offline attacks (cracking leaked hashes). When in doubt, aim for 60+ bits.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
himi uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
## Included Wordlists
|
||||||
|
|
||||||
|
Word lists are curled from GitHub and cleaned up. Here's how each built-in list was made, you can use these as examples in case you want to add your own. The built-in wordlists provide 15175 total words, after deduplication.
|
||||||
|
|
||||||
|
### bips.txt (2048 words)
|
||||||
|
```bash
|
||||||
|
curl 'https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt' | \
|
||||||
|
tr -d '" ,[]\t' | tr 'A-Z' 'a-z' |
|
||||||
|
sed '/^$/d' | sort -u > bips.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### eff.txt (7776 words)
|
||||||
|
```bash
|
||||||
|
curl 'https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt | cut -f2' | \
|
||||||
|
tr -d '" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u > eff.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### female_names.txt (4775 words)
|
||||||
|
```bash
|
||||||
|
curl 'https://raw.githubusercontent.com/arineng/arincli/refs/heads/master/lib/female-first-names.txt' \
|
||||||
|
| tr -d '\" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u > ftemp
|
||||||
|
curl 'https://raw.githubusercontent.com/attackdebris/kerberos_enum_userlists/refs/heads/master/Female_First_Names_Top_500.txt' \
|
||||||
|
| tr -d '" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u >> ftemp
|
||||||
|
curl 'https://raw.githubusercontent.com/righteousgambit/quiet-riot/refs/heads/main/wordlists/femalenames-usa-top1000.txt' \
|
||||||
|
| tr -d '" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u >> ftemp
|
||||||
|
cat ftemp | sort -u > female_names.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### male_names.txt (1403 words)
|
||||||
|
```bash
|
||||||
|
curl 'https://raw.githubusercontent.com/arineng/arincli/refs/heads/master/lib/male-first-names.txt' \
|
||||||
|
| tr -d '" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u > mtemp
|
||||||
|
curl 'https://raw.githubusercontent.com/attackdebris/kerberos_enum_userlists/refs/heads/master/Male_First_Names_Top_500.txt' \
|
||||||
|
| tr -d '" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u >> mtemp
|
||||||
|
curl 'https://raw.githubusercontent.com/righteousgambit/quiet-riot/refs/heads/main/wordlists/malenames-usa-top1000.txt' \
|
||||||
|
| tr -d '" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u >> mtemp
|
||||||
|
cat mtemp | sort -u > male_names.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### pokemon.txt (1024 words)
|
||||||
|
```bash
|
||||||
|
curl 'https://raw.githubusercontent.com/sindresorhus/pokemon/main/data/en.json' \
|
||||||
|
| tr -d '" ,[]\t' | tr 'A-Z' 'a-z' \
|
||||||
|
| sed '/^$/d' | sort -u > pokemon.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### greek_gods.txt (445 words)
|
||||||
|
```bash
|
||||||
|
curl 'https://raw.githubusercontent.com/katkaypettitt/greek-gods/refs/heads/main/greek_gods.csv' \
|
||||||
|
| cut -d ',' -f1 | tr -d ' [],\"' |tr 'A-Z' 'a-z' | sort -u > greek_gods.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### colors.txt (5611 words)
|
||||||
|
```bash
|
||||||
|
curl -Ls 'https://raw.githubusercontent.com/meodai/color-names/refs/heads/main/src/colornames.csv' | \
|
||||||
|
awk -F',' 'NR > 1 {print $1}' | grep -E "^[A-Za-z]{4,12}$" | tr 'A-Z' 'a-z' \
|
||||||
|
>> colors.tmp
|
||||||
|
curl -Ls 'https://raw.githubusercontent.com/k-kawakami/colorfulnet/refs/heads/master/example_data/wikipedia-list-of-colors.txt' | \
|
||||||
|
tr 'A-Z ' 'a-z\n' | tr -d '()' | grep -E "^[a-z]{4,12}$" | sort -u >> colors.tmp
|
||||||
|
cat colors.tmp | sort -u | tee colors.txt | wc -l
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### food.txt (1307 words)
|
||||||
|
```bash
|
||||||
|
curl -Ls 'https://raw.githubusercontent.com/schollz/food-identicon/master/ingredients.txt' | \
|
||||||
|
grep -Ev "(\ |:|â|¼|ã|\.)" | tr 'A-Z' 'a-z' | \
|
||||||
|
grep -E "^[a-z-]{4,12}$" >> food.tmp
|
||||||
|
curl -Ls 'https://gist.githubusercontent.com/peterdemin/920ec3eaaa0a9f3cafd3a855557f5e0c/raw/9c7337d7f6274de704f9018ed363d51dd7a0b128/food.txt' | \
|
||||||
|
tr ' -' '\n' | tr 'A-Z' 'a-z' | \
|
||||||
|
grep -E "^[a-z]{4,12}$" | sort -u >> food.tmp
|
||||||
|
cat food.tmp | sort -u | tee food.txt | wc -l
|
||||||
|
```
|
||||||
|
|
||||||
|
### animals.txt (1109 words)
|
||||||
|
```bash
|
||||||
|
curl -Ls 'https://raw.githubusercontent.com/skjorrface/animals.txt/refs/heads/master/animals.txt' | \
|
||||||
|
tr ' ' '\n' | tr 'A-Z' 'a-z' | grep -E "^[a-z-]{4,12}$" | \
|
||||||
|
sort -u >> animals.tmp
|
||||||
|
curl -Ls 'https://raw.githubusercontent.com/timgates42/animal-list-factory/refs/heads/master/animals.txt' | \
|
||||||
|
tr 'A-Z' 'a-z' | grep -E "^[a-z-]{4,12}$" | \
|
||||||
|
sort -u >> animals.tmp
|
||||||
|
curl -Ls 'https://raw.githubusercontent.com/juliandefreitas/entity_lists/refs/heads/master/animals.csv' | \
|
||||||
|
tr 'A-Z' 'a-z' | grep -E "^[a-z-]{4,12}$" | \
|
||||||
|
sort -u >> animals.tmp
|
||||||
|
curl -Ls 'https://raw.githubusercontent.com/MikeInnes/SourceWalk.jl/refs/heads/master/animals.txt' | \
|
||||||
|
tr 'A-Z' 'a-z' | grep -E "^[a-z-]{4,12}$" | \
|
||||||
|
sort -u >> animals.tmp
|
||||||
|
cat animals.tmp | sort -u | tee animals.txt | wc -l
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#compdef himi
|
||||||
|
|
||||||
|
# himi zsh completion
|
||||||
|
# Drop in ~/.zfunc/ then run: compinit
|
||||||
|
|
||||||
|
_himi() {
|
||||||
|
local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
||||||
|
local lists_dir="${config_dir}/lists"
|
||||||
|
local pool_dir="${config_dir}/pool"
|
||||||
|
|
||||||
|
# Get template names from config
|
||||||
|
local -a templates
|
||||||
|
if [[ -f "${config_dir}/config.sh" ]]; then
|
||||||
|
templates=(${(f)"$(grep -E '^\s*"[a-z]+\|' "${config_dir}/config.sh" 2>/dev/null | sed 's/.*"\([a-z]*\)|.*/\1/')"})
|
||||||
|
fi
|
||||||
|
[[ ${#templates} -eq 0 ]] && templates=(default easy secure pin diceware)
|
||||||
|
|
||||||
|
# Get list names
|
||||||
|
local -a all_lists enabled_lists disabled_lists
|
||||||
|
disabled_lists=(all)
|
||||||
|
enabled_lists=(all)
|
||||||
|
if [[ -d "$lists_dir" ]]; then
|
||||||
|
for f in "$lists_dir"/*.txt(N); do
|
||||||
|
local name=${${f:t}%.txt}
|
||||||
|
all_lists+=($name)
|
||||||
|
if [[ -L "$pool_dir/$name.txt" ]]; then
|
||||||
|
enabled_lists+=($name)
|
||||||
|
else
|
||||||
|
disabled_lists+=($name)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a commands
|
||||||
|
commands=(
|
||||||
|
'add:Add and enable a word list'
|
||||||
|
'enable:Enable word list(s), or all'
|
||||||
|
'disable:Disable word list(s), or all'
|
||||||
|
'remove:Delete a word list permanently'
|
||||||
|
'rebuild:Rebuild pool from enabled lists'
|
||||||
|
'list:Show all lists and status'
|
||||||
|
'entropy:Show entropy for templates'
|
||||||
|
'test:Test a custom pattern'
|
||||||
|
'config:Edit config in $EDITOR'
|
||||||
|
'uninstall:Remove himi and all data'
|
||||||
|
)
|
||||||
|
|
||||||
|
_arguments -C \
|
||||||
|
'1:command:->cmd' \
|
||||||
|
'*:argument:->args' \
|
||||||
|
'-o[Output to stdout]' \
|
||||||
|
'-n[Number of secrets]:count:(1 3 5 10)' \
|
||||||
|
'-h[Show help]' \
|
||||||
|
'--help[Show help]'
|
||||||
|
|
||||||
|
case "$state" in
|
||||||
|
cmd)
|
||||||
|
_describe -t commands 'commands' commands
|
||||||
|
_describe -t templates 'templates' templates
|
||||||
|
;;
|
||||||
|
args)
|
||||||
|
case "${words[2]}" in
|
||||||
|
add)
|
||||||
|
_files -g '*.txt'
|
||||||
|
;;
|
||||||
|
enable)
|
||||||
|
_describe -t lists 'available lists' disabled_lists
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
_describe -t lists 'enabled lists' enabled_lists
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
_describe -t lists 'available lists' all_lists
|
||||||
|
;;
|
||||||
|
entropy)
|
||||||
|
_describe -t templates 'templates' templates
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
_message 'pattern (e.g. {word}-{num:3})'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
_himi "$@"
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# secret config — source'd by the secret command
|
||||||
|
# Location: ~/.config/himi/config.sh
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Character sets for random portions
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Crockford Base32 (32 chars, 5.0 bits/char)
|
||||||
|
ALPHA_CHARSET='0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
||||||
|
# Ultra-safe - no easily confused characters (24 chars, 4.6 bits/char)
|
||||||
|
SAFE_CHARSET='ACDEFGHJKMNPQRTWXY234679'
|
||||||
|
NUM_CHARSET='0123456789'
|
||||||
|
SPECIAL_CHARSET='!?@#%~=+'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Word filtering (applied when building pool.txt)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
WORD_MIN_LENGTH=4
|
||||||
|
WORD_MAX_LENGTH=9
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Templates
|
||||||
|
# Format: "name|pattern"
|
||||||
|
#
|
||||||
|
# Tokens:
|
||||||
|
# {word} — random word (lowercase)
|
||||||
|
# {WORD} — random word (UPPERCASE)
|
||||||
|
# {Word} — random word (Title Case)
|
||||||
|
# {word!} — random lower or UPPER (+1 bit entropy)
|
||||||
|
# {Word!} — random lower/Title/UPPER (+1.58 bits entropy)
|
||||||
|
# {alpha:N} — N chars, Crockford Base32 (5.0 bits/char)
|
||||||
|
# {safe:N} — N chars, ultra-safe (4.6 bits/char)
|
||||||
|
# {num:N} — N digits (3.3 bits/char)
|
||||||
|
# {special:N} — N characters from SPECIAL_CHARSET
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
TEMPLATES=(
|
||||||
|
"default|{word!}{num:2}-{word!}{num:2}-{alpha:3}{num:2}"
|
||||||
|
"easy|{word}-{alpha:3}-{num:3}"
|
||||||
|
"secure|{word}-{word}-{alpha:5}-{word}-{alpha:5}{special:1}"
|
||||||
|
"pin|{num:6}"
|
||||||
|
"diceware|{word}-{word}-{word}-{word}-{word}"
|
||||||
|
"wifi|{word!}-{word!}{num:4}"
|
||||||
|
"phone|{safe:4}-{safe:4}-{safe:4}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Default template (used when no template name specified)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
DEFAULT_TEMPLATE="default"
|
||||||
@@ -0,0 +1,932 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# === himi ===
|
||||||
|
# Memorable secret generator with configurable word lists and templates.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# himi rebuild Rebuild pool from enabled lists
|
||||||
|
# himi Generate secret (default template) → clipboard
|
||||||
|
# himi <template> Generate secret (named template) → clipboard
|
||||||
|
# himi -o [template] Output to stdout + clipboard
|
||||||
|
# himi -n N Generate N secrets
|
||||||
|
# himi -e [template] Show entropy for template(s)
|
||||||
|
# himi -h Show help
|
||||||
|
|
||||||
|
readonly VERSION="1.0.0"
|
||||||
|
readonly CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
||||||
|
readonly CONFIG_FILE="${CONFIG_DIR}/config.sh"
|
||||||
|
readonly LISTS_DIR="${CONFIG_DIR}/lists"
|
||||||
|
readonly POOL_DIR="${CONFIG_DIR}/pool"
|
||||||
|
readonly POOL_FILE="${POOL_DIR}/pool.txt"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Defaults (overridden by config.sh)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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
|
||||||
|
NUM_CHARSET='0123456789'
|
||||||
|
SPECIAL_CHARSET='!?@#'
|
||||||
|
WORD_MIN_LENGTH=4
|
||||||
|
WORD_MAX_LENGTH=8
|
||||||
|
|
||||||
|
TEMPLATES=(
|
||||||
|
"default|{word}-{word}-{alpha:5}"
|
||||||
|
"easy|{word}-{alpha:3}-{num:3}"
|
||||||
|
"secure|{word}-{word}-{alpha:5}-{word}-{alpha:5}{special:1}"
|
||||||
|
"pin|{num:6}"
|
||||||
|
"diceware|{word}-{word}-{word}-{word}-{word}"
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_TEMPLATE="default"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Colors (disabled if not a terminal)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
readonly RED='\033[0;31m'
|
||||||
|
readonly GREEN='\033[0;32m'
|
||||||
|
readonly YELLOW='\033[0;33m'
|
||||||
|
readonly DIM='\033[0;90m'
|
||||||
|
readonly BOLD='\033[1m'
|
||||||
|
readonly NC='\033[0m'
|
||||||
|
else
|
||||||
|
readonly RED='' GREEN='' YELLOW='' DIM='' BOLD='' NC=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output helpers
|
||||||
|
err() { printf "${RED}error:${NC} %s\n" "$1" >&2; }
|
||||||
|
ok() { printf "${GREEN}✓${NC} %s\n" "$1" >&2; }
|
||||||
|
warn() { printf "${YELLOW}!${NC} %s\n" "$1" >&2; }
|
||||||
|
dim() { printf "${DIM}%s${NC}" "$1"; }
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Platform detection
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
detect_platform() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin*) PLATFORM="macos" ;;
|
||||||
|
Linux*) PLATFORM="linux" ;;
|
||||||
|
*) PLATFORM="unknown" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Cross-platform shuffle (requires GNU coreutils)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
shuffle_lines() {
|
||||||
|
if command -v shuf &>/dev/null; then
|
||||||
|
shuf "$@"
|
||||||
|
elif command -v gshuf &>/dev/null; then
|
||||||
|
gshuf "$@"
|
||||||
|
else
|
||||||
|
err "GNU coreutils required"
|
||||||
|
if [[ "$PLATFORM" == "macos" ]]; then
|
||||||
|
printf " Install with: brew install coreutils\n" >&2
|
||||||
|
else
|
||||||
|
printf " Install with: sudo apt install coreutils (or equivalent)\n" >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Load user config if exists
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
load_config() {
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Ensure directories exist
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
init_dirs() {
|
||||||
|
mkdir -p "$LISTS_DIR" "$POOL_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Filter words: length range + de-duplicate derivatives
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
filter_words() {
|
||||||
|
local min_len="${1:-$WORD_MIN_LENGTH}"
|
||||||
|
local max_len="${2:-$WORD_MAX_LENGTH}"
|
||||||
|
|
||||||
|
grep -E "^[a-z]{${min_len},${max_len}}$" \
|
||||||
|
| sort -u \
|
||||||
|
| awk '{
|
||||||
|
if (prev && index($0, prev) == 1 && length($0) - length(prev) <= 4) next
|
||||||
|
prev = $0; print
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Add: import and enable a local word list
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_add() {
|
||||||
|
local file="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$file" ]]; then
|
||||||
|
err "usage: himi add <wordlist-file>"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
err "file not found: $file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
init_dirs
|
||||||
|
|
||||||
|
# Derive name from filename (strip path and extension)
|
||||||
|
local name
|
||||||
|
name=$(basename "$file" | sed 's/\.[^.]*$//' | tr 'A-Z' 'a-z')
|
||||||
|
local listfile="${LISTS_DIR}/${name}.txt"
|
||||||
|
|
||||||
|
# Clean: lowercase, alpha only, sort, dedupe
|
||||||
|
tr 'A-Z' 'a-z' < "$file" \
|
||||||
|
| grep -E '^[a-z]+$' \
|
||||||
|
| sort -u \
|
||||||
|
> "$listfile"
|
||||||
|
|
||||||
|
local count
|
||||||
|
count=$(wc -l < "$listfile")
|
||||||
|
ok "added $name ($count words)"
|
||||||
|
|
||||||
|
# Enable by default
|
||||||
|
cmd_enable "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Remove: delete list and disable it
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_remove() {
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
err "usage: himi remove <list>..."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
init_dirs
|
||||||
|
local had_errors=0
|
||||||
|
|
||||||
|
for name in "$@"; do
|
||||||
|
local listfile="${LISTS_DIR}/${name}.txt"
|
||||||
|
local linkfile="${POOL_DIR}/${name}.txt"
|
||||||
|
|
||||||
|
if [[ ! -f "$listfile" ]]; then
|
||||||
|
warn "list '$name' not found"
|
||||||
|
had_errors=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove symlink if it's currently enabled
|
||||||
|
[[ -L "$linkfile" ]] && rm "$linkfile"
|
||||||
|
|
||||||
|
# Remove the actual list
|
||||||
|
rm "$listfile"
|
||||||
|
ok "removed $name"
|
||||||
|
done
|
||||||
|
|
||||||
|
rebuild_pool
|
||||||
|
return $had_errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Enable: symlink list(s) into pool/
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
enable_one() {
|
||||||
|
local name="$1"
|
||||||
|
local listfile="${LISTS_DIR}/${name}.txt"
|
||||||
|
local linkfile="${POOL_DIR}/${name}.txt"
|
||||||
|
|
||||||
|
if [[ ! -f "$listfile" ]]; then
|
||||||
|
err "list '$name' not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -L "$linkfile" ]]; then
|
||||||
|
warn "'$name' already enabled"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -s "../lists/${name}.txt" "$linkfile"
|
||||||
|
ok "enabled $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_enable() {
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
err "usage: himi enable <list>... | all"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
init_dirs
|
||||||
|
|
||||||
|
local had_errors=0
|
||||||
|
for name in "$@"; do
|
||||||
|
if [[ "$name" == "all" ]]; then
|
||||||
|
local f
|
||||||
|
for f in "$LISTS_DIR"/*.txt; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
enable_one "$(basename "$f" .txt)" || had_errors=1
|
||||||
|
done
|
||||||
|
else
|
||||||
|
enable_one "$name" || had_errors=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rebuild_pool
|
||||||
|
return $had_errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Disable: remove symlink(s) from pool/
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
disable_one() {
|
||||||
|
local name="$1"
|
||||||
|
local linkfile="${POOL_DIR}/${name}.txt"
|
||||||
|
|
||||||
|
if [[ ! -L "$linkfile" ]]; then
|
||||||
|
warn "'$name' not enabled"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm "$linkfile"
|
||||||
|
ok "disabled $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_disable() {
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
err "usage: himi disable <list>... | all"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
init_dirs
|
||||||
|
|
||||||
|
local had_errors=0
|
||||||
|
for name in "$@"; do
|
||||||
|
if [[ "$name" == "all" ]]; then
|
||||||
|
local f
|
||||||
|
for f in "$POOL_DIR"/*.txt; do
|
||||||
|
[[ -L "$f" ]] || continue
|
||||||
|
disable_one "$(basename "$f" .txt)" || had_errors=1
|
||||||
|
done
|
||||||
|
else
|
||||||
|
disable_one "$name" || had_errors=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rebuild_pool
|
||||||
|
return $had_errors
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Rebuild: regenerate pool.txt from enabled lists
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_rebuild() {
|
||||||
|
init_dirs
|
||||||
|
rebuild_pool
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Rebuild pool from enabled lists (symlinks in pool/)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
rebuild_pool() {
|
||||||
|
local before=0
|
||||||
|
[[ -f "$POOL_FILE" ]] && before=$(wc -l < "$POOL_FILE")
|
||||||
|
|
||||||
|
# Only include symlinked files (enabled lists), exclude pool.txt itself
|
||||||
|
local enabled_lists
|
||||||
|
enabled_lists=$(find "$POOL_DIR" -maxdepth 1 -type l -name '*.txt' 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$enabled_lists" ]]; then
|
||||||
|
warn "no lists enabled"
|
||||||
|
> "$POOL_FILE"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat $enabled_lists \
|
||||||
|
| filter_words "$WORD_MIN_LENGTH" "$WORD_MAX_LENGTH" \
|
||||||
|
> "$POOL_FILE"
|
||||||
|
|
||||||
|
local after
|
||||||
|
after=$(wc -l < "$POOL_FILE")
|
||||||
|
local diff=$((after - before))
|
||||||
|
|
||||||
|
if [[ $before -eq 0 ]]; then
|
||||||
|
ok "pool: $after words $(dim "($(calc_log2 "$after") bits/word)")"
|
||||||
|
elif [[ $diff -ne 0 ]]; then
|
||||||
|
local sign="+"
|
||||||
|
[[ $diff -lt 0 ]] && sign=""
|
||||||
|
ok "pool: $after words $(dim "($(calc_log2 "$after") bits/word)") [${sign}${diff}]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$after" -gt 0 && "$after" -lt 256 ]]; then
|
||||||
|
warn "pool has only $after words ($(calc_log2 "$after") bits/word) — secrets will be weak"
|
||||||
|
warn "enable more lists with 'himi enable' or add lists with 'himi add'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# List: show all lists and enabled status
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_list() {
|
||||||
|
init_dirs
|
||||||
|
|
||||||
|
if ! ls "${LISTS_DIR}"/*.txt &>/dev/null; then
|
||||||
|
warn "no word lists found — run 'himi add <file>'"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " %-16s %7s\n" "LIST" "WORDS"
|
||||||
|
printf " %-16s %7s\n" "────────────────" "───────"
|
||||||
|
|
||||||
|
for f in "${LISTS_DIR}"/*.txt; do
|
||||||
|
local name count marker
|
||||||
|
name=$(basename "$f" .txt)
|
||||||
|
count=$(wc -l < "$f")
|
||||||
|
|
||||||
|
if [[ -L "${POOL_DIR}/${name}.txt" ]]; then
|
||||||
|
marker="${GREEN}●${NC}"
|
||||||
|
else
|
||||||
|
marker="${DIM}○${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " %b %-16s %7d\n" "$marker" "$name" "$count"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf " %-16s %7s\n" "────────────────" "───────"
|
||||||
|
|
||||||
|
if [[ -f "$POOL_FILE" ]]; then
|
||||||
|
local pool_count
|
||||||
|
pool_count=$(wc -l < "$POOL_FILE")
|
||||||
|
if [[ "$pool_count" -gt 0 ]]; then
|
||||||
|
printf " %-16s %7d ${DIM}(%.1f bits/word)${NC}\n" "pool" "$pool_count" "$(calc_log2 "$pool_count")"
|
||||||
|
else
|
||||||
|
printf " %-16s %7d ${DIM}(empty)${NC}\n" "pool" "0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Uninstall: remove himi and all data
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_uninstall() {
|
||||||
|
read -p "Are you sure you want to remove himi and all custom wordlists? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Clean up Shell Profiles
|
||||||
|
local profiles=(".bashrc" ".zshrc" ".bash_profile" ".profile")
|
||||||
|
local start_marker="# >>> himi initialize >>>"
|
||||||
|
local end_marker="# <<< himi initialize <<<"
|
||||||
|
|
||||||
|
for p in "${profiles[@]}"; do
|
||||||
|
if [[ -f "$HOME/$p" ]]; then
|
||||||
|
# Uses sed to delete everything between the markers (inclusive)
|
||||||
|
if grep -q "$start_marker" "$HOME/$p"; then
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
# macOS requires the extension to be attached to the -i flag
|
||||||
|
sed -i '.bak' "/$start_marker/,/$end_marker/d" "$HOME/$p"
|
||||||
|
else
|
||||||
|
# Linux/GNU
|
||||||
|
sed -i.bak "/$start_marker/,/$end_marker/d" "$HOME/$p"
|
||||||
|
fi
|
||||||
|
ok "Cleaned up $p (backup created as $p.bak)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. Remove completions
|
||||||
|
rm -f "${HOME}/.zfunc/_himi"
|
||||||
|
rm -f "${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/himi"
|
||||||
|
|
||||||
|
# 3. Remove data and binary
|
||||||
|
rm -rf "$CONFIG_DIR"
|
||||||
|
ok "Removed configuration and wordlists."
|
||||||
|
|
||||||
|
# Final step: Binary deletes itself
|
||||||
|
local self
|
||||||
|
self=$(command -v himi)
|
||||||
|
rm -f "$self"
|
||||||
|
|
||||||
|
ok "himi has been uninstalled."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Calculate log base 2 using awk
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
calc_log2() {
|
||||||
|
awk -v n="$1" 'BEGIN { printf "%.2f", log(n)/log(2) }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Get charset length
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
charset_len() {
|
||||||
|
printf "%s" "$1" | wc -c
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Calculate entropy for a template string
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
calc_entropy() {
|
||||||
|
local template="$1"
|
||||||
|
local entropy=0
|
||||||
|
local pool_size word_bits
|
||||||
|
|
||||||
|
pool_size=$(wc -l < "$POOL_FILE" 2>/dev/null || echo 0)
|
||||||
|
[[ "$pool_size" -eq 0 ]] && { echo "0"; return; }
|
||||||
|
|
||||||
|
word_bits=$(calc_log2 "$pool_size")
|
||||||
|
|
||||||
|
# Count word variants (most specific first)
|
||||||
|
# {word!} = pool + 1 bit (2 cases)
|
||||||
|
local word_bang_count
|
||||||
|
word_bang_count=$(grep -o '{word!}' <<< "$template" | wc -l)
|
||||||
|
word_bang_count="${word_bang_count:-0}"
|
||||||
|
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) }')
|
||||||
|
template="${template//\{word!\}/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# {Word!} = pool + log2(3) bits (3 cases)
|
||||||
|
local Word_bang_count
|
||||||
|
Word_bang_count=$(grep -o '{Word!}' <<< "$template" | wc -l)
|
||||||
|
Word_bang_count="${Word_bang_count:-0}"
|
||||||
|
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)) }')
|
||||||
|
template="${template//\{Word!\}/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# {WORD}, {Word}, {word} = pool bits only (no case entropy)
|
||||||
|
local WORD_count Word_count word_count
|
||||||
|
WORD_count=$(grep -o '{WORD}' <<< "$template" | wc -l)
|
||||||
|
WORD_count="${WORD_count:-0}"
|
||||||
|
template="${template//\{WORD\}/}"
|
||||||
|
|
||||||
|
Word_count=$(grep -o '{Word}' <<< "$template" | wc -l)
|
||||||
|
Word_count="${Word_count:-0}"
|
||||||
|
template="${template//\{Word\}/}"
|
||||||
|
|
||||||
|
word_count=$(grep -o '{word}' <<< "$template" | wc -l)
|
||||||
|
word_count="${word_count:-0}"
|
||||||
|
|
||||||
|
local total_plain_words=$(( WORD_count + Word_count + word_count ))
|
||||||
|
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) }')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count {alpha:N} - extract N and calculate
|
||||||
|
while [[ "$template" =~ \{alpha:([0-9]+)\} ]]; do
|
||||||
|
local n="${BASH_REMATCH[1]}"
|
||||||
|
local charset_size
|
||||||
|
charset_size=$(charset_len "$ALPHA_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/\{alpha:$n\}/}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Count {num:N}
|
||||||
|
while [[ "$template" =~ \{num:([0-9]+)\} ]]; do
|
||||||
|
local n="${BASH_REMATCH[1]}"
|
||||||
|
local charset_size
|
||||||
|
charset_size=$(charset_len "$NUM_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/\{num:$n\}/}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
printf "%.1f" "$entropy"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Pick N random words from pool
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
pick_words() {
|
||||||
|
local n="${1:-1}"
|
||||||
|
shuffle_lines -n "$n" "$POOL_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Generate random string from charset
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
rand_chars() {
|
||||||
|
local charset="$1"
|
||||||
|
local count="$2"
|
||||||
|
LC_ALL=C tr -dc "$charset" < /dev/urandom 2>/dev/null | head -c "$count"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Portable case conversion (works on bash 3.2/macOS)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
to_upper() { tr '[:lower:]' '[:upper:]'; }
|
||||||
|
to_lower() { tr '[:upper:]' '[:lower:]'; }
|
||||||
|
to_title() {
|
||||||
|
local w
|
||||||
|
read -r w
|
||||||
|
printf "%s%s" "$(printf '%s' "${w:0:1}" | to_upper)" "$(printf '%s' "${w:1}" | to_lower)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Expand a template into a secret
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
expand_template() {
|
||||||
|
local template="$1"
|
||||||
|
local result="$template"
|
||||||
|
local word
|
||||||
|
|
||||||
|
# Process word tokens (most specific first to avoid partial matches)
|
||||||
|
|
||||||
|
# {word!} - random lower/UPPER (+1 bit)
|
||||||
|
while [[ "$result" =~ \{word!\} ]]; do
|
||||||
|
word=$(pick_words 1)
|
||||||
|
if (( RANDOM % 2 )); then
|
||||||
|
word=$(printf '%s' "$word" | to_upper)
|
||||||
|
fi
|
||||||
|
result="${result/\{word!\}/$word}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# {Word!} - random lower/Title/UPPER (+1.58 bits)
|
||||||
|
while [[ "$result" =~ \{Word!\} ]]; do
|
||||||
|
word=$(pick_words 1)
|
||||||
|
case $(( RANDOM % 3 )) in
|
||||||
|
1) word=$(printf '%s' "$word" | to_title) ;;
|
||||||
|
2) word=$(printf '%s' "$word" | to_upper) ;;
|
||||||
|
esac
|
||||||
|
result="${result/\{Word!\}/$word}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# {WORD} - all upper
|
||||||
|
while [[ "$result" =~ \{WORD\} ]]; do
|
||||||
|
word=$(pick_words 1 | to_upper)
|
||||||
|
result="${result/\{WORD\}/$word}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# {Word} - title case
|
||||||
|
while [[ "$result" =~ \{Word\} ]]; do
|
||||||
|
word=$(pick_words 1 | to_title)
|
||||||
|
result="${result/\{Word\}/$word}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# {word} - all lower (default)
|
||||||
|
while [[ "$result" =~ \{word\} ]]; do
|
||||||
|
word=$(pick_words 1)
|
||||||
|
result="${result/\{word\}/$word}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Replace {alpha:N}
|
||||||
|
while [[ "$result" =~ \{alpha:([0-9]+)\} ]]; do
|
||||||
|
local n="${BASH_REMATCH[1]}"
|
||||||
|
local chars
|
||||||
|
chars=$(rand_chars "$ALPHA_CHARSET" "$n")
|
||||||
|
result="${result/\{alpha:$n\}/$chars}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Replace {num:N}
|
||||||
|
while [[ "$result" =~ \{num:([0-9]+)\} ]]; do
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Get template string by name
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
get_template() {
|
||||||
|
local name="$1"
|
||||||
|
for entry in "${TEMPLATES[@]}"; do
|
||||||
|
IFS='|' read -r tname tpattern <<< "$entry"
|
||||||
|
if [[ "$tname" == "$name" ]]; then
|
||||||
|
printf "%s" "$tpattern"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
err "unknown template '$name'"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copy to clipboard (cross-platform)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
to_clipboard() {
|
||||||
|
case "$PLATFORM" in
|
||||||
|
macos)
|
||||||
|
pbcopy
|
||||||
|
;;
|
||||||
|
linux)
|
||||||
|
if command -v xclip &>/dev/null; then
|
||||||
|
xclip -selection clipboard
|
||||||
|
elif command -v xsel &>/dev/null; then
|
||||||
|
xsel --clipboard --input
|
||||||
|
elif command -v wl-copy &>/dev/null; then
|
||||||
|
wl-copy
|
||||||
|
else
|
||||||
|
cat # fallback: output to stdout
|
||||||
|
warn "install xclip, xsel, or wl-copy for clipboard"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
cat # fallback: output to stdout
|
||||||
|
warn "clipboard not supported on this platform"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Show entropy for templates
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_entropy() {
|
||||||
|
local specific="${1:-}"
|
||||||
|
|
||||||
|
if [[ ! -f "$POOL_FILE" ]] || [[ ! -s "$POOL_FILE" ]]; then
|
||||||
|
err "pool empty — run 'himi rebuild' first"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%-12s %-40s %s\n" "TEMPLATE" "EXAMPLE" "ENTROPY"
|
||||||
|
printf "%-12s %-40s %s\n" "--------" "-------" "-------"
|
||||||
|
|
||||||
|
for entry in "${TEMPLATES[@]}"; do
|
||||||
|
IFS='|' read -r name pattern <<< "$entry"
|
||||||
|
|
||||||
|
# If specific template requested, skip others
|
||||||
|
[[ -n "$specific" && "$name" != "$specific" ]] && continue
|
||||||
|
|
||||||
|
local example entropy
|
||||||
|
example=$(expand_template "$pattern")
|
||||||
|
entropy=$(calc_entropy "$pattern")
|
||||||
|
|
||||||
|
printf "%-12s %-40s ≈%s bits\n" "$name" "$example" "$entropy"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Config: open config file in editor
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_config() {
|
||||||
|
local editor="${EDITOR:-${VISUAL:-vi}}"
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
err "config not found: $CONFIG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local before after
|
||||||
|
before=$(cksum "$CONFIG_FILE")
|
||||||
|
"$editor" "$CONFIG_FILE"
|
||||||
|
after=$(cksum "$CONFIG_FILE")
|
||||||
|
|
||||||
|
if [[ "$before" != "$after" ]]; then
|
||||||
|
ok "config changed — rebuilding pool"
|
||||||
|
load_config
|
||||||
|
rebuild_pool
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Test: one-off pattern test with entropy comparison
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_test() {
|
||||||
|
local pattern="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$pattern" ]]; then
|
||||||
|
err "usage: himi test '{pattern}'"
|
||||||
|
printf "\n Example: himi test '{word!}-{num:2}-{word!}'\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$POOL_FILE" ]] || [[ ! -s "$POOL_FILE" ]]; then
|
||||||
|
err "pool empty — run 'himi rebuild' first"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local entropy
|
||||||
|
entropy=$(calc_entropy "$pattern")
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
printf " ${BOLD}Pattern:${NC} %s\n" "$pattern"
|
||||||
|
printf " ${BOLD}Entropy:${NC} ≈%s bits\n" "$entropy"
|
||||||
|
printf "\n"
|
||||||
|
printf " ${BOLD}Examples:${NC}\n"
|
||||||
|
for ((i = 0; i < 5; i++)); do
|
||||||
|
printf " %s\n" "$(expand_template "$pattern")"
|
||||||
|
done
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# Show comparison with built-in templates
|
||||||
|
printf " ${DIM}%-12s %s${NC}\n" "TEMPLATE" "ENTROPY"
|
||||||
|
printf " ${DIM}%-12s %s${NC}\n" "────────────" "───────"
|
||||||
|
printf " ${BOLD}%-12s ≈%s bits${NC} ← your pattern\n" "(test)" "$entropy"
|
||||||
|
|
||||||
|
for entry in "${TEMPLATES[@]}"; do
|
||||||
|
IFS='|' read -r tname tpattern <<< "$entry"
|
||||||
|
local tent
|
||||||
|
tent=$(calc_entropy "$tpattern")
|
||||||
|
printf " %-12s ≈%s bits\n" "$tname" "$tent"
|
||||||
|
done
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Generate secrets
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_generate() {
|
||||||
|
local template_name="${1:-$DEFAULT_TEMPLATE}"
|
||||||
|
local count="${2:-1}"
|
||||||
|
local use_stdout="${3:-false}"
|
||||||
|
|
||||||
|
if [[ ! -f "$POOL_FILE" ]] || [[ ! -s "$POOL_FILE" ]]; then
|
||||||
|
err "pool empty — run 'himi rebuild' first"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sanity check: ensure pool has enough unique words for meaningful entropy
|
||||||
|
local unique_words
|
||||||
|
unique_words=$(sort -u "$POOL_FILE" | wc -l)
|
||||||
|
if [[ "$unique_words" -lt 256 ]]; then
|
||||||
|
err "pool has only $unique_words unique words — refusing to generate (need ≥256)"
|
||||||
|
warn "run 'himi rebuild' or 'himi enable' to fix"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pattern
|
||||||
|
pattern=$(get_template "$template_name") || return 1
|
||||||
|
|
||||||
|
local secrets=""
|
||||||
|
for ((i = 0; i < count; i++)); do
|
||||||
|
[[ -n "$secrets" ]] && secrets+=$'\n'
|
||||||
|
secrets+=$(expand_template "$pattern")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Always copy to clipboard
|
||||||
|
if printf "%s" "$secrets" | to_clipboard; then
|
||||||
|
ok "copied to clipboard"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also print to stdout if -o was passed
|
||||||
|
if [[ "$use_stdout" == "true" ]]; then
|
||||||
|
printf "%s\n" "$secrets"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Show help
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cmd_help() {
|
||||||
|
cat <<EOF
|
||||||
|
himi v${VERSION} — memorable secret generator
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
himi [template] Generate → clipboard (default: $DEFAULT_TEMPLATE)
|
||||||
|
himi -o [template] Generate → clipboard + stdout
|
||||||
|
himi -n N [template] Generate N secrets
|
||||||
|
|
||||||
|
himi add <file> Add and enable a word list
|
||||||
|
himi enable <list>... Enable word list(s), or 'all'
|
||||||
|
himi disable <list>... Disable word list(s), or 'all'
|
||||||
|
himi list Show all lists and status
|
||||||
|
himi rebuild Rebuild pool from enabled lists
|
||||||
|
|
||||||
|
himi entropy [name] Show entropy for template(s)
|
||||||
|
himi test '{pattern}' Test a custom pattern
|
||||||
|
himi config Edit config in \$EDITOR
|
||||||
|
himi uninstall Remove himi and all data
|
||||||
|
himi -h, --help Show this help
|
||||||
|
|
||||||
|
Templates (configure in $CONFIG_FILE):
|
||||||
|
EOF
|
||||||
|
for entry in "${TEMPLATES[@]}"; do
|
||||||
|
IFS='|' read -r name pattern <<< "$entry"
|
||||||
|
printf " %-12s %s\n" "$name" "$pattern"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Tokens:
|
||||||
|
{word} Random word (lowercase)
|
||||||
|
{WORD} Random word (UPPERCASE)
|
||||||
|
{Word} Random word (Title Case)
|
||||||
|
{word!} Random word (lower or UPPER, +1 bit)
|
||||||
|
{Word!} Random word (lower/Title/UPPER, +1.58 bits)
|
||||||
|
{alpha:N} N chars, Crockford Base32 (5.0 bits/char)
|
||||||
|
{safe:N} N chars, ultra-safe (4.6 bits/char)
|
||||||
|
{num:N} N digits (3.3 bits/char)
|
||||||
|
{special:N} N from: $SPECIAL_CHARSET
|
||||||
|
|
||||||
|
Config: $CONFIG_FILE
|
||||||
|
Pool: $POOL_FILE
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
detect_platform
|
||||||
|
load_config
|
||||||
|
init_dirs
|
||||||
|
|
||||||
|
# 1. Intercept top-level commands first
|
||||||
|
case "${1:-}" in
|
||||||
|
add|enable|disable|remove|rebuild|list|test|config|entropy|uninstall)
|
||||||
|
local cmd="cmd_$1"
|
||||||
|
shift
|
||||||
|
"$cmd" "$@" # Pass all remaining arguments correctly
|
||||||
|
return $?
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 2. If it's not a command, parse generation flags and templates
|
||||||
|
local use_stdout="false"
|
||||||
|
local count=1
|
||||||
|
local template_name=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-o|--stdout)
|
||||||
|
use_stdout="true"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-n|--number)
|
||||||
|
if [[ -z "${2:-}" || ! "${2:-}" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
err "-n requires a positive integer (got: '${2:-}')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
count="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
cmd_help
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
err "Unknown option: $1"
|
||||||
|
cmd_help >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -n "$template_name" ]]; then
|
||||||
|
err "Unexpected argument: $1. Did you mean to use a command?"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
template_name="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$template_name" ]] && template_name="$DEFAULT_TEMPLATE"
|
||||||
|
cmd_generate "$template_name" "$count" "$use_stdout"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# himi bash completion
|
||||||
|
# Source this or drop in ~/.local/share/bash-completion/completions/
|
||||||
|
|
||||||
|
_himi() {
|
||||||
|
local cur prev words cword
|
||||||
|
|
||||||
|
# Compatibility for older bash
|
||||||
|
if type _init_completion &>/dev/null; then
|
||||||
|
_init_completion || return
|
||||||
|
else
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
cword=$COMP_CWORD
|
||||||
|
words=("${COMP_WORDS[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local commands="add enable disable remove rebuild list entropy test config uninstall"
|
||||||
|
local flags="-o -n -h --help"
|
||||||
|
|
||||||
|
local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
||||||
|
local lists_dir="${config_dir}/lists"
|
||||||
|
local pool_dir="${config_dir}/pool"
|
||||||
|
|
||||||
|
# Check if we're in a multi-arg command (enable/disable)
|
||||||
|
local cmd=""
|
||||||
|
local i
|
||||||
|
for ((i=1; i < cword; i++)); do
|
||||||
|
case "${words[i]}" in
|
||||||
|
enable|disable|add|entropy|test|config)
|
||||||
|
cmd="${words[i]}"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
enable)
|
||||||
|
# Show disabled lists + 'all' keyword
|
||||||
|
if [[ -d "$lists_dir" ]]; then
|
||||||
|
local available="all "
|
||||||
|
local f
|
||||||
|
for f in "$lists_dir"/*.txt; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
local name=$(basename "$f" .txt)
|
||||||
|
[[ ! -L "$pool_dir/$name.txt" ]] && available+="$name "
|
||||||
|
done
|
||||||
|
COMPREPLY=($(compgen -W "$available" -- "$cur"))
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
# Show enabled lists + 'all' keyword
|
||||||
|
if [[ -d "$pool_dir" ]]; then
|
||||||
|
local enabled="all "
|
||||||
|
local f
|
||||||
|
for f in "$pool_dir"/*.txt; do
|
||||||
|
[[ -L "$f" ]] || continue
|
||||||
|
enabled+="$(basename "$f" .txt) "
|
||||||
|
done
|
||||||
|
COMPREPLY=($(compgen -W "$enabled" -- "$cur"))
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
if [[ -d "$lists_dir" ]]; then
|
||||||
|
local lists=""
|
||||||
|
for f in "$lists_dir"/*.txt; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
lists+="$(basename "$f" .txt) "
|
||||||
|
done
|
||||||
|
COMPREPLY=($(compgen -W "$lists" -- "$cur"))
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
add)
|
||||||
|
COMPREPLY=($(compgen -f -X '!*.txt' -- "$cur"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
entropy)
|
||||||
|
local templates="default easy secure pin diceware"
|
||||||
|
if [[ -f "${config_dir}/config.sh" ]]; then
|
||||||
|
local parsed
|
||||||
|
parsed=$(grep -E '^\s*"[a-z]+\|' "${config_dir}/config.sh" 2>/dev/null | sed 's/.*"\([a-z]*\)|.*/\1/' | tr '\n' ' ')
|
||||||
|
[[ -n "$parsed" ]] && templates="$parsed"
|
||||||
|
fi
|
||||||
|
COMPREPLY=($(compgen -W "$templates" -- "$cur"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
test|config)
|
||||||
|
# No completion for test (user types pattern) or config (no args)
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Handle flags
|
||||||
|
case "$prev" in
|
||||||
|
-n)
|
||||||
|
COMPREPLY=($(compgen -W "1 3 5 10" -- "$cur"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
-o)
|
||||||
|
local templates="default easy secure pin diceware"
|
||||||
|
if [[ -f "${config_dir}/config.sh" ]]; then
|
||||||
|
local parsed
|
||||||
|
parsed=$(grep -E '^\s*"[a-z]+\|' "${config_dir}/config.sh" 2>/dev/null | sed 's/.*"\([a-z]*\)|.*/\1/' | tr '\n' ' ')
|
||||||
|
[[ -n "$parsed" ]] && templates="$parsed"
|
||||||
|
fi
|
||||||
|
COMPREPLY=($(compgen -W "$templates" -- "$cur"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# First argument: commands, flags, or templates
|
||||||
|
if [[ ${cword} -eq 1 ]]; then
|
||||||
|
local templates="default easy secure pin diceware"
|
||||||
|
if [[ -f "${config_dir}/config.sh" ]]; then
|
||||||
|
local parsed
|
||||||
|
parsed=$(grep -E '^\s*"[a-z]+\|' "${config_dir}/config.sh" 2>/dev/null | sed 's/.*"\([a-z]*\)|.*/\1/' | tr '\n' ' ')
|
||||||
|
[[ -n "$parsed" ]] && templates="$parsed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMPREPLY=($(compgen -W "$commands $flags $templates" -- "$cur"))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _himi himi
|
||||||
Executable
+195
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# himi installer
|
||||||
|
# Installs the himi CLI, word lists, and shell completions
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BIN_DIR="${HOME}/.local/bin"
|
||||||
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/himi"
|
||||||
|
BASH_COMPLETION_DIR="${XDG_DATA_HOME:-$HOME/.local/share/bash-completion/completions}"
|
||||||
|
ZSH_COMPLETION_DIR="${HOME}/.zfunc"
|
||||||
|
|
||||||
|
# Define markers for removal later
|
||||||
|
MARKER_START="# >>> himi initialize >>>"
|
||||||
|
MARKER_END="# <<< himi initialize <<<"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
DIM='\033[0;90m'
|
||||||
|
NC='\033[0m'
|
||||||
|
else
|
||||||
|
RED='' GREEN='' YELLOW='' DIM='' NC=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
err() { printf "${RED}error:${NC} %s\n" "$1" >&2; }
|
||||||
|
ok() { printf "${GREEN}✓${NC} %s\n" "$1" >&2; }
|
||||||
|
warn() { printf "${YELLOW}!${NC} %s\n" "$1" >&2; }
|
||||||
|
dim() { printf "${DIM}%s${NC}" "$1"; }
|
||||||
|
|
||||||
|
show_splash() {
|
||||||
|
local splash="${SCRIPT_DIR}/splash.txt"
|
||||||
|
[[ -f "$splash" ]] || return 0
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
printf '\033[38;5;197m'
|
||||||
|
while IFS= read -r line; do
|
||||||
|
printf '%s\n' "$line"
|
||||||
|
sleep 0.02
|
||||||
|
done < "$splash"
|
||||||
|
printf '\033[0m'
|
||||||
|
sleep 1
|
||||||
|
else
|
||||||
|
cat "$splash"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_shell() {
|
||||||
|
# Check the parent process or SHELL env var instead of local vars
|
||||||
|
if [[ "${SHELL:-}" == */zsh ]]; then
|
||||||
|
echo "zsh"
|
||||||
|
elif [[ "${SHELL:-}" == */bash ]]; then
|
||||||
|
echo "bash"
|
||||||
|
else
|
||||||
|
echo "bash" # Safe default
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_deps() {
|
||||||
|
dim "Checking dependencies... "
|
||||||
|
if ! command -v shuf &>/dev/null && ! command -v gshuf &>/dev/null; then
|
||||||
|
echo
|
||||||
|
err "himi requires GNU coreutils (shuf)."
|
||||||
|
[[ "$OSTYPE" == "darwin"* ]] && warn "Install with: brew install coreutils"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "done."
|
||||||
|
}
|
||||||
|
|
||||||
|
install_binary() {
|
||||||
|
[[ -f "${SCRIPT_DIR}/himi" ]] || { err "himi binary not found in installer directory"; return 1; }
|
||||||
|
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
if cp "${SCRIPT_DIR}/himi" "$BIN_DIR/"; then
|
||||||
|
chmod +x "$BIN_DIR/himi"
|
||||||
|
ok "Installed himi to $BIN_DIR"
|
||||||
|
else
|
||||||
|
err "Failed to copy himi to $BIN_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_config() {
|
||||||
|
mkdir -p "${CONFIG_DIR}/lists" "${CONFIG_DIR}/pool"
|
||||||
|
|
||||||
|
if [[ -f "${SCRIPT_DIR}/config.sh" ]]; then
|
||||||
|
if [[ ! -f "${CONFIG_DIR}/config.sh" ]]; then
|
||||||
|
cp "${SCRIPT_DIR}/config.sh" "${CONFIG_DIR}/"
|
||||||
|
ok "Installed default config to $CONFIG_DIR"
|
||||||
|
else
|
||||||
|
dim "Config already exists, skipping.\n"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err "config.sh missing from installer directory"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_lists() {
|
||||||
|
local lists_src="${SCRIPT_DIR}/lists"
|
||||||
|
local count=0
|
||||||
|
|
||||||
|
if [[ -d "$lists_src" ]]; then
|
||||||
|
for f in "${lists_src}"/*.txt; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
# Use count+=1 to avoid the exit code 1 issue with ((count++)) when count is 0
|
||||||
|
cp "$f" "${CONFIG_DIR}/lists/" && ((count+=1)) || true
|
||||||
|
done
|
||||||
|
ok "Installed $count wordlists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable defaults
|
||||||
|
local enabled=0
|
||||||
|
for name in eff bips; do
|
||||||
|
if [[ -f "${CONFIG_DIR}/lists/${name}.txt" ]]; then
|
||||||
|
ln -sf "../lists/${name}.txt" "${CONFIG_DIR}/pool/${name}.txt" && ((enabled+=1)) || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ $enabled -gt 0 ]] && ok "Enabled default lists (eff, bips)"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_completions() {
|
||||||
|
local shell_type=$(detect_shell)
|
||||||
|
local profile=""
|
||||||
|
local comp_src=""
|
||||||
|
local comp_dest=""
|
||||||
|
local installed=""
|
||||||
|
|
||||||
|
case "$shell_type" in
|
||||||
|
bash)
|
||||||
|
profile="${HOME}/.bashrc"
|
||||||
|
comp_src="${SCRIPT_DIR}/himi.bash"
|
||||||
|
comp_dest="${BASH_COMPLETION_DIR}/himi"
|
||||||
|
mkdir -p "$BASH_COMPLETION_DIR"
|
||||||
|
;;
|
||||||
|
zsh)
|
||||||
|
profile="${HOME}/.zshrc"
|
||||||
|
comp_src="${SCRIPT_DIR}/_himi"
|
||||||
|
comp_dest="${ZSH_COMPLETION_DIR}/_himi"
|
||||||
|
mkdir -p "$ZSH_COMPLETION_DIR"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -n "$comp_src" && -f "$comp_src" ]]; then
|
||||||
|
if cp "$comp_src" "$comp_dest"; then
|
||||||
|
installed="$shell_type"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -z "$installed" ]] && return
|
||||||
|
|
||||||
|
# Shell Environment Hook
|
||||||
|
touch "$profile"
|
||||||
|
if ! grep -q "$MARKER_START" "$profile"; then
|
||||||
|
{
|
||||||
|
printf "\n%s\n" "$MARKER_START"
|
||||||
|
printf 'export PATH="${HOME}/.local/bin:${PATH}"\n'
|
||||||
|
if [[ "$installed" == "bash" ]]; then
|
||||||
|
printf '[[ -f "%s" ]] && source "%s"\n' "$comp_dest" "$comp_dest"
|
||||||
|
else
|
||||||
|
printf 'fpath=(%s $fpath)\nautoload -Uz compinit && compinit\n' "$ZSH_COMPLETION_DIR"
|
||||||
|
fi
|
||||||
|
printf "%s\n" "$MARKER_END"
|
||||||
|
} >> "$profile"
|
||||||
|
ok "Updated $profile with PATH and completions"
|
||||||
|
else
|
||||||
|
ok "Completions already configured in $profile"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
show_splash
|
||||||
|
printf "Installing himi...\n\n"
|
||||||
|
|
||||||
|
check_deps
|
||||||
|
install_binary
|
||||||
|
install_config
|
||||||
|
install_lists
|
||||||
|
install_completions
|
||||||
|
|
||||||
|
# Initialize the pool so the tool works immediately
|
||||||
|
dim "Initializing wordlist pool... "
|
||||||
|
if "${BIN_DIR}/himi" rebuild >/dev/null 2>&1; then
|
||||||
|
echo "done."
|
||||||
|
else
|
||||||
|
echo "failed."
|
||||||
|
warn "Initial pool rebuild failed. You may need to run 'himi rebuild' manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n${GREEN}Done!${NC} Restart your terminal or run: source %s\n" "$( [[ "$(detect_shell)" == "zsh" ]] && echo '~/.zshrc' || echo '~/.bashrc' )"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
+1109
File diff suppressed because it is too large
Load Diff
+2048
File diff suppressed because it is too large
Load Diff
+5611
File diff suppressed because it is too large
Load Diff
+7776
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1307
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,445 @@
|
|||||||
|
aceso
|
||||||
|
achelous
|
||||||
|
acheron
|
||||||
|
achlys
|
||||||
|
achos
|
||||||
|
acis
|
||||||
|
acmon
|
||||||
|
acratopotes
|
||||||
|
actaeus
|
||||||
|
acte
|
||||||
|
adephagia
|
||||||
|
adikia
|
||||||
|
adonis
|
||||||
|
adrasteia
|
||||||
|
aegaeon
|
||||||
|
aegle
|
||||||
|
aeolus
|
||||||
|
aergia
|
||||||
|
aether
|
||||||
|
aetna
|
||||||
|
agathodaemon
|
||||||
|
agdistis
|
||||||
|
aglaea
|
||||||
|
agon
|
||||||
|
aiakos
|
||||||
|
aidos
|
||||||
|
aion
|
||||||
|
aisa
|
||||||
|
aitnaios
|
||||||
|
alala
|
||||||
|
alastor
|
||||||
|
alcyone
|
||||||
|
alecto
|
||||||
|
alectrona
|
||||||
|
aletheia
|
||||||
|
alexiares
|
||||||
|
alke
|
||||||
|
alkon
|
||||||
|
alpheus
|
||||||
|
amechania
|
||||||
|
amphiaraus
|
||||||
|
amphictyonis
|
||||||
|
amphitrite
|
||||||
|
anaideia
|
||||||
|
ananke
|
||||||
|
anatolia
|
||||||
|
angelia
|
||||||
|
angelos
|
||||||
|
ania
|
||||||
|
anicetus
|
||||||
|
anteros
|
||||||
|
antheia
|
||||||
|
anthousai
|
||||||
|
aoide
|
||||||
|
aparctias
|
||||||
|
apate
|
||||||
|
aphaea
|
||||||
|
apheleia
|
||||||
|
apheliotes
|
||||||
|
aphrodite
|
||||||
|
aphroditus
|
||||||
|
apollo
|
||||||
|
apollonis
|
||||||
|
arche
|
||||||
|
ares
|
||||||
|
arete
|
||||||
|
arethusa
|
||||||
|
argestes
|
||||||
|
argyron
|
||||||
|
aristaeus
|
||||||
|
arke
|
||||||
|
arktos
|
||||||
|
artemis
|
||||||
|
asbetos
|
||||||
|
asclepius
|
||||||
|
askalaphos
|
||||||
|
asopus
|
||||||
|
asteria
|
||||||
|
astraea
|
||||||
|
astraeus
|
||||||
|
astraios
|
||||||
|
atabyrius
|
||||||
|
atë
|
||||||
|
athena
|
||||||
|
atlas
|
||||||
|
atropos
|
||||||
|
attis
|
||||||
|
auge
|
||||||
|
aura
|
||||||
|
aurai
|
||||||
|
auxesia
|
||||||
|
auxo
|
||||||
|
benthesikyme
|
||||||
|
bia
|
||||||
|
boreas
|
||||||
|
borysthenis
|
||||||
|
britomartis
|
||||||
|
brizo
|
||||||
|
caerus
|
||||||
|
caicias
|
||||||
|
calleis
|
||||||
|
calliope
|
||||||
|
celaeno
|
||||||
|
cephisso
|
||||||
|
ceraon
|
||||||
|
ceto
|
||||||
|
chalcon
|
||||||
|
chaos
|
||||||
|
charon
|
||||||
|
cheimon
|
||||||
|
chione
|
||||||
|
chloris
|
||||||
|
chronos
|
||||||
|
chryson
|
||||||
|
chrysus
|
||||||
|
circe
|
||||||
|
circios
|
||||||
|
cladeus
|
||||||
|
cleta
|
||||||
|
clio
|
||||||
|
clotho
|
||||||
|
coeus
|
||||||
|
comus
|
||||||
|
corus
|
||||||
|
corymbus
|
||||||
|
crius
|
||||||
|
cronus
|
||||||
|
cyamites
|
||||||
|
cybele
|
||||||
|
cyllenus
|
||||||
|
cymopoleia
|
||||||
|
damia
|
||||||
|
damnameneus
|
||||||
|
damon
|
||||||
|
daphne
|
||||||
|
deimos
|
||||||
|
deipneus
|
||||||
|
delas
|
||||||
|
demeter
|
||||||
|
despoina
|
||||||
|
dexithea
|
||||||
|
dikaiosyne
|
||||||
|
dike
|
||||||
|
dione
|
||||||
|
dionysus
|
||||||
|
dolos
|
||||||
|
dryades
|
||||||
|
dynamene
|
||||||
|
dysis
|
||||||
|
dysnomia
|
||||||
|
dyssebeia
|
||||||
|
echo
|
||||||
|
eiar
|
||||||
|
eidothea
|
||||||
|
eileithyia
|
||||||
|
eirene
|
||||||
|
eiresione
|
||||||
|
ekecheiria
|
||||||
|
electra
|
||||||
|
eleos
|
||||||
|
elpis
|
||||||
|
empusa
|
||||||
|
enyalius
|
||||||
|
enyo
|
||||||
|
eos
|
||||||
|
eosphorus
|
||||||
|
epiales
|
||||||
|
epidotes
|
||||||
|
epimedes
|
||||||
|
epimeliades
|
||||||
|
epimetheus
|
||||||
|
epione
|
||||||
|
epiphron
|
||||||
|
erato
|
||||||
|
erebos
|
||||||
|
erebus
|
||||||
|
eridanos
|
||||||
|
eris
|
||||||
|
eros
|
||||||
|
ersa
|
||||||
|
eucleia
|
||||||
|
eudaimonia
|
||||||
|
eulabeia
|
||||||
|
eunomia
|
||||||
|
eunostus
|
||||||
|
eupheme
|
||||||
|
euphrosyne
|
||||||
|
euporie
|
||||||
|
eupraxia
|
||||||
|
euronotus
|
||||||
|
eurotas
|
||||||
|
eurus
|
||||||
|
eurymedon
|
||||||
|
eusebeia
|
||||||
|
euterpe
|
||||||
|
euthenia
|
||||||
|
euthymia
|
||||||
|
gaia
|
||||||
|
galene
|
||||||
|
gelos
|
||||||
|
geras
|
||||||
|
glaucus
|
||||||
|
glycon
|
||||||
|
gorgyra
|
||||||
|
gymnastica
|
||||||
|
hades
|
||||||
|
hamadryades
|
||||||
|
harmonia
|
||||||
|
harpocrates
|
||||||
|
hebe
|
||||||
|
hecate
|
||||||
|
hecaterus
|
||||||
|
hedone
|
||||||
|
hedylogos
|
||||||
|
hegemone
|
||||||
|
heimarmene
|
||||||
|
hekaerge
|
||||||
|
helios
|
||||||
|
hemera
|
||||||
|
hephaestus
|
||||||
|
hera
|
||||||
|
heracles
|
||||||
|
hermaphroditus
|
||||||
|
hermes
|
||||||
|
hesperis
|
||||||
|
hesperus
|
||||||
|
hestia
|
||||||
|
himeros
|
||||||
|
homados
|
||||||
|
homonoia
|
||||||
|
horkos
|
||||||
|
horme
|
||||||
|
hybris
|
||||||
|
hygieia
|
||||||
|
hymenaios
|
||||||
|
hypate
|
||||||
|
hyperion
|
||||||
|
hypnos
|
||||||
|
iapetus
|
||||||
|
iasios
|
||||||
|
iaso
|
||||||
|
ichnaea
|
||||||
|
idyia
|
||||||
|
ioke
|
||||||
|
iris
|
||||||
|
iynx
|
||||||
|
kakia
|
||||||
|
karpo
|
||||||
|
kelmis
|
||||||
|
keuthonymos
|
||||||
|
koalemos
|
||||||
|
kokytos
|
||||||
|
kratos
|
||||||
|
kydoimos
|
||||||
|
lachesis
|
||||||
|
lelantos
|
||||||
|
lethe
|
||||||
|
leto
|
||||||
|
leucothea
|
||||||
|
limos
|
||||||
|
lips
|
||||||
|
loxo
|
||||||
|
lupe
|
||||||
|
lycos
|
||||||
|
lysagora
|
||||||
|
lyssa
|
||||||
|
macaria
|
||||||
|
maenades
|
||||||
|
maia
|
||||||
|
makelo
|
||||||
|
mania
|
||||||
|
matton
|
||||||
|
megaera
|
||||||
|
megalesius
|
||||||
|
melete
|
||||||
|
meliae
|
||||||
|
melinoe
|
||||||
|
melpomene
|
||||||
|
men
|
||||||
|
menoetes
|
||||||
|
menoetius
|
||||||
|
merope
|
||||||
|
mese
|
||||||
|
mesembria
|
||||||
|
methe
|
||||||
|
metis
|
||||||
|
metope
|
||||||
|
minos
|
||||||
|
minthe
|
||||||
|
mneme
|
||||||
|
mnemosyne
|
||||||
|
momus
|
||||||
|
moros
|
||||||
|
morpheus
|
||||||
|
musica
|
||||||
|
mylas
|
||||||
|
name-english
|
||||||
|
nemesis
|
||||||
|
nephele
|
||||||
|
nereus
|
||||||
|
nerites
|
||||||
|
nesoi
|
||||||
|
nete
|
||||||
|
nike
|
||||||
|
nikon
|
||||||
|
nilus
|
||||||
|
nomos
|
||||||
|
notus
|
||||||
|
nymphe
|
||||||
|
nyx
|
||||||
|
oceanus
|
||||||
|
oizys
|
||||||
|
omodamos
|
||||||
|
oneiroi
|
||||||
|
onnes
|
||||||
|
ormenos
|
||||||
|
orphne
|
||||||
|
orthosie
|
||||||
|
oupis
|
||||||
|
ourea
|
||||||
|
paean
|
||||||
|
paidia
|
||||||
|
palaemon
|
||||||
|
palaestra
|
||||||
|
palioxis
|
||||||
|
pallas
|
||||||
|
pan
|
||||||
|
panacea
|
||||||
|
pandaisia
|
||||||
|
pandia
|
||||||
|
pannychis
|
||||||
|
pasithea
|
||||||
|
peitharchia
|
||||||
|
peitho
|
||||||
|
peneus
|
||||||
|
penia
|
||||||
|
penthus
|
||||||
|
pepromene
|
||||||
|
persephone
|
||||||
|
perses
|
||||||
|
phaenna
|
||||||
|
phaenon
|
||||||
|
phaethon
|
||||||
|
phanes
|
||||||
|
pheme
|
||||||
|
pherousa
|
||||||
|
philomelus
|
||||||
|
philophrosyne
|
||||||
|
philotes
|
||||||
|
phlegethon
|
||||||
|
phobos
|
||||||
|
phoebe
|
||||||
|
phorcys
|
||||||
|
phrike
|
||||||
|
phthonus
|
||||||
|
pistis
|
||||||
|
plutus
|
||||||
|
poine
|
||||||
|
polemos
|
||||||
|
polyhymnia
|
||||||
|
polymatheia
|
||||||
|
ponos
|
||||||
|
pontos
|
||||||
|
pontus
|
||||||
|
poros
|
||||||
|
poseidon
|
||||||
|
pothos
|
||||||
|
praxidike
|
||||||
|
priapus
|
||||||
|
proioxis
|
||||||
|
prometheus
|
||||||
|
prophasis
|
||||||
|
proteus
|
||||||
|
psamathe
|
||||||
|
pthinoporon
|
||||||
|
ptocheia
|
||||||
|
pyroeis
|
||||||
|
pyrrhichos
|
||||||
|
rhadamanthys
|
||||||
|
rhapso
|
||||||
|
rhea
|
||||||
|
sabaktes
|
||||||
|
sabazios
|
||||||
|
sangarius
|
||||||
|
scamander
|
||||||
|
selene
|
||||||
|
silenus
|
||||||
|
simon
|
||||||
|
skeiron
|
||||||
|
skelmis
|
||||||
|
skythes
|
||||||
|
smaragos
|
||||||
|
sophrosyne
|
||||||
|
soter
|
||||||
|
soteria
|
||||||
|
sponde
|
||||||
|
sterope
|
||||||
|
stilbon
|
||||||
|
styx
|
||||||
|
syntribos
|
||||||
|
tartarus
|
||||||
|
taygete
|
||||||
|
telesphorus
|
||||||
|
telete
|
||||||
|
terpsichore
|
||||||
|
tethys
|
||||||
|
thalassa
|
||||||
|
thalia
|
||||||
|
thallo
|
||||||
|
thanatos
|
||||||
|
thaumas
|
||||||
|
theamphilogiai
|
||||||
|
theandroktasiai
|
||||||
|
thearae
|
||||||
|
thehesperides
|
||||||
|
thehysminai
|
||||||
|
theia
|
||||||
|
thekeres
|
||||||
|
thelitae
|
||||||
|
thelxinoe
|
||||||
|
themachai
|
||||||
|
themis
|
||||||
|
theneikea
|
||||||
|
theoneiroi
|
||||||
|
theourea
|
||||||
|
thepalici
|
||||||
|
thephonoi
|
||||||
|
theros
|
||||||
|
thetis
|
||||||
|
thoosa
|
||||||
|
thrasos
|
||||||
|
tisiphone
|
||||||
|
titias
|
||||||
|
tonnes
|
||||||
|
triptolemus
|
||||||
|
triteia
|
||||||
|
triton
|
||||||
|
tritones
|
||||||
|
tyche
|
||||||
|
urania
|
||||||
|
uranus
|
||||||
|
zagreus
|
||||||
|
zelos
|
||||||
|
zephyrus
|
||||||
|
zeus
|
||||||
File diff suppressed because it is too large
Load Diff
+1024
File diff suppressed because it is too large
Load Diff
+27
@@ -0,0 +1,27 @@
|
|||||||
|
.x$xxx.
|
||||||
|
$+++++++++$
|
||||||
|
$$+++++++X$$$
|
||||||
|
;;++++++;+;+++
|
||||||
|
x:+;;;;+:;;+++
|
||||||
|
+;;$$X$$:$$;+++
|
||||||
|
+;:X$$x$$$$;++: x
|
||||||
|
+::::$$$$:;;+ x;:
|
||||||
|
; :$x+$$$; ;: ;;
|
||||||
|
;;;+:$;$xx$;;$$X;;; ;:XX+x;
|
||||||
|
;;:;;X;+:$$$$$$$$;;;;;: ;:+;;X;;
|
||||||
|
;;;;;;;;:;::::;;;;;;;:;; ;;:+:+;:;
|
||||||
|
+;;;;;;;;:;;;;;;;;;;;;;;;;; ;;;;;;:
|
||||||
|
:+;+++;;;;;;;x;;;;:;;;;;;;;;;: ;;;;
|
||||||
|
$$$x;+X+;;;;;x$$;;;;;;;;;;;;;+X ;;:
|
||||||
|
$$$xx;+Xxx++x$$$x+++++;XX++XX++x ;;;
|
||||||
|
$$$$$;;:+xx$x$$$xxXx+++xX++;X; $xx
|
||||||
|
:$$$$$;;;+x$$$$$xxxxxxX;;$$$$$$$$$$x;+x
|
||||||
|
;$$$$$;$$$+$$x$$$$$$$x:;$$$$$$$xxx:$$$
|
||||||
|
;X$$$$x$xX$$x$$x$$$$$$$;;;;:$$$xxxxx:
|
||||||
|
;$$x$$$$$$x$$$$X$$$$$$$;;;;;; x;
|
||||||
|
;$$x$$$$$$x$$$$X$$$$$$;;;;; x.
|
||||||
|
##. .## ####.##. ..##.####.###### .##### .##. ##
|
||||||
|
## ..## ##. ####.#### ##. .##. ## . . ## .##
|
||||||
|
.#######. ## .##.### ##. ## .## . ##### ## .##
|
||||||
|
.##. ##..## .## .# .##. ## . ##. # . ## ## .##
|
||||||
|
.## .##.#### ##. ..## ####. .##. ##### .#####
|
||||||
Reference in New Issue
Block a user