Skip to content

Instantly share code, notes, and snippets.

@RickCogley
Last active May 3, 2023 00:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RickCogley/519d54596acd459586a8f6e00dce702e to your computer and use it in GitHub Desktop.
Save RickCogley/519d54596acd459586a8f6e00dce702e to your computer and use it in GitHub Desktop.
Nushell password generator

Nushell Password Generator "nupass"

This nushell command randomly retrieves a specified number of words from a dictionary file (English with Japanese words added by @rickcogley) less than or equal to a given parameter's length, formats the words randomly with capitalization, then separates the words with some random symbols and numbers to return a password.

To use:

  1. Get the dictionary file to your system using nushell's http:
http get https://raw.githubusercontent.com/RickCogley/jpassgen/master/genpass-dict-jp.txt | save genpass-dict-jp

...which has also been included in this folder for convenience.

  1. Confirm your $env.NU_LIB_DIRS location, and copy the below script 2. nupass.nu there as nupass.nu.
  2. Set the script as executable like chmod +x nupass.nu
  3. Specify the dictionary file's location in the script:
let dictfile = $"/path/to/my/genpass-dict-jp"
  1. In the main function's flags section, confirm and edit the default symbols list, diceware delimiter and threads for par-each (double the number of your CPU cores seems to be a good sweet spot):
--symbols (-s): string = "!@#$%^&()_-+[]{}" # Symbols to use in password
--delimiter (-m): string = "-" # Delimiter for diceware
--threads (-t): int = 16  # Number of threads to use in par-each
  1. Load the script with use in your config.nu, something like:
use nupass.nu

(you can specify the path as use /path/to/nupass.nu if you're not taking advantage of $env.NU_LIB_DIRS)

Reload nu, then run it to test:

nupass -h
nupass 5
nupass 6 --debug
nupass 8 -v diceware
nupass 8 -v diceware -m _
nupass 4 -v mixnmatch
nupass 6 -v alphanum
nupass 5 -l 8 

Testing

If you're making changes to the script while testing, you can just re-source the script by doing:

use nupass.nu

... which will reload the latest you have saved.

From nu version 0.79.1, you can use the standard library, and use its bench command to do a benchmark. Load the standard library by adding use std in your env.nu, reload, then assuming nupass.nu is in your path, you can benchmark like so:

std bench --rounds 10 --verbose {nupass 10}
std bench --rounds 10 --verbose {nupass 100 -v diceware}
std bench --rounds 10 --verbose {nupass 1000 -v mixnmatch}

If you change the par-each to each in the main list builders for instance, you'll see a significant performance hit. When I benchmarked nupass 100, using just each took 7 sec per round, whereas changing to par-each dropped that to about 1 sec per round.

You can tweak it a little further by setting the threads for par-each.

std bench --rounds 10 --verbose {nupass 100 -v diceware -t 8}
std bench --rounds 10 --verbose {nupass 100 -v diceware -t 16}
std bench --rounds 10 --verbose {nupass 100 -v diceware -t 32}

image

Caveats

I've been scripting for quite a long time, but not in nu. Input appreciated to make this more nu-esque! The script is in a decent place as of the 20230501 version.

Obviously you can just use the random chars or random integers commands but I like to have words I can read in my passwords, and I think those generated by this script have sufficient entropy.

This command doesn't let you specify a precise length.

Acknowledgements

Thanks everyone on Discord for putting up with and answering my nubie questions @amtoine, @fdncred, @jelle, @sygmei, @kubouch and for the feedback after try number 1.

image

# Script to generate a password from a dictionary file
# Author: @rickcogley
# Thanks: @amtoine, @fdncred, @jelle, @sygmei, @kubouch
# Updates: 20230415 - initial version
# 20230416 - add @amtoine's slick probabilistic "random decimal" char capitalization
# 20230417 - add script duration output in debug block
# 20230421 - add length of symbol chars to get-random-symbol function
# 20230422 - add variant flag to generate different styles of passwords
# 20230501 - refactor to allow number of words to be specified, use list manipulation and reduce to string
# 20230502 - improve performance on list builders with par-each
# 20230503 - add threads flag for fine tuning par-each
#======= NUPASS PASSWORD GENERATOR =======
# Generate password of 3 dictionary file words, numbers and symbols
export def main [
words: int = 3 # Number of words in password
--word_length (-l): int = 5 # Max length of words in password
--symbols (-s): string = "!@#$%^&()_-+[]{}" # Symbols to use in password
--variant (-v): string = "regular" # Password style to generate in regular, mixnmatch, alphanum, alpha, diceware
--delimiter (-m): string = "-" # Delimiter for diceware
--threads (-t): int = 16 # Number of threads to use in par-each
--debug (-d) # Include debug info
] {
##### Main function #####
# Get dictionary file:
# http get https://raw.githubusercontent.com/RickCogley/jpassgen/master/genpass-dict-jp.txt | save genpass-dict-jp
# Set the path:
let dictfile = $"/usr/local/bin/genpass-dict-jp"
let starttime = (date now)
# Find number of lines with strings less than or equal to the supplied length
let num_lines = (open ($dictfile) | lines | wrap word | upsert len {|it| $it.word | split chars | length} | where len <= ($word_length) | length)
# Get random words from dictionary file
let random_words = (1..$words | par-each { |i| $dictfile | get-random-word $word_length $num_lines | random-format-word } --threads $threads)
# Get some symbols to sprinkle like salt bae
# Update default symbol chars in symbols flag
let symbols_len = ($symbols | str length)
let random_symbols = (1..$words | par-each { |i| $symbols | get-random-symbol $symbols $symbols_len } --threads $threads)
# Get some random numbers
let random_numbers = (1..$words | par-each { |i| (random integer 0..99) } --threads $threads)
# Print some vars if debug flag is set
if $debug {
print $"(ansi bg_red) ====== DEBUG INFO ====== (ansi reset)"
print $"(ansi bg_blue) 🔔 Number of lines in dict with words under ($word_length) chars: (ansi reset)"
print $num_lines
print $"(ansi bg_blue) 🔔 Words from randomly selected lines: (ansi reset)"
print $random_words
print $"(ansi bg_blue) 🔔 Randomly selected symbols: (ansi reset)"
print $random_symbols
print $"(ansi bg_blue) 🔔 Randomly selected numbers: (ansi reset)"
print $random_numbers
let endtime = (date now)
print $"(ansi bg_green) 🔔 Generated password in ($endtime - $starttime): (ansi reset)"
}
# Return password in selected variant
if $variant == "regular" {
# Default variant, with regular distribution
# Generate new list w symbol, words, numbers, then reduce to string
return (0..($words - 1) | each { |it| ($random_symbols | get $it) + ($random_words | get $it) + ($random_numbers | get $it | into string) } | reduce { |it, acc| $acc + $it })
} else if $variant == "mixnmatch" {
# Combine lists, shuffle randomly, reduce to string
return (($random_words ++ $random_symbols ++ $random_numbers | shuffle) | reduce { |it, acc| ($acc | into string) + ($it | into string) })
} else if $variant == "alphanum" {
# Combined random int and random word, reduce to string
return (0..($words - 1) | each { |it| (random integer 0..99 | into string) + ($random_words | get $it) } | reduce { |it, acc| $acc + $it })
} else if $variant == "alpha" {
# Reduce random words only to string
return ($random_words | reduce { |it, acc| $acc + $it })
} else if $variant == "diceware" {
# Reduce to string with hyphen between words
return ($random_words | reduce { |it, acc| $acc + $"($delimiter)($it)" })
}
}
##### Utility functions #####
# Function to get random word from a dictionary file
def get-random-word [
wordlength: int
numlines: int
] {
open
| lines
| wrap word
| upsert len {|it| $it.word | str length}
| where len <= ($wordlength)
| get (random integer 1..($numlines))
| get word
}
# Function to format a word randomly
def random-format-word [] {
par-each {|it|
let rint = (random integer 1..4)
if $rint == 1 {
($it | str capitalize)
} else if $rint == 2 {
($it | str upcase)
} else if $rint == 3 {
($it | split chars | each {|c| if (random decimal) < 0.2 { $c | str upcase } else { $c }} | str join "")
} else {
($it | str downcase)
}
}
}
# Function to get random symbol from list of symbols
def get-random-symbol [
symbolchars: string
symbolcharslen: int
] {
$symbolchars
| split chars
| get (random integer 0..($symbolcharslen - 1))
}
@RickCogley
Copy link
Author

Consider from @amtoine - "put some of the duplicate lines into separate tool commands"

@amtoine
Copy link

amtoine commented Apr 14, 2023

🥳

@RickCogley
Copy link
Author

Consider from @fdncred: consider replacing | split chars | length with | str length

@RickCogley
Copy link
Author

updated after separating out into its own script file

@amtoine
Copy link

amtoine commented Apr 15, 2023

that looks cool, nice job 💪

@RickCogley
Copy link
Author

RickCogley commented Apr 15, 2023 via email

@RickCogley
Copy link
Author

Updated verbiage related to $enf.NU_LIB_DIRS since it isn't necessarily "scripts"

@RickCogley
Copy link
Author

Added script duration output with debug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment