Created
March 7, 2019 15:20
-
-
Save JohnLBevan/3cd5ec5e826033c1f9283f1b31dc2a82 to your computer and use it in GitHub Desktop.
Code to pull values from Dynamics 365 for Finance and Operations (aka DFO365 / unified operations) Web.Config file and decrypt values where required, to find relevant credential information used by the OneBox install.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [string[]]$Assemblies = @( | |
| 'C:\AOSService\webroot\bin\Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll' | |
| ) | |
| [string]$CSSource = @" | |
| using System; | |
| using Microsoft.Dynamics.Ax.Xpp.Security; | |
| namespace n201903071243 //use an odd namespace so I don't have to reload powershell each time I want to tweak this code; just tweak the NS | |
| { | |
| public class Dfo365CertificateThumbprintProvider: Microsoft.Dynamics.Ax.Xpp.Security.ICertificateThumbprintProvider | |
| { | |
| public string EncryptionThumbprint {get;private set;} | |
| public string SigningThumbprint {get;private set;} | |
| public Dfo365CertificateThumbprintProvider(string encryptionThumbprint, string signingThumbprint) | |
| { | |
| EncryptionThumbprint = encryptionThumbprint; | |
| SigningThumbprint = signingThumbprint; | |
| } | |
| } | |
| public class Dfo365EncryptionExceptionHandler: Microsoft.Dynamics.Ax.Xpp.Security.IEncryptionExceptionHandler | |
| { | |
| private Action<Exception> exceptionHandler; | |
| public Dfo365EncryptionExceptionHandler() | |
| { | |
| exceptionHandler = WriteToConsoleThenThrow; | |
| } | |
| public Dfo365EncryptionExceptionHandler(Action<Exception> exceptionHandler) | |
| { | |
| this.exceptionHandler = exceptionHandler; | |
| } | |
| public void HandleException(Exception exception) | |
| { | |
| if (exceptionHandler != null) | |
| { | |
| exceptionHandler(exception); | |
| } | |
| } | |
| public static void WriteToConsoleThenThrow(Exception exception) | |
| { | |
| Console.WriteLine(exception.ToString()); | |
| throw exception; | |
| } | |
| } | |
| } | |
| "@ | |
| Add-Type -ReferencedAssemblies $Assemblies -TypeDefinition $CSSource -Language 'CSharp' | |
| Add-Type -Path 'C:\AOSService\webroot\bin\Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll' | |
| function Decrypt-Dfo365EncryptedString { | |
| [CmdletBinding(DefaultParameterSetName = 'ByEncryptionEngine')] | |
| Param ( | |
| [Parameter(ParameterSetName = 'ByEncryptionEngine', Mandatory = $true)] | |
| [Microsoft.Dynamics.Ax.Xpp.Security.EncryptionEngine]$EncryptionEngine = $null | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] | |
| [string]$PathToWebConfig | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByXml')] | |
| [xml]$WebConfig | |
| , | |
| [Parameter(Mandatory = $true, ValueFromPipeline = $true)] | |
| [string]$EncryptedString | |
| ) | |
| Begin { | |
| if ($PSCmdlet.ParameterSetName -eq 'ByPath') { | |
| $WebConfig = [xml](Get-Content -Path $PathToWebConfig | Out-String) | |
| } | |
| if ($PSCmdlet.ParameterSetName -ne 'ByEncryptionEngine') { | |
| $EncryptionEngine = New-Dfo365EncryptionEngine -WebConfig $WebConfig | |
| } | |
| [string]$purpose = 'PurposeName' | |
| } | |
| Process { | |
| [byte[]]$cipher = [Convert]::FromBase64String($EncryptedString) | |
| $encryptionEngine.Decrypt($cipher, $purpose) | |
| } | |
| } | |
| function Get-Dfo365ConfigSetting { | |
| [CmdletBinding(DefaultParameterSetName = 'ByPath')] | |
| Param ( | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] | |
| [string]$PathToWebConfig | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByXml')] | |
| [xml]$WebConfig | |
| , | |
| [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] | |
| [string]$PropertyName | |
| ) | |
| Begin { | |
| if ($PSCmdlet.ParameterSetName -eq 'ByPath') { | |
| $WebConfig = [xml](Get-Content -Path $PathToWebConfig | Out-String) | |
| } | |
| } | |
| Process { | |
| [string]$xpath = ("/configuration/appSettings/add[@key='{0}']/@value" -f $PropertyName) #I've not bothered adding escaping logic as key names unlikely to contain apostrophies; but may be worth adding at some point? | |
| $WebConfig.SelectSingleNode($xpath) | Select-Object -ExpandProperty 'value' | |
| } | |
| } | |
| function Get-Dfo365EncryptedConfigSetting { | |
| [CmdletBinding(DefaultParameterSetName = 'ByPath')] | |
| Param ( | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] | |
| [string]$PathToWebConfig | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByXml')] | |
| [xml]$WebConfig | |
| , | |
| [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] | |
| [string]$PropertyName | |
| , | |
| [Parameter(Mandatory = $false)] | |
| [Microsoft.Dynamics.Ax.Xpp.Security.EncryptionEngine]$EncryptionEngine = $null | |
| ) | |
| Begin { | |
| if ($PSCmdlet.ParameterSetName -eq 'ByPath') { | |
| $WebConfig = [xml](Get-Content -Path $PathToWebConfig | Out-String) | |
| } | |
| if ($EncryptionEngine -eq $null) { | |
| $EncryptionEngine = New-Dfo365EncryptionEngine -WebConfig $WebConfig | |
| } | |
| } | |
| Process { | |
| $encryptedValue = Get-Dfo365ConfigSetting -WebConfig $WebConfig -PropertyName $PropertyName | |
| Decrypt-Dfo365EncryptedString -EncryptionEngine $EncryptionEngine -EncryptedString $encryptedValue | |
| } | |
| } | |
| function New-Dfo365EncryptionEngine { | |
| [CmdletBinding(DefaultParameterSetName = 'AllObjects')] | |
| Param( | |
| [Parameter(ParameterSetName = 'AllObjects', Mandatory = $true)] | |
| [Parameter(ParameterSetName = 'ByPath', Mandatory = $false)] | |
| [Parameter(ParameterSetName = 'ByXml', Mandatory = $false)] | |
| [Microsoft.Dynamics.Ax.Xpp.Security.ICertificateThumbprintProvider]$certificateThumbprintProvider = $null | |
| , | |
| [Parameter(ParameterSetName = 'AllObjects', Mandatory = $true)] | |
| [Parameter(ParameterSetName = 'ByPath', Mandatory = $false)] | |
| [Parameter(ParameterSetName = 'ByXml', Mandatory = $false)] | |
| [System.Collections.Generic.IDictionary[[string], [string]]]$certificateHandlerSettings = $null | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] | |
| [string]$PathToWebConfig | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByXml')] | |
| [xml]$WebConfig | |
| , | |
| [Parameter(Mandatory = $false)] | |
| [Microsoft.Dynamics.Ax.Xpp.Security.IEncryptionExceptionHandler]$encryptionExceptionHandler = $null #gets defaulted in the Begin block if left as null | |
| , | |
| [Parameter(Mandatory = $false)] | |
| [Microsoft.Dynamics.Ax.Xpp.Security.ICertificateThumbprintProvider]$legacyCertificateThumbprintProvider = $null | |
| ) | |
| Begin { | |
| if ($PSCmdlet.ParameterSetName -eq 'ByPath') { | |
| $WebConfig = [xml](Get-Content -Path $PathToWebConfig | Out-String) | |
| } | |
| if ($PSCmdlet.ParameterSetName -ne 'AllObjects') { | |
| if ($certificateThumbprintProvider -eq $null) {$certificateThumbprintProvider = New-CertificateThumbprintProvider -WebConfig $WebConfig} | |
| if ($certificateHandlerSettings -eq $null) {$certificateHandlerSettings = New-CertificateHandlerSettings -WebConfig $WebConfig} | |
| #legacyCertificateThumbprintProvider doesn't seem to be used; though has values in the web.config keys... leave as null/provided for now | |
| #if ($legacyCertificateThumbprintProvider -eq $null) {$legacyCertificateThumbprintProvider = New-CertificateThumbprintProvider -WebConfig $WebConfig -EncryptionThumbprintKey 'DataAccess.DataEncryptionCertificateThumbprintLegacy' -SigningThumbprintKey 'DataAccess.DataSigningCertificateThumbprintLegacy'} | |
| } | |
| if ($encryptionExceptionHandler -eq $null) {$encryptionExceptionHandler = New-Dfo365EncryptionExceptionHandler} | |
| } | |
| Process { | |
| (New-Object -TypeName 'Microsoft.Dynamics.Ax.Xpp.Security.EncryptionEngine' -ArgumentList $certificateThumbprintProvider, $encryptionExceptionHandler, $certificateHandlerSettings, $legacyCertificateThumbprintProvider) | |
| } | |
| } | |
| function New-Dfo365EncryptionExceptionHandler { | |
| [CmdletBinding()] | |
| Param () | |
| Process { | |
| (New-Object -TypeName 'n201903071243.Dfo365EncryptionExceptionHandler') | |
| } | |
| } | |
| function New-CertificateThumbprintProvider { | |
| [CmdletBinding(DefaultParameterSetName = 'ByPath')] | |
| Param ( | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] | |
| [string]$PathToWebConfig | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByXml')] | |
| [xml]$WebConfig | |
| , | |
| #make these parameters in case we want to reuse this for fetching the legacy thumprints too (i.e. to reuse for legacyCertificateThumbprintProvider) | |
| [Parameter()] | |
| [string]$EncryptionThumbprintKey = 'DataAccess.DataEncryptionCertificateThumbprint' | |
| , | |
| [Parameter()] | |
| [string]$SigningThumbprintKey = 'DataAccess.DataSigningCertificateThumbprint' | |
| ) | |
| Begin { | |
| if ($PSCmdlet.ParameterSetName -eq 'ByPath') { | |
| $WebConfig = [xml](Get-Content -Path $PathToWebConfig | Out-String) | |
| } | |
| } | |
| Process { | |
| $encr = Get-Dfo365ConfigSetting -WebConfig $WebConfig -PropertyName $EncryptionThumbprintKey | |
| $sign = Get-Dfo365ConfigSetting -WebConfig $WebConfig -PropertyName $SigningThumbprintKey | |
| (New-Object -TypeName 'n201903071243.Dfo365CertificateThumbprintProvider' -ArgumentList $encr, $sign) | |
| } | |
| } | |
| function New-CertificateHandlerSettings { | |
| [CmdletBinding(DefaultParameterSetName = 'ByPath')] | |
| Param ( | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] | |
| [string]$PathToWebConfig | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByXml')] | |
| [xml]$WebConfig | |
| ) | |
| Begin { | |
| if ($PSCmdlet.ParameterSetName -eq 'ByPath') { | |
| $WebConfig = [xml](Get-Content -Path $PathToWebConfig | Out-String) | |
| } | |
| } | |
| Process { | |
| [System.Collections.Generic.IDictionary[[string], [string]]]$result = New-Object -TypeName 'System.Collections.Generic.Dictionary[[string], [string]]' | |
| [PSObject[]]$keyValuePairs = $WebConfig.SelectNodes("/configuration/appSettings/add[starts-with(@key,'CertificateHandler')]") | Select-Object @('key', 'value') | |
| foreach ($kvp in $keyValuePairs) { | |
| $result.Add($kvp.key, $kvp.value) | |
| } | |
| $result | |
| } | |
| } | |
| function Get-Dfo365CredentialData { | |
| [CmdletBinding(DefaultParameterSetName = 'ByPath')] | |
| Param ( | |
| [Parameter(Mandatory = $false, ParameterSetName = 'ByPath')] | |
| [string]$PathToWebConfig = 'C:\AOSService\webroot\web.config' | |
| , | |
| [Parameter(Mandatory = $true, ParameterSetName = 'ByXml')] | |
| [xml]$WebConfig | |
| ) | |
| Begin { | |
| if ($PSCmdlet.ParameterSetName -eq 'ByPath') { | |
| $WebConfig = [xml](Get-Content -Path $PathToWebConfig | Out-String) | |
| } | |
| [Microsoft.Dynamics.Ax.Xpp.Security.EncryptionEngine]$encryptionEngine = New-Dfo365EncryptionEngine -WebConfig $WebConfig | |
| } | |
| Process { | |
| [PSObject[]]$settings = @( | |
| @{Key='AzureStorage.StorageConnectionString';Encrypted=$true} | |
| ,@{Key='DataAccess.Database';Encrypted=$false} | |
| ,@{Key='DataAccess.DbServer';Encrypted=$false} | |
| ,@{Key='DataAccess.SqlPwd';Encrypted=$true} | |
| ,@{Key='DataAccess.SqlUser';Encrypted=$false} | |
| ,@{Key='Provisioning.AdminPrincipalName';Encrypted=$false} | |
| ,@{Key='BiReporting.DW';Encrypted=$false} | |
| ,@{Key='BiReporting.DWServer';Encrypted=$false} | |
| ,@{Key='BiReporting.DWUser';Encrypted=$false} | |
| ,@{Key='BiReporting.DWPwd';Encrypted=$true} | |
| ,@{Key='BiReporting.DWRuntimeUser';Encrypted=$false} | |
| ,@{Key='BiReporting.DWRuntimePwd';Encrypted=$true} | |
| ,@{Key='DataAccess.AxAdminSqlUser';Encrypted=$false} | |
| ,@{Key='DataAccess.AxAdminSqlPwd';Encrypted=$true} | |
| ) | ForEach-Object {(New-Object -TypeName 'PSObject' -Property $_)} | |
| $settings | ForEach-Object { | |
| $setting = $_.Key | |
| $value = if ($_.Encrypted -eq $true) { | |
| Get-Dfo365EncryptedConfigSetting -WebConfig $WebConfig -PropertyName $setting -EncryptionEngine $encryptionEngine | |
| } else { | |
| Get-Dfo365ConfigSetting -WebConfig $WebConfig -PropertyName $setting | |
| } | |
| (New-Object -TypeName 'PSObject' -Property @{Key=$setting;Value=$value}) | |
| } | |
| } | |
| } | |
| Clear-Host | |
| Get-Dfo365CredentialData | Format-Table -AutoSize | |
| <# Sample output | |
| Key Value | |
| --- ----- | |
| AzureStorage.StorageConnectionString UseDevelopmentStorage=true | |
| DataAccess.Database AxDB | |
| DataAccess.DbServer localhost | |
| DataAccess.SqlPwd AOSWebSite@123 | |
| DataAccess.SqlUser axdbadmin | |
| Provisioning.AdminPrincipalName myusername@mycompany.com | |
| BiReporting.DW AxDW | |
| BiReporting.DWServer localhost | |
| BiReporting.DWUser axdwadmin | |
| BiReporting.DWPwd AOSWebSite@123 | |
| BiReporting.DWRuntimeUser axdwruntimeuser | |
| BiReporting.DWRuntimePwd AOSWebSite@123 | |
| DataAccess.AxAdminSqlUser axdbadmin | |
| DataAccess.AxAdminSqlPwd AOSWebSite@123 | |
| #> | |
| <# NB: At some point investigate whether we can use this bit of the config to sync OneBox data back to LCS | |
| <!-- Begin: LCS Config Section --> | |
| <add key="LCS.APIEndPoint" value="https://lcsapi.lcs.tie.dynamics.com" /> | |
| <add key="LCS.GettingStartedLibrary" value="" /> | |
| <add key="LCS.LcsClientCertificateThumbprint" value="" /> | |
| <add key="LCS.GERConfigurationImport" value="" /> | |
| <add key="LCS.EnvironmentId" value="" /> | |
| <add key="LCS.BpmAuthClient" value="SysBpmCertClient" /> | |
| <add key="LCS.ProjectId" value="" /> | |
| <!-- End: LCS Config Section--> | |
| #> |
Author
Author
I had a bit more of a hunt and found this: https://www.powershellgallery.com/packages/d365fo.tools
The code for that's here: https://github.com/d365collaborative/d365fo.tools/blob/d3e5588dc2dd66365317bd5c232c9e976cec335c/d365fo.tools/internal/functions/new-decryptedfile.ps1#L36C68-L36C119
That uses an exe called Microsoft.Dynamics.AX.Framework.ConfigEncryptor.exe to handle encryption; so maybe that gives an alternate route to a solution for your scenario?
There's a blog post giving more info on that here: https://stoneridgesoftware.com/how-to-connect-your-dev-and-uat-environments-in-dynamics-365-finance-and-operations/#:~:text=4.%20Encrypt%20the%20web.config%20file%20using%20the%20commands%20below%3A
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hmm, your code looks good / I'd have expected that to work; but if you're finding that decrypting what you'd encrypted is giving a different result, then agreed something's wrong.
Sadly I don't use D365FO these days, nor do I have access to that DLL; and I can't find the documentation/blog/info I'd used when creating the initial cmdlets. All I could find was a comment on a blog I'd made after knocking up this script: https://axcalated.blogspot.com/2018/06/d365fo-onebox-default-account.html.
I don't think I can be more help here - it's probably best to ping the D365FO community ( https://community.dynamics.com/forums/thread/?partialUrl=dynamics-365-general ) or ask on StackOverflow in case anyone there has any ideas ( https://stackoverflow.com/questions/tagged/dynamics-365-operations ). Hope that gives you a lead...