CommonUtilities.psm1

<#
.Synopsis
    Generates a cryptographically secure password.

.Description
    The cmdlet generates strong passwords with cryptographically secure random number generator.

    By default this cmdlets generates a password of length 16 with upper case, lower case, numeral and special characters.

    "newpwd" is the alias of this cmdlet.

.Parameter Length
    The length of the output. It must be at least 4 and at most 256. The default is 16.

.Parameter RNGImplementation
    The name of the implementation of cryptographically secure random number generation algorithm.

    "RNGAlgorithm" and "RNG" are the aliases of this parameter.

.Parameter NoUpperCaseCharacters
    Suppresses upper case characters from the output. These include ABCDEFGHIJKLMNOPQRSTUVWXYZ.

    "NoUC" is the alias of this switch.

.Parameter NoLowerCaseCharacters
    Suppresses lower case characters from the output. These include abcdefghijklmnopqrstuvwxyz.

    "NoLC" is the alias of this switch.

.Parameter NoNumeralCharacters
    Suppresses numeral characters from the output. These include 0123456789.

    "NoNum" is the alias of this switch.

.Parameter NoSpecialCharacters
    Suppresses special characters from the output. These include `~!@#$%^&*()_+-={}[]|\;':"<>?,./ and space.

    "NoSpecial" is the alias of this switch.

.Parameter AllowSimilarCharacters
    Allowes similar characters in the output. These include 1, l and I, 0 and O and `, ' and ".

.Parameter AllowSpace
    Allowes the space character in the output.

.Parameter UseSecureString
    Pipes out a System.Security.SecureString instead of string.

.Parameter Elder
    Forces the output to end with "+1s". If this switch is set, -UseSecureString will be cleared even if set explicitly. However, NONE of -NoLowerCaseCharacters, -NoNumeralCharacters and -NoSpecialCharacters are required to be cleared.

    "ls" is the alias of this switch. Therefore you get one second subtracted if you extend the elder's life for one second.

.Example
    New-Password -Length 20 -AllowSimilarCharacters

    This creates a 20-character long password possibly with similar characters.

    Possible output: "X9kw5Bc2~W^16EzuU]jJ"

.Example
    New-Password -NoSpecialCharacters -UseSecureString

    This creates a 16-character long password without special characters as a SecureString.

    Possible output: A System.Security.SecureString object.

.Example
    New-Password -Elder

    This creates a 16-character long password that ends with "+1s".

    Possible output: ">nvaM!$HAAr;v+1s"

.Link
    https://github.com/GeeLaw/PowerShellThingies/blob/master/modules/CommonUtilities/New-Password.md

#>

Function New-Password
{
    [CmdletBinding()]
    [Alias('newpwd')]
    Param
    (
        [Parameter(ValueFromPipeline = $true)]
        [ValidateRange(4, 256)]
        [int]$Length = 16,
        [Alias("RNGAlgorithm", "RNG")]
        [string]$RNGImplementation,
        [Alias("NoUC")]
        [switch]$NoUpperCaseCharacters,
        [Alias("NoLC")]
        [switch]$NoLowerCaseCharacters,
        [Alias("NoNum")]
        [switch]$NoNumeralCharacters,
        [Alias("NoSpecial")]
        [switch]$NoSpecialCharacters,
        [switch]$AllowSimilarCharacters,
        [switch]$AllowSpace,
        [switch]$UseSecureString,
        [Alias("ls", "o-o")]
        [switch]$Elder
    )
    Process
    {
        $local:uc = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
        $local:lc = 'abcdefghijkmnopqrstuvwxyz';
        $local:nu = '234567892345678923456789';
        $local:sp = '~!@#$%^&*()_+{}|[]\-=:;<>?,./';
        If ($AllowSimilarCharacters)
        {
            $uc += 'IO'; $lc += 'l';
            $nu += '010101'; $sp += "'" + '`"';
        }
        If ($AllowSpace)
        {
            $sp += ' ';
        }
        $local:lib = '';
        If (-not $NoUpperCaseCharacters)
        {
            $lib += $uc;
        }
        If (-not $NoLowerCaseCharacters)
        {
            $lib += $lc;
        }
        If (-not $NoNumeralCharacters)
        {
            $lib += $nu;
        }
        If (-not $NoSpecialCharacters)
        {
            $lib += $sp;
        }
        If ($lib.Length -eq 0)
        {
            Write-Error 'At least one category of characters must be allowed.';
            Return;
        }
        If ($Elder)
        {
            If ($UseSecureString)
            {
                Write-Warning '-UseSecureString is cleared by -Elder.';
            }
            $UseSecureString = $false;
            <# Sets these switches so that the algorithm no longer checks them.
             # But $lib already contains the specified characters, therefore
             # the generation rule is still correct.
             #>

            $NoLowerCaseCharacters = $true;
            $NoNumeralCharacters = $true;
            $NoSpecialCharacters = $true;
            $Length -= 3;
        }
        $local:rnd = $null;
        If ([string]::IsNullOrEmpty($RNGImplementation))
        {
            $rnd = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider';
        }
        Else
        {
            $rnd = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider' -ArgumentList $RNGImplementation;
        }
        $local:result = $null;
        $local:byteHolder = New-Object -TypeName 'byte[]' -ArgumentList @(1);
        If ($UseSecureString)
        {
            <# This instance will be disposed immediately in the first round of the loop. #>
            $result = New-Object -TypeName 'System.Security.SecureString';
        }
        $local:hasUC = $false; $local:hasLC = $false; $local:hasNU = $false; $local:hasSP = $false;
        $local:trimming = $false;
        $local:i = 0;
        Do
        {
            $hasUC = $false; $hasLC = $false; $hasNU = $false; $hasSP = $false;
            $trimming = $false;
            If ($UseSecureString)
            {
                $result.Dispose();
                $result = New-Object -TypeName 'System.Security.SecureString';
                For ($i = 0; $i -lt $Length; ++$i)
                {
                    Do
                    {
                        $rnd.GetBytes($byteHolder);
                    }
                    Until ([int]($byteHolder[0] / $lib.Length) -ne [int](256 / $lib.Length));
                    $result.AppendChar($lib[$byteHolder[0] % $lib.Length]);
                    If ($uc.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasUC = $true;
                    }
                    If ($lc.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasLC = $true;
                    }
                    If ($nu.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasNU = $true;
                    }
                    If ($sp.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasSP = $true;
                    }
                    if ($lib[$byteHolder[0] % $lib.Length] -eq ' '[0] -and ($i -eq 0 -or $i -eq ($Length - 1)))
                    {
                        $trimming = $true;
                    }
                }
                $result.MakeReadOnly();
            }
            Else
            {
                $result = '';
                For ($i = 0; $i -lt $Length; ++$i)
                {
                    Do
                    {
                        $rnd.GetBytes($byteHolder);
                    }
                    Until ([int]($byteHolder[0] / $lib.Length) -ne [int](256 / $lib.Length));
                    $result += $lib[$byteHolder[0] % $lib.Length];
                    If ($uc.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasUC = $true;
                    }
                    If ($lc.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasLC = $true;
                    }
                    If ($nu.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasNU = $true;
                    }
                    If ($sp.Contains($lib[$byteHolder[0] % $lib.Length]))
                    {
                        $hasSP = $true;
                    }
                }
                If ($Elder)
                {
                    $result += '+1s';
                }
                If ($result[0] -eq ' '[0] -or $result[$result.Length - 1] -eq ' '[0])
                {
                    $trimming = $true;
                }
            }
            $byteHolder[0] = 0;
        }
        Until (-not $trimming -and ($NoUpperCaseCharacters -or $hasUC) -and ($NoLowerCaseCharacters -or $hasLC) -and ($NoNumeralCharacters -or $hasNU) -and ($NoSpecialCharacters -or $hasSP));
        $rnd.Dispose();
        Return $result;
    }
}


<#
.Synopsis
    Switches to elevated PowerShell or PowerShell run as another user.

.Description
    When called from an usual PowerShell prompt, it tries to start the elevated PowerShell. If it succeeds, the calling window is hidden. When the elevated PowerShell exits (by invoking exit or any other means), the calling window reappears, providing a seamless experience of elevation.

    The same rule applies for switching to another user.

.Link
    https://github.com/GeeLaw/PowerShellThingies/blob/master/modules/CommonUtilities/Switch-User.md

#>

Function Switch-User
{
    [CmdletBinding()]
    [Alias('su')]
    Param
    (
        [Parameter(ValueFromPipeline = $true)]
        [Alias('user', 'as', 'to')]
        [System.Management.Automation.PSCredential]
        $Credential = [System.Management.Automation.PSCredential]::Empty
    )
    Process
    {
        $local:ErrorActionPreference = 'Stop';
        If ($Host.Name -ne 'ConsoleHost')
        {
            Write-Error 'This cmdlet can only be invoked from PowerShell.';
            Return;
        }
        $local:IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator');
        $local:currentPathUnicodeBase64 = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes((Get-Location).Path));
        $local:suProcessInitCmd = '& { ';
        $suProcessInitCmd += 'Set-Location -Path ([System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String(';
        $suProcessInitCmd += "'";
        $suProcessInitCmd += $currentPathUnicodeBase64;
        $suProcessInitCmd += "'";
        $suProcessInitCmd += '))); ';
        $suProcessInitCmd += '}';
        $local:suProcess = $null;
        $local:currentProcess = $null;
        $local:wasVisible = $true;
        If ($IsAdmin -and [object]::ReferenceEquals($Credential, [System.Management.Automation.PSCredential]::Empty))
        {
            $Credential = Get-Credential -Message 'Please specify the credential to run PowerShell.';
            If ($Credential -eq $null)
            {
                Write-Error 'Action cancelled by user.' -Category OperationStopped;
                Return;
            }
        }
        If ([object]::ReferenceEquals($Credential, $null) -or [object]::ReferenceEquals($Credential, [System.Management.Automation.PSCredential]::Empty))
        {
            $suProcess = Start-Process -PassThru -FilePath 'PowerShell.exe' -Verb 'runas' `
                -ArgumentList @('-NoExit', '-ExecutionPolicy', (Get-ExecutionPolicy).ToString(), '-Command', $suProcessInitCmd);
            If ($suProcess -eq $null)
            {
                Return;
            }
            Write-Verbose "Another process started on $($suProcess.StartTime), ProcessId = $($suProcess.Id).";
            $currentProcess = Get-Process -Id $pid;
            $wasVisible = [__3b043047842e4cfa94dbcb39a5ccf3e5.SwitchUser]::ShowWindow($currentProcess.MainWindowHandle, 0);
            $suProcess.WaitForExit();
            Write-Verbose "The process exited on $($suProcess.ExitTime).";
            If ($suProcess.ExitCode -eq 0)
            {
                Write-Verbose 'The process exited with code 0.';
            }
            Else
            {
                Write-Warning "The process exited with code $($suProcess.ExitCode).";
            }
        }
        Else
        {
            $suProcess = Start-Process -PassThru -FilePath 'PowerShell.exe' `
                -ArgumentList @('-NoExit', '-ExecutionPolicy', (Get-ExecutionPolicy).ToString(), '-Command', $suProcessInitCmd) `
                -Credential $Credential;
            If ($suProcess -eq $null)
            {
                Return;
            }
            Write-Verbose "Another process started on $($suProcess.StartTime), ProcessId = $($suProcess.Id).";
            $currentProcess = Get-Process -Id $pid;
            $wasVisible = [__3b043047842e4cfa94dbcb39a5ccf3e5.SwitchUser]::ShowWindow($currentProcess.MainWindowHandle, 0);
            $suProcess.WaitForExit();
            Write-Verbose 'Another process has exited. However, running as another user does not give ExitTime or ExitCode.';
        }
        $suProcess.Dispose();
        If ($wasVisible)
        {
            [__3b043047842e4cfa94dbcb39a5ccf3e5.SwitchUser]::ShowWindow($currentProcess.MainWindowHandle, 5) | Out-Null;
            [__3b043047842e4cfa94dbcb39a5ccf3e5.SwitchUser]::SwitchToThisWindow($currentProcess.MainWindowHandle, $True);
            [__3b043047842e4cfa94dbcb39a5ccf3e5.SwitchUser]::BringWindowToTop($currentProcess.MainWindowHandle) | Out-Null;
            If (-not [__3b043047842e4cfa94dbcb39a5ccf3e5.SwitchUser]::SetForegroundWindow($currentProcess.MainWindowHandle))
            {
                $currentProcess.Dispose();
                Write-Error 'Failed to recover the hidden window.';
                Return;
            }
        }
        $currentProcess.Dispose();
        Return;
    }
}

<#
.Synopsis
    A shortcut for Set-AuthenticodeSignature.

.Description
    Use this cmdlet to sign your code (PowerShell scripts, modules, manifests
    and so on). When no certificate is supplied, the cmdlet tries to use your
    code-signing certificate(s).

    "sign" is the alias of this cmdlet.

.Parameter Scripts
    The path of the scripts to sign. This parameter is mandatory. It receives
    value from the pipeline by value and by property (aliased FullName so that
    you can pipe FileInfo object generated by Get-ChildItem).

.Parameter Certificate
    The certificate to use.

    If unspecified, the cmdlet enumerates all your personal code-signing
    certificates. If there is only one such certificate, the scripts are signed
    with this certificate; otherwise you interactively choose one certificate.
    If no such certificate is found, the cmdlet fails.

.Example
    Sign-Scripts -Scripts $profile

    This line signs your profile script with your personal code-signing
    certificate(s).

.Example
    Get-ChildItem -File -Recurse | Sign-Scripts

    This line signs all the files (including those in subfolders) with your
    personal code-signing certificate(s). This line works best if you have one
    and only one such certificate in your personal store.

.Link
    https://github.com/GeeLaw/PowerShellThingies/blob/master/modules/CommonUtilities/Sign-Scripts.md

#>

Function Sign-Scripts
{
    [CmdletBinding()]
    [Alias('sign')]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]$Scripts,
        [Parameter(Mandatory = $false)]
        [Alias('with')]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )
    Begin
    {
        If ($Certificate -eq $null)
        {
            $local:certs = Get-ChildItem -LiteralPath 'Cert:\CurrentUser\My' -CodeSigningCert;
            If ($certs.Count -eq 1)
            {
                Write-Verbose 'Signing code with the following certificate:';
                $certs[0] | Format-List | Out-String | Write-Verbose;
                $Certificate = $certs[0];
            }
            ElseIf ($certs.Count -gt 1)
            {
                Write-Host 'Multiple certificates are available in your personal storage.';
                Write-Host;
                $local:i = 0;
                $certs | ForEach-Object {
                        Write-Host "Certificate[$i]:";
                        $_ | Format-List | Out-String | Write-Host;
                        $i = $i + 1;
                    };
                $local:choice = Read-Host -Prompt 'Please specify the certificate';
                $local:choiceInt = 0;
                If ([int]::TryParse($choice, [ref] $choiceInt))
                {
                    If ($choiceInt -ge 0 -and $choiceInt -lt $certs.Count)
                    {
                        $Certificate = $certs;
                    }
                    Else
                    {
                        Throw [IndexOutOfRangeException];
                    }
                }
                Else
                {
                    Throw [FormatException] 'You must specifiy an index.';
                }
            }
            Else
            {
                throw [Exception] 'You do not have a certifcate in your personal storage. Please specify the certificate in the command.';
            }
        }
        If ($Certificate -eq $null)
        {
            Break;
        }
    }
    Process
    {
        If ($Certificate -ne $null)
        {
            Set-AuthenticodeSignature -FilePath $Scripts -Certificate $Certificate;
        }
    }
}

<#
.SYNOPSIS
    Restarts the host.

.LINK
    https://github.com/GeeLaw/PowerShellThingies/tree/master/modules/CommonUtilities

#>

Function Restart-Host
{
    [CmdletBinding()]
    [Alias('restart')]
    Param()
    Process
    {
        Start-Process -FilePath (Get-Process -Id $pid).Path;
        Exit;
    }
}

<#
.SYNOPSIS
    Sets a fast credential.

.DESCRIPTION
    This advanced function stores a credential with default
    encryption method under %LOCALAPPDATA%\FastCredentials.

    It supports user names in the form of USERNAME or
    DOMAIN\USERNAME, where DOMAIN and USERNAME must consist
    of only valid file name characters, and not end with a
    full stop.

    Under proper security configuration (up-to-date Windows,
    BitLocker, proper permission on %USERPROFILE% and strong
    password), the stored credential is only accessible from
    the current user. See the online help for canonical usage.

.PARAMETER Credential
    One single credential to be stored. The value can be piped
    from another command like Get-Credential.

.EXAMPLE
    Get-Credential | Set-FastCredential

    This example reads a credential from GUI and saves it.

.LINK
    https://github.com/GeeLaw/PowerShellThingies/blob/master/modules/CommonUtilities/FastCredential.md
#>

Function Set-FastCredential
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSCredential]$Credential
    )
    Process
    {
        Try
        {
            $local:ssString = ConvertFrom-SecureString -SecureString $Credential.Password;
            $local:localAppData = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData);
            If (-not (Test-Path -LiteralPath ([System.IO.Path]::Combine($local:localAppData, 'FastCredentials'))))
            {
                New-Item -Path $local:localAppData -Name 'FastCredentials' -ItemType 'Directory' | Out-Null;
            }
            Push-Location -LiteralPath ([System.IO.Path]::Combine($local:localAppData, 'FastCredentials'));
            Try
            {
                $Credential.UserName.Split('\') | Select-Object -SkipLast 1 | ForEach-Object {
                    If (-not (Test-Path -LiteralPath $_))
                    {
                        New-Item -Name $_ -ItemType 'Directory' | Out-Null;
                    }
                    Set-Location -LiteralPath $_;
                };
            }
            Catch
            {
                Throw;
            }
            Finally
            {
                Pop-Location;
            }
            $local:fileName = [System.IO.Path]::Combine($local:localAppData, 'FastCredentials', $Credential.UserName + '.fastcred');
            [System.IO.File]::WriteAllText($fileName, $ssString);
        }
        Catch
        {
            Throw;
        }
    }
}

<#
.SYNOPSIS
    Retrieves a fast credential.

.DESCRIPTION
    This advanced function reads a credential with default
    encryption method under %LOCALAPPDATA%\FastCredentials.

    It supports user names in the form of USERNAME or
    DOMAIN\USERNAME, where DOMAIN and USERNAME must consist
    of only valid file name characters, and not end with a
    full stop.

.PARAMETER UserName
    User name of the credential to be retrieved.

.EXAMPLE
    Start-Process -FilePath 'powershell' `
        -Credential (Get-FastCredential 'AnotherUser')

    Starts PowerShell as AnotherUser.

.LINK
    https://github.com/GeeLaw/PowerShellThingies/blob/master/modules/CommonUtilities/FastCredential.md
#>

Function Get-FastCredential
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$UserName
    )
    Process
    {
        Try
        {
            $local:localAppData = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData);
            $local:fileName = [System.IO.Path]::Combine($local:localAppData, 'FastCredentials', $UserName + '.fastcred');
            $local:ssString = [System.IO.File]::ReadAllText($fileName);
            $local:ssString = ConvertTo-SecureString -String $local:ssString;
            $local:cred = [PSCredential]::new($UserName, $local:ssString);
            Return $local:cred;
        }
        Catch
        {
            Throw;
        }
    }
}

<#
.SYNOPSIS
    Removes a fast credential.

.DESCRIPTION
    This advanced function removes a credential under
    %LOCALAPPDATA%\FastCredentials.

    It supports user names in the form of USERNAME or
    DOMAIN\USERNAME, where DOMAIN and USERNAME must consist
    of only valid file name characters, and not end with a
    full stop.

.PARAMETER UserName
    User name of the credential to be removed.

.LINK
    https://github.com/GeeLaw/PowerShellThingies/blob/master/modules/CommonUtilities/FastCredential.md
#>

Function Remove-FastCredential
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$UserName
    )
    Process
    {
        Try
        {
            $local:localAppData = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData);
            $local:fileName = [System.IO.Path]::Combine($local:localAppData, 'FastCredentials', $UserName + '.fastcred');
            Remove-Item -LiteralPath $local:fileName -Recurse:$false;
        }
        Catch
        {
            Throw;
        }
    }
}

Export-ModuleMember -Function @('New-Password', 'Switch-User', 'Sign-Scripts', 'Restart-Host', 'Set-FastCredential', 'Get-FastCredential', 'Remove-FastCredential') -Alias @('newpwd', 'su', 'sign', 'restart') -Cmdlet @() -Variable @();