glueckkanja-pki / PKI-Configuration-Tools

A collection of scripts and tools that help configure clients and servers in a PKI
GNU Affero General Public License v3.0
8 stars 4 forks source link

Suggestions (Offerings?) #2

Open timothy-byles opened 1 year ago

timothy-byles commented 1 year ago

First, thank you for this project. I'm working on something simlar and your dissection of the binary sections eliminated all the heavy lifting for me.

I worked on a project similar to this recently which was to add an LDAP address book. I think this script may benefit from some of the logic. One suggestion is instead of nabbing the default profile, loop through the profile list and compare the email address from the encryption cert to the profile email

$pk = "HKCU:\SOFTWARE\Microsoft\Office\16.0\Outlook\Profiles\"
$enc = [system.Text.Encoding]::Unicode

ForEach ($pn in (Get-Item $pk).GetSubKeyNames()) {

    Write-Host "Working on profile $pn"
    $keyname = $null; $emailAddress = $null
    If ((Get-Item "$pk$pn\0a0d020000000000c000000000000046").Property -contains "01023d15") {
        $keyname = ((Get-Item "$pk$pn\0a0d020000000000c000000000000046").GetValue("01023d15") |
                ForEach-Object ToString X2) -Join ''
        $emailAddress = $enc.GetString((Get-Item "$pk$pn\$keyname").GetValue("001f3001"))
        Write-Host "Email address: $emailAddress"
    } Else {
        Write-Host "Skipping empty profile"
    }
}

Second suggestion is to notify the user that Outlook needs to be restarted but only if Outlook is running, only if it's by the current user, and only if you modified the profile that Outlook has open. Of course this is only relevant if Outlook needs to be restarted for the changes. In my case, it was necessary

# abAdds is a collection of $pn from the above code. If the profile was modified, then it's added to the col
If ($abAdds.Count) {
$curUser = $env:username
$runningProfile = $null

$awkward = $false

# we sleep for 2 seconds x 450 loops so 900s = 15m
# if the user opens Outlook and doesn't change focus to something else within 15 minutes, then we
#    can't detect which profile is opened
For ($i=0;$i -lt 450;$i++) {
    If (Get-UserProcess("outlook")) {
        $error.clear()
        Try {$o = [Runtime.Interopservices.Marshal]::GetActiveObject("Outlook.Application")
        } Catch {
            # Outlook has not added itself to ROT
            # Could be that it is starting up or closing
            # Could also be that it's been started and hasn't lost focus yet
            $awkward = $true
        }

        If ($o) {
            If ($o.Session) {
                $awkward = $false
                $runningProfile = $o.Session.CurrentProfileName
                Break
            } Else {$awkward = $true}
        }
        $o = $null
        Start-Sleep 2
    }
    Else {Break}
}

if ($runningProfile) {
    If ($abAdds.Contains($runningProfile)) {
        $msg += "Please restart Outlook for changes to take effect"
    }
} Elseif ($awkward) {
    # Outlook process was detected but even after 15 minutes we couldn't determine the active profile
    # AWKWARD!
    # This could be that the user opened Outlook and then walked away or actually has spent this whole time in Outlook?
    # I guess we should put up a generic "Please restart Outlook" message just in case?
    # Maybe if only one profile is available we can assume that this is the running profile?

    $sMsg += "Outlook appears to be running but the current profile was not detected`nPlease restart Outlook for changes to take effect"
}
Add-Type -Assembly PresentationFramework
[System.Windows.MessageBox]::Show("$msg")
bb-froggy commented 1 year ago

Thank you for your suggestions!

So, these code snippets are copied from your LDAP project or do you happen to have a modified version of the ActivateSignatures.ps1 with these improvements? In the latter case, it would be nice if you could open two Pull Requests for these suggestions, which would help put these additions into the right place in ActivateSignatures.ps1.

And since you mention the LDAP address book project ... I did that once, too, a long time ago, and wrote a tool in C++ as part of my thesis at the university. In my thesis, I described the registry settings for LDAP address books in Outlook (pages 83-87). I am attaching it here, maybe you get something out of it for your LDAP code. It is in German though.

Diplomarbeit_Informatik_Final.pdf

Is your LDAP address book tool available to the public somewhere? Some of my colleagues are interested in such a tool if it was a nice PowerShell script instead of decade-old C++.

timothy-byles commented 1 year ago

The code I provided are snippets from my project. I can't share it in full as-is because it has references to private servers. Maybe I can remove those and provide a more generic version? It's based off of this project written in vbscript https://www.codeproject.com/Articles/14053/Adding-an-LDAP-address-book-to-MS-Outlook

I did not plan to use ActivateSignatures as-is because I need to make some significant modifications and some of these may be specific to our environment. We have over 100k users. Some of them may have multiple smartcards. Some users have other users insert their smartcards which means one user will have many other unused certificates in their cert store. I intend to do these actions

  1. enumerate all certificates on all smartcards, filtering out empty card readers (done)
  2. find an identity certificate based on the UPN of the user running the script (done)
  3. find the encryption certificate located on the same smartcard (done)
  4. ensure the "My" cert store has any certificates that belong to smartcards no longer connected (partially done)
  5. ensure only this certificate is configured in Outlook Trust Center
  6. ensure only this certificate is configured in Azure AD
timothy-byles commented 1 year ago

So here is some more information on the binary blob for ActivateSignatures.ps1 0x00 uint32 - Number of entries. First entry offset will always begin at offset 4 + (number entries * 0x10)

Next section is number of entries * 0x10 0x00 uint64 - size of this entry 0x08 uint64 - offset of this entry

Then comes each entry 0x00 16 bytes Looks to be a static GUID. Same for each entry 0x10 uint32 - Enum structure for options 0x14 begininng of settings which you have discovered

enum for options 1 and 2 are the "Default Security Setting..." options. Can't tell which is which because I'm unable to check one without the other 4 is the option to "Send these certificates with signed messages"

So (at least for my purposes), Default option will be 7, others will be 4

This information is important to me because I need to be able to parse multiple entries and check each one. Users may purposely have configured multiple settings but my app will remove any entries which point to certificates which are no longer available

If any of this information is not clear, just let me know and I will try to explain better

timothy-byles commented 1 year ago

I don't fully understand the significance of this but my algorithms section is different from yours. Mine is 0x89 bytes long and looks to include 10 elements. For reference, I am using Office 365 C2R

30 81 82 30 0b 06 09 60 86 48 01 65 03 04 01 2a 30 0b 06 09 60 86 48 01 65 03 04 01 16 30 0a 06 08 2a 86 48 86 f7 0d 03 07 30 0b 06 09 60 86 48 01 65 03 04 01 02 30 0e 06 08 2a 86 48 86 f7 0d 03 02 02 02 00 80 30 0d 06 08 2a 86 48 86 f7 0d 03 02 02 01 40 30 07 06 05 2b 0e 03 02 1a 30 0b 06 09 60 86 48 01 65 03 04 02 03 30 0b 06 09 60 86 48 01 65 03 04 02 02 30 0b 06 09 60 86 48 01 65 03 04 02 01

bb-froggy commented 1 year ago

This is your algorithms ASN.1-decoded:

    <30 81 82>
000  82: SEQUENCE {
    <30 0B>
003   B:   SEQUENCE {
    <06 09>
005   9:     OBJECT IDENTIFIER aes256-CBC (2 16 840 1 101 3 4 1 42)
       :       (NIST Algorithm)
       :     }
    <30 0B>
010   B:   SEQUENCE {
    <06 09>
012   9:     OBJECT IDENTIFIER aes192-CBC (2 16 840 1 101 3 4 1 22)
       :       (NIST Algorithm)
       :     }
    <30 0A>
01D   A:   SEQUENCE {
    <06 08>
01F   8:     OBJECT IDENTIFIER des-EDE3-CBC (1 2 840 113549 3 7)
       :       (RSADSI encryptionAlgorithm)
       :     }
    <30 0B>
029   B:   SEQUENCE {
    <06 09>
02B   9:     OBJECT IDENTIFIER aes128-CBC (2 16 840 1 101 3 4 1 2)
       :       (NIST Algorithm)
       :     }
    <30 0E>
036   E:   SEQUENCE {
    <06 08>
038   8:     OBJECT IDENTIFIER rc2CBC (1 2 840 113549 3 2)
       :       (RSADSI encryptionAlgorithm)
    <02 02>
042   2:     INTEGER 128
       :     }
    <30 0D>
046   D:   SEQUENCE {
    <06 08>
048   8:     OBJECT IDENTIFIER rc2CBC (1 2 840 113549 3 2)
       :       (RSADSI encryptionAlgorithm)
    <02 01>
052   1:     INTEGER 64
       :     }
    <30 07>
055   7:   SEQUENCE {
    <06 05>
057   5:     OBJECT IDENTIFIER sha1 (1 3 14 3 2 26)
       :       (OIW)
       :     }
    <30 0B>
05E   B:   SEQUENCE {
    <06 09>
060   9:     OBJECT IDENTIFIER sha-512 (2 16 840 1 101 3 4 2 3)
       :       (NIST Algorithm)
       :     }
    <30 0B>
06B   B:   SEQUENCE {
    <06 09>
06D   9:     OBJECT IDENTIFIER sha-384 (2 16 840 1 101 3 4 2 2)
       :       (NIST Algorithm)
       :     }
    <30 0B>
078   B:   SEQUENCE {
    <06 09>
07A   9:     OBJECT IDENTIFIER sha-256 (2 16 840 1 101 3 4 2 1)
       :       (NIST Algorithm)
       :     }
       :   }

This is what ActivateSignatures writes:

    <30 5A>
000  5A: SEQUENCE {
    <30 0B>
002   B:   SEQUENCE {
    <06 09>
004   9:     OBJECT IDENTIFIER aes256-CBC (2 16 840 1 101 3 4 1 42)
       :       (NIST Algorithm)
       :     }
    <30 0B>
00F   B:   SEQUENCE {
    <06 09>
011   9:     OBJECT IDENTIFIER aes192-CBC (2 16 840 1 101 3 4 1 22)
       :       (NIST Algorithm)
       :     }
    <30 0A>
01C   A:   SEQUENCE {
    <06 08>
01E   8:     OBJECT IDENTIFIER des-EDE3-CBC (1 2 840 113549 3 7)
       :       (RSADSI encryptionAlgorithm)
       :     }
    <30 0B>
028   B:   SEQUENCE {
    <06 09>
02A   9:     OBJECT IDENTIFIER aes128-CBC (2 16 840 1 101 3 4 1 2)
       :       (NIST Algorithm)
       :     }
    <30 0B>
035   B:   SEQUENCE {
    <06 09>
037   9:     OBJECT IDENTIFIER sha-256 (2 16 840 1 101 3 4 2 1)
       :       (NIST Algorithm)
       :     }
    <30 0B>
042   B:   SEQUENCE {
    <06 09>
044   9:     OBJECT IDENTIFIER sha-512 (2 16 840 1 101 3 4 2 3)
       :       (NIST Algorithm)
       :     }
    <30 0B>
04F   B:   SEQUENCE {
    <06 09>
051   9:     OBJECT IDENTIFIER sha-384 (2 16 840 1 101 3 4 2 2)
       :       (NIST Algorithm)
       :     }
       :   }

This change was a result of #1. I removed some older algorithms to prevent downgrade attacks.

bb-froggy commented 1 year ago

Interesting finding about multiple entries! I haven't tested it, but your analysis that the first number is the number of entries (my code assumes it is always 1) is plausible and also the offset thing that I assumed to be the length of the header (and it is if there is just one entry).

The actual entry seems to consist a list of properties. Each property (I call them "packets" in the code) has a two-byte type and then a two-byte length. The length value includes the four bytes of type and length and then the remaining data. What you found to be a static Guid are two properties, that I also found to be the same all the time. The first is of type 1 and value 1 and the second is type 6 and value 1, whatever that means. And then what you found to be the enum structure for options is type 0x20 and I always write value 7. You says that this is the default, but other values are also possible.

And then come the name of the setting (type 0x51 as Unicode and 0x0b as ASCII), the algorithms used (type 0x02), the hash of the signing certificate (type 0x09) and the hash of the encryption certificate (type 0x22).

If there are multiple entries/Security Settings, I wonder how Outlook knows which one to use. I would have suspected that this is what the properties of 1 and 6 might be for, but you said they were always the same so this speaks strongly against it. This requires further analysis.

bb-froggy commented 1 year ago

I just started with code to parse multiple entries in the pull request above.

timothy-byles commented 1 year ago

I'm working on a custom class for the whole blob as well as for each settings entry. It's not quite ready yet but here's what I have so far

Set-Variable ESConfigGuid -Option ReadOnly -value ([guid]'00080001-0001-0000-2000-080001000000')
Set-Variable AlgsAsn1 -Option ReadOnly -value ([byte[]]@(0x30,0x81,0x82,0x30,0x0b,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x01,0x2a,0x30,0x0b,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x01,0x16,0x30,0x0a,0x06,0x08,0x2a,0x86,0x48,0x86,0xf7,0x0d,0x03,0x07,0x30,0x0b,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x01,0x02,0x30,0x0e,0x06,0x08,0x2a,0x86,0x48,0x86,0xf7,0x0d,0x03,0x02,0x02,0x02,0x00,0x80,0x30,0x0d,0x06,0x08,0x2a,0x86,0x48,0x86,0xf7,0x0d,0x03,0x02,0x02,0x01,0x40,0x30,0x07,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x30,0x0b,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x03,0x30,0x0b,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x02,0x30,0x0b,0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01))

[Flags()] enum ESConfigOption {
    Default1 = 1
    Default2 = 2
    SendWithMsgs = 4
}

enum ESConfigItemID {
    AsnHashList = 0x02
    SignatureCertHash = 0x09
    NameA = 0x0B
    EncryptionCertHash = 0x22
    NameW = 0x51
}

class ESConfigEntry {
    [string]$Name = ''
    [ESConfigOption]$Options = 4
    [string]$EncryptionCertThumbprint
    [string]$SignatureCertThumbprint
    [byte[]]$HashAlgorithms = $AlgsAsn1
    [byte[]]$EncryptionCertHash
    [byte[]]$SignatureCertHash
    [byte[]]$RawData

    ESConfigEntry() {
        $this.PSObject.Properties.Add([PSScriptProperty]::New('Length',{$this.uLength},{}))
    }

    ESConfigEntry([byte[]]$blob) {
        $this.RawData = $blob
        $guid = [guid]::new($blob[0..15])
        if (! $ESConfigGuid -ne $guid) {throw "Unexpected Guid - $guid"}
        $pos = 0x14
        $this.Options = [bitconverter]::ToUint32($blob, 16)
        while ($pos -lt $blob.length) {
            $Id = [bitconverter]::ToUint16($blob, $pos)
            $itemSize = [bitconverter]::ToUint16($blob, $pos + 4)
            $end = $pos + $itemSize - 1
            switch ($Id) {
                [ESConfigItemID]::NameW {
                    if ($this.Name.Length -eq 0) {$this.Name =
                        [Text.Encoding]::Unicode.GetString($blob[($pos + 8)..$end])}
                }
                [ESConfigItemID]::NameA {
                    if ($this.Name.Length -eq 0) {$this.Name = 
                        [Text.Encoding]::ASCII.GetString($blob[$pos + 8)..$end])}
                    }
                }
                [ESConfigItemID]::EncryptionCertHash {
                    if ($itemSize -ne 0x14) {throw "Invalid hash size: $itemSize, expected 20"}
                    $data = $blob[($pos + 8)..$end]
                    $this.EncryptionCertThumbprint = ([bitconverter]::ToString($data)).Replace('-','')
                    $this.EncryptionCertHash = $data
                }
                [ESConfigItemID]::SignatureCertHash {
                    if ($itemSize -ne 0x14) {throw "Invalid hash size: $itemSize, expected 20"}
                    $data = $blob[($pos + 8)..$end]
                    $this.SignatureCertThumbprint = ([bitconverter]::ToString($data)).Replace('-','')
                    $this.SignatureCertHash = $data
                }
                [ESConfigItemID]::AsnHashList
                # Maybe I can decode the Asn data later if need arises
                # For now we'll just use what I found in my Outlook M365
                $this.HashAlgorithms = $blob[($pos + 8)..$end]
            }
            $pos = $end + 1
        }
    }

    [byte[]]GetBytes() {
        #Calculate Total size and create a byte array of that size
        #Since we need the name in ASCII as well as Unicode and both null-terminated...
        $size = ($this.Name.Length + 1) * 3
        $size += 
    }
}

class ESConfig {
    [ESConfigEntry[]]$Entries
    ESConfig( [byte[]]$ba ) {
        # Do some basic error checking
        $pos = 0
        $numEntries = [bitconverter]::ToUint32($ba, 0)
        if ($ba.Length -lt (4 + ($numEntries * 0x10))) {throw 'Unable to parse data'}
        $pos = (($numEntries - 1) * 0x10) + 4
        if ([bitconverter]::ToUint64($ba, $pos) + [bitconverter]::ToUint32($ba, $pos + 8) -ne $ba.Length) {
                throw 'Unable to parse data'}
        $this.Entries = [ESConfigEntry[]]::new($numEntries)
        for ($i = 0;$i -lt $numEntries;$i++) {
            $pos = 4 + ($i * 0x10)
            $entrySize = [bitconverter]::ToUint64($ba, $pos)
            $offset = [bitconverter]::ToUint64($ba, $pos + 8)
            [byte[]]$blob = $ba[($offset)..($offset + $entrySize - 1)]
            $this.Entries[$i] = [ESConfigEntry]::new($blob)
        }
    }

}
timothy-byles commented 1 year ago

When the class is complete you'll use it like this

$RegEntry = (Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Office\16.0\Outlook\Profiles\Outlook\c02ebc5353d9cd11975200aa004ae40e' -Name '11020355').11020355 $Settings = [ESConfig]::New($RegEntry)

This object will hold an array of the settings entries $Settings.Entries

Add/remove or modify properties of individual settings and then when done you get the whole binary blob back like this [byte[]]$NewRegEntry = $Settings.GetBytes()

bb-froggy commented 1 year ago

Sounds good. So I suggest you write that class and I review it and include it in ActivateSignatures.ps1. Then we don't have duplicate work and good quality with a four-eye-principle.

Regarding the GetBytes ... I think you don't need to know the length at that point. It's the ESConfig::GetBytes that needs to know the length, not the ESConfigEntry::GetBytes.

timothy-byles commented 1 year ago

I think you're referring to this line? $this.PSObject.Properties.Add([PSScriptProperty]::New('Length',{$this.uLength},{})

This dynamically adds a readonly property to the object instance. It allows you to get the length of the binary blob and doesn't allow to write to the property.

bb-froggy commented 1 year ago

Interesting, didn't know that about dynamically added properties and I missed that line when I looked onto the code. So actually, I was referring to this other part:

    [byte[]]GetBytes() {
        #Calculate Total size and create a byte array of that size
        #Since we need the name in ASCII as well as Unicode and both null-terminated...
        $size = ($this.Name.Length + 1) * 3
        $size += 
    }
timothy-byles commented 1 year ago

Oh ok. You are correct, that belongs in the GetBytes() method of ESConfig instead. TY

timothy-byles commented 1 year ago

Actually, I remember now that I need that there so that I can create the correct sized Array. This will be more efficient than generating separate arrays and trying to concatenate them

timothy-byles commented 1 year ago

Just wanted to give a progress update. Decided to make a custom class for the Algorithm blob. You can now use flags to set the 'chosen' algorith for each as well as what others are available in the dropdown.

I'm currently working diligently to finish the class for settings entry and then the settings collection should be trivial and quick compared to the first two.

Here is the Algorithm class. I may neeed to clean up a few uneccessary bits that I used during testing

# Helper functions

# Unless I missed something, PS doesn't provide methods to find index of a sub-array
# This works just like [string].SubString
function FindInArray([byte[]]$source, [byte[]]$find) {
    if ($find.Length -gt $source.Length) { throw 'search bytes are longer than source' }
    $end = $source.Length - $find.Length
    $iFound = -1
    for ($pos = 0; ($iFound -eq -1) -and ($pos -le ($end)); $pos++) {
        if ($source[$pos] -eq $find[0]) {
            $iFound = $pos
            for ($pos2 = 1; $pos2 -lt $find.Length; $pos2++) {
                if ($source[$pos + $pos2] -ne $find[$pos2]) {$iFound = -1; break}
            }
        }
    }
    return $iFound
}

# Very efficient and clever function I found on Stackoverflow, but adapted to PS
function CountFlags([enum]$flags) {
    $count = 0
    while ($flags) {
        $flags = $flags -band ($flags - 1)
        $count++
    }
    return $count
}

###########################  BEGIN ESAlgorithmAsn custom class  ###########################

[Flags()] enum CryptAlgs {
    AES_256 = 1
    AES_192 = 2
    TripleDES = 4
    AES_128 = 8
    RC2_128 = 16
    RC2_64 = 32
}

# I could not figure out how to assign these during initialization so we'll just do them one-by-one >:-[
# Hashtable for ASN1 encoded encryption algorithms
$CryptAsn = @{}
$CryptAsn[[CryptAlgs]::AES_256] = [byte[]]@(0x30, 0x0B, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2A)
$CryptAsn[[CryptAlgs]::AES_192] = [byte[]]@(0x30, 0x0B, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x16)
$CryptAsn[[CryptAlgs]::TripleDES] = [byte[]]@(0x30, 0x0A, 0x06, 0x08, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x07)
$CryptAsn[[CryptAlgs]::AES_128] = [byte[]]@(0x30, 0x0B, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x02)
$CryptAsn[[CryptAlgs]::RC2_128] = [byte[]]@(0x30, 0x0E, 0x06, 0x08, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x02, 0x02, 0x02, 0x00, 0x80)
$CryptAsn[[CryptAlgs]::RC2_64] = [byte[]]@(0x30, 0x0D, 0x06, 0x08, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x02, 0x02, 0x01, 0x40)

[Flags()] enum HashAlgs {
    SHA1 = 1
    SHA_512 = 2
    SHA_384 = 4
    SHA_256 = 8
}

# Hashtable for ASN1 encoded hash algorithms
$HashAsn = @{}
$HashAsn[[HashAlgs]::SHA1] = [byte[]]@(0x30, 0x07, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A)
$HashAsn[[HashAlgs]::SHA_512] = [byte[]]@(0x30, 0x0B, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03)
$HashAsn[[HashAlgs]::SHA_384] = [byte[]]@(0x30, 0x0B, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02)
$HashAsn[[HashAlgs]::SHA_256] = [byte[]]@(0x30, 0x0B, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01)

# Custom class to handle creating or modifying the ASN1 data that Outlook holds in it's email security settings
class ESAlgorithmAsn {
    # It's just one contiguous stream but the 'chosen' algs are moved to the head of the stream
    # All encryption entries are listed first then hash entries
    # This is strictly ASN1 encoded data. The accompanying header is handled by the ESConfigEntry class

    [CryptAlgs]$EncryptionAlgorithm
    [CryptAlgs]$OtherEncryptionAlgorithms
    [HashAlgs]$HashAlgorithm
    [HashAlgs]$OtherHashAlgorithms

    # Create a new instance with only one of each default and set them to the strongest available
    # This could be moved to a settings section at the beginning of the script
    ESAlgorithmAsn() {
        $this.EncryptionAlgorithm = [CryptAlgs]::AES_256
        $this.HashAlgorithm = [HashAlgs]::SHA_512
    }

    # Is the appropriate way to handle this? Should it be a function like LoadData($stream) ?
    # Maybe we could define a class function and here we would just use that function?
    ESAlgorithmAsn([byte[]]$stream) {
        $CryptAsn = $Global:CryptAsn
        $HashAsn = $Global:HashAsn

        # Currently we use FindInArray to search for each blob in the stream
        # I wonder if it would be more efficient to step through the stream?
        # Need to see what that code looks like since doing it this way loops through the stream quite a few times
        # However, this is very fast as-is and stepping through the stream means needing to do some basic parsing

        # firstEnum holds the enum of the blob found with the lowest index
        # This becomes the default while others are added to the $this.Otherxxx
        $firstEnum = 0
        $firstPos = -1

        foreach ($enum in [CryptAlgs].GetEnumNames()) {
            $pos = FindInArray $stream $CryptAsn[[CryptAlgs]::$enum]
            if ($pos -ne -1) {
                $this.OtherEncryptionAlgorithms += [CryptAlgs]::$enum
                if (($firstPos -eq -1) -or ($pos -lt $firstPos)) {
                    $firstPos = $pos
                    $firstEnum = [CryptAlgs]::$enum
                }
            }
        }
        $this.EncryptionAlgorithm = $firstEnum
        # We know that Other also includes the default so we can subtract instead of using -band -bnot (see in GetBytes())
        $this.OtherEncryptionAlgorithms -= $firstEnum

        # Forgot to reset these at first and of course, got some unexpected results
        $firstEnum = 0
        $firstPos = -1

        foreach ($enum in [HashAlgs].GetEnumNames()) {
            $pos = FindInArray $stream $HashAsn[[HashAlgs]::$enum]
            if ($pos -ne -1) {
                $this.OtherHashAlgorithms += [HashAlgs]::$enum
                if (($firstPos -eq -1) -or ($pos -lt $firstPos)) {
                    $firstPos = $pos
                    $firstEnum = [HashAlgs]::$enum
                }
            }
        }
        $this.HashAlgorithm = $firstEnum
        $this.OtherHashAlgorithms -= $firstEnum
    }# End constructor ESAlgorithmAsn($stream)

    # Build and return the stream
    [byte[]]GetBytes() {
        if ($this.HashAlgorithm -eq 0) {throw 'Must include default hash algorithm'}
        if ($this.EncryptionAlgorithm -eq 0) {throw 'Must include default encryption algorithm'}
        if ((CountFlags $this.HashAlgorithm) -ne 1) {throw "Invalid flags for default Hash Algorithm: $this.HashAlgorithm"}
        if ((CountFlags $this.EncryptionAlgorithm) -ne 1) {throw "Invalid flags for default Encryption Algorithm: $this.EncryptionAlgorithm"}

        # Make sure that Other doesn't contain the default flag
        $this.OtherEncryptionAlgorithms = $this.OtherEncryptionAlgorithms -band (-bnot $this.EncryptionAlgorithm)
        $this.OtherHashAlgorithms = $this.OtherHashAlgorithms -band (-bnot $this.HashAlgorithm)

        # Globals aren't available here... who knew...
        $CryptAsn = $Global:CryptAsn
        $HashAsn = $Global:HashAsn

        [uint32]$size = 0
        $outputArray = $null
        $pos = 0
        $allCrypts = $this.EncryptionAlgorithm -bor $this.OtherEncryptionAlgorithms
        $allHashes = $this.HashAlgorithm -bor $this.OtherHashAlgorithms
        $defaultCrypt = $null

        # Loop through enum values and see if each flag exists
        foreach ($enum in [CryptAlgs].GetEnumNames()) {
            if ($allCrypts -band [CryptAlgs]::$enum) {$size += $CryptAsn[[CryptAlgs]::$enum].length}
        }
        foreach ($enum in [HashAlgs].GetEnumNames()) {
            if ($allHashes -band [HashAlgs]::$enum){$size += $HashAsn[[HashAlgs]::$enum].length}
        }

        # I can't imagine this string being longer than 65535 but we're going to code for it anyway
        $sizeBytes = [bitconverter]::GetBytes($size)
        $pos = 2

        # I thought about using -band 0x80 here but if, for example, the size is 0x100, if would fail the check
        if ($size -lt 0x80) {
            $outputArray = [byte[]]::new($size + $pos)
            $outputArray[0] = [byte]0x30
            $outputArray[1] = [byte]$size
        } else {
            #When total length is greater than 0x80, we have to set the greatest bit and use the
            #   rest of this byte to define how many additional bytes are needed to express the total size
            #   For example: if total size is 0xFF00FF then the ASN1 header will look like this
            #   0x0 = 0x30
            #   0x1 = 0x83  <- 0x80 || 0x03 for 3 bytes to express FF00FF
            #   0x30 0x83 0xFF 0x00 0xFF <rest of ASN1 data>

            # Find the last non-zero byte
            # If this is running on a Big-Endian platform, this needs to be re-worked to detect endianness
            $iSize = 0
            foreach ($i in ($sizeBytes.Length - 1)..0) {if ($sizeBytes[$i]) {$iSize = $i; break}}

            $outputArray = [byte[]]::new($size + $pos + $iSize + 1)
            $outputArray[0] = [byte]0x30
            $outputArray[1] = [byte](0x80 + $iSize + 1)

            # ASN1 stores numbers in Big-Endian so we need to reverse the byte order
            foreach ($i in $iSize..0) {
                $outputArray[$pos++] = $sizeBytes[$i]   #Set the byte at $pos and then move to the next byte
            }
        }
        # Header is complete, now we can compile the ASN1 data
        $defaultCrypt = $CryptAsn[$this.EncryptionAlgorithm]
        $defaultCrypt.CopyTo($outputArray, $pos)
        $pos += $defaultCrypt.Length
        if ($this.OtherEncryptionAlgorithms) {
            foreach ($enum in [CryptAlgs].GetEnumNames()) {
                if ($this.OtherEncryptionAlgorithms -band [CryptAlgs]::$enum) {
                    $addBytes = $CryptAsn[[CryptAlgs]::$enum]
                    $addBytes.CopyTo($outputArray, $pos)
                    $pos += $addBytes.Length
                }
            }
        }
        $defaultHash = $HashAsn[$this.HashAlgorithm]
        $defaultHash.CopyTo($outputArray, $pos)
        $pos += $defaultHash.Length
        if ($this.OtherHashAlgorithms) {
            foreach ($enum in [HashAlgs].GetEnumNames()) {
                if ($this.OtherHashAlgorithms -band [HashAlgs]::$enum) {
                    $addBytes = $HashAsn[[HashAlgs]::$enum]
                    $addBytes.CopyTo($outputArray, $pos)
                    $pos += $addBytes.Length
                }
            }
        }
        return $outputArray
    }# End method GetBytes
}# End class ESAlgorithmAsn

###########################  END ESAlgorithmAsn custom class  ###########################
timothy-byles commented 1 year ago

Updated previous comment. Fixed a few bugs and cleaned up as well as added more comments. It's currently on CodeReview stack exchange. Hopefully I can finish the other classes in the next day or two

bb-froggy commented 1 year ago

Excellent, if I have any comments on this part of the code, I will post it on Code Review.

timothy-byles commented 1 year ago

I have finished the project. I am currently reviewing my own code and adding comments. I will also document usage and then update the code review page as well as upload to my github repo

timothy-byles commented 1 year ago

I finally got this comlete, commented, and documented

https://github.com/timothy-byles/powershell-OutlookES-classes

timothy-byles commented 1 year ago

Next I'll see if I can strip the sensitive info from the Address book script and upload that to another repo

bb-froggy commented 1 year ago

Looks very good, I will see to it that I add this as a dependency here and use it directly.

timothy-byles commented 1 year ago

RE: the address book script. I found a project that does this via MAPI https://github.com/andreighita/MAPIToolkit I think my script is very specific and it has binary data that I haven't been able to decode so I don't know if it includes sensitive data

The best I could do is pull out all the data, and whoever uses the script would need to manually create an address book to be able to get the data for those sections and insert them into the script. I remember one registry value had data that seemed to be different each time I crated a new entry. I think that the above linked MAPI project should work better and with some time and effort, Powershell could perform that function