Security is hard. One of the common mistakes I’ve seen people make in creating a password generation function is using Get-Random
or Math.Random
to generate the password instead of using System.Security.Cryptography.RandomNumberGenerator
.
Get-Random
uses the Math.Random
under the covers which is not cryptographically secure.
I’ve put together a function for generating passwords in Powershell, New-Password, using RandomNumberGenerator
. It allows you to specify:
- characters sets that you want to use to seed the password
- specify adhoc characters in case the default sets are too broad.
- the exact length of the password
- verify the password, which allows you to check to ensure that the password has certain characters.
- return the password as a secure string.
You can copy the code below and save it to a file e.g. New-Password.ps1 and then you can dot source the file. e.g. . "C:\User\${Env:UserName}\OneDrive\New-Password.ps1"
#TODO: Add Entropy
$passwordCharSets = @{
LatinAlphaUpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
LatinAlphaLowerCase = "abcdefghijklmnopqrstuvwxyz";
Digits = "0123456789";
Hyphen = "-";
Underscore = "_";
Brackets = "[]{}()<>";
Special = "~`&%$#@*+=|\/,:;^";
Space = " ";
}
function Merge-PasswordCharSets() {
[cmdletbinding()]
Param(
[ValidateSet('LatinAlphaUpperCase', 'LatinAlphaLowerCase', 'Digits', 'Hyphen', 'Underscore', 'Brackets', 'Special', 'Space')]
[Parameter()]
[string[]] $CharSets
)
if($CharSets -eq $null -or $CharSets.Length -eq 0) { return $null }
$result = $null;
if($CharSets -ne $null -and $CharSets -gt 0) {
$sb1 = New-Object System.Text.StringBuilder
foreach($setName in $CharSets) {
if($passwordCharSets.ContainsKey($setName)) {
$characters = $passwordCharSets[$setName];
$sb1.Append($characters) | Out-Null
}
}
$result = $sb1.ToString();
}
return $result;
}
function Test-Password() {
Param(
[Char[]] $Characters,
[ScriptBlock] $Validate
)
if($characters -eq $null -or $characters.Length -eq 0) {
return $false;
}
if($Validate -ne $null) {
& $Validate -Characters $Characters;
}
$lower = $false;
$upper = $false;
$digit = $false;
for($i = 0; $i -lt $characters.Length; $i++) {
if($lower -and $upper -and $digit) {
return $true;
}
$char = [char]$characters[$i];
if([Char]::IsDigit($char)) {
$digit = $true;
continue;
}
if([Char]::IsLetter($char)) {
if([Char]::IsUpper($char)) {
$upper = $true;
continue;
}
if([Char]::IsLower($char)) {
$lower = $true;
}
}
}
return $false;
}
function New-Password() {
Param(
[int] $Length,
[ValidateSet('LatinAlphaUpperCase', 'LatinAlphaLowerCase', 'Digits', 'Hyphen', 'Underscore', 'Brackets', 'Special', 'Space')]
[string[]] $CharSets = $null,
[string] $Chars = $null,
[ScriptBlock] $Validate = $null,
[switch] $AsSecureString
)
if($Length -eq 0) {
$Length = 16;
}
$sb = New-Object System.Text.StringBuilder
if($CharSets -ne $Null -and $CharSets.Length -gt 0) {
$set = (Merge-PasswordCharSets $CharSets)
if($set -and $set.Length -gt 0) {
$sb.Append($set) | Out-Null
}
}
if(![string]::IsNullOrWhiteSpace($Chars)) {
$sb.Append($Chars) | Out-Null
}
if($sb.Length -eq 0) {
$sets = Merge-PasswordCharSets (@('LatinAlphaUpperCase', 'LatinAlphaLowerCase', 'Digits', 'Hyphen', 'Underscore'))
$sb.Append($sets) | Out-Null
}
$permittedChars = $sb.ToString();
$password = [char[]]@(0) * $Length;
$bytes = [byte[]]@(0) * $Length;
while( (Test-Password $password -Validate $Validate) -eq $false) {
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($bytes);
$rng.Dispose();
for($i = 0; $i -lt $Length; $i++) {
$index = [int] ($bytes[$i] % $permittedChars.Length);
$password[$i] = [char] $permittedChars[$index];
}
}
$result = -join $password;
if($AsSecureString.ToBool()) {
return $result | ConvertTo-SecureString -AsPlainText
}
return $result;
}
Once you save the script to OneDrive or another location of your choosing, you can execute it like so.
. "C:\User\${Env:UserName}\OneDrive\New-Password.ps1"
New-Password