Last active
April 23, 2026 15:43
-
-
Save dadatuputi/1f19e434514fe5278fea60890642dc39 to your computer and use it in GitHub Desktop.
Bash Script (Linux, macOS) to generate new DS Login password
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # Generates DS Logon compliant passwords. | |
| # Based on https://gist.github.com/dadatuputi/d2ead8ee219da5922bbcbf6b7325857f | |
| set -e | |
| LENGTH=128 | |
| LOWERCASE='abcdefghijklmnopqrstuvwxyz' | |
| UPPERCASE='ABCDEFGHIJKLMNOPQRSTUVWXYZ' | |
| NUMBERS='0123456789' | |
| # @ _ # / , ; ~ ` % & = ' : ! $ * + ( ) . { } | ? > < ^ [ ] - " \ | |
| SPECIAL=$'@_#/,;~`%&=\':!$*+().{}|?><^[]-"\\' | |
| # See https://www.reddit.com/r/VeteransBenefits/comments/1avm2wf/satisfying_ds_logon_password_requirements/ | |
| NEW_PW_CHARS_DIFFERENT=8 | |
| show_help() { | |
| cat <<'EOF' | |
| Generates DS Logon compliant passwords | |
| PARAMETERS | |
| -l, --length <int> | |
| Length of password (15-128 chars) | |
| Default: 128 | |
| -h, --help | |
| Shows this help message | |
| BEHAVIOR | |
| Always prompts for current password. If empty password entered, | |
| generates completely new password. If current password provided, | |
| ensures new password meets '8 different characters' requirement. | |
| EXAMPLES | |
| Generate password (will prompt for current): | |
| ./new-dslogon-password.sh --length 30 | |
| Show help: | |
| ./new-dslogon-password.sh --help | |
| EOF | |
| } | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -l|--length) | |
| LENGTH="$2" | |
| shift 2 | |
| ;; | |
| -h|--help|-\?) | |
| show_help | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Unknown argument: $1" >&2 | |
| show_help >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| # Random integer in [0, max) using /dev/urandom. | |
| rand_int() { | |
| local max=$1 | |
| local n | |
| n=$(od -An -N4 -tu4 /dev/urandom | tr -d '[:space:]') | |
| echo $(( n % max )) | |
| } | |
| # True when haystack contains needle (needle is matched literally). | |
| contains_char() { | |
| local haystack=$1 needle=$2 | |
| [[ "$haystack" == *"$needle"* ]] | |
| } | |
| pick_random() { | |
| local str=$1 | |
| local len=${#str} | |
| local idx | |
| idx=$(rand_int "$len") | |
| printf '%s' "${str:$idx:1}" | |
| } | |
| # Fisher-Yates shuffle of a string. | |
| shuffle_string() { | |
| local str=$1 | |
| local n=${#str} | |
| if (( n == 0 )); then | |
| return | |
| fi | |
| local -a chars | |
| local i | |
| for (( i=0; i<n; i++ )); do | |
| chars[$i]=${str:$i:1} | |
| done | |
| local j tmp | |
| for (( i=n-1; i>0; i-- )); do | |
| j=$(rand_int $(( i + 1 ))) | |
| tmp=${chars[i]} | |
| chars[i]=${chars[j]} | |
| chars[j]=$tmp | |
| done | |
| local result="" | |
| for (( i=0; i<n; i++ )); do | |
| result+="${chars[i]}" | |
| done | |
| printf '%s' "$result" | |
| } | |
| # Characters of $1 not present in $2. Avoids ${var//pat/} so glob metachars | |
| # (* ? [ ] { }) in the remove-set aren't interpreted as patterns. | |
| remove_chars() { | |
| local source=$1 to_remove=$2 | |
| local result="" | |
| local i ch | |
| for (( i=0; i<${#source}; i++ )); do | |
| ch=${source:$i:1} | |
| if ! contains_char "$to_remove" "$ch"; then | |
| result+="$ch" | |
| fi | |
| done | |
| printf '%s' "$result" | |
| } | |
| sort_string() { | |
| printf '%s' "$1" | fold -w1 | sort | tr -d '\n' | |
| } | |
| generate_password() { | |
| local length=$1 | |
| local current_pass=$2 | |
| local lowercase=$LOWERCASE | |
| local uppercase=$UPPERCASE | |
| local numbers=$NUMBERS | |
| local special=$SPECIAL | |
| local all_chars="$lowercase$uppercase$numbers$special" | |
| local guaranteed_different="" | |
| if [[ -n "$current_pass" ]]; then | |
| local chars_not_in_current | |
| chars_not_in_current=$(remove_chars "$all_chars" "$current_pass") | |
| if (( ${#chars_not_in_current} < NEW_PW_CHARS_DIFFERENT )); then | |
| echo "Not enough unique characters available different from current password. You may have to reset your password through the 'Forgot Password' process." >&2 | |
| exit 1 | |
| fi | |
| guaranteed_different=$(shuffle_string "$chars_not_in_current") | |
| guaranteed_different=${guaranteed_different:0:NEW_PW_CHARS_DIFFERENT} | |
| fi | |
| local upper_limit=${#all_chars} | |
| local lower_limit=$(( upper_limit - NEW_PW_CHARS_DIFFERENT )) | |
| local reserve_count | |
| if (( length >= upper_limit )); then | |
| reserve_count=$NEW_PW_CHARS_DIFFERENT | |
| elif (( length < lower_limit )); then | |
| reserve_count=0 | |
| else | |
| reserve_count=$(( length - lower_limit )) | |
| fi | |
| if (( reserve_count > 0 )); then | |
| local reserved_chars | |
| reserved_chars=$(shuffle_string "$all_chars") | |
| reserved_chars=${reserved_chars:0:reserve_count} | |
| lowercase=$(remove_chars "$lowercase" "$reserved_chars") | |
| uppercase=$(remove_chars "$uppercase" "$reserved_chars") | |
| numbers=$(remove_chars "$numbers" "$reserved_chars") | |
| special=$(remove_chars "$special" "$reserved_chars") | |
| echo "Reserved $reserve_count characters for future use: $(sort_string "$reserved_chars")" >&2 | |
| fi | |
| local password="" | |
| password+=$(pick_random "$lowercase") | |
| password+=$(pick_random "$uppercase") | |
| password+=$(pick_random "$numbers") | |
| password+=$(pick_random "$special") | |
| password+="$guaranteed_different" | |
| local pool="$lowercase$uppercase$numbers$special" | |
| while (( ${#password} < length )); do | |
| password+=$(pick_random "$pool") | |
| done | |
| shuffle_string "$password" | |
| } | |
| test_password() { | |
| local current_pass=$1 new_pass=$2 | |
| local different="" | |
| local i ch | |
| for (( i=0; i<${#new_pass}; i++ )); do | |
| ch=${new_pass:$i:1} | |
| if ! contains_char "$current_pass" "$ch" \ | |
| && ! contains_char "$different" "$ch"; then | |
| different+="$ch" | |
| fi | |
| done | |
| echo "Number of unique characters in new password not present in current password: ${#different}" >&2 | |
| echo "Different characters: $(sort_string "$different")" >&2 | |
| } | |
| read -r -s -p "Enter current password (press Enter for none): " CURRENT_PASS | |
| echo | |
| if [[ -z "$CURRENT_PASS" ]]; then | |
| echo "No current password provided - generating completely new password" >&2 | |
| NEW_PASS=$(generate_password "$LENGTH" "") | |
| else | |
| echo "Current password provided - ensuring new password is sufficiently different" >&2 | |
| NEW_PASS=$(generate_password "$LENGTH" "$CURRENT_PASS") | |
| test_password "$CURRENT_PASS" "$NEW_PASS" | |
| fi | |
| # Scrub the current-password variable from the environment. | |
| CURRENT_PASS="" | |
| unset CURRENT_PASS | |
| echo "New password: $NEW_PASS" | |
| copy_to_clipboard() { | |
| if command -v pbcopy >/dev/null 2>&1; then | |
| pbcopy | |
| elif [[ -n "${WAYLAND_DISPLAY:-}" ]] && command -v wl-copy >/dev/null 2>&1; then | |
| wl-copy | |
| elif command -v xclip >/dev/null 2>&1; then | |
| xclip -selection clipboard | |
| elif command -v xsel >/dev/null 2>&1; then | |
| xsel --clipboard --input | |
| else | |
| return 1 | |
| fi | |
| } | |
| if printf '%s' "$NEW_PASS" | copy_to_clipboard; then | |
| echo "Password copied to clipboard" >&2 | |
| else | |
| echo "No clipboard utility found (install xclip, xsel, or wl-clipboard)" >&2 | |
| fi |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Powershell version