Skip to content

Instantly share code, notes, and snippets.

@AfroThundr3007730
Last active March 31, 2024 20:13
Show Gist options
  • 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 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