Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## Password-generation restoration (unreleased)

- Restore New-RandomString, New-RandomPassword and New-PhoneticPassword.
- Use secure unbiased integer selection and Fisher-Yates composition shuffling.
- Preserve the phonetic alphabet, labels, counts, aliases and display options.
- Use Set-Clipboard for PowerShell 7; honor WhatIf/Confirm for clipboard writes.
- Validate lengths/counts and remove the old random-password alphabet-length cap.
- Add behavioral tests with mocked console and clipboard operations.

## Authenticated string encryption (unreleased)

- Restore New-StringEncryption/New-StringDecryption with AES-256-GCM and PBKDF2-HMAC-SHA256.
Expand Down
3 changes: 3 additions & 0 deletions IT-ToolBox.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
'Test-IsDate'
'New-StringEncryption'
'New-StringDecryption'
'New-RandomString'
'New-RandomPassword'
'New-PhoneticPassword'
)
CmdletsToExport = @()
VariablesToExport = @()
Expand Down
3 changes: 3 additions & 0 deletions IT-ToolBox.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ Export-ModuleMember -Function @(
'Test-IsDate'
'New-StringEncryption'
'New-StringDecryption'
'New-RandomString'
'New-RandomPassword'
'New-PhoneticPassword'
)
Original file line number Diff line number Diff line change
@@ -1,81 +1,6 @@
function New-PhoneticPassword
{
[CmdletBinding(DefaultParameterSetName = 'Default',
ConfirmImpact = 'Medium',
PositionalBinding = $true,
SupportsShouldProcess = $true)]
[OutputType([string])]
param
(
[Parameter(ParameterSetName = 'Default',
Mandatory = $false,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
Position = 0,
HelpMessage = 'Specifies password length')]
[ValidateNotNull()]
[Alias('length')]
[int]
$PasswordLength = 12,
[Parameter(ParameterSetName = 'Option',
ValueFromPipelineByPropertyName = $true,
Position = 0)]
[ValidateNotNull()]
[Alias('LowerCase', 'Small')]
[int]
$LowerCaseLetters = 0,
[Parameter(ParameterSetName = 'Option',
ValueFromPipelineByPropertyName = $true,
Position = 1)]
[ValidateNotNull()]
[Alias('UpperCase', 'Capital')]
[int]
$CapitalCaseLetters = 0,
[Parameter(ParameterSetName = 'Option',
ValueFromPipelineByPropertyName = $true,
Position = 2)]
[ValidateNotNull()]
[int]
$NumberDigits = 0,
[Parameter(ParameterSetName = 'Option',
ValueFromPipelineByPropertyName = $true,
Position = 3)]
[ValidateNotNull()]
[Alias('SpecialLetter')]
[int]
$Symbol = 0,
[Parameter(ParameterSetName = 'Option',
Position = 4)]
[Parameter(ParameterSetName = 'Default')]
[switch]
$ColorOutput,
[Parameter(Position = 5)]
[switch]
$PasswordToClipboard,
[Parameter(ParameterSetName = 'Option')]
[Parameter(ParameterSetName = 'Default')]
[Alias('NoOuput')]
[switch]
$NoPasswordSpell
)

Begin
{
$tempRandomPassword = @()
$returnPassword = @()
$returnLowerCasePassword = @()
$returnCapitalCasePassword = @()
$returnSymbolPassword = @()
$returnDigitsPassword = @()

# Generate Json AsciiCodes table
<#
Excluded Codes:
{"Index": "28","Number": "96","AsciiCode": "`","Phonetic": "Grave accent","Type": "Symbol"},
{ "Index": "32", "Number": "126", "AsciiCode": "~", "Phonetic": "Equivalency sign - Tilde", "Type": "Symbol" },
#>

[string]$asciiJson = @"
function Get-ITToolBoxPhoneticCharacters {
# Original spelling, labels, categories and alphabet are retained verbatim.
$json = @'
[
{"Index": "1","Number": "33","AsciiCode": "!","Phonetic": "Exclamation point","Type": "Symbol"},
{"Index": "2","Number": "34","AsciiCode": "\"","Phonetic":"Double quotes","Type": "Symbol"},
Expand Down Expand Up @@ -170,137 +95,6 @@ function New-PhoneticPassword
{"Index": "93","Number": "56","AsciiCode": "8","Phonetic": "Eight","Type": "Number"},
{"Index": "94","Number": "57","AsciiCode": "9","Phonetic": "Nine","Type": "Number"}
]
"@
# Generate Seed
function Get-RandomSeed
{
# Instantiate object
$randomBytes = New-Object -TypeName 'System.Byte[]' 64

# Use RNGCryptoServiceProvider for better entropy
[System.Security.Cryptography.RNGCryptoServiceProvider]$Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider'
$Random.GetBytes($randomBytes)
[BitConverter]::ToInt32($randomBytes, 0)
}
}
Process
{
# Generate characters tables
$charsTable = ConvertFrom-Json -InputObject $asciiJson
$symbolsTable = $charsTable | Where-Object { $_.Type -eq 'Symbol' }
$capitalLettersTable = $charsTable | Where-Object { $_.Type -eq 'Capital Letter' }
$lowerCaseLettersTable = $charsTable | Where-Object { $_.Type -eq 'Lowercase Letter' }
$digitsTable = $charsTable | Where-Object { $_.Type -eq 'Number' }

switch ($PsCmdlet.ParameterSetName)
{
'Default'
{
for ($passwordLengthIdex = 1; $passwordLengthIdex -le $PasswordLength; $passwordLengthIdex++)
{
$seedNumber = Get-RandomSeed
$passwordSalt = Get-Random -InputObject $charsTable -SetSeed $seedNumber
$returnPassword += $passwordSalt
}
}
'Option'
{
if ($LowerCaseLetters -ne 0)
{
for ($lowerCaseLetterIndex = 1; $lowerCaseLetterIndex -le $LowerCaseLetters; $lowerCaseLetterIndex++)
{
$lowerCaseSeed = Get-RandomSeed
$returnLowerCasePassword += Get-Random -InputObject $lowerCaseLettersTable -SetSeed $lowerCaseSeed
}
}

if ($CapitalCaseLetters -ne 0)
{
for ($capitalLetterIndex = 1; $capitalLetterIndex -le $CapitalCaseLetters; $capitalLetterIndex++)
{
$capitalCaseSeed = Get-RandomSeed
$returnCapitalCasePassword += Get-Random -InputObject $capitalLettersTable -SetSeed $capitalCaseSeed
}
}

if ($NumberDigits -ne 0)
{
for ($numberDigitsIndex = 1; $numberDigitsIndex -le $NumberDigits; $numberDigitsIndex++)
{
$numberDigitSeed = Get-RandomSeed
$returnDigitsPassword += Get-Random -InputObject $digitsTable -SetSeed $numberDigitSeed
}
}

if ($Symbol -ne 0)
{
for ($sy = 1; $sy -le $Symbol; $sy++)
{
$SymbolUniqueNumber = Get-RandomSeed
$returnSymbolPassword += Get-Random -InputObject $symbolsTable -SetSeed $SymbolUniqueNumber
}
}

# Concatenate passwords
$tempRandomPassword += $returnLowerCasePassword + $returnCapitalCasePassword + $returnDigitsPassword + $returnSymbolPassword

# String scamble
$returnPassword = $tempRandomPassword |
Select-Object '*', @{
N = 'Sort'; E = {
1 .. 500 |
Get-Random (Get-RandomSeed)
}
} |
Sort-Object -Property Sort
}
}
}
End
{
# Format string for output
$finalPassword = $returnPassword.AsciiCode -Join ''
$spellOutTable = $returnPassword |
Select-Object @{
Name = 'Character'; Expression = { $_.'AsciiCode' }
}, 'Phonetic', 'Type'

# Send password to clipboard
if ($PasswordToClipboard)
{
$poshVersion = $PSVersionTable.PSVersion.Major

# Check PoSh version
switch ($poshVersion)
{
5
{
$finalPassword | Set-Clipboard
break
}
default
{
$finalPassword | clip
break
}
}
}

if (!$NoPasswordSpell)
{
# Colored output
if ($ColorOutput)
{
Write-Host ($spellOutTable | Out-String) -ForegroundColor 'Green'
Write-Host $finalPassword
}
else
{
Write-Host ($spellOutTable | Out-String)
Write-Host "Final password is:" $finalPassword
}
}

return $finalPassword
}
}
'@
return ConvertFrom-Json -InputObject $json
}
4 changes: 4 additions & 0 deletions Private/Get-ITToolBoxRandomIndex.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function Get-ITToolBoxRandomIndex {
param([int]$UpperBound)
return [System.Security.Cryptography.RandomNumberGenerator]::GetInt32($UpperBound)
}
12 changes: 12 additions & 0 deletions Private/Get-ITToolBoxShuffledItems.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function Get-ITToolBoxShuffledItems {
param([object[]]$Items)
# Fisher-Yates: shuffle a copy without changing the caller's collection.
[object[]]$result = $Items.Clone()
for ($index = $result.Length - 1; $index -gt 0; $index--) {
$other = Get-ITToolBoxRandomIndex -UpperBound ($index + 1)
$temporary = $result[$index]
$result[$index] = $result[$other]
$result[$other] = $temporary
}
return $result
}
80 changes: 80 additions & 0 deletions Public/New-PhoneticPassword.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
function New-PhoneticPassword {
<#
.SYNOPSIS
Generates passwords with phonetic spelling and optional exact category counts.
.DESCRIPTION
Retains the original phonetic alphabet, aliases, default 12-character length,
display/color switches, clipboard option and string output. Pipeline input is
accumulated into one returned password, matching historical length-mode behavior.
Generation uses secure random selection and a Fisher-Yates shuffle. Clipboard
writes use Set-Clipboard and honor WhatIf/Confirm. Invalid counts are rejected.
#>
[CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
[OutputType([string])]
param(
[Parameter(ParameterSetName = 'Default', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
[ValidateRange(1,4096)][Alias('length')][int]$PasswordLength = 12,
[Parameter(ParameterSetName = 'Option', ValueFromPipelineByPropertyName = $true, Position = 0)]
[ValidateRange(0,4096)][Alias('LowerCase', 'Small')][int]$LowerCaseLetters = 0,
[Parameter(ParameterSetName = 'Option', ValueFromPipelineByPropertyName = $true, Position = 1)]
[ValidateRange(0,4096)][Alias('UpperCase', 'Capital')][int]$CapitalCaseLetters = 0,
[Parameter(ParameterSetName = 'Option', ValueFromPipelineByPropertyName = $true, Position = 2)]
[ValidateRange(0,4096)][int]$NumberDigits = 0,
[Parameter(ParameterSetName = 'Option', ValueFromPipelineByPropertyName = $true, Position = 3)]
[ValidateRange(0,4096)][Alias('SpecialLetter')][int]$Symbol = 0,
[Parameter(Position = 4)][switch]$ColorOutput,
[Parameter(Position = 5)][switch]$PasswordToClipboard,
[Alias('NoOuput', 'NoOutput')][switch]$NoPasswordSpell
)
begin {
$characters = @(Get-ITToolBoxPhoneticCharacters)
$passwordItems = [System.Collections.Generic.List[object]]::new()
}
process {
$currentItems = [System.Collections.Generic.List[object]]::new()
if ($PSCmdlet.ParameterSetName -eq 'Default') {
for ($index = 0; $index -lt $PasswordLength; $index++) {
$currentItems.Add($characters[(Get-ITToolBoxRandomIndex -UpperBound $characters.Length)])
}
}
else {
$total = $LowerCaseLetters + $CapitalCaseLetters + $NumberDigits + $Symbol
if ($total -lt 1 -or $total -gt 4096) {
throw [ArgumentException]::new('Character counts must total between 1 and 4096.')
}
foreach ($category in @(
@{ Type = 'Lowercase Letter'; Count = $LowerCaseLetters }
@{ Type = 'Capital Letter'; Count = $CapitalCaseLetters }
@{ Type = 'Number'; Count = $NumberDigits }
@{ Type = 'Symbol'; Count = $Symbol }
)) {
$pool = @($characters | Where-Object Type -eq $category.Type)
for ($index = 0; $index -lt $category.Count; $index++) {
$currentItems.Add($pool[(Get-ITToolBoxRandomIndex -UpperBound $pool.Length)])
}
}
$shuffled = @(Get-ITToolBoxShuffledItems -Items $currentItems.ToArray())
$currentItems.Clear()
$currentItems.AddRange($shuffled)
}
$passwordItems.AddRange($currentItems.ToArray())
}
end {
[string]$finalPassword = $passwordItems.AsciiCode -join ''
if ($PasswordToClipboard -and $PSCmdlet.ShouldProcess('Clipboard', 'Copy generated password')) {
Set-Clipboard -Value $finalPassword -ErrorAction Stop
}
if (-not $NoPasswordSpell) {
$spellOutTable = $passwordItems | Select-Object @{ Name = 'Character'; Expression = { $_.AsciiCode } }, Phonetic, Type
if ($ColorOutput) {
Write-Host ($spellOutTable | Out-String) -ForegroundColor Green
Write-Host $finalPassword
}
else {
Write-Host ($spellOutTable | Out-String)
Write-Host 'Final password is:' $finalPassword
}
}
return $finalPassword
}
}
20 changes: 20 additions & 0 deletions Public/New-RandomPassword.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function New-RandomPassword {
<#
.SYNOPSIS
Generates a password from the original punctuation, digit and letter alphabet.
.DESCRIPTION
Retains the 8-character default and Complex switch for compatibility.
Complex did not change the historical character selection and remains equivalent
to the default. Use New-PhoneticPassword for exact character-category counts.
Characters are independently selected with replacement using a cryptographic RNG.
#>
[CmdletBinding()]
[OutputType([string])]
param([ValidateRange(1,4096)][int]$PasswordLength = 8, [switch]$Complex)
[char[]]$characters = @(33..47) + @(48..57) + @(58..64) + @(92..96) + @(123..126) + @(65..90) + @(97..122)
$result = [System.Text.StringBuilder]::new($PasswordLength)
for ($index = 0; $index -lt $PasswordLength; $index++) {
[void]$result.Append($characters[(Get-ITToolBoxRandomIndex -UpperBound $characters.Length)])
}
return $result.ToString()
}
15 changes: 15 additions & 0 deletions Public/New-RandomString.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function New-RandomString {
<#
.SYNOPSIS
Generates a string using the original character alphabet and secure random selection.
#>
[CmdletBinding()]
[OutputType([string])]
param([ValidateRange(1,4096)][int]$StringLength = 12)
[string]$characters = 'ABCDE@#FGH]IJKL12MNOP_!Q{RSTUV89WXYZa}bcd67ef[ghijklmnopq(rs%^&tuvwxyz)03$*45'
$result = [System.Text.StringBuilder]::new($StringLength)
for ($index = 0; $index -lt $StringLength; $index++) {
[void]$result.Append($characters[(Get-ITToolBoxRandomIndex -UpperBound $characters.Length)])
}
return $result.ToString()
}
Loading
Loading