Skip to content

Instantly share code, notes, and snippets.

@AfroThundr3007730
Last active March 31, 2024 20:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AfroThundr3007730/60cb4d3cc7f24e9e7d3ace6ed11f1479 to your computer and use it in GitHub Desktop.
Save AfroThundr3007730/60cb4d3cc7f24e9e7d3ace6ed11f1479 to your computer and use it in GitHub Desktop.
bash and powershell functions to generate strong passwords
# Original version
function New-SecurePassword {
<# .SYNOPSIS
Generates high entropy passwords of configurable length #>
[Alias('genpw')]
Param(
# Length of passwords to genereate
[int]$Length = 20,
# How many passwords to generate
[int]$Count = 5,
# Quiet mode, only emit passwords
[switch]$Quiet
)
if (!$Quiet) {
Write-Output ("Generating {0} passwords of length {1}`n" -f $Count, $Length)
}
foreach ($_ in (1..$Count)) {
(& { foreach ($_ in (1..$Length)) {
do { $b = [Security.Cryptography.RandomNumberGenerator]::GetBytes(1) }
until ($b -ge 33 -and $b -le 126); [char[]]$b
} }) -join ''
}
if (!$Quiet) {
Write-Output ("`nEffective entropy of each: {0:n}" -f `
([math]::log([math]::pow(94, $Length)) / [math]::log(2)))
}
}
# PSCore 6 or later (3-4x faster)
function New-SecurePassword {
<# .SYNOPSIS
Generates high entropy passwords of configurable length #>
[Alias('genpw')]
Param(
# Length of passwords to genereate
[int]$Length = 20,
# How many passwords to generate
[int]$Count = 5,
# Quiet mode, only emit passwords
[switch]$Quiet
)
if (!$Quiet) {
Write-Output ("Generating {0} passwords of length {1}`n" -f $Count, $Length)
}
foreach ($_ in (1..$Count)) {
[char[]](Get-Random -Min 33 -Max 126 -Count $Length) -join ''
}
if (!$Quiet) {
Write-Output ("`nEffective entropy of each: {0:n}" -f `
[math]::log2([math]::pow(94, $Length)))
}
}
#!/bin/bash
function genpw() {
local length=20 count=5 l c
while [[ -n $1 ]]; do
[[ $1 =~ -l|--length ]] && { shift; length=$1; shift; }
[[ $1 =~ -c|--count ]] && { shift; count=$1; shift; }
[[ $1 =~ -q|--quiet ]] && { local quiet=true; shift; }
done
[[ -n $quiet ]] || printf 'Generating %d passwords of length %d\n\n' "$count" "$length"
printf '%b\n' "$(
tr -dc '[:graph:]' < /dev/urandom | fold -w "$length" | head -n "$count"
)";
[[ -n $quiet ]] ||
printf '\nEffective entropy of each: %.2f\n' "$(bc -l <<< "l(94^$length)/l(2)")"
}
@AfroThundr3007730
Copy link
Author

AfroThundr3007730 commented Sep 10, 2022

Some observations on bash loop performance, builtins vs binaries, and the impact of subshells.

Expand for code
#!/bin/bash

length=20
count=100

# First variant, not performant, using external binaries
f1 () {
    for ((c=0; c<count; c++)); do
        printf '%s\n' "$(tr -dc '[:graph:]' < /dev/urandom | head -c $length)";
    done
}

# Second variant, optimized, using external binaries (best time)
f2 () {
    tr -dc '[:graph:]' < /dev/urandom | fold -w "$length" | head -n "$count"
}

# Third variant, not performant, using bash builtins (worst time)
f3 () {
    for ((c=0; c<count; c++)); do
        for ((l=0; l<length; l++)); do
            printf '%b' "\\$(printf '%o' $((RANDOM%(126-32)+33)))";
        done;
        printf '\n';
    done
}

# Fourth variant, reduced subshells, using builtins (third best time)
f4 () {
    for ((c=0; c<count; c++)); do
        printf '%b' "$(
            for ((l=0; l<length; l++)); do
                printf "\\%o" $((RANDOM%(126-32)+33))
            done
        )\n";
    done
}

# Fifth variant, single subshell, using builtins (second best time)
f5 () {
    printf '%b\n' "$(
        for ((c=0; c<count; c++)); do
            for ((l=0; l<length; l++)); do
                printf "\\%o" $((RANDOM%(126-32)+33))
            done
            printf '\n'
        done
    )";
}

# Times are taken after several executions

time f1&>/dev/null
time f2&>/dev/null
time f3&>/dev/null
time f4&>/dev/null
time f5&>/dev/null

# real    0m0.474s
# user    0m0.195s
# sys     0m0.524s

# real    0m0.003s
# user    0m0.000s
# sys     0m0.005s

# real    0m1.271s
# user    0m0.662s
# sys     0m0.737s

# real    0m0.103s
# user    0m0.050s
# sys     0m0.064s

# real    0m0.017s
# user    0m0.012s
# sys     0m0.013s

count=5

time f1&>/dev/null
time f2&>/dev/null
time f3&>/dev/null
time f4&>/dev/null
time f5&>/dev/null

#real    0m0.019s
#user    0m0.008s
#sys     0m0.022s

#real    0m0.003s
#user    0m0.001s
#sys     0m0.005s

#real    0m0.053s
#user    0m0.030s
#sys     0m0.029s

#real    0m0.003s
#user    0m0.003s
#sys     0m0.001s

#real    0m0.001s
#user    0m0.001s
#sys     0m0.001s

count=1

time f1&>/dev/null
time f2&>/dev/null
time f3&>/dev/null
time f4&>/dev/null
time f5&>/dev/null

#real    0m0.011s
#user    0m0.004s
#sys     0m0.012s

#real    0m0.013s
#user    0m0.004s
#sys     0m0.018s

#real    0m0.030s
#user    0m0.006s
#sys     0m0.026s

#real    0m0.002s
#user    0m0.001s
#sys     0m0.002s

#real    0m0.003s
#user    0m0.002s
#sys     0m0.001s

It seems that minimizing the use of subshells results in faster execution. For higher counts, the loop can be made faster by using external binaries, but I elected to use bash builtins, as the performance hit is minimal. This varies depending on the number of strings generated, as higher counts offset the overhead of calling external binaries. For lower counts, they aren't really worth it.

Per the below comment, the modulus operation introduces a bias, and the external method is performant enough anyway.

@AfroThundr3007730
Copy link
Author

AfroThundr3007730 commented Oct 12, 2022

Observations on the effects of transforming values in a uniform distribution (attempting to optimize out the Get-Random call).

Expand for code

Removes usage of Get-Random but introduces a 3:2 distribution bias on chars 0-67 (255%94):

function New-SecurePassword {
    <# .SYNOPSIS
    Generates high entropy passwords of configurable length #>
    [Alias('genpw')]
    Param(
        # Length of passwords to genereate
        [int]$Length = 20,
        # How many passwords to generate
        [int]$Count = 5,
        # Quiet mode, only emit passwords
        [switch]$Quiet
    )

    if (!$Quiet) {
        Write-Output ("Generating {0} passwords of length {1}`n" -f $Count, $Length)
    }
    foreach ($_ in (1..$Count)) {
        [char[]]([Security.Cryptography.RandomNumberGenerator]::GetBytes($Length) |
                & { process { $_ % 94 + 33 } }) -join '' # Distribution bias 3:2 on chars 0 to 67
    }
    if (!$Quiet) {
        Write-Output ("`nEffective entropy of each: {0:n}" -f `
            ([math]::log([math]::pow(94, $Length)) / [math]::log(2)))
    }
}

Can be tested with:

# 10000 char sample
$a = genpw 10000 1 -q
$count = @{}
[char[]]$a | & { process { $count[$_]++ } }
$count.keys | Sort-Object | & { process { Write-Host $_ = $count[$_] } }
# 0 to 255 sample
$b = [char[]](0..255 | & { process { $_ % 94 + 33 } }) -join ''
$count = @{}
[char[]]$b | & { process { $count[$_]++ } }
$count.keys | Sort-Object | & { process { Write-Host $_ = $count[$_] } }

Transforming the output disturbs the uniform distribution. To preserve it, we must keep sampling until the selection criteria are met.

function New-SecurePassword {
    <# .SYNOPSIS
    Generates high entropy passwords of configurable length #>
    [Alias('genpw')]
    Param(
        # Length of passwords to genereate
        [int]$Length = 20,
        # How many passwords to generate
        [int]$Count = 5,
        # Quiet mode, only emit passwords
        [switch]$Quiet
    )

    if (!$Quiet) {
        Write-Output ("Generating {0} passwords of length {1}`n" -f $Count, $Length)
    }
    foreach ($_ in (1..$Count)) {
        (& { foreach ($_ in (1..$Length)) {
                do { $b = [Security.Cryptography.RandomNumberGenerator]::GetBytes(1) }
                until ($b -ge 33 -and $b -le 126); [char[]]$b
            } }) -join ''
    }
    if (!$Quiet) {
        Write-Output ("`nEffective entropy of each: {0:n}" -f `
            ([math]::log([math]::pow(94, $Length)) / [math]::log(2)))
    }
}

The revised version no longer has a bias:

# 10000 char sample
$a = genpw 10000 1 -q
$count = @{}
[char[]]$a | & { process { $count[$_]++ } }
$count.keys | Sort-Object | & { process { Write-Host $_ = $count[$_] } }
# 10000 char Get-Random
$b = [char[]](Get-Random -Min 33 -Max 126 -Count 10000) -join ''
$count = @{}
[char[]]$b | & { process { $count[$_]++ } }
$count.keys | Sort-Object | & { process { Write-Host $_ = $count[$_] } }

The final version tends to iterate ~2.5 times $Length for each password. It does however, ensure the uniform distribution is preserved. This exercise would be wholly unnecessary if Get-Random supported the Count parameter in earlier PowerShell versions, as it's actually faster. Invoking it for each character (the old behavior), however, is much slower.

@AfroThundr3007730
Copy link
Author

Now part of my HelperFunctions module.

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