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 @@
## String utilities restoration (unreleased)

- Restore character conversion, string checksums and SHA-256 hashing.
- Preserve the character map while fixing index leakage and default space handling.
- Copy custom maps; normalize text and process Unicode text elements.
- Retain historical MD5 checksum formatting and allow stronger digest selection.
- Default SHA-256 output to hexadecimal; expose LegacyFormat for old comparisons.
- Add behavioral and known-vector tests.

## API request restoration (unreleased)

- Restore New-ApiRequest with independent optional-parameter handling.
Expand Down
3 changes: 3 additions & 0 deletions IT-ToolBox.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
'New-RandomPassword'
'New-PhoneticPassword'
'New-ApiRequest'
'New-StringConversion'
'Get-StringCheckSum'
'Get-StringHashCode'
)
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 @@ -24,4 +24,7 @@ Export-ModuleMember -Function @(
'New-RandomPassword'
'New-PhoneticPassword'
'New-ApiRequest'
'New-StringConversion'
'Get-StringCheckSum'
'Get-StringHashCode'
)
148 changes: 148 additions & 0 deletions Private/Get-ITToolBoxCharacterMap.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
function Get-ITToolBoxCharacterMap {
# Retain the original domain-specific mapping; this is not universal transliteration.
return @{

# a
'æ' = 'a'
'à' = 'a'
'â' = 'a'
'ã' = 'a'
'å' = 'a'
'ā' = 'a'
'ă' = 'a'
'ą' = 'a'
'ä' = 'a'
'á' = 'a'

# b
'ƀ' = 'b'
'ƃ' = 'b'

# Tone six
'ƅ' = 'b'

# c
'ç' = 'c'
'ć' = 'c'
'ĉ' = 'c'
'ċ' = 'c'
'č' = 'c'
'ƈ' = 'c'

# d
'ď' = 'd'
'đ' = 'd'
'ƌ' = 'd'

# e
'è' = 'e'
'é' = 'e'
'ê' = 'e'
'ë' = 'e'
'ē' = 'e'
'ĕ' = 'e'
'ė' = 'e'
'ę' = 'e'
'ě' = 'e'
'&' = 'e'

# g
'ĝ' = 'e'
'ğ' = 'e'
'ġ' = 'e'
'ģ' = 'e'

# h
'ĥ' = 'h'
'ħ' = 'h'

# i
'ì' = 'i'
'í' = 'i'
'î' = 'i'
'ï' = 'i'
'ĩ' = 'i'
'ī' = 'i'
'ĭ' = 'i'
'į' = 'i'
'ı' = 'i'

# j
'ij' = 'j'
'ĵ' = 'j'

# k
'ķ' = 'k'
'ĸ' = 'k'

# l
'ĺ' = 'l'
'ļ' = 'l'
'ľ' = 'l'
'ŀ' = 'l'
'ł' = 'l'

# n
'ñ' = 'n'
'ń' = 'n'
'ņ' = 'n'
'ň' = 'n'
'ʼn' = 'n'
'ŋ' = 'n'

# o
'ð' = 'o'
'ó' = 'o'
'õ' = 'o'
'ô' = 'o'
'ö' = 'o'
'ø' = 'o'
'ō' = 'o'
'ŏ' = 'o'
'ő' = 'o'
'œ' = 'o'

# r
'ŕ' = 'r'
'ŗ' = 'r'
'ř' = 'r'

# s
'ś' = 's'
'ŝ' = 's'
'ş' = 's'
'š' = 's'
'ß' = 'ss'
'ſ' = 's'

# t
'ţ' = 't'
'ť' = 't'
'ŧ' = 't'

# u
'ù' = 'u'
'ú' = 'u'
'û' = 'u'
'ü' = 'u'
'ũ' = 'u'
'ū' = 'u'
'ŭ' = 'u'
'ů' = 'u'
'ű' = 'u'
'ų' = 'u'

# w
'ŵ' = 'w'

# y
'ý' = 'y'
'ÿ' = 'y'
'ŷ' = 'y'

# z
'ź' = 'z'
'ż' = 'z'
'ž' = 'z'
}
}
12 changes: 12 additions & 0 deletions Private/Get-ITToolBoxStringDigest.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function Get-ITToolBoxStringDigest {
param([string]$Text, [string]$Algorithm)
[byte[]]$bytes = [System.Text.Encoding]::UTF8.GetBytes($Text)
[byte[]]$digest = switch ($Algorithm.ToUpperInvariant()) {
'MD5' { [System.Security.Cryptography.MD5]::HashData($bytes) }
'SHA256' { [System.Security.Cryptography.SHA256]::HashData($bytes) }
'SHA384' { [System.Security.Cryptography.SHA384]::HashData($bytes) }
'SHA512' { [System.Security.Cryptography.SHA512]::HashData($bytes) }
default { throw [ArgumentException]::new('Unsupported digest algorithm.') }
}
return ,$digest
}
19 changes: 19 additions & 0 deletions Public/Get-StringCheckSum.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function Get-StringCheckSum {
<#
.SYNOPSIS
Returns a UTF-8 text checksum as uppercase hyphen-separated hexadecimal bytes.
.DESCRIPTION
MD5 remains the default for compatibility with existing checksums. It is for
non-security comparison only. SHA256, SHA384 and SHA512 can be selected explicitly.
An unkeyed digest does not authenticate data or provide password storage.
Text is encoded as supplied without Unicode normalization or a BOM.
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StringToCheck,
[ValidateSet('MD5','SHA256','SHA384','SHA512')][string]$Algorithm = 'MD5'
)
[byte[]]$digest = Get-ITToolBoxStringDigest -Text $StringToCheck -Algorithm $Algorithm
return [BitConverter]::ToString($digest)
}
26 changes: 26 additions & 0 deletions Public/Get-StringHashCode.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
function Get-StringHashCode {
<#
.SYNOPSIS
Returns the SHA-256 digest of UTF-8 text as 64 uppercase hexadecimal characters.
.DESCRIPTION
This is SHA-256, not .NET GetHashCode. LegacyFormat returns the historical
delimiter-free decimal byte concatenation for migration/comparison only.
Legacy output is an ambiguous encoding and should not be used for new identifiers.
No Unicode normalization is performed. This is not password hashing or authentication.
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StringToHash,
[switch]$LegacyFormat
)
[byte[]]$digest = Get-ITToolBoxStringDigest -Text $StringToHash -Algorithm SHA256
if ($LegacyFormat) {
$result = [System.Text.StringBuilder]::new()
foreach ($value in $digest) {
[void]$result.Append($value.ToString([System.Globalization.CultureInfo]::InvariantCulture))
}
return $result.ToString()
}
return [Convert]::ToHexString($digest)
}
52 changes: 52 additions & 0 deletions Public/New-StringConversion.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
function New-StringConversion {
<#
.SYNOPSIS
Converts mapped characters and replaces unsupported text elements.
.DESCRIPTION
Retains the historical character map and permitted ASCII punctuation.
Default spaces become hyphens. Use IgnoreSpaces, RemoveSpaces or ReplaceSpaces
to select another policy. Custom maps replace the default map and are not mutated.
Input is normalized to Unicode Form C; each remaining text element is processed
once. This is a domain-specific conversion, not general Unicode transliteration.
Returns exactly one string, without internal collection indices.
#>
[CmdletBinding(DefaultParameterSetName = 'ReplaceSpaces')]
[OutputType([string])]
param(
[Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StringToConvert,
[ValidateNotNull()][hashtable]$UnicodeHashTable,
[Parameter(ParameterSetName = 'IgnoreSpaces')][switch]$IgnoreSpaces,
[Parameter(ParameterSetName = 'RemoveSpaces')][switch]$RemoveSpaces,
[Parameter(ParameterSetName = 'ReplaceSpaces')][AllowEmptyString()][ValidateNotNull()][string]$ReplaceSpaces = '-',
[ValidateNotNullOrEmpty()][string]$UnknownCharacter = '?'
)
if ($PSBoundParameters.ContainsKey('UnicodeHashTable')) {
$map = @{}
foreach ($key in $UnicodeHashTable.Keys) {
$normalizedKey = ([string]$key).Normalize([System.Text.NormalizationForm]::FormC).ToLowerInvariant()
if ([string]::IsNullOrEmpty($normalizedKey)) {
throw [ArgumentException]::new('Character map keys cannot be empty.')
}
$map[$normalizedKey] = [string]$UnicodeHashTable[$key]
}
}
else { $map = Get-ITToolBoxCharacterMap }
$map[' '] = if ($IgnoreSpaces) { ' ' } elseif ($RemoveSpaces) { '' } else { $ReplaceSpaces }
$normalized = $StringToConvert.Normalize([System.Text.NormalizationForm]::FormC)
$elements = [System.Globalization.StringInfo]::GetTextElementEnumerator($normalized)
$result = [System.Text.StringBuilder]::new()
while ($elements.MoveNext()) {
[string]$element = $elements.GetTextElement()
[string]$lower = $element.ToLowerInvariant()
if ($map.ContainsKey($lower)) {
[string]$replacement = $map[$lower]
if ($element -cne $lower) { $replacement = $replacement.ToUpperInvariant() }
[void]$result.Append($replacement)
}
elseif ($element -cmatch '^[0-9a-zA-Z!#$@.''^_~-]$') {
[void]$result.Append($element)
}
else { [void]$result.Append($UnknownCharacter) }
}
return $result.ToString()
}
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ The foundation imports without WinSCP, GnuPG, Active Directory or Exchange depen
| New-RandomPassword | Secure random passwords from the historical mixed alphabet |
| New-PhoneticPassword | Passwords with phonetic spelling and exact category counts |
| New-ApiRequest | OAuth-style form/JSON client requests |
| New-StringConversion | Domain-specific character mapping and space handling |
| Get-StringCheckSum | UTF-8 checksums with selectable digest algorithms |
| Get-StringHashCode | Standard SHA-256 hex with explicit legacy output mode |

```powershell
Import-Module ./IT-ToolBox.psd1
Expand All @@ -46,10 +49,10 @@ module variables. Redaction is opt-in and does not guarantee detection of every
- SCP and GnuPG wrappers and bundled WinSCP binaries are removed. Separate modules
will own file transfer and OpenPGP; no replacement is bundled here.
- `Legacy/` retains string encryption, Exchange and script-context helpers for reference.
- `Staging/v3/` retains 12 candidate commands pending tests and compatibility fixes.
- `Staging/v3/` retains 9 candidate commands pending tests and compatibility fixes.
These include existing validators, strings, password generation, API requests,
registry, uptime and AD utilities. They are not currently exported.
- Only the fifteen listed commands are exported. Private helpers, variables and aliases
- Only the eighteen listed commands are exported. Private helpers, variables and aliases
are not exported. Existing calls to other v2 commands require the v2 release until
those commands return to the supported API.
- The module GUID and Git history are preserved.
Expand Down Expand Up @@ -179,3 +182,27 @@ Connection timeout defaults to 30 seconds and can be configured with
wall-clock deadline. Failures terminate with the original Invoke-RestMethod error;
no retries or custom logging are added. Tests mock all HTTP calls: real endpoint
behavior and TLS are not integration-tested.

## String conversion and hashing

`New-StringConversion` retains the original character map, custom-map parameter,
unknown-character replacement and space options. Its default now matches the
documented behavior: spaces become hyphens. It returns exactly one string rather
than leaking collection indices. The map remains domain-specific (for example,
`&` maps to `e`); it is not a general-purpose transliteration or safe-path generator.

Custom maps replace the default map and are copied internally. Space policy
overrides an existing space mapping without mutating the caller's table. Conversion
normalizes text to Form C and handles unsupported Unicode text elements once,
including supplementary characters. This changes output for decomposed accents
and surrogate pairs compared with the old UTF-16 character loop.

`Get-StringCheckSum` retains uppercase hyphen-separated MD5 output by default for
existing non-security comparisons. Select `-Algorithm SHA256`, `SHA384` or `SHA512`
when needed. MD5 is unsuitable for adversarial integrity checks.

`Get-StringHashCode` returns 64 uppercase SHA-256 hexadecimal characters by default.
Use `-LegacyFormat` only when comparing with old delimiter-free decimal output.
The hash algorithm remains SHA-256; the default representation intentionally changes.
Both hash commands use UTF-8 without a BOM and do not normalize text. Neither is
a password-storage function or an authentication mechanism.
30 changes: 0 additions & 30 deletions Staging/v3/Get-StringCheckSum.ps1

This file was deleted.

Loading
Loading