From 029068e22911173029f2ba1bd272f19b810206de Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Mon, 4 May 2020 14:12:23 -0700 Subject: [PATCH 01/19] Add CI release/publish to RemotingTools, SecretManagement, ThreadJob modules --- .../.ci/ci.yml | 7 ++- .../.ci/release.yml | 23 +++++-- .../build.ps1 | 18 +++++- .../.ci/ci.yml | 7 ++- .../.ci/release.yml | 23 +++++-- .../build.ps1 | 60 +++++++++---------- .../pspackageproject.json | 1 + .../Microsoft.PowerShell.ThreadJob/.ci/ci.yml | 7 ++- .../.ci/release.yml | 23 +++++-- .../Microsoft.PowerShell.ThreadJob/build.ps1 | 60 +++++++++---------- .../pspackageproject.json | 1 + 11 files changed, 142 insertions(+), 88 deletions(-) diff --git a/Modules/Microsoft.PowerShell.RemotingTools/.ci/ci.yml b/Modules/Microsoft.PowerShell.RemotingTools/.ci/ci.yml index 40e9495..c9022eb 100644 --- a/Modules/Microsoft.PowerShell.RemotingTools/.ci/ci.yml +++ b/Modules/Microsoft.PowerShell.RemotingTools/.ci/ci.yml @@ -56,10 +56,11 @@ stages: Install-Module -Name PSPackageProject -Force $config = Get-PSPackageProjectConfiguration $signSrcPath = "$($config.BuildOutputPath)\$($config.ModuleName)" - $signOutPath = "$($config.BuildOutputPath)\$($config.ModuleName)\Signed" + $signOutPath = "$($config.SignedOutputPath)\$($config.ModuleName)" if (! (Test-Path -Path $signOutPath)) { $null = New-Item -Path $signOutPath -ItemType Directory } + Write-Host "Signed output path is: $signOutPath" $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" Write-Host "sending " + $vstsCommandString Write-Host "##$vstsCommandString" @@ -88,6 +89,10 @@ stages: binVersionOverride: '' condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) + - pwsh: | + $(Build.SourcesDirectory)/build.ps1 -Publish -Signed + displayName: Create signed artifact + - stage: Compliance displayName: Compliance dependsOn: Build diff --git a/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml b/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml index 650d624..3dfc390 100644 --- a/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml +++ b/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml @@ -1,23 +1,34 @@ parameters: jobName: release imageName: windows-2019 - displayName: Release + displayName: 'Release Microsoft.PowerShell.RemotingTools to NuGet' jobs: - job: ${{ parameters.jobName }} pool: vmImage: ${{ parameters.imageName }} displayName: ${{ parameters.displayName }} + steps: - task: DownloadBuildArtifacts@0 displayName: 'Download artifacts' inputs: buildType: current - downloadType: single - artifactName: NuPkg + downloadType: specific + itemPattern: '**/*.nupkg' downloadPath: '$(System.ArtifactsDirectory)' + - task: NuGetToolInstaller@1 displayName: 'Install NuGet' - - pwsh: | - nuget push $(System.ArtifactsDirectory)\nupkg\*.nupkg -ApiKey $(NuGetApiKey) -Source https://www.powershellgallery.com/api/v2/package/ -NonInteractive - displayName: Publish Package + + - powershell: | + Get-ChildItem '$(Build.ArtifactStagingDirectory)/Microsoft.PowerShell.RemotingTools.*.nupkg' + displayName: 'Capture NuGet package' + + - task: NuGetCommand@2 + displayName: 'Push PSThreadJob artifacts to AzArtifactsFeed' + inputs: + command: push + packagesToPush: '$(System.ArtifactsDirectory)/Microsoft.PowerShell.RemotingTools.*.nupkg' + nuGetFeedType: external + publishFeedCredentials: AzArtifactFeed diff --git a/Modules/Microsoft.PowerShell.RemotingTools/build.ps1 b/Modules/Microsoft.PowerShell.RemotingTools/build.ps1 index d68da86..e374464 100644 --- a/Modules/Microsoft.PowerShell.RemotingTools/build.ps1 +++ b/Modules/Microsoft.PowerShell.RemotingTools/build.ps1 @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +# Do NOT edit this file. Edit dobuild.ps1 +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [Parameter(ParameterSetName="build")] [switch] @@ -10,6 +12,14 @@ param ( [switch] $Build, + [Parameter(ParameterSetName="publish")] + [switch] + $Publish, + + [Parameter(ParameterSetName="publish")] + [switch] + $Signed, + [Parameter(ParameterSetName="build")] [switch] $Test, @@ -29,6 +39,7 @@ $config = Get-PSPackageProjectConfiguration -ConfigPath $PSScriptRoot $script:ModuleName = $config.ModuleName $script:SrcPath = $config.SourcePath $script:OutDirectory = $config.BuildOutputPath +$script:SignedDirectory = $config.SignedOutputPath $script:TestPath = $config.TestPath $script:ModuleRoot = $PSScriptRoot @@ -100,7 +111,12 @@ else if ($Build.IsPresent) { $sb = (Get-Item Function:DoBuild).ScriptBlock - Invoke-PSPackageProjectBuild -BuildScript $sb + Invoke-PSPackageProjectBuild -BuildScript $sb -SkipPublish +} + +if ($Publish.IsPresent) +{ + Invoke-PSPackageProjectPublish -Signed:$Signed.IsPresent } if ( $Test.IsPresent ) { diff --git a/Modules/Microsoft.PowerShell.SecretManagement/.ci/ci.yml b/Modules/Microsoft.PowerShell.SecretManagement/.ci/ci.yml index 10518d9..9e9b3db 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/.ci/ci.yml +++ b/Modules/Microsoft.PowerShell.SecretManagement/.ci/ci.yml @@ -71,10 +71,11 @@ stages: Install-Module -Name PSPackageProject -Force $config = Get-PSPackageProjectConfiguration $signSrcPath = "$($config.BuildOutputPath)\$($config.ModuleName)" - $signOutPath = "$($config.BuildOutputPath)\$($config.ModuleName)\Signed" + $signOutPath = "$($config.SignedOutputPath)\$($config.ModuleName)" if (! (Test-Path -Path $signOutPath)) { $null = New-Item -Path $signOutPath -ItemType Directory } + Write-Host "Signed output path is: $signOutPath" $signXmlPath = "$($config.SourcePath)\..\sign-module-files.xml" # Set signing src path variable $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" @@ -107,6 +108,10 @@ stages: binVersionOverride: '' condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) + - pwsh: | + $(Build.SourcesDirectory)/build.ps1 -Publish -Signed + displayName: Create signed artifact + - stage: Compliance displayName: Compliance dependsOn: Build diff --git a/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml b/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml index 650d624..e389472 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml +++ b/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml @@ -1,23 +1,34 @@ parameters: jobName: release imageName: windows-2019 - displayName: Release + displayName: 'Release Microsoft.PowerShell.SecretManagement to NuGet' jobs: - job: ${{ parameters.jobName }} pool: vmImage: ${{ parameters.imageName }} displayName: ${{ parameters.displayName }} + steps: - task: DownloadBuildArtifacts@0 displayName: 'Download artifacts' inputs: buildType: current - downloadType: single - artifactName: NuPkg + downloadType: specific + artifactName: '**/*.nupkg' downloadPath: '$(System.ArtifactsDirectory)' + - task: NuGetToolInstaller@1 displayName: 'Install NuGet' - - pwsh: | - nuget push $(System.ArtifactsDirectory)\nupkg\*.nupkg -ApiKey $(NuGetApiKey) -Source https://www.powershellgallery.com/api/v2/package/ -NonInteractive - displayName: Publish Package + + - powershell: | + Get-ChildItem '$(System.ArtifactsDirectory)/Microsoft.PowerShell.SecretManagement.*.nupkg' + displayName: 'Capture NuGet package' + + - task: NuGetCommand@2 + displayName: 'Push Microsoft.PowerShell.SecretManagement artifacts to AzArtifactsFeed' + inputs: + command: push + packagesToPush: '$(System.ArtifactsDirectory)/Microsoft.PowerShell.SecretManagement.*.nupkg' + nuGetFeedType: external + publishFeedCredentials: AzArtifactFeed diff --git a/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 b/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 index d00e1cd..a3a0fc5 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +# Do NOT edit this file. Edit dobuild.ps1 +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [Parameter(ParameterSetName="build")] [switch] @@ -10,6 +12,14 @@ param ( [switch] $Build, + [Parameter(ParameterSetName="publish")] + [switch] + $Publish, + + [Parameter(ParameterSetName="publish")] + [switch] + $Signed, + [Parameter(ParameterSetName="build")] [switch] $Test, @@ -39,6 +49,7 @@ $config = Get-PSPackageProjectConfiguration -ConfigPath $PSScriptRoot $script:ModuleName = $config.ModuleName $script:SrcPath = $config.SourcePath $script:OutDirectory = $config.BuildOutputPath +$script:SignedDirectory = $config.SignedOutputPath $script:TestPath = $config.TestPath $script:ModuleRoot = $PSScriptRoot @@ -48,39 +59,17 @@ $script:HelpPath = $config.HelpPath $script:BuildConfiguration = $BuildConfiguration $script:BuildFramework = $BuildFramework -. "$PSScriptRoot\doBuild.ps1" - -# The latest DotNet (3.1.1) is needed to perform binary build. -$dotNetCmd = Get-Command -Name dotNet -ErrorAction SilentlyContinue -$dotnetVersion = $null -if ($dotNetCmd -ne $null) { - $info = dotnet --info - foreach ($item in $info) { - $index = $item.IndexOf('Version:') - if ($index -gt -1) { - $versionStr = $item.SubString('Version:'.Length + $index) - $null = [version]::TryParse($versionStr, [ref] $dotnetVersion) - break - } - } -} -# DotNet 3.1.1 is installed in ci.yml. Just check installation and version here. -Write-Verbose -Verbose -Message "Installed DotNet found: $($dotNetCmd -ne $null), version: $versionStr" -<# -$dotNetVersionOk = ($dotnetVersion -ne $null) -and ((($dotnetVersion.Major -eq 3) -and ($dotnetVersion.Minor -ge 1)) -or ($dotnetVersion.Major -gt 3)) -if (! $dotNetVersionOk) { - - Write-Verbose -Verbose -Message "Installing dotNet..." - $installObtainUrl = "https://dotnet.microsoft.com/download/dotnet-core/scripts/v1" - - Remove-Item -ErrorAction SilentlyContinue -Recurse -Force ~\AppData\Local\Microsoft\dotnet - $installScript = "dotnet-install.ps1" - Invoke-WebRequest -Uri $installObtainUrl/$installScript -OutFile $installScript - - & ./$installScript -Channel 'release' -Version '3.1.101' - Write-Verbose -Verbose -Message "dotNet installation complete." +if ($env:TF_BUILD) { + $vstsCommandString = "vso[task.setvariable variable=BUILD_OUTPUT_PATH]$OutDirectory" + Write-Host ("sending " + $vstsCommandString) + Write-Host "##$vstsCommandString" + + $vstsCommandString = "vso[task.setvariable variable=SIGNED_OUTPUT_PATH]$SignedDirectory" + Write-Host ("sending " + $vstsCommandString) + Write-Host "##$vstsCommandString" } -#> + +. $PSScriptRoot\dobuild.ps1 if ($Clean -and (Test-Path $OutDirectory)) { @@ -109,7 +98,12 @@ else if ($Build.IsPresent) { $sb = (Get-Item Function:DoBuild).ScriptBlock - Invoke-PSPackageProjectBuild -BuildScript $sb + Invoke-PSPackageProjectBuild -BuildScript $sb -SkipPublish +} + +if ($Publish.IsPresent) +{ + Invoke-PSPackageProjectPublish -Signed:$Signed.IsPresent } if ( $Test.IsPresent ) { diff --git a/Modules/Microsoft.PowerShell.SecretManagement/pspackageproject.json b/Modules/Microsoft.PowerShell.SecretManagement/pspackageproject.json index fa3ee08..6e4e247 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/pspackageproject.json +++ b/Modules/Microsoft.PowerShell.SecretManagement/pspackageproject.json @@ -2,6 +2,7 @@ "ModuleName": "Microsoft.PowerShell.SecretManagement", "Culture": "en-US", "BuildOutputPath": "out", + "SignedOutputPath": "signed", "HelpPath": "help", "TestPath": "test", "SourcePath": "src" diff --git a/Modules/Microsoft.PowerShell.ThreadJob/.ci/ci.yml b/Modules/Microsoft.PowerShell.ThreadJob/.ci/ci.yml index 8423bf0..ef17bd5 100644 --- a/Modules/Microsoft.PowerShell.ThreadJob/.ci/ci.yml +++ b/Modules/Microsoft.PowerShell.ThreadJob/.ci/ci.yml @@ -71,10 +71,11 @@ stages: Install-Module -Name PSPackageProject -Force $config = Get-PSPackageProjectConfiguration $signSrcPath = "$($config.BuildOutputPath)\$($config.ModuleName)" - $signOutPath = "$($config.BuildOutputPath)\$($config.ModuleName)\Signed" + $signOutPath = "$($config.SignedOutputPath)\$($config.ModuleName)" if (! (Test-Path -Path $signOutPath)) { $null = New-Item -Path $signOutPath -ItemType Directory } + Write-Host "Signed output path is: $signOutPath" $signXmlPath = "$($config.SourcePath)\..\sign-module-files.xml" # Set signing src path variable $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" @@ -107,6 +108,10 @@ stages: binVersionOverride: '' condition: and(and(succeeded(), eq(variables['Build.Reason'], 'Manual')), ne(variables['SkipSigning'], 'True')) + - pwsh: | + $(Build.SourcesDirectory)/build.ps1 -Publish -Signed + displayName: Create signed artifact + - stage: Compliance displayName: Compliance dependsOn: Build diff --git a/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml b/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml index 650d624..2ac1989 100644 --- a/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml +++ b/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml @@ -1,23 +1,34 @@ parameters: jobName: release imageName: windows-2019 - displayName: Release + displayName: 'Release Microsoft.PowerShell.ThreadJob to NuGet' jobs: - job: ${{ parameters.jobName }} pool: vmImage: ${{ parameters.imageName }} displayName: ${{ parameters.displayName }} + steps: - task: DownloadBuildArtifacts@0 displayName: 'Download artifacts' inputs: buildType: current - downloadType: single - artifactName: NuPkg + downloadType: specific + itemPattern: '**/*.nupkg' downloadPath: '$(System.ArtifactsDirectory)' + - task: NuGetToolInstaller@1 displayName: 'Install NuGet' - - pwsh: | - nuget push $(System.ArtifactsDirectory)\nupkg\*.nupkg -ApiKey $(NuGetApiKey) -Source https://www.powershellgallery.com/api/v2/package/ -NonInteractive - displayName: Publish Package + + - powershell: | + Get-ChildItem '$(System.ArtifactsDirectory)/Microsoft.PowerShell.ThreadJob.*.nupkg' + displayName: 'Capture NuGet package' + + - task: NuGetCommand@2 + displayName: 'Push PSThreadJob artifacts to AzArtifactsFeed' + inputs: + command: push + packagesToPush: '$(System.ArtifactsDirectory)/Microsoft.PowerShell.ThreadJob.*.nupkg' + nuGetFeedType: external + publishFeedCredentials: AzArtifactFeed diff --git a/Modules/Microsoft.PowerShell.ThreadJob/build.ps1 b/Modules/Microsoft.PowerShell.ThreadJob/build.ps1 index 7d44f4d..5353230 100644 --- a/Modules/Microsoft.PowerShell.ThreadJob/build.ps1 +++ b/Modules/Microsoft.PowerShell.ThreadJob/build.ps1 @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +# Do NOT edit this file. Edit dobuild.ps1 +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [Parameter(ParameterSetName="build")] [switch] @@ -10,6 +12,14 @@ param ( [switch] $Build, + [Parameter(ParameterSetName="publish")] + [switch] + $Publish, + + [Parameter(ParameterSetName="publish")] + [switch] + $Signed, + [Parameter(ParameterSetName="build")] [switch] $Test, @@ -39,6 +49,7 @@ $config = Get-PSPackageProjectConfiguration -ConfigPath $PSScriptRoot $script:ModuleName = $config.ModuleName $script:SrcPath = $config.SourcePath $script:OutDirectory = $config.BuildOutputPath +$script:SignedDirectory = $config.SignedOutputPath $script:TestPath = $config.TestPath $script:ModuleRoot = $PSScriptRoot @@ -48,39 +59,17 @@ $script:HelpPath = $config.HelpPath $script:BuildConfiguration = $BuildConfiguration $script:BuildFramework = $BuildFramework -. "$PSScriptRoot\doBuild.ps1" - -# The latest DotNet (3.1.1) is needed to perform binary build. -$dotNetCmd = Get-Command -Name dotNet -ErrorAction SilentlyContinue -$dotnetVersion = $null -if ($dotNetCmd -ne $null) { - $info = dotnet --info - foreach ($item in $info) { - $index = $item.IndexOf('Version:') - if ($index -gt -1) { - $versionStr = $item.SubString('Version:'.Length + $index) - $null = [version]::TryParse($versionStr, [ref] $dotnetVersion) - break - } - } -} -# DotNet 3.1.1 is installed in ci.yml. Just check installation and version here. -Write-Verbose -Verbose -Message "Installed DotNet found: $($dotNetCmd -ne $null), version: $versionStr" -<# -$dotNetVersionOk = ($dotnetVersion -ne $null) -and ((($dotnetVersion.Major -eq 3) -and ($dotnetVersion.Minor -ge 1)) -or ($dotnetVersion.Major -gt 3)) -if (! $dotNetVersionOk) { - - Write-Verbose -Verbose -Message "Installing dotNet..." - $installObtainUrl = "https://dotnet.microsoft.com/download/dotnet-core/scripts/v1" - - Remove-Item -ErrorAction SilentlyContinue -Recurse -Force ~\AppData\Local\Microsoft\dotnet - $installScript = "dotnet-install.ps1" - Invoke-WebRequest -Uri $installObtainUrl/$installScript -OutFile $installScript - - & ./$installScript -Channel 'release' -Version '3.1.101' - Write-Verbose -Verbose -Message "dotNet installation complete." +if ($env:TF_BUILD) { + $vstsCommandString = "vso[task.setvariable variable=BUILD_OUTPUT_PATH]$OutDirectory" + Write-Host ("sending " + $vstsCommandString) + Write-Host "##$vstsCommandString" + + $vstsCommandString = "vso[task.setvariable variable=SIGNED_OUTPUT_PATH]$SignedDirectory" + Write-Host ("sending " + $vstsCommandString) + Write-Host "##$vstsCommandString" } -#> + +. $PSScriptRoot\dobuild.ps1 if ($Clean -and (Test-Path $OutDirectory)) { @@ -109,7 +98,12 @@ else if ($Build.IsPresent) { $sb = (Get-Item Function:DoBuild).ScriptBlock - Invoke-PSPackageProjectBuild -BuildScript $sb + Invoke-PSPackageProjectBuild -BuildScript $sb -SkipPublish +} + +if ($Publish.IsPresent) +{ + Invoke-PSPackageProjectPublish -Signed:$Signed.IsPresent } if ( $Test.IsPresent ) { diff --git a/Modules/Microsoft.PowerShell.ThreadJob/pspackageproject.json b/Modules/Microsoft.PowerShell.ThreadJob/pspackageproject.json index 1a81614..0ae6b35 100644 --- a/Modules/Microsoft.PowerShell.ThreadJob/pspackageproject.json +++ b/Modules/Microsoft.PowerShell.ThreadJob/pspackageproject.json @@ -2,6 +2,7 @@ "ModuleName": "Microsoft.PowerShell.ThreadJob", "Culture": "en-US", "BuildOutputPath": "out", + "SignedOutputPath": "signed", "HelpPath": "help", "TestPath": "test", "SourcePath": "src" From 8012b9a7a48c7008f66d73f133834bd1bfcc01e9 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Mon, 4 May 2020 15:07:58 -0700 Subject: [PATCH 02/19] Fix artifact path --- Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml | 4 ++-- Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml | 4 ++-- Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml b/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml index 3dfc390..4c68cbe 100644 --- a/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml +++ b/Modules/Microsoft.PowerShell.RemotingTools/.ci/release.yml @@ -22,13 +22,13 @@ jobs: displayName: 'Install NuGet' - powershell: | - Get-ChildItem '$(Build.ArtifactStagingDirectory)/Microsoft.PowerShell.RemotingTools.*.nupkg' + Get-ChildItem '$(Build.ArtifactStagingDirectory)/nupkg/Microsoft.PowerShell.RemotingTools.*.nupkg' displayName: 'Capture NuGet package' - task: NuGetCommand@2 displayName: 'Push PSThreadJob artifacts to AzArtifactsFeed' inputs: command: push - packagesToPush: '$(System.ArtifactsDirectory)/Microsoft.PowerShell.RemotingTools.*.nupkg' + packagesToPush: '$(System.ArtifactsDirectory)/nupkg/Microsoft.PowerShell.RemotingTools.*.nupkg' nuGetFeedType: external publishFeedCredentials: AzArtifactFeed diff --git a/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml b/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml index e389472..ce8f15c 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml +++ b/Modules/Microsoft.PowerShell.SecretManagement/.ci/release.yml @@ -22,13 +22,13 @@ jobs: displayName: 'Install NuGet' - powershell: | - Get-ChildItem '$(System.ArtifactsDirectory)/Microsoft.PowerShell.SecretManagement.*.nupkg' + Get-ChildItem '$(System.ArtifactsDirectory)/nupkg/Microsoft.PowerShell.SecretManagement.*.nupkg' displayName: 'Capture NuGet package' - task: NuGetCommand@2 displayName: 'Push Microsoft.PowerShell.SecretManagement artifacts to AzArtifactsFeed' inputs: command: push - packagesToPush: '$(System.ArtifactsDirectory)/Microsoft.PowerShell.SecretManagement.*.nupkg' + packagesToPush: '$(System.ArtifactsDirectory)/nupkg/Microsoft.PowerShell.SecretManagement.*.nupkg' nuGetFeedType: external publishFeedCredentials: AzArtifactFeed diff --git a/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml b/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml index 2ac1989..40bff93 100644 --- a/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml +++ b/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml @@ -22,13 +22,13 @@ jobs: displayName: 'Install NuGet' - powershell: | - Get-ChildItem '$(System.ArtifactsDirectory)/Microsoft.PowerShell.ThreadJob.*.nupkg' + Get-ChildItem '$(System.ArtifactsDirectory)/nupkg/Microsoft.PowerShell.ThreadJob.*.nupkg' displayName: 'Capture NuGet package' - task: NuGetCommand@2 displayName: 'Push PSThreadJob artifacts to AzArtifactsFeed' inputs: command: push - packagesToPush: '$(System.ArtifactsDirectory)/Microsoft.PowerShell.ThreadJob.*.nupkg' + packagesToPush: '$(System.ArtifactsDirectory)/nupkg/Microsoft.PowerShell.ThreadJob.*.nupkg' nuGetFeedType: external publishFeedCredentials: AzArtifactFeed From 07e467dde5f3bdca7098c2c7407e4a153b1a3846 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 19 May 2020 10:23:10 -0700 Subject: [PATCH 03/19] Add x-platform local secure store implementation --- ...Microsoft.PowerShell.SecretManagement.psd1 | 6 +- ...crosoft.PowerShell.SecretManagement.csproj | 6 +- .../src/code/SecretManagement.cs | 45 +- .../src/code/Utils.cs | 2159 ++++++++++++----- .../.ci/release.yml | 10 +- 5 files changed, 1634 insertions(+), 592 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 index 708d281..4f28c73 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 @@ -7,7 +7,7 @@ RootModule = '.\Microsoft.PowerShell.SecretManagement.dll' # Version number of this module. -ModuleVersion = '0.2.1' +ModuleVersion = '0.3.0' # Supported PSEditions CompatiblePSEditions = @('Core') @@ -45,6 +45,10 @@ All previous vault extensions will need to be updated. ***** This is an alpha version of the module that currently works only on Windows platforms. ***** + +***** +Breaking change for 0.3.0: Adding new local secure store that works cross platform. +***** " # Minimum version of the PowerShell engine required by this module diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Microsoft.PowerShell.SecretManagement.csproj b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Microsoft.PowerShell.SecretManagement.csproj index 1e2506d..e4c4047 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Microsoft.PowerShell.SecretManagement.csproj +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Microsoft.PowerShell.SecretManagement.csproj @@ -5,9 +5,9 @@ Library Microsoft.PowerShell.SecretManagement Microsoft.PowerShell.SecretManagement - 0.2.1.0 - 0.2.1 - 0.2.1 + 0.3.0.0 + 0.3.0 + 0.3.0 netstandard2.0 diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index b7d2850..52328cb 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -400,16 +400,16 @@ private void StoreVaultParameters( parametersName = ScriptParamTag + vaultName + "_"; // Store parameters in built-in local secure vault. - int errorCode = 0; + string errorMsg = ""; if (!LocalSecretStore.Instance.WriteObject( name: parametersName, parameters, - ref errorCode)) + ref errorMsg)) { var msg = string.Format( CultureInfo.InvariantCulture, "Unable to register vault extension because writing script parameters to the built-in local store failed with error: {0}", - LocalSecretStore.Instance.GetErrorMessage(errorCode)); + errorMsg); ThrowTerminatingError( new ErrorRecord( @@ -540,12 +540,11 @@ private void RemoveParamSecrets( var parametersName = (string) vaultInfo[ParametersNameKey]; if (!string.IsNullOrEmpty(parametersName)) { - int errorCode = 0; - if (!LocalSecretStore.Instance.DeleteObject(parametersName, ref errorCode)) + string errorMsg = ""; + if (!LocalSecretStore.Instance.DeleteObject(parametersName, ref errorMsg)) { - var errorMessage = LocalSecretStore.Instance.GetErrorMessage(errorCode); var msg = string.Format(CultureInfo.InvariantCulture, - "Removal of vault info script parameters {0} failed with error {1}", parametersName, errorMessage); + "Removal of vault info script parameters {0} failed with error {1}", parametersName, errorMsg); WriteError( new ErrorRecord( new PSInvalidOperationException(msg), @@ -763,11 +762,11 @@ private void WriteResults( private void SearchLocalStore(string name) { // Search through the built-in local vault. - int errorCode = 0; + string errorMsg = ""; if (LocalSecretStore.Instance.EnumerateObjectInfo( filter: Name, outSecretInfo: out SecretInformation[] outSecretInfo, - errorCode: ref errorCode)) + errorMsg: ref errorMsg)) { WriteResults( results: outSecretInfo, @@ -954,11 +953,11 @@ private void WriteNotFoundError() private bool SearchLocalStore(string name) { - int errorCode = 0; + string errorMsg = ""; if (LocalSecretStore.Instance.ReadObject( name: name, outObject: out object outObject, - ref errorCode)) + ref errorMsg)) { WriteSecret(outObject); return true; @@ -1081,34 +1080,31 @@ protected override void EndProcessing() } // Add to default built-in vault (after NoClobber check). - int errorCode = 0; + string errorMsg = ""; if (NoClobber) { if (LocalSecretStore.Instance.ReadObject( name: Name, out object _, - ref errorCode)) + ref errorMsg)) { - var msg = string.Format(CultureInfo.InvariantCulture, - "A secret with name {0} already exists in the local default vault", Name); ThrowTerminatingError( new ErrorRecord( - new PSInvalidOperationException(msg), + new PSInvalidOperationException(errorMsg), "AddSecretAlreadyExists", ErrorCategory.ResourceExists, this)); } } - errorCode = 0; + errorMsg = ""; if (!LocalSecretStore.Instance.WriteObject( name: Name, objectToWrite: secretToWrite, - ref errorCode)) + ref errorMsg)) { - var errorMessage = LocalSecretStore.Instance.GetErrorMessage(errorCode); var msg = string.Format(CultureInfo.InvariantCulture, - "The secret could not be written to the local default vault. Error: {0}", errorMessage); + "The secret could not be written to the local default vault. Error: {0}", errorMsg); ThrowTerminatingError( new ErrorRecord( new PSInvalidOperationException(msg), @@ -1164,14 +1160,13 @@ protected override void ProcessRecord() if (Vault.Equals(RegisterSecretVaultCommand.BuiltInLocalVault, StringComparison.OrdinalIgnoreCase)) { // Remove from local built-in default vault. - int errorCode = 0; + string errorMsg = ""; if (!LocalSecretStore.Instance.DeleteObject( name: Name, - ref errorCode)) + errorMsg: ref errorMsg)) { - var errorMessage = LocalSecretStore.Instance.GetErrorMessage(errorCode); var msg = string.Format(CultureInfo.InvariantCulture, - "The secret could not be removed from the local default vault. Error: {0}", errorMessage); + "The secret could not be removed from the local default vault. Error: {0}", errorMsg); ThrowTerminatingError( new ErrorRecord( new PSInvalidOperationException(msg), @@ -1224,7 +1219,7 @@ protected override void EndProcessing() bool success; if (Vault.Equals(RegisterSecretVaultCommand.BuiltInLocalVault, StringComparison.OrdinalIgnoreCase)) { - // TODO: Add test for CredMan, Keyring, etc. + // TODO: Add test for SecureStore success = true; } else diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 73b3fc0..7486b74 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -10,318 +10,1559 @@ using System.Management.Automation; using System.Runtime.InteropServices; using System.Security; +using System.Security.Cryptography; using System.Text; +using System.Threading; using Dbg = System.Diagnostics.Debug; namespace Microsoft.PowerShell.SecretManagement { - #region BaseLocalSecretStore + #region Utils - internal abstract class BaseLocalSecretStore + internal static class Utils { #region Members - protected const string PSTag = "ps:"; - protected const string PSHashtableTag = "psht:"; - protected const string ByteArrayType = "ByteArrayType"; - protected const string StringType = "StringType"; - protected const string SecureStringType = "SecureStringType"; - protected const string PSCredentialType = "CredentialType"; - protected const string HashtableType = "HashtableType"; +#if UNIX + private static readonly string LocalLocation = Environment.GetEnvironmentVariable("HOME"); +#else + private static readonly string LocalLocation = Environment.GetEnvironmentVariable("USERPROFILE"); +#endif + private static readonly string SMLocalPath = Path.Combine(LocalLocation, ".secretmanagement"); + + private const string ConvertJsonToHashtableScript = @" + param ( + [string] $json + ) + + function ConvertToHash + { + param ( + [pscustomobject] $object + ) + + $output = @{} + $object | Get-Member -MemberType NoteProperty | ForEach-Object { + $name = $_.Name + $value = $object.($name) + + if ($value -is [object[]]) + { + $array = @() + $value | ForEach-Object { + $array += (ConvertToHash $_) + } + $output.($name) = $array + } + elseif ($value -is [pscustomobject]) + { + $output.($name) = (ConvertToHash $value) + } + else + { + $output.($name) = $value + } + } + + $output + } + + $customObject = ConvertFrom-Json -InputObject $json -Depth 5 + return ConvertToHash $customObject + "; + + #endregion + + #region Properties + + public static string SecretManagementLocalPath + { + get { return SMLocalPath; } + } + + #endregion + + #region Methods + + public static Hashtable ConvertJsonToHashtable(string json) + { + var results = PowerShellInvoker.InvokeScript( + script: ConvertJsonToHashtableScript, + args: new object[] { json }, + error: out Exception _); + + return (results.Count > 0) ? results[0] : null; + } + + public static PSObject ConvertJsonToPSObject(string json) + { + var results = PowerShellInvoker.InvokeScript( + script: @"param ([string] $json) ConvertFrom-Json -InputObject $json -Depth 5", + args: new object[] { json }, + error: out Exception _); + + return (results.Count > 0) ? results[0] : null; + } + + public static string ConvertHashtableToJson(Hashtable hashtable) + { + var results = PowerShellInvoker.InvokeScript( + script: @"param ([hashtable] $hashtable) ConvertTo-Json -InputObject $hashtable -Depth 5", + args: new object[] { hashtable }, + error: out Exception _); + + return (results.Count > 0) ? results[0] : null; + } + + #endregion + } + + #endregion + + #region SecureStore + + internal static class CryptoUtils + { + #region Public methods + + public static byte[] GenerateKey() + { + using (var aes = Aes.Create()) + { + return aes.Key; + } + } + + public static byte[] EncryptWithKey( + SecureString passWord, + byte[] key, + byte[] data) + { + var keyToUse = (passWord != null) ? + DeriveFromKey(passWord, key) : + key; + + using (var aes = Aes.Create()) + { + aes.IV = new byte[16]; // Set IV to zero + aes.Key = keyToUse; + using (var encryptor = aes.CreateEncryptor()) + using (var sourceStream = new MemoryStream(data)) + using (var targetStream = new MemoryStream()) + { + using (var cryptoStream = new CryptoStream(targetStream, encryptor, CryptoStreamMode.Write)) + { + sourceStream.CopyTo(cryptoStream); + } + + return targetStream.ToArray(); + } + } + } + + public static byte[] DecryptWithKey( + SecureString passWord, + byte[] key, + byte[] data) + { + var keyToUse = (passWord != null) ? + DeriveFromKey(passWord, key) : + key; + + using (var aes = Aes.Create()) + { + aes.IV = new byte[16]; // Set IV to zero + aes.Key = keyToUse; + using (var decryptor = aes.CreateDecryptor()) + using (var sourceStream = new MemoryStream(data)) + using (var targetStream = new MemoryStream()) + { + using (var cryptoStream = new CryptoStream(sourceStream, decryptor, CryptoStreamMode.Read)) + { + cryptoStream.CopyTo(targetStream); + } + + return targetStream.ToArray(); + } + } + } + + public static void ZeroOutData(byte[] data) + { + for (int i = 0; i < data.Length; i++) + { + data[i] = 0; + } + } + + #endregion + + #region Private methods + + private static byte[] DeriveFromKey( + SecureString passWord, + byte[] key) + { + var passWordData = GetDataFromSecureString(passWord); + try + { + var derivedBytes = new Rfc2898DeriveBytes(passWordData, key, 1000); + return derivedBytes.GetBytes(key.Length); + } + finally + { + ZeroOutData(passWordData); + } + } + + private static byte[] GetDataFromSecureString(SecureString secureString) + { + IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(secureString); + if (ptr == IntPtr.Zero) + { + throw new InvalidOperationException("Unable to read secure string."); + } + + try + { + var data = new byte[secureString.Length * 2]; + Marshal.Copy(ptr, data, 0, data.Length); + return data; + } + finally + { + Marshal.ZeroFreeCoTaskMemUnicode(ptr); + } + } + + #endregion + } + + internal enum SecureStoreScope + { + Local = 1, + Machine + } + + internal sealed class SecureStoreConfig + { + #region Properties + + public SecureStoreScope Scope + { + get; + private set; + } + + public bool PasswordRequired + { + get; + private set; + } + + public int PasswordTimeout + { + get; + private set; + } + + public bool DoNotPrompt + { + get; + private set; + } + + #endregion + + #region Constructor + + private SecureStoreConfig() + { + } + + public SecureStoreConfig( + SecureStoreScope scope, + bool passwordRequired, + int passwordTimeout, + bool doNotPrompt) + { + Scope = scope; + PasswordRequired = passwordRequired; + PasswordTimeout = passwordTimeout; + DoNotPrompt = doNotPrompt; + } + + public SecureStoreConfig( + string json) + { + ConvertFromJson(json); + } + + #endregion + + # region Public methods + + public string ConvertToJson() + { + // Config data + var configHashtable = new Hashtable(); + configHashtable.Add( + key: "StoreScope", + value: Scope); + configHashtable.Add( + key: "PasswordRequired", + value: PasswordRequired); + configHashtable.Add( + key: "PasswordTimeout", + value: PasswordTimeout); + configHashtable.Add( + key: "DoNotPrompt", + value: DoNotPrompt); + + var dataDictionary = new Hashtable(); + dataDictionary.Add( + key: "ConfigData", + value: configHashtable); + + return Utils.ConvertHashtableToJson(dataDictionary); + } + + #endregion + + #region Private methods + + private void ConvertFromJson(string json) + { + dynamic configDataObj = (Utils.ConvertJsonToPSObject(json)); + Scope = (SecureStoreScope) configDataObj.ConfigData.StoreScope; + PasswordRequired = (bool) configDataObj.ConfigData.PasswordRequired; + PasswordTimeout = (int) configDataObj.ConfigData.PasswordTimeout; + DoNotPrompt = (bool) configDataObj.ConfigData.DoNotPrompt; + } + + #endregion + + #region Static methods + + public static SecureStoreConfig GetDefault() + { + return new SecureStoreConfig( + scope: SecureStoreScope.Local, + passwordRequired: true, + passwordTimeout: -1, + doNotPrompt: false); + } + + #endregion + } + + internal sealed class SecureStoreMetadata + { + #region Properties + + public string Name + { + get; + private set; + } + + public string TypeName + { + get; + private set; + } + + public int Offset + { + get; + set; + } + + public int Size + { + get; + private set; + } + + public ReadOnlyDictionary Attributes + { + get; + private set; + } + + #endregion + + #region Constructor + + private SecureStoreMetadata() + { + } + + public SecureStoreMetadata( + string name, + string typeName, + int offset, + int size, + ReadOnlyDictionary attributes) + { + Name = name; + TypeName = typeName; + Offset = offset; + Size = size; + Attributes = attributes; + } + + #endregion + } + + internal sealed class SecureStoreData + { + #region Properties + + internal byte[] Key { get; set; } + internal byte[] Blob { get; set; } + internal Dictionary MetaData { get; set; } + + #endregion + + #region Constructor + + public SecureStoreData() + { + } + + public SecureStoreData( + byte[] key, + string json, + byte[] blob) + { + Key = key; + Blob = blob; + ConvertJsonToMeta(json); + } + + #endregion + + #region Public methods + + // Example of store data as Hashtable + /* + @{ + ConfigData = + @{ + StoreScope='LocalScope' + PasswordRequired=$true + PasswordTimeout=-1, + DoNotPrompt=$false + } + MetaData = + @( + @{Name='TestSecret1'; Type='SecureString'; Offset=14434; Size=5000; Attributes=@{}} + @{Name='TestSecret2'; Type='String'; Offset=34593; Size=5100; Attributes=@{}} + @{Name='TestSecret3'; Type='PSCredential'; Offset=59837; Size=4900; Attributes=@{UserName='UserA'}} + @{Name='TestSecret4'; Type='Hashtable'; Offset=77856; Size=3500; Attributes=@{Element1='SecretElement1'; Element2='SecretElement2'}} + ) + } + */ + + public string ConvertMetaToJson() + { + // Meta data array + var listMetadata = new List(MetaData.Count); + foreach (var item in MetaData.Values) + { + var metaHashtable = new Hashtable(); + metaHashtable.Add( + key: "Name", + value: item.Name); + metaHashtable.Add( + key: "Type", + value: item.TypeName); + metaHashtable.Add( + key: "Offset", + value: item.Offset); + metaHashtable.Add( + key: "Size", + value: item.Size); + metaHashtable.Add( + key: "Attributes", + value: item.Attributes); + + listMetadata.Add(metaHashtable); + } + + var dataDictionary = new Hashtable(); + dataDictionary.Add( + key: "MetaData", + value: listMetadata.ToArray()); + + return Utils.ConvertHashtableToJson(dataDictionary); + } + + public void Clear() + { + if (Key != null) + { + CryptoUtils.ZeroOutData(Key); + } + + if (Blob != null) + { + CryptoUtils.ZeroOutData(Blob); + } + + if (MetaData != null) + { + MetaData.Clear(); + } + } + + #endregion + + #region Private methods + + // Example meta data json + /* + "MetaData": [ + { + "Name": "TestSecret1", + "Type": "String", + "Offset": 34593, + "Size": 3500, + "Attributes": {} + }, + { + "Name": "TestSecret2", + "Type": "PSCredential", + "Offset": 59837, + "Size": 4200, + "Attributes": { + "UserName": "UserA" + }, + } + ] + } + */ + + private void ConvertJsonToMeta(string json) + { + dynamic data = Utils.ConvertJsonToPSObject(json); + + // Validate + if (data == null) + { + throw new InvalidDataException("The data from the local secure store is unusable."); + } + + // Meta data + dynamic metaDataArray = data.MetaData; + MetaData = new Dictionary( + metaDataArray.Length, + StringComparer.CurrentCultureIgnoreCase); + foreach (var item in metaDataArray) + { + var attributesDictionary = new Dictionary(); + var attributes = item.Attributes; + foreach (var prop in ((PSObject)attributes).Properties) + { + attributesDictionary.Add( + key: prop.Name, + value: prop.Value); + } + + MetaData.Add( + key: item.Name, + value: new SecureStoreMetadata( + name: item.Name, + typeName: item.Type, + offset: (int) item.Offset, + size: (int) item.Size, + attributes: new ReadOnlyDictionary(attributesDictionary))); + } + } + + #endregion + } + + internal sealed class SecureStorePasswordException : InvalidOperationException + { + #region Constructor + + public SecureStorePasswordException() + : base("Password is required to access local store.") + { + } + + #endregion + } + + // TODO: + // a. Add support for SM local store password prompt + // b. Add support for SM local store configuration (password) + // c. Add auto update + // d. Add support for SM local store configuration (scope) + // e. Add Disposed check (?) + // f. Add local store only tests + // g. Create AzKeyVault extension vault for demo + // h. [DONE] Integrate into LocalStore + // i. [DONE] Test with current tests + + internal sealed class SecureStore : IDisposable + { + #region Members + + private SecureString _password; + private SecureStoreData _data; + private SecureStoreConfig _configData; + private Timer _passwordTimer; + private readonly object _syncObject = new object(); + + #endregion + + #region Properties + + public SecureStoreData Data + { + get + { + return _data; + } + } + + public SecureStoreConfig ConfigData + { + get + { + return _configData; + } + } + + internal SecureString Password + { + get + { + lock (_syncObject) + { + if (ConfigData.PasswordRequired && (_password == null)) + { + throw new SecureStorePasswordException(); + } + + return (_password != null) ? _password.Copy() : null; + } + } + } + + #endregion + + #region Constructor + + public SecureStore( + SecureStoreData data, + SecureStoreConfig configData, + SecureString password = null) + { + _data = data; + _configData = configData; + _password = password; + } + + #endregion + + #region IDisposable + + public void Dispose() + { + _passwordTimer?.Dispose(); + _password?.Clear(); + _data?.Clear(); + } + + #endregion + + #region Public methods + + public void SetPassword( + SecureString password, + int timeoutMilliseconds = -1) + { + int passwordTimeout; + lock (_syncObject) + { + _password = password; + passwordTimeout = _configData.PasswordTimeout; + } + + if (passwordTimeout > 0) + { + _passwordTimer = new Timer( + callback: (_) => + { + lock (_syncObject) + { + _password = null; + } + }, + state: null, + dueTime: passwordTimeout, + period: Timeout.Infinite); + } + } + + public bool WriteBlob( + string name, + byte[] blob, + string typeName, + Dictionary attributes, + ref string errorMsg) + { + if (EnumerateBlobs( + filter: name, + metaData: out SecureStoreMetadata[] _, + ref errorMsg)) + { + return ReplaceBlobImpl( + name, + blob, + typeName, + attributes, + ref errorMsg); + } + + return WriteBlobImpl( + name, + blob, + typeName, + attributes, + ref errorMsg); + } + + public bool ReadBlob( + string name, + out byte[] blob, + out SecureStoreMetadata metaData, + ref string errorMsg) + { + byte[] encryptedBlob = null; + byte[] key = null; + lock (_syncObject) + { + // Get blob + if (!_data.MetaData.TryGetValue( + key: name, + value: out metaData)) + { + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"Unable to read item {0}.", + name); + blob = null; + metaData = null; + return false; + } + + key = _data.Key; + var offset = metaData.Offset; + var size = metaData.Size; + encryptedBlob = new byte[size]; + Buffer.BlockCopy(_data.Blob, offset, encryptedBlob, 0, size); + } + + // Decrypt blob + var password = Password; + try + { + blob = CryptoUtils.DecryptWithKey( + passWord: password, + key: key, + data: encryptedBlob); + } + finally + { + if (password != null) + { + password.Clear(); + } + } + + return true; + } + + public bool EnumerateBlobs( + string filter, + out SecureStoreMetadata[] metaData, + ref string errorMsg) + { + var filterPattern = new WildcardPattern( + pattern: filter, + options: WildcardOptions.IgnoreCase); + var foundBlobs = new List(); + + lock (_syncObject) + { + foreach (var key in _data.MetaData.Keys) + { + if (filterPattern.IsMatch(key)) + { + var data = _data.MetaData[key]; + foundBlobs.Add( + new SecureStoreMetadata( + name: data.Name, + typeName: data.TypeName, + offset: data.Offset, + size: data.Size, + attributes: data.Attributes)); + } + } + } + + metaData = foundBlobs.ToArray(); + return (metaData.Length > 0); + } + + public bool DeleteBlob( + string name, + ref string errorMsg) + { + lock (_syncObject) + { + if (!_data.MetaData.TryGetValue( + key: name, + value: out SecureStoreMetadata metaData)) + { + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"Unable to find item {0} for removal.", + name); + return false; + } + _data.MetaData.Remove(name); + + // Create new blob + var oldBlob = _data.Blob; + var offset = metaData.Offset; + var size = metaData.Size; + var newSize = (oldBlob.Length - size); + var newBlob = new byte[newSize]; + Buffer.BlockCopy(oldBlob, 0, newBlob, 0, offset); + Buffer.BlockCopy(oldBlob, (offset + size), newBlob, offset, (newSize - offset)); + _data.Blob = newBlob; + CryptoUtils.ZeroOutData(oldBlob); + + // Fix up meta data offsets + foreach (var metaItem in _data.MetaData.Values) + { + if (metaItem.Offset > offset) + { + metaItem.Offset -= size; + } + } + } + + // Write to file + var password = Password; + try + { + return SecureStoreFile.WriteFile( + password: password, + data: _data, + ref errorMsg); + } + finally + { + if (password != null) + { + password.Clear(); + } + } + } + + public bool UpdateConfigData( + SecureStoreConfig configData, + ref string errorMsg) + { + lock (_syncObject) + { + _configData = configData; + return true; + } + + // TODO: Implement configuration helper method(s) that will: + // a. Update blob data to re-encrypt with/without password + // b. ??? + } + + public bool UpdateFromFile(ref string errorMsg) + { + if (!SecureStoreFile.ReadFile( + password: Password, + data: out SecureStoreData data, + ref errorMsg)) + { + return false; + } + + lock (_syncObject) + { + _data = data; + } + + return true; + } + + #endregion + + #region Private methods + + private bool WriteBlobImpl( + string name, + byte[] blob, + string typeName, + Dictionary attributes, + ref string errorMsg) + { + var password = Password; + try + { + var newData = new SecureStoreData(); + newData.MetaData = _data.MetaData; + newData.Key = _data.Key; + + // Encrypt blob + var blobToWrite = CryptoUtils.EncryptWithKey( + passWord: password, + key: _data.Key, + data: blob); + + lock (_syncObject) + { + // Create new store blob + var oldBlob = _data.Blob; + var offset = oldBlob.Length; + var newBlob = new byte[offset + blobToWrite.Length]; + Buffer.BlockCopy(oldBlob, 0, newBlob, 0, offset); + Buffer.BlockCopy(blobToWrite, 0, newBlob, offset, blobToWrite.Length); + newData.Blob = newBlob; + + // Create new meta item + newData.MetaData.Add( + key: name, + value: new SecureStoreMetadata( + name: name, + typeName: typeName, + offset: offset, + size: blobToWrite.Length, + attributes: new ReadOnlyDictionary(attributes))); + + // Update store data + _data = newData; + CryptoUtils.ZeroOutData(oldBlob); + } + + // Write to file + return SecureStoreFile.WriteFile( + password: password, + data: _data, + ref errorMsg); + } + finally + { + if (password != null) + { + password.Clear(); + } + } + } + + private bool ReplaceBlobImpl( + string name, + byte[] blob, + string typeName, + Dictionary attributes, + ref string errorMsg) + { + lock (_syncObject) + { + // Remove old blob + if (!DeleteBlob( + name: name, + ref errorMsg)) + { + errorMsg = "Unable to replace existing store item, error: " + errorMsg; + return false; + } + + // Add new blob + return WriteBlobImpl( + name: name, + blob: blob, + typeName: typeName, + attributes: attributes, + ref errorMsg); + } + } + + #endregion + + #region Static methods + + public static SecureStore GetDefault() + { + var data = new SecureStoreData() + { + Key = CryptoUtils.GenerateKey(), + Blob = new byte[0], + MetaData = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + }; + + return new SecureStore( + data: data, + configData: SecureStoreConfig.GetDefault()); + } + + public static SecureStore GetStore( + SecureString password) + { + string errorMsg = ""; + + // Read config from file. + SecureStoreConfig configData; + if (!SecureStoreFile.ReadConfigFile( + configData: out configData, + errorMsg: ref errorMsg)) + { + if (errorMsg.Equals("NoConfigFile", StringComparison.OrdinalIgnoreCase)) + { + if (SecureStoreFile.StoreFileExists()) + { + // This indicates a corrupted store configuration or inadvertent file deletion. + // TODO: Throw an error that explains the configuration must be set to correct + // settings needed for store, or must re-create local store. + throw new InvalidOperationException("Secure local store is in inconsistent state. TODO: Provide user instructions."); + } + + // First time, use default configuration. + configData = SecureStoreConfig.GetDefault(); + if (!SecureStoreFile.WriteConfigFile( + configData, + ref errorMsg)) + { + throw new PSInvalidOperationException( + string.Format(CultureInfo.InvariantCulture, + @"Unable to write store configuration data to file with error: {0}", errorMsg)); + } + } + } + + // Enforce required password configuration. + if (configData.PasswordRequired && (password == null)) + { + throw new SecureStorePasswordException(); + } + + // Read store from file. + if (SecureStoreFile.ReadFile( + password: password, + data: out SecureStoreData data, + ref errorMsg)) + { + return new SecureStore( + data: data, + configData: configData, + password: password); + } + + // If no file, create a default store + if (errorMsg.Equals("NoFile", StringComparison.OrdinalIgnoreCase)) + { + var secureStore = GetDefault(); + if (!SecureStoreFile.WriteFile( + password: password, + data: secureStore.Data, + ref errorMsg)) + { + throw new PSInvalidOperationException( + string.Format(CultureInfo.InvariantCulture, + @"Unable to write store data to file with error: {0}", errorMsg)); + } + + secureStore.SetPassword(password); + return secureStore; + } + + throw new PSInvalidOperationException( + string.Format(CultureInfo.InvariantCulture, + @"Unable to read store data from file with error: {0}", errorMsg)); + } + + #endregion + } + + internal static class SecureStoreFile + { + #region Members + + private static readonly string LocalStorePath = Path.Combine(Utils.SecretManagementLocalPath, ".localstore"); + private static readonly string LocalStoreFilePath = Path.Combine(LocalStorePath, "storefile"); + private static readonly string LocalConfigFilePath = Path.Combine(LocalStorePath, "storeconfig"); + + private static readonly FileSystemWatcher _storeFileWatcher; + private static bool _allowAutoUpdate; + + #endregion + + #region Constructor + + static SecureStoreFile() + { + if (!Directory.Exists(LocalStorePath)) + { + Directory.CreateDirectory(LocalStorePath); + } + + _storeFileWatcher = new FileSystemWatcher(LocalStorePath); + _storeFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; + _storeFileWatcher.Filter = "LocalStore"; + _storeFileWatcher.EnableRaisingEvents = true; + _storeFileWatcher.Changed += (sender, args) => { UpdateData(); }; + _storeFileWatcher.Created += (sender, args) => { UpdateData(); }; + _storeFileWatcher.Deleted += (sender, args) => { UpdateData(fileDeleted: true); }; + + _allowAutoUpdate = true; + } + + #endregion + + #region Events + + public static event EventHandler FileUpdated; + private static void RaiseFileUpdatedEvent(FileUpdateEventArgs args) + { + if (FileUpdated != null) + { + FileUpdated.Invoke(null, args); + } + } + + #endregion + + #region Public methods + + // File structure + /* + int: key blob size + int: json blob size + byte[]: key blob + byte[]: json blob + byte[]: data blob + */ + + public static bool WriteFile( + SecureString password, + SecureStoreData data, + ref string errorMsg) + { + var count = 0; + Exception exFail = null; + do + { + _allowAutoUpdate = false; + + try + { + // Encrypt json meta data. + var jsonStr = data.ConvertMetaToJson(); + var jsonBlob = CryptoUtils.EncryptWithKey( + passWord: password, + key: data.Key, + data: Encoding.UTF8.GetBytes(jsonStr)); + + using (var fileStream = File.OpenWrite(LocalStoreFilePath)) + { + fileStream.Seek(0, 0); + + // Write blob sizes + var intSize = sizeof(Int32); + var keyBlobSize = data.Key.Length; + var jsonBlobSize = jsonBlob.Length; + byte[] intField = BitConverter.GetBytes(keyBlobSize); + fileStream.Write(intField, 0, intSize); + intField = BitConverter.GetBytes(jsonBlobSize); + fileStream.Write(intField, 0, intSize); + + // Write key blob + fileStream.Write(data.Key, 0, keyBlobSize); + + // Write json blob + fileStream.Write(jsonBlob, 0, jsonBlobSize); + + // Write data blob + fileStream.Write(data.Blob, 0, data.Blob.Length); + + if (fileStream.Position != fileStream.Length) + { + fileStream.SetLength(fileStream.Position); + } + + return true; + } + } + catch (IOException exIO) + { + // Make up to four attempts. + exFail = exIO; + } + catch (Exception ex) + { + // Unexpected error. + exFail = ex; + break; + } + finally + { + _allowAutoUpdate = true; + } + + System.Threading.Thread.Sleep(250); + + } while (++count < 4); + + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"Unable to write to local store file with error: {0}", + exFail.Message); + + return false; + } + + public static bool ReadFile( + SecureString password, + out SecureStoreData data, + ref string errorMsg) + { + data = null; + + if (!File.Exists(LocalStoreFilePath)) + { + errorMsg = "NoFile"; + return false; + } + + // Open and read from file stream + var count = 0; + Exception exFail = null; + do + { + try + { + using (var fileStream = File.OpenRead(LocalStoreFilePath)) + { + // Read offsets + var intSize = sizeof(Int32); + byte[] intField = new byte[intSize]; + fileStream.Read(intField, 0, intSize); + var keyBlobSize = BitConverter.ToInt32(intField, 0); + fileStream.Read(intField, 0, intSize); + var jsonBlobSize = BitConverter.ToInt32(intField, 0); + + // Read key blob + byte[] keyBlob = new byte[keyBlobSize]; + fileStream.Read(keyBlob, 0, keyBlobSize); + + // Read json blob and decrypt + byte[] jsonBlob = new byte[jsonBlobSize]; + fileStream.Read(jsonBlob, 0, jsonBlobSize); + var jsonStr = Encoding.UTF8.GetString( + CryptoUtils.DecryptWithKey( + passWord: password, + key: keyBlob, + jsonBlob)); + + // Read data blob + var dataBlobSize = (int) (fileStream.Length - (keyBlobSize + jsonBlobSize + (intSize * 2 ))); + byte[] dataBlob = new byte[dataBlobSize]; + fileStream.Read(dataBlob, 0, dataBlobSize); + + data = new SecureStoreData( + key: keyBlob, + json: jsonStr, + blob: dataBlob); + + return true; + } + } + catch (IOException exIO) + { + // Make up to four attempts. + exFail = exIO; + } + catch (Exception ex) + { + // Unexpected error. + exFail = ex; + break; + } + + System.Threading.Thread.Sleep(250); + + } while (++count < 4); + + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"Unable to read from local store file with error: {0}", + exFail.Message); + + return false; + } + + public static bool WriteConfigFile( + SecureStoreConfig configData, + ref string errorMsg) + { + var count = 0; + Exception exFail = null; + do + { + try + { + // Encrypt json meta data. + var jsonStr = configData.ConvertToJson(); + File.WriteAllText( + path: LocalConfigFilePath, + contents: jsonStr); + + return true; + } + catch (IOException exIO) + { + // Make up to four attempts. + exFail = exIO; + } + catch (Exception ex) + { + // Unexpected error. + exFail = ex; + break; + } + + System.Threading.Thread.Sleep(250); + + } while (++count < 4); + + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"Unable to write to local configuration file with error: {0}", + exFail.Message); + + return false; + } + + public static bool ReadConfigFile( + out SecureStoreConfig configData, + ref string errorMsg) + { + configData = null; + + if ((!File.Exists(LocalConfigFilePath))) + { + errorMsg = "NoConfigFile"; + return false; + } + + // Open and read from file stream + var count = 0; + Exception exFail = null; + do + { + try + { + var configJson = File.ReadAllText(LocalConfigFilePath); + configData = new SecureStoreConfig(configJson); + return true; + } + catch (IOException exIO) + { + // Make up to four attempts. + exFail = exIO; + } + catch (Exception ex) + { + // Unexpected error. + exFail = ex; + break; + } - protected const int MaxHashtableItemCount = 20; + System.Threading.Thread.Sleep(250); - #endregion + } while (++count < 4); - #region Static Constructor + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"Unable to read from local store configuration file with error: {0}", + exFail.Message); - static BaseLocalSecretStore() - { - Instance = new LocalSecretStore(); + return false; } - #endregion - - #region Properties - - public static BaseLocalSecretStore Instance + public static bool StoreFileExists() { - get; - private set; + return File.Exists(LocalStoreFilePath); } #endregion - #region Abstract methods - - // - // Vault methods currently support the following types - // - // byte[] (blob) - // string - // SecureString - // PSCredential - // Hashtable - // Dictionary - // ,where object type is: byte[], string, SecureString, Credential - // + #region Private methods - /// - /// Writes an object to the local secret vault for the current logged on user. - /// - /// Name of object to write. - /// Object to write to vault. - /// Error code or zero. - /// True on successful write. - public abstract bool WriteObject( - string name, - T objectToWrite, - ref int errorCode); - - /// - /// Reads an object from the local secret vault for the current logged on user. - /// - /// Name of object to read from vault. - /// Object read from vault. - /// Error code or zero. - /// True on successful read. - public abstract bool ReadObject( - string name, - out object outObject, - ref int errorCode); + private static void UpdateData(bool fileDeleted = false) + { + if (_allowAutoUpdate) + { + RaiseFileUpdatedEvent( + new FileUpdateEventArgs(fileDeleted)); + } + } - /// - /// Enumerate objects in the vault based on the current user and filter parameter, - /// and return information about each object but not the object itself. - /// - /// Search string for object enumeration. - /// Array of SecretInformation objects. - /// Error code or zero. - /// True when objects are found. - public abstract bool EnumerateObjectInfo( - string filter, - out SecretInformation[] outSecretInfo, - ref int errorCode); + #endregion + } - /// - /// Delete vault object. - /// - /// Name of vault item to delete. - /// Error code or zero. - /// True if object successfully deleted. - public abstract bool DeleteObject( - string name, - ref int errorCode); + #region Event args - /// - /// Returns an error message based on provided error code. - /// - /// Error code. - /// Error message. - public abstract string GetErrorMessage(int errorCode); + internal sealed class FileUpdateEventArgs : EventArgs + { + public bool FileDeleted + { + private set; + get; + } - #endregion + public FileUpdateEventArgs(bool fileDeleted) + { + FileDeleted = fileDeleted; + } } #endregion -#if !UNIX - #region CredMan + #endregion + + #region LocalSecretStore /// - /// Windows Credential Manager (CredMan) native method PInvokes. + /// Default local secret store /// - internal static class NativeUtils + internal sealed class LocalSecretStore { - #region Constants + #region Members - /// - /// CREDENTIAL Flags - /// - public enum CRED_FLAGS + private const string PSTag = "ps:"; + private const string PSHashtableTag = "psht:"; + private const string ByteArrayType = "ByteArrayType"; + private const string StringType = "StringType"; + private const string SecureStringType = "SecureStringType"; + private const string PSCredentialType = "CredentialType"; + private const string HashtableType = "HashtableType"; + private const int MaxHashtableItemCount = 20; + + private readonly SecureStore _secureStore; + + private static object SyncObject; + private static LocalSecretStore LocalStore; + private static Dictionary DefaultTag; + + #endregion + + #region Constructor + + private LocalSecretStore() { - PROMPT_NOW = 2, - USERNAME_TARGET = 4 } - /// - /// CREDENTIAL Types - /// - public enum CRED_TYPE + public LocalSecretStore( + SecureStore secureStore) { - GENERIC = 1, - DOMAIN_PASSWORD = 2, - DOMAIN_CERTIFICATE = 3, - DOMAIN_VISIBLE_PASSWORD = 4, - GENERIC_CERTIFICATE = 5, - DOMAIN_EXTENDED = 6, - MAXIMUM = 7 + _secureStore = secureStore; } - /// - /// Credential Persist - /// - public enum CRED_PERSIST + static LocalSecretStore() { - SESSION = 1, - LOCAL_MACHINE = 2, - ENTERPRISE = 3 - } + SyncObject = new object(); - // Credential Read/Write GetLastError errors (winerror.h) - public const uint PS_ERROR_BUFFER_TOO_LARGE = 1783; // Error code 1783 seems to appear for too large buffer (2560 string characters) - public const uint ERROR_NO_SUCH_LOGON_SESSION = 1312; - public const uint ERROR_INVALID_PARAMETER = 87; - public const uint ERROR_INVALID_FLAGS = 1004; - public const uint ERROR_BAD_USERNAME = 2202; - public const uint ERROR_NOT_FOUND = 1168; - public const uint SCARD_E_NO_READERS_AVAILABLE = 0x8010002E; - public const uint SCARD_E_NO_SMARTCARD = 0x8010000C; - public const uint SCARD_W_REMOVED_CARD = 0x80100069; - public const uint SCARD_W_WRONG_CHV = 0x8010006B; + DefaultTag = new Dictionary() + { + { "Tag", "PSItem" } + }; + } #endregion - #region Data structures - - [StructLayout(LayoutKind.Sequential)] - public class CREDENTIALA - { - /// - /// Specifies characteristics of the credential. - /// - public uint Flags; - - /// - /// Type of Credential. - /// - public uint Type; - - /// - /// Name of the credential. - /// - [MarshalAsAttribute(UnmanagedType.LPWStr)] - public string TargetName; - - /// - /// Comment string. - /// - [MarshalAsAttribute(UnmanagedType.LPWStr)] - public string Comment; - - /// - /// Last modification of credential. - /// - public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; - - /// - /// Size of credential blob in bytes. - /// - public uint CredentialBlobSize; - - /// - /// Secret data for credential. - /// - public IntPtr CredentialBlob; - - /// - /// Defines persistence of credential. - /// - public uint Persist; - - /// - /// Number of attributes associated with this credential. - /// - public uint AttributeCount; - - /// - /// Application defined attributes for credential. - /// - public IntPtr Attributes; - - /// - /// Alias for the target name (max size 256 characters). - /// - [MarshalAsAttribute(UnmanagedType.LPWStr)] - public string TargetAlias; - - /// - /// User name of account for TargetName (max size 513 characters). - /// - [MarshalAsAttribute(UnmanagedType.LPWStr)] - public string UserName; - } + #region Public static - #endregion + public static LocalSecretStore Instance + { + get + { + if (LocalStore == null) + { + lock (SyncObject) + { + if (LocalStore == null) + { + // TODO: Remove after password cmdlets and prompt for password is added. + var results = PowerShellInvoker.InvokeScript( + script: @"param([string] $value) ConvertTo-SecureString -String $value -AsPlainText -Force", + args: new object[] { "hello" }, + out ErrorRecord[] errors); + var password = (results.Count > 0) ? results[0] : null; + + LocalStore = new LocalSecretStore( + SecureStore.GetStore(password)); + } + } + } - #region Methods + return LocalStore; + } + } - [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CredWriteW( - IntPtr Credential, - uint Flags); - - [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CredReadW( - [InAttribute()] - [MarshalAsAttribute(UnmanagedType.LPWStr)] - string TargetName, - int Type, - int Flags, - out IntPtr Credential); - - [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CredDeleteW( - [InAttribute()] - [MarshalAsAttribute(UnmanagedType.LPWStr)] - string TargetName, - int Type, - int Flags); - - [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CredEnumerateW( - [InAttribute()] - [MarshalAsAttribute(UnmanagedType.LPWStr)] - string Filter, - int Flags, - out int Count, - out IntPtr Credentials); - - [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CredFree( - IntPtr Buffer); + public static void SetPassword( + SecureString password) + { + if (LocalStore == null) + { + lock (SyncObject) + { + if (LocalStore == null) + { + LocalStore = new LocalSecretStore( + SecureStore.GetStore(password)); + return; + } + } + } + + LocalStore._secureStore.SetPassword(password); + } #endregion - } - /// - /// Default local secret store - /// - internal class LocalSecretStore : BaseLocalSecretStore - { - #region Public method overrides + #region Public methods - public override bool WriteObject( + public bool WriteObject( string name, T objectToWrite, - ref int errorCode) + ref string errorMsg) { return WriteObjectImpl( PrependTag(name), objectToWrite, - ref errorCode); + ref errorMsg); } - private static bool WriteObjectImpl( + private bool WriteObjectImpl( string name, T objectToWrite, - ref int errorCode) + ref string errorMsg) { switch (objectToWrite) { @@ -330,58 +1571,58 @@ private static bool WriteObjectImpl( name, blobToWrite, ByteArrayType, - ref errorCode); + ref errorMsg); case string stringToWrite: return WriteString( name, stringToWrite, - ref errorCode); + ref errorMsg); case SecureString secureStringToWrite: return WriteSecureString( name, secureStringToWrite, - ref errorCode); + ref errorMsg); case PSCredential credentialToWrite: return WritePSCredential( name, credentialToWrite, - ref errorCode); + ref errorMsg); case Hashtable hashtableToWrite: return WriteHashtable( name, hashtableToWrite, - ref errorCode); + ref errorMsg); default: throw new InvalidOperationException("Invalid type. Types supported: byte[], string, SecureString, PSCredential, Hashtable"); } } - public override bool ReadObject( + public bool ReadObject( string name, out object outObject, - ref int errorCode) + ref string errorMsg) { return ReadObjectImpl( PrependTag(name), out outObject, - ref errorCode); + ref errorMsg); } - private static bool ReadObjectImpl( + private bool ReadObjectImpl( string name, out object outObject, - ref int errorCode) + ref string errorMsg) { if (!ReadBlob( name, out byte[] outBlob, out string typeName, - ref errorCode)) + ref errorMsg)) { outObject = null; return false; @@ -413,22 +1654,22 @@ private static bool ReadObjectImpl( name, outBlob, out outObject, - ref errorCode); + ref errorMsg); default: throw new InvalidOperationException("Invalid type. Types supported: byte[], string, SecureString, PSCredential, Hashtable"); } } - public override bool EnumerateObjectInfo( + public bool EnumerateObjectInfo( string filter, out SecretInformation[] outSecretInfo, - ref int errorCode) + ref string errorMsg) { if (!EnumerateBlobs( PrependTag(filter), out EnumeratedBlob[] outBlobs, - ref errorCode)) + ref errorMsg)) { outSecretInfo = null; return false; @@ -479,24 +1720,21 @@ public override bool EnumerateObjectInfo( vaultName: RegisterSecretVaultCommand.BuiltInLocalVault)); break; } - - // Delete local copy of blob. - ZeroOutData(item.Data); } outSecretInfo = outList.ToArray(); return true; } - public override bool DeleteObject( + public bool DeleteObject( string name, - ref int errorCode) + ref string errorMsg) { // Hash tables are complex and require special processing. if (!ReadObject( name, out object outObject, - ref errorCode)) + ref errorMsg)) { return false; } @@ -508,51 +1746,12 @@ public override bool DeleteObject( case Hashtable hashtable: return DeleteHashtable( name, - ref errorCode); + ref errorMsg); default: return DeleteBlob( name, - ref errorCode); - } - } - - public override string GetErrorMessage(int errorCode) - { - switch ((uint)errorCode) - { - case NativeUtils.PS_ERROR_BUFFER_TOO_LARGE: - return nameof(NativeUtils.PS_ERROR_BUFFER_TOO_LARGE); - - case NativeUtils.ERROR_BAD_USERNAME: - return nameof(NativeUtils.ERROR_BAD_USERNAME); - - case NativeUtils.ERROR_INVALID_FLAGS: - return nameof(NativeUtils.ERROR_INVALID_FLAGS); - - case NativeUtils.ERROR_INVALID_PARAMETER: - return nameof(NativeUtils.ERROR_INVALID_PARAMETER); - - case NativeUtils.ERROR_NOT_FOUND: - return nameof(NativeUtils.ERROR_NOT_FOUND); - - case NativeUtils.ERROR_NO_SUCH_LOGON_SESSION: - return nameof(NativeUtils.ERROR_NO_SUCH_LOGON_SESSION); - - case NativeUtils.SCARD_E_NO_READERS_AVAILABLE: - return nameof(NativeUtils.SCARD_E_NO_READERS_AVAILABLE); - - case NativeUtils.SCARD_E_NO_SMARTCARD: - return nameof(NativeUtils.SCARD_E_NO_SMARTCARD); - - case NativeUtils.SCARD_W_REMOVED_CARD: - return nameof(NativeUtils.SCARD_W_REMOVED_CARD); - - case NativeUtils.SCARD_W_WRONG_CHV: - return nameof(NativeUtils.SCARD_W_WRONG_CHV); - - default: - return string.Format(CultureInfo.InvariantCulture, "Unknown error code: {0}", errorCode); + ref errorMsg); } } @@ -644,202 +1843,102 @@ private static bool GetDataFromSecureString( return false; } - private static void ZeroOutData(byte[] data) - { - for (int i = 0; i < data.Length; i++) - { - data[i] = 0; - } - } - #endregion #region Blob methods - private static bool WriteBlob( + private bool WriteBlob( string name, byte[] blob, string typeName, - ref int errorCode) + ref string errorMsg) { - bool success = false; - var blobHandle = GCHandle.Alloc(blob, GCHandleType.Pinned); - var credPtr = IntPtr.Zero; - - try - { - var credential = new NativeUtils.CREDENTIALA(); - credential.Type = (uint) NativeUtils.CRED_TYPE.GENERIC; - credential.TargetName = name; - credential.Comment = typeName; - credential.CredentialBlobSize = (uint) blob.Length; - credential.CredentialBlob = blobHandle.AddrOfPinnedObject(); - credential.Persist = (uint) NativeUtils.CRED_PERSIST.LOCAL_MACHINE; - - credPtr = Marshal.AllocHGlobal(Marshal.SizeOf(credential)); - Marshal.StructureToPtr(credential, credPtr, false); - - success = NativeUtils.CredWriteW( - Credential: credPtr, - Flags: 0); - - errorCode = Marshal.GetLastWin32Error(); - } - finally - { - blobHandle.Free(); - if (credPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(credPtr); - } - } - - return success; + return _secureStore.WriteBlob( + name: name, + blob: blob, + typeName: typeName, + attributes: DefaultTag, + errorMsg: ref errorMsg); } - private static bool ReadBlob( + private bool ReadBlob( string name, out byte[] blob, out string typeName, - ref int errorCode) - { - blob = null; - typeName = null; - var success = false; - - // Read Credential structure from vault given provided name. - IntPtr credPtr = IntPtr.Zero; - try - { - success = NativeUtils.CredReadW( - TargetName: name, - Type: (int) NativeUtils.CRED_TYPE.GENERIC, - Flags: 0, - Credential: out credPtr); - - errorCode = Marshal.GetLastWin32Error(); - - if (success) - { - // Copy returned credential to managed memory. - var credential = Marshal.PtrToStructure(credPtr); - typeName = credential.Comment; - - // Copy returned blob from credential structure. - var ansiString = Marshal.PtrToStringAnsi( - ptr: credential.CredentialBlob, - len: (int) credential.CredentialBlobSize); - blob = Encoding.ASCII.GetBytes(ansiString); - } - } - finally + ref string errorMsg) + { + if (!_secureStore.ReadBlob( + name: name, + blob: out blob, + metaData: out SecureStoreMetadata metadata, + errorMsg: ref errorMsg)) { - if (credPtr != IntPtr.Zero) - { - NativeUtils.CredFree(credPtr); - } + typeName = null; + return false; } - - return success; + + typeName = metadata.TypeName; + return true; } private struct EnumeratedBlob { public string Name; public string TypeName; - public byte[] Data; } - private static bool EnumerateBlobs( + private bool EnumerateBlobs( string filter, out EnumeratedBlob[] blobs, - ref int errorCode) + ref string errorMsg) { - blobs = null; - var success = false; - - int count = 0; - IntPtr credPtrPtr = IntPtr.Zero; - try + if (!_secureStore.EnumerateBlobs( + filter: filter, + metaData: out SecureStoreMetadata[] metadata, + ref errorMsg)) { - success = NativeUtils.CredEnumerateW( - Filter: filter, - Flags: 0, - Count: out count, - Credentials: out credPtrPtr); - - errorCode = Marshal.GetLastWin32Error(); - - if (success) - { - List blobArray = new List(count); - - // The returned credPtrPtr is an array of credential pointers. - for (int i=0; i(credPtr); - - if (credential.CredentialBlob != IntPtr.Zero) - { - // Copy returned blob from credential structure. - var ansiString = Marshal.PtrToStringAnsi( - ptr: credential.CredentialBlob, - len: (int) credential.CredentialBlobSize); - - blobArray.Add( - new EnumeratedBlob { - Name = credential.TargetName, - TypeName = credential.Comment, - Data = Encoding.ASCII.GetBytes(ansiString) - }); - } - } - - blobs = blobArray.ToArray(); - } + blobs = null; + return false; } - finally + + List blobArray = new List(metadata.Length); + foreach (var metaItem in metadata) { - if (credPtrPtr != IntPtr.Zero) - { - NativeUtils.CredFree(credPtrPtr); - } + blobArray.Add( + new EnumeratedBlob + { + Name = metaItem.Name, + TypeName = metaItem.TypeName + }); } - return success; + blobs = blobArray.ToArray(); + return true; } - private static bool DeleteBlob( + private bool DeleteBlob( string name, - ref int errorCode) + ref string errorMsg) { - var success = NativeUtils.CredDeleteW( - TargetName: name, - Type: (int) NativeUtils.CRED_TYPE.GENERIC, - Flags: 0); - - errorCode = Marshal.GetLastWin32Error(); - - return success; + return _secureStore.DeleteBlob( + name: name, + errorMsg: ref errorMsg); } #endregion #region String methods - private static bool WriteString( + private bool WriteString( string name, string strToWrite, - ref int errorCode) + ref string errorMsg) { return WriteBlob( name: name, blob: Encoding.UTF8.GetBytes(strToWrite), typeName: StringType, - errorCode: ref errorCode); + errorMsg: ref errorMsg); } private static bool ReadString( @@ -864,10 +1963,10 @@ private static bool ReadString( // ... // - private static bool WriteStringArray( + private bool WriteStringArray( string name, string[] strsToWrite, - ref int errorCode) + ref string errorMsg) { // Compute blob size int arrayCount = strsToWrite.Length; @@ -917,7 +2016,7 @@ private static bool WriteStringArray( name: name, blob: blob, typeName: HashtableType, - errorCode: ref errorCode); + errorMsg: ref errorMsg); } private static void ReadStringArray( @@ -945,10 +2044,10 @@ private static void ReadStringArray( #region SecureString methods - private static bool WriteSecureString( + private bool WriteSecureString( string name, SecureString strToWrite, - ref int errorCode) + ref string errorMsg) { if (GetDataFromSecureString( secureString: strToWrite, @@ -960,11 +2059,11 @@ private static bool WriteSecureString( name: name, blob: data, typeName: SecureStringType, - errorCode: ref errorCode); + errorMsg: ref errorMsg); } finally { - ZeroOutData(data); + CryptoUtils.ZeroOutData(data); } } @@ -987,7 +2086,7 @@ private static bool ReadSecureString( } finally { - ZeroOutData(ssBlob); + CryptoUtils.ZeroOutData(ssBlob); } outSecureString = null; @@ -1005,10 +2104,10 @@ private static bool ReadSecureString( // Contains Password SecureString bytes Length: ssData bytes // - private static bool WritePSCredential( + private bool WritePSCredential( string name, PSCredential credential, - ref int errorCode) + ref string errorMsg) { if (GetDataFromSecureString( secureString: credential.Password, @@ -1047,15 +2146,15 @@ private static bool WritePSCredential( name: name, blob: blob, typeName: PSCredentialType, - errorCode: ref errorCode); + errorMsg: ref errorMsg); } finally { - ZeroOutData(ssData); + CryptoUtils.ZeroOutData(ssData); if (blob != null) { - ZeroOutData(blob); + CryptoUtils.ZeroOutData(blob); } } } @@ -1094,11 +2193,11 @@ private static bool ReadPSCredential( } finally { - ZeroOutData(blob); + CryptoUtils.ZeroOutData(blob); if (ssData != null) { - ZeroOutData(ssData); + CryptoUtils.ZeroOutData(ssData); } } @@ -1124,10 +2223,10 @@ private static bool ReadPSCredential( // ... // - private static bool WriteHashtable( + private bool WriteHashtable( string name, Hashtable hashtable, - ref int errorCode) + ref string errorMsg) { // Impose size limit if (hashtable.Count > MaxHashtableItemCount) @@ -1174,7 +2273,7 @@ private static bool WriteHashtable( if (!WriteStringArray( name: name, strsToWrite: hashTableEntryNames.ToArray(), - errorCode: ref errorCode)) + errorMsg: ref errorMsg)) { return false; } @@ -1188,7 +2287,7 @@ private static bool WriteHashtable( success = WriteObjectImpl( name: entry.Key, objectToWrite: entry.Value, - errorCode: ref errorCode); + errorMsg: ref errorMsg); if (!success) { @@ -1204,27 +2303,27 @@ private static bool WriteHashtable( { // Roll back. // Remove any Hashtable secret that was written, ignore errors. - int error = 0; + string error = ""; foreach (var entry in entries) { DeleteBlob( name: entry.Key, - errorCode: ref error); + errorMsg: ref error); } // Remove the Hashtable member names. DeleteBlob( name: name, - ref error); + errorMsg: ref error); } } } - private static bool ReadHashtable( + private bool ReadHashtable( string name, byte[] blob, out object outHashtable, - ref int errorCode) + ref string errorMsg) { // Get array of Hashtable secret names. ReadStringArray( @@ -1238,7 +2337,7 @@ private static bool ReadHashtable( if (ReadObjectImpl( entryName, out object outObject, - ref errorCode)) + ref errorMsg)) { hashtable.Add( RecoverKeyname(entryName, name), @@ -1250,16 +2349,16 @@ private static bool ReadHashtable( return true; } - private static bool DeleteHashtable( + private bool DeleteHashtable( string name, - ref int errorCode) + ref string errorMsg) { // Get array of Hashtable secret names. if (!ReadBlob( name, out byte[] blob, out string typeName, - ref errorCode)) + ref errorMsg)) { return false; } @@ -1273,13 +2372,13 @@ private static bool DeleteHashtable( { DeleteBlob( name: entryName, - ref errorCode); + ref errorMsg); } // Delete the Hashtable secret names list. DeleteBlob( name: name, - ref errorCode); + ref errorMsg); return true; } @@ -1290,13 +2389,6 @@ private static bool DeleteHashtable( } #endregion -#else - #region Keyring - - // TODO: Implement via Gnome Keyring - - #endregion -#endif #region Enums @@ -2164,11 +3256,11 @@ private Hashtable GetAdditionalParams() { if (!string.IsNullOrEmpty(VaultParametersName)) { - int errorCode = 0; + string errorMsg = ""; if (LocalSecretStore.Instance.ReadObject( name: VaultParametersName, outObject: out object outObject, - ref errorCode)) + ref errorMsg)) { if (outObject is Hashtable hashtable) { @@ -2184,11 +3276,11 @@ private static IReadOnlyDictionary GetParamsFromStore(string par { if (!string.IsNullOrEmpty(paramsName)) { - int errorCode = 0; + string errorMsg = ""; if (LocalSecretStore.Instance.ReadObject( paramsName, out object outObject, - ref errorCode)) + ref errorMsg)) { var hashtable = outObject as Hashtable; var dictionary = new Dictionary(hashtable.Count); @@ -2217,51 +3309,8 @@ internal static class RegisteredVaultCache #region Strings - private const string ConvertJsonToHashtableScript = @" - param ( - [string] $json - ) - - function ConvertToHash - { - param ( - [pscustomobject] $object - ) - - $output = @{} - $object | Get-Member -MemberType NoteProperty | ForEach-Object { - $name = $_.Name - $value = $object.($name) - - if ($value -is [object[]]) - { - $array = @() - $value | ForEach-Object { - $array += (ConvertToHash $_) - } - $output.($name) = $array - } - elseif ($value -is [pscustomobject]) - { - $output.($name) = (ConvertToHash $value) - } - else - { - $output.($name) = $value - } - } - - $output - } - - $customObject = ConvertFrom-Json $json - return ConvertToHash $customObject - "; - - private static readonly string RegistryDirectoryPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + - @"\Microsoft\PowerShell\SecretVaultRegistry"; - - private static readonly string RegistryFilePath = RegistryDirectoryPath + @"\VaultInfo"; + private static readonly string RegistryDirectoryPath = Path.Combine(Utils.SecretManagementLocalPath, "SecretVaultRegistry"); + private static readonly string RegistryFilePath = Path.Combine(RegistryDirectoryPath, "VaultInfo"); #endregion @@ -2411,16 +3460,6 @@ private static void RefreshCache() } } - private static Hashtable ConvertJsonToHashtable(string json) - { - var results = PowerShellInvoker.InvokeScript( - script: ConvertJsonToHashtableScript, - args: new object[] { json }, - error: out Exception _); - - return results[0]; - } - /// /// Reads the current user secret vault registry information from file. /// @@ -2429,7 +3468,7 @@ private static Hashtable ConvertJsonToHashtable(string json) private static bool TryReadSecretVaultRegistry( out Hashtable vaultInfo) { - vaultInfo = new Hashtable(); + vaultInfo = null; if (!File.Exists(RegistryFilePath)) { @@ -2442,7 +3481,7 @@ private static bool TryReadSecretVaultRegistry( try { string jsonInfo = File.ReadAllText(RegistryFilePath); - vaultInfo = ConvertJsonToHashtable(jsonInfo); + vaultInfo = Utils.ConvertJsonToHashtable(jsonInfo); return true; } catch (IOException) @@ -2479,11 +3518,7 @@ private static void DeleteSecretVaultRegistryFile() /// Hashtable containing registered vault information. private static void WriteSecretVaultRegistry(Hashtable dataToWrite) { - var results = PowerShellInvoker.InvokeScript( - script: @"param ([hashtable] $dataToWrite) ConvertTo-Json $dataToWrite", - args: new object[] { dataToWrite }, - error: out Exception _); - string jsonInfo = results[0]; + string jsonInfo = Utils.ConvertHashtableToJson(dataToWrite); _allowAutoRefresh = false; try diff --git a/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml b/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml index 40bff93..da72d33 100644 --- a/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml +++ b/Modules/Microsoft.PowerShell.ThreadJob/.ci/release.yml @@ -26,9 +26,17 @@ jobs: displayName: 'Capture NuGet package' - task: NuGetCommand@2 - displayName: 'Push PSThreadJob artifacts to AzArtifactsFeed' + displayName: 'Push PSThreadJob artifacts to AzArtifactFeed' inputs: command: push packagesToPush: '$(System.ArtifactsDirectory)/nupkg/Microsoft.PowerShell.ThreadJob.*.nupkg' nuGetFeedType: external publishFeedCredentials: AzArtifactFeed + + - task: NuGetCommand@2 + displayName: 'Push PSThreadJob artifacts from AzArtifactFeed to PSGallery feed' + inputs: + command: push + packagesToPush: '$(System.ArtifactsDirectory)/nupkg/Microsoft.PowerShell.ThreadJob.*.nupkg' + nuGetFeedType: external + publishFeedCredentials: PHPowerShellGalleryFeed From 488d6e53424a570e94b90a79006a06f50aab6fb2 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 19 May 2020 12:01:58 -0700 Subject: [PATCH 04/19] Fix path name for x-plat --- Modules/Microsoft.PowerShell.SecretManagement/build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 b/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 index a3a0fc5..a6c65df 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/build.ps1 @@ -69,7 +69,7 @@ if ($env:TF_BUILD) { Write-Host "##$vstsCommandString" } -. $PSScriptRoot\dobuild.ps1 +. $PSScriptRoot/doBuild.ps1 if ($Clean -and (Test-Path $OutDirectory)) { From 13cd237763b1b406fe1d3cde5e0af598609a4dfe Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 20 May 2020 09:51:08 -0700 Subject: [PATCH 05/19] Add local store password cmdlets --- ...Microsoft.PowerShell.SecretManagement.psd1 | 3 +- .../src/code/SecretManagement.cs | 168 ++++++++-- .../src/code/Utils.cs | 317 +++++++++++++----- 3 files changed, 376 insertions(+), 112 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 index 4f28c73..208b42c 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 @@ -62,7 +62,8 @@ FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( - 'Register-SecretVault','Unregister-SecretVault','Get-SecretVault','Set-Secret','Remove-Secret','Get-Secret','Get-SecretInfo','Test-SecretVault') + 'Register-SecretVault','Unregister-SecretVault','Get-SecretVault','Set-Secret','Remove-Secret','Get-Secret','Get-SecretInfo','Test-SecretVault', + 'Set-LocalStorePassword','Update-LocalStorePassword') # Variables to export from this module VariablesToExport = '*' diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 52328cb..9f4d9f1 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -401,7 +401,7 @@ private void StoreVaultParameters( // Store parameters in built-in local secure vault. string errorMsg = ""; - if (!LocalSecretStore.Instance.WriteObject( + if (!LocalSecretStore.GetInstance(cmdlet: this).WriteObject( name: parametersName, parameters, ref errorMsg)) @@ -541,7 +541,7 @@ private void RemoveParamSecrets( if (!string.IsNullOrEmpty(parametersName)) { string errorMsg = ""; - if (!LocalSecretStore.Instance.DeleteObject(parametersName, ref errorMsg)) + if (!LocalSecretStore.GetInstance(cmdlet: this).DeleteObject(parametersName, ref errorMsg)) { var msg = string.Format(CultureInfo.InvariantCulture, "Removal of vault info script parameters {0} failed with error {1}", parametersName, errorMsg); @@ -763,7 +763,7 @@ private void SearchLocalStore(string name) { // Search through the built-in local vault. string errorMsg = ""; - if (LocalSecretStore.Instance.EnumerateObjectInfo( + if (LocalSecretStore.GetInstance(cmdlet: this).EnumerateObjectInfo( filter: Name, outSecretInfo: out SecretInformation[] outSecretInfo, errorMsg: ref errorMsg)) @@ -909,7 +909,7 @@ private void WriteSecret(object secret) { // Write a string secret type only if explicitly requested with the -AsPlainText // parameter switch. Otherwise return it as a SecureString type. - WriteObject(ConvertToSecureString(stringSecret)); + WriteObject(Utils.ConvertToSecureString(stringSecret)); return; } @@ -924,22 +924,6 @@ private void WriteSecret(object secret) WriteObject(secret); } - private SecureString ConvertToSecureString(string secret) - { - var results = InvokeCommand.InvokeScript( - script: @" - param ([string] $secret) - - ConvertTo-SecureString -String $secret -AsPlainText -Force - ", - useNewScope: false, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { secret }); - - return (results.Count == 1) ? results[0].BaseObject as SecureString : null; - } - private void WriteNotFoundError() { var msg = string.Format(CultureInfo.InvariantCulture, "The secret {0} was not found.", Name); @@ -954,7 +938,7 @@ private void WriteNotFoundError() private bool SearchLocalStore(string name) { string errorMsg = ""; - if (LocalSecretStore.Instance.ReadObject( + if (LocalSecretStore.GetInstance(cmdlet: this).ReadObject( name: name, outObject: out object outObject, ref errorMsg)) @@ -1083,7 +1067,7 @@ protected override void EndProcessing() string errorMsg = ""; if (NoClobber) { - if (LocalSecretStore.Instance.ReadObject( + if (LocalSecretStore.GetInstance(cmdlet: this).ReadObject( name: Name, out object _, ref errorMsg)) @@ -1098,7 +1082,7 @@ protected override void EndProcessing() } errorMsg = ""; - if (!LocalSecretStore.Instance.WriteObject( + if (!LocalSecretStore.GetInstance(cmdlet: this).WriteObject( name: Name, objectToWrite: secretToWrite, ref errorMsg)) @@ -1161,7 +1145,7 @@ protected override void ProcessRecord() { // Remove from local built-in default vault. string errorMsg = ""; - if (!LocalSecretStore.Instance.DeleteObject( + if (!LocalSecretStore.GetInstance(cmdlet: this).DeleteObject( name: Name, errorMsg: ref errorMsg)) { @@ -1243,4 +1227,140 @@ protected override void EndProcessing() } #endregion + + #region Local store cmdlets + + #region Set-LocalStorePassword + + /// + /// Sets the local store password for the current session. + /// Password will remain in effect for the session until the timeout expires. + /// The password timeout is set in the local store configuration. + /// + [Cmdlet(VerbsCommon.Set, "LocalStorePassword", + DefaultParameterSetName = SecureStringParameterSet)] + public sealed class SetLocalStorePasswordCommand : PSCmdlet + { + #region Members + + private const string StringParameterSet = "StringParameterSet"; + private const string SecureStringParameterSet = "SecureStringParameterSet"; + + #endregion + + #region Parameters + + /// + /// Gets or sets a plain text password. + /// + [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=StringParameterSet)] + public string Password { get; set; } + + /// + /// Gets or sets a SecureString password. + /// + [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=SecureStringParameterSet)] + public SecureString SecureStringPassword { get; set; } + + #endregion + + #region Overrides + + protected override void EndProcessing() + { + var passwordToSet = (ParameterSetName == StringParameterSet) ? Utils.ConvertToSecureString(Password) : SecureStringPassword; + LocalSecretStore.GetInstance(password: passwordToSet).SetPassword(passwordToSet); + } + + #endregion + + } + + #endregion + + #region Update-LocalStorePassword + + /// + /// Updates the local store password to the new password provided. + /// + [Cmdlet(VerbsData.Update, "LocalStorePassword", + DefaultParameterSetName = SecureStringParameterSet)] + public sealed class UpdateLocalStorePasswordCommand : PSCmdlet + { + #region Members + + private const string StringParameterSet = "StringParameterSet"; + private const string SecureStringParameterSet = "SecureStringParameterSet"; + + #endregion + + #region Parameters + + [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=StringParameterSet)] + public string NewPassword { get; set; } + + [Parameter(Position=1, Mandatory=true, ParameterSetName=StringParameterSet)] + public string OldPassword { get; set; } + + [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=SecureStringParameterSet)] + public SecureString NewSecureStringPassword { get; set; } + + [Parameter(Position=1, Mandatory=true, ParameterSetName=SecureStringParameterSet)] + public SecureString OldSecureStringPassword { get; set; } + + #endregion + + #region Overrides + + protected override void EndProcessing() + { + SecureString newPassword; + SecureString oldPassword; + if (ParameterSetName == StringParameterSet) + { + newPassword = Utils.ConvertToSecureString(NewPassword); + oldPassword = Utils.ConvertToSecureString(OldPassword); + } + else + { + newPassword = NewSecureStringPassword; + oldPassword = OldSecureStringPassword; + } + + var errorMsg = ""; + if (!LocalSecretStore.GetInstance(password: oldPassword).UpdatePassword( + newPassword, + oldPassword, + ref errorMsg)) + { + var msg = string.Format(CultureInfo.InvariantCulture, + "Cannot update local store password with error: {0}", + errorMsg); + WriteError( + new ErrorRecord( + new PSInvalidOperationException(msg), + "CannotUpdateLocalStorePassword", + ErrorCategory.InvalidOperation, + this)); + } + } + + #endregion + } + + #endregion + + #region Get-LocalStoreConfiguration + + + + #endregion + + #region Set-LocalStoreConfiguration + + + + #endregion + + #endregion } diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 7486b74..0181155 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -115,6 +115,146 @@ public static string ConvertHashtableToJson(Hashtable hashtable) return (results.Count > 0) ? results[0] : null; } + public static SecureString ConvertToSecureString(string secret) + { + var results = PowerShellInvoker.InvokeScript( + script: @"param([string] $value) ConvertTo-SecureString -String $value -AsPlainText -Force", + args: new object[] { secret }, + error: out Exception _); + + return (results.Count > 0) ? results[0] : null; + } + + public static bool GetSecureStringFromData( + byte[] data, + out SecureString outSecureString) + { + if ((data.Length % 2) != 0) + { + Dbg.Assert(false, "Blob length for SecureString secure must be even."); + outSecureString = null; + return false; + } + + outSecureString = new SecureString(); + var strLen = data.Length / 2; + for (int i=0; i < strLen; i++) + { + int index = (2 * i); + + var ch = (char)(data[index + 1] * 256 + data[index]); + outSecureString.AppendChar(ch); + } + + return true; + } + + public static bool GetDataFromSecureString( + SecureString secureString, + out byte[] data) + { + IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(secureString); + + if (ptr != IntPtr.Zero) + { + try + { + data = new byte[secureString.Length * 2]; + Marshal.Copy(ptr, data, 0, data.Length); + return true; + } + finally + { + Marshal.ZeroFreeCoTaskMemUnicode(ptr); + } + } + + data = null; + return false; + } + + private static bool ComparePasswords( + SecureString password1, + SecureString password2) + { + byte[] data1 = null; + byte[] data2 = null; + var passwordEqual = false; + try + { + if (!GetDataFromSecureString( + password1, + out data1)) + { + return false; + } + + if (!GetDataFromSecureString( + password2, + out data2)) + { + return false; + } + + passwordEqual = (data1.Length == data2.Length); + if (passwordEqual) + { + for (int i=0; i + /// Sets the current session password, and resets the password timeout. + /// public void SetPassword( SecureString password, int timeoutMilliseconds = -1) @@ -708,6 +864,39 @@ public void SetPassword( dueTime: passwordTimeout, period: Timeout.Infinite); } + + // Validate password + string errorMsg = ""; + bool success; + try + { + success = SecureStoreFile.ReadFile( + password: password, + out SecureStoreData _, + ref errorMsg); + } + catch (SecureStorePasswordException) + { + success = false; + } + if (!success) + { + throw new SecureStorePasswordException( + "The provided password for the local store is incorrect."); + } + } + + /// + /// Updates the store password to the new value provided. + /// Re-encrypts secret data and store file with new password. + /// + public bool UpdatePassword( + SecureString newpassword, + SecureString oldPassword, + ref string errorMsg) + { + // TODO: Implement. + throw new PSNotImplementedException(); } public bool WriteBlob( @@ -1498,50 +1687,36 @@ static LocalSecretStore() #region Public static - public static LocalSecretStore Instance + public static LocalSecretStore GetInstance( + SecureString password = null, + PSCmdlet cmdlet = null) { - get - { - if (LocalStore == null) - { - lock (SyncObject) - { - if (LocalStore == null) - { - // TODO: Remove after password cmdlets and prompt for password is added. - var results = PowerShellInvoker.InvokeScript( - script: @"param([string] $value) ConvertTo-SecureString -String $value -AsPlainText -Force", - args: new object[] { "hello" }, - out ErrorRecord[] errors); - var password = (results.Count > 0) ? results[0] : null; - - LocalStore = new LocalSecretStore( - SecureStore.GetStore(password)); - } - } - } - return LocalStore; - } - } - - public static void SetPassword( - SecureString password) - { if (LocalStore == null) { lock (SyncObject) { if (LocalStore == null) { - LocalStore = new LocalSecretStore( - SecureStore.GetStore(password)); - return; + try + { + LocalStore = new LocalSecretStore( + SecureStore.GetStore(password)); + } + catch (SecureStorePasswordException) + { + if (cmdlet != null) + { + password = Utils.PromptForPassword(cmdlet); + LocalStore = new LocalSecretStore( + SecureStore.GetStore(password)); + } + } } } } - LocalStore._secureStore.SetPassword(password); + return LocalStore; } #endregion @@ -1755,6 +1930,22 @@ public bool DeleteObject( } } + public void SetPassword(SecureString password) + { + _secureStore.SetPassword(password); + } + + public bool UpdatePassword( + SecureString newPassword, + SecureString oldPassword, + ref string errorMsg) + { + return _secureStore.UpdatePassword( + newPassword, + oldPassword, + ref errorMsg); + } + #endregion #region Private methods @@ -1795,54 +1986,6 @@ private static string RecoverKeyname( return str.Substring((PSHashtableTag + hashName).Length); } - private static bool GetSecureStringFromData( - byte[] data, - out SecureString outSecureString) - { - if ((data.Length % 2) != 0) - { - Dbg.Assert(false, "Blob length for SecureString secure must be even."); - outSecureString = null; - return false; - } - - outSecureString = new SecureString(); - var strLen = data.Length / 2; - for (int i=0; i < strLen; i++) - { - int index = (2 * i); - - var ch = (char)(data[index + 1] * 256 + data[index]); - outSecureString.AppendChar(ch); - } - - return true; - } - - private static bool GetDataFromSecureString( - SecureString secureString, - out byte[] data) - { - IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(secureString); - - if (ptr != IntPtr.Zero) - { - try - { - data = new byte[secureString.Length * 2]; - Marshal.Copy(ptr, data, 0, data.Length); - return true; - } - finally - { - Marshal.ZeroFreeCoTaskMemUnicode(ptr); - } - } - - data = null; - return false; - } - #endregion #region Blob methods @@ -2049,7 +2192,7 @@ private bool WriteSecureString( SecureString strToWrite, ref string errorMsg) { - if (GetDataFromSecureString( + if (Utils.GetDataFromSecureString( secureString: strToWrite, data: out byte[] data)) { @@ -2076,7 +2219,7 @@ private static bool ReadSecureString( { try { - if (GetSecureStringFromData( + if (Utils.GetSecureStringFromData( data: ssBlob, outSecureString: out SecureString outString)) { @@ -2109,7 +2252,7 @@ private bool WritePSCredential( PSCredential credential, ref string errorMsg) { - if (GetDataFromSecureString( + if (Utils.GetDataFromSecureString( secureString: credential.Password, data: out byte[] ssData)) { @@ -2183,7 +2326,7 @@ private static bool ReadPSCredential( ssData[index++] = blob[i]; } - if (GetSecureStringFromData( + if (Utils.GetSecureStringFromData( ssData, out SecureString secureString)) { @@ -3257,7 +3400,7 @@ private Hashtable GetAdditionalParams() if (!string.IsNullOrEmpty(VaultParametersName)) { string errorMsg = ""; - if (LocalSecretStore.Instance.ReadObject( + if (LocalSecretStore.GetInstance().ReadObject( name: VaultParametersName, outObject: out object outObject, ref errorMsg)) @@ -3277,7 +3420,7 @@ private static IReadOnlyDictionary GetParamsFromStore(string par if (!string.IsNullOrEmpty(paramsName)) { string errorMsg = ""; - if (LocalSecretStore.Instance.ReadObject( + if (LocalSecretStore.GetInstance().ReadObject( paramsName, out object outObject, ref errorMsg)) From 2b943e60811dc213f0ac45e1386d6ec315e0a3b5 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 22 May 2020 14:05:27 -0700 Subject: [PATCH 06/19] Add password management --- ...Microsoft.PowerShell.SecretManagement.psd1 | 2 +- .../src/code/SecretManagement.cs | 70 ++-- .../src/code/Utils.cs | 364 +++++++++++++----- 3 files changed, 309 insertions(+), 127 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 index 208b42c..dab5706 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 @@ -63,7 +63,7 @@ FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( 'Register-SecretVault','Unregister-SecretVault','Get-SecretVault','Set-Secret','Remove-Secret','Get-Secret','Get-SecretInfo','Test-SecretVault', - 'Set-LocalStorePassword','Update-LocalStorePassword') + 'Unlock-LocalStore','Update-LocalStorePassword') # Variables to export from this module VariablesToExport = '*' diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 9f4d9f1..539eac7 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1230,16 +1230,16 @@ protected override void EndProcessing() #region Local store cmdlets - #region Set-LocalStorePassword + #region Unlock-LocalStore /// /// Sets the local store password for the current session. /// Password will remain in effect for the session until the timeout expires. /// The password timeout is set in the local store configuration. /// - [Cmdlet(VerbsCommon.Set, "LocalStorePassword", + [Cmdlet(VerbsCommon.Unlock, "LocalStore", DefaultParameterSetName = SecureStringParameterSet)] - public sealed class SetLocalStorePasswordCommand : PSCmdlet + public sealed class UnlockLocalStoreCommand : PSCmdlet { #region Members @@ -1269,11 +1269,10 @@ public sealed class SetLocalStorePasswordCommand : PSCmdlet protected override void EndProcessing() { var passwordToSet = (ParameterSetName == StringParameterSet) ? Utils.ConvertToSecureString(Password) : SecureStringPassword; - LocalSecretStore.GetInstance(password: passwordToSet).SetPassword(passwordToSet); + LocalSecretStore.GetInstance(password: passwordToSet).UnlockLocalStore(passwordToSet); } #endregion - } #endregion @@ -1284,11 +1283,12 @@ protected override void EndProcessing() /// Updates the local store password to the new password provided. /// [Cmdlet(VerbsData.Update, "LocalStorePassword", - DefaultParameterSetName = SecureStringParameterSet)] + DefaultParameterSetName = NoParameterSet)] public sealed class UpdateLocalStorePasswordCommand : PSCmdlet { #region Members + private const string NoParameterSet = "NoParameterSet"; private const string StringParameterSet = "StringParameterSet"; private const string SecureStringParameterSet = "SecureStringParameterSet"; @@ -1296,6 +1296,9 @@ public sealed class UpdateLocalStorePasswordCommand : PSCmdlet #region Parameters + [Parameter(ParameterSetName=NoParameterSet)] + public SwitchParameter PromptForPassword { get; set; } = true; + [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=StringParameterSet)] public string NewPassword { get; set; } @@ -1316,33 +1319,35 @@ protected override void EndProcessing() { SecureString newPassword; SecureString oldPassword; - if (ParameterSetName == StringParameterSet) - { - newPassword = Utils.ConvertToSecureString(NewPassword); - oldPassword = Utils.ConvertToSecureString(OldPassword); - } - else + switch (ParameterSetName) { - newPassword = NewSecureStringPassword; - oldPassword = OldSecureStringPassword; + case StringParameterSet: + newPassword = Utils.ConvertToSecureString(NewPassword); + oldPassword = Utils.ConvertToSecureString(OldPassword); + break; + + case SecureStringParameterSet: + newPassword = NewSecureStringPassword; + oldPassword = OldSecureStringPassword; + break; + + default: + // NoParameterSet + if (!PromptForPassword) { return; } + oldPassword = Utils.PromptForPassword( + cmdlet: this, + verifyPassword: false, + message: "Old password"); + newPassword = Utils.PromptForPassword( + cmdlet: this, + verifyPassword: true, + message: "New password"); + break; } - var errorMsg = ""; - if (!LocalSecretStore.GetInstance(password: oldPassword).UpdatePassword( + LocalSecretStore.GetInstance(password: oldPassword).UpdatePassword( newPassword, - oldPassword, - ref errorMsg)) - { - var msg = string.Format(CultureInfo.InvariantCulture, - "Cannot update local store password with error: {0}", - errorMsg); - WriteError( - new ErrorRecord( - new PSInvalidOperationException(msg), - "CannotUpdateLocalStorePassword", - ErrorCategory.InvalidOperation, - this)); - } + oldPassword); } #endregion @@ -1360,6 +1365,13 @@ protected override void EndProcessing() + #endregion + + #region Reset-LocalStore + + + + #endregion #endregion diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 0181155..3ca5572 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -87,40 +87,40 @@ public static string SecretManagementLocalPath public static Hashtable ConvertJsonToHashtable(string json) { - var results = PowerShellInvoker.InvokeScript( + var results = PowerShellInvoker.InvokeScriptCommon( script: ConvertJsonToHashtableScript, args: new object[] { json }, - error: out Exception _); + error: out ErrorRecord _); return (results.Count > 0) ? results[0] : null; } public static PSObject ConvertJsonToPSObject(string json) { - var results = PowerShellInvoker.InvokeScript( + var results = PowerShellInvoker.InvokeScriptCommon( script: @"param ([string] $json) ConvertFrom-Json -InputObject $json -Depth 5", args: new object[] { json }, - error: out Exception _); + error: out ErrorRecord _); return (results.Count > 0) ? results[0] : null; } public static string ConvertHashtableToJson(Hashtable hashtable) { - var results = PowerShellInvoker.InvokeScript( + var results = PowerShellInvoker.InvokeScriptCommon( script: @"param ([hashtable] $hashtable) ConvertTo-Json -InputObject $hashtable -Depth 5", args: new object[] { hashtable }, - error: out Exception _); + error: out ErrorRecord _); return (results.Count > 0) ? results[0] : null; } public static SecureString ConvertToSecureString(string secret) { - var results = PowerShellInvoker.InvokeScript( + var results = PowerShellInvoker.InvokeScriptCommon( script: @"param([string] $value) ConvertTo-SecureString -String $value -AsPlainText -Force", args: new object[] { secret }, - error: out Exception _); + error: out ErrorRecord _); return (results.Count > 0) ? results[0] : null; } @@ -226,30 +226,37 @@ private static bool ComparePasswords( } public static SecureString PromptForPassword( - PSCmdlet cmdlet) + PSCmdlet cmdlet, + bool verifyPassword = false, + string message = null) { SecureString password = null; - var isVerified = false; - cmdlet.WriteObject("A password is required for Secret Management module local store"); + cmdlet.WriteObject( + string.IsNullOrEmpty(message) ? + "A password is required for Secret Management module local store" + : message); + var isVerified = !verifyPassword; do { // Initial prompt cmdlet.WriteObject("Enter password:"); password = cmdlet.Host.UI.ReadLineAsSecureString(); - // Verification prompt - cmdlet.WriteObject("Enter password again for verification:"); - var passwordVerified = cmdlet.Host.UI.ReadLineAsSecureString(); + if (verifyPassword) + { + // Verification prompt + cmdlet.WriteObject("Enter password again for verification:"); + var passwordVerified = cmdlet.Host.UI.ReadLineAsSecureString(); - isVerified = ComparePasswords(password, passwordVerified); + isVerified = ComparePasswords(password, passwordVerified); - if (!isVerified) - { - cmdlet.WriteObject("\nThe two entered passwords do not match. Please re-enter the passwords.\n"); + if (!isVerified) + { + cmdlet.WriteObject("\nThe two entered passwords do not match. Please re-enter the passwords.\n"); + } } - } while (!isVerified); return password; @@ -326,7 +333,6 @@ public static byte[] DecryptWithKey( } catch (CryptographicException) { - // TODO: Think about whether this is the right user experience. throw new SecureStorePasswordException(); } } @@ -498,7 +504,7 @@ public static SecureStoreConfig GetDefault() return new SecureStoreConfig( scope: SecureStoreScope.Local, passwordRequired: true, - passwordTimeout: -1, + passwordTimeout: 900000, // 15 minute timeout doNotPrompt: false); } @@ -751,15 +757,16 @@ public SecureStorePasswordException(string msg) } // TODO: - // a. Add support for SM local store password prompt - // b. Add support for SM local store configuration (password) - // c. Add auto update - // d. Add support for SM local store configuration (scope) + // b. Add auto update + // c. Add support for SM local store configuration (password) + // d. Think about adding file back-up support // e. Add Disposed check (?) // f. Add local store only tests // g. Create AzKeyVault extension vault for demo - // h. [DONE] Integrate into LocalStore - // i. [DONE] Test with current tests + // h. Add support for SM local store configuration (scope) + // a. [DONE] Add support for SM local store password prompt + // i. [DONE] Integrate into LocalStore + // j. [DONE] Test with current tests internal sealed class SecureStore : IDisposable { @@ -770,6 +777,7 @@ internal sealed class SecureStore : IDisposable private SecureStoreConfig _configData; private Timer _passwordTimer; private readonly object _syncObject = new object(); + private static TimeSpan _updateDelay = TimeSpan.FromSeconds(15); #endregion @@ -818,7 +826,9 @@ public SecureStore( { _data = data; _configData = configData; - _password = password; + SetPassword(password); + + SecureStoreFile.FileUpdated += (sender, args) => HandleFileUpdateEvent(sender, args); } #endregion @@ -839,10 +849,10 @@ public void Dispose() /// /// Sets the current session password, and resets the password timeout. /// - public void SetPassword( - SecureString password, - int timeoutMilliseconds = -1) + public void SetPassword(SecureString password) { + VerifyPasswordRequired(); + int passwordTimeout; lock (_syncObject) { @@ -864,39 +874,61 @@ public void SetPassword( dueTime: passwordTimeout, period: Timeout.Infinite); } - - // Validate password - string errorMsg = ""; - bool success; - try - { - success = SecureStoreFile.ReadFile( - password: password, - out SecureStoreData _, - ref errorMsg); - } - catch (SecureStorePasswordException) - { - success = false; - } - if (!success) - { - throw new SecureStorePasswordException( - "The provided password for the local store is incorrect."); - } } /// /// Updates the store password to the new value provided. /// Re-encrypts secret data and store file with new password. /// - public bool UpdatePassword( + public void UpdatePassword( SecureString newpassword, - SecureString oldPassword, - ref string errorMsg) + SecureString oldPassword) { - // TODO: Implement. - throw new PSNotImplementedException(); + VerifyPasswordRequired(); + + lock (_syncObject) + { + // Verify password. + var errorMsg = ""; + if (!SecureStoreFile.ReadFile( + oldPassword, + out SecureStoreData data, + ref errorMsg)) + { + throw new SecureStorePasswordException("Unable to access local store with provided oldPassword."); + } + + // Re-encrypt blob data with new password. + var newBlob = ReEncryptBlob( + newPassword: newpassword, + oldPassword: oldPassword, + metaData: data.MetaData, + key: data.Key, + blob: data.Blob, + outMetaData: out Dictionary newMetaData); + + // Write data to file with new password. + var newData = new SecureStoreData() + { + Key = data.Key, + Blob = newBlob, + MetaData = newMetaData + }; + + if (!SecureStoreFile.WriteFile( + password: newpassword, + data: newData, + errorMsg: ref errorMsg)) + { + throw new PSInvalidOperationException( + string.Format(CultureInfo.InvariantCulture, + @"Unable to update password with error: {0}", + errorMsg)); + } + + _data = newData; + SetPassword(newpassword); + } } public bool WriteBlob( @@ -1082,28 +1114,102 @@ public bool UpdateConfigData( // b. ??? } - public bool UpdateFromFile(ref string errorMsg) + public void UpdateFromFile() { + var errorMsg = ""; if (!SecureStoreFile.ReadFile( password: Password, data: out SecureStoreData data, ref errorMsg)) { - return false; + throw new PSInvalidOperationException(errorMsg); } lock (_syncObject) { _data = data; } - - return true; } #endregion #region Private methods + private void HandleFileUpdateEvent(object sender, FileUpdateEventArgs args) + { + // This is a 'best effort' intent to keep the current session data in sync + // with external changes. + // Only update if the reported change is after the latest write from this session. + try + { + var fileChangeTime = System.IO.File.GetLastWriteTime(args.FileChangedArgs.FullPath); + if ((fileChangeTime - SecureStoreFile.LastWriteTime) > _updateDelay) + { + UpdateFromFile(); + } + } + catch + { + } + } + + private static byte[] ReEncryptBlob( + SecureString newPassword, + SecureString oldPassword, + Dictionary metaData, + byte[] key, + byte[] blob, + out Dictionary outMetaData) + { + if (blob.Length == 0) + { + outMetaData = metaData; + return blob; + } + + outMetaData = new Dictionary(metaData.Count, StringComparer.InvariantCultureIgnoreCase); + List newBlobArray = new List(blob.Length); + + int offset = 0; + foreach (var metaItem in metaData.Values) + { + var oldBlobItem = new byte[metaItem.Size]; + Buffer.BlockCopy(blob, metaItem.Offset, oldBlobItem, 0, metaItem.Size); + var decryptedBlobItem = CryptoUtils.DecryptWithKey( + passWord: oldPassword, + key: key, + data: oldBlobItem); + + byte[] newBlobItem; + try + { + newBlobItem = CryptoUtils.EncryptWithKey( + passWord: newPassword, + key: key, + data: decryptedBlobItem); + } + finally + { + CryptoUtils.ZeroOutData(decryptedBlobItem); + } + + outMetaData.Add( + key: metaItem.Name, + value: new SecureStoreMetadata( + name: metaItem.Name, + typeName: metaItem.TypeName, + offset: offset, + size: newBlobItem.Length, + attributes: metaItem.Attributes)); + + newBlobArray.AddRange(newBlobItem); + + offset += newBlobItem.Length; + } + + return newBlobArray.ToArray(); + } + private bool WriteBlobImpl( string name, byte[] blob, @@ -1192,6 +1298,15 @@ private bool ReplaceBlobImpl( } } + private void VerifyPasswordRequired() + { + if (!_configData.PasswordRequired) + { + throw new PSInvalidOperationException( + "The local store is not configured to use a password."); + } + } + #endregion #region Static methods @@ -1226,7 +1341,6 @@ public static SecureStore GetStore( if (SecureStoreFile.StoreFileExists()) { // This indicates a corrupted store configuration or inadvertent file deletion. - // TODO: Throw an error that explains the configuration must be set to correct // settings needed for store, or must re-create local store. throw new InvalidOperationException("Secure local store is in inconsistent state. TODO: Provide user instructions."); } @@ -1237,9 +1351,7 @@ public static SecureStore GetStore( configData, ref errorMsg)) { - throw new PSInvalidOperationException( - string.Format(CultureInfo.InvariantCulture, - @"Unable to write store configuration data to file with error: {0}", errorMsg)); + throw new PSInvalidOperationException(errorMsg); } } } @@ -1250,6 +1362,13 @@ public static SecureStore GetStore( throw new SecureStorePasswordException(); } + // Check password configuration consistency. + if ((password != null) && !configData.PasswordRequired) + { + throw new PSInvalidOperationException( + "The local store is not configured to use a password. First change the store configuration to require a password."); + } + // Read store from file. if (SecureStoreFile.ReadFile( password: password, @@ -1280,9 +1399,7 @@ public static SecureStore GetStore( return secureStore; } - throw new PSInvalidOperationException( - string.Format(CultureInfo.InvariantCulture, - @"Unable to read store data from file with error: {0}", errorMsg)); + throw new PSInvalidOperationException(errorMsg); } #endregion @@ -1297,7 +1414,8 @@ internal static class SecureStoreFile private static readonly string LocalConfigFilePath = Path.Combine(LocalStorePath, "storeconfig"); private static readonly FileSystemWatcher _storeFileWatcher; - private static bool _allowAutoUpdate; + private static DateTime _lastWriteTime; + private static object _syncObject; #endregion @@ -1311,14 +1429,13 @@ static SecureStoreFile() } _storeFileWatcher = new FileSystemWatcher(LocalStorePath); - _storeFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; - _storeFileWatcher.Filter = "LocalStore"; + _storeFileWatcher.NotifyFilter = NotifyFilters.LastWrite; + _storeFileWatcher.Filter = "storefile"; _storeFileWatcher.EnableRaisingEvents = true; - _storeFileWatcher.Changed += (sender, args) => { UpdateData(); }; - _storeFileWatcher.Created += (sender, args) => { UpdateData(); }; - _storeFileWatcher.Deleted += (sender, args) => { UpdateData(fileDeleted: true); }; + _storeFileWatcher.Changed += (sender, args) => { UpdateData(args); }; - _allowAutoUpdate = true; + _syncObject = new object(); + _lastWriteTime = DateTime.MinValue; } #endregion @@ -1336,6 +1453,21 @@ private static void RaiseFileUpdatedEvent(FileUpdateEventArgs args) #endregion + #region Properties + + public static DateTime LastWriteTime + { + get + { + lock (_syncObject) + { + return _lastWriteTime; + } + } + } + + #endregion + #region Public methods // File structure @@ -1356,8 +1488,6 @@ public static bool WriteFile( Exception exFail = null; do { - _allowAutoUpdate = false; - try { // Encrypt json meta data. @@ -1394,6 +1524,11 @@ public static bool WriteFile( fileStream.SetLength(fileStream.Position); } + lock (_syncObject) + { + _lastWriteTime = DateTime.Now; + } + return true; } } @@ -1408,10 +1543,6 @@ public static bool WriteFile( exFail = ex; break; } - finally - { - _allowAutoUpdate = true; - } System.Threading.Thread.Sleep(250); @@ -1603,13 +1734,9 @@ public static bool StoreFileExists() #region Private methods - private static void UpdateData(bool fileDeleted = false) + private static void UpdateData(FileSystemEventArgs args) { - if (_allowAutoUpdate) - { - RaiseFileUpdatedEvent( - new FileUpdateEventArgs(fileDeleted)); - } + RaiseFileUpdatedEvent(new FileUpdateEventArgs(args)); } #endregion @@ -1619,15 +1746,15 @@ private static void UpdateData(bool fileDeleted = false) internal sealed class FileUpdateEventArgs : EventArgs { - public bool FileDeleted + public FileSystemEventArgs FileChangedArgs { private set; get; } - public FileUpdateEventArgs(bool fileDeleted) + public FileUpdateEventArgs(FileSystemEventArgs args) { - FileDeleted = fileDeleted; + FileChangedArgs = args; } } @@ -1684,7 +1811,7 @@ static LocalSecretStore() } #endregion - + #region Public static public static LocalSecretStore GetInstance( @@ -1930,20 +2057,26 @@ public bool DeleteObject( } } - public void SetPassword(SecureString password) + public void UnlockLocalStore(SecureString password) { _secureStore.SetPassword(password); + try + { + _secureStore.UpdateFromFile(); + } + catch (SecureStorePasswordException) + { + throw new SecureStorePasswordException("Unable to unlock local store. Password is invalid."); + } } - public bool UpdatePassword( + public void UpdatePassword( SecureString newPassword, - SecureString oldPassword, - ref string errorMsg) + SecureString oldPassword) { - return _secureStore.UpdatePassword( + _secureStore.UpdatePassword( newPassword, - oldPassword, - ref errorMsg); + oldPassword); } #endregion @@ -3706,6 +3839,13 @@ private static void WriteSecretVaultRegistry(Hashtable dataToWrite) internal static class PowerShellInvoker { + #region Members + + private static System.Management.Automation.PowerShell _powershell = + System.Management.Automation.PowerShell.Create(RunspaceMode.NewRunspace); + + #endregion + #region Methods public static Collection InvokeScript( @@ -3715,6 +3855,7 @@ public static Collection InvokeScript( { using (var powerShell = System.Management.Automation.PowerShell.Create()) { + powerShell.Commands.Clear(); Collection results; try { @@ -3752,6 +3893,35 @@ public static Collection InvokeScript( return results; } + public static Collection InvokeScriptCommon( + string script, + object[] args, + out ErrorRecord error) + { + Collection results; + try + { + results = _powershell.AddScript(script).AddParameters(args).Invoke(); + error = (_powershell.Streams.Error.Count > 0) ? _powershell.Streams.Error[0] : null; + } + catch (Exception ex) + { + error = new ErrorRecord( + exception: ex, + errorId: "PowerShellInvokerInvalidOperation", + errorCategory: ErrorCategory.InvalidOperation, + targetObject: null); + results = new Collection(); + } + finally + { + _powershell.Commands.Clear(); + _powershell.Runspace.ResetRunspaceState(); + } + + return results; + } + #endregion } From d66b6fb52ca5ba7101a2f580f220333ace359ea3 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 22 May 2020 16:07:05 -0700 Subject: [PATCH 07/19] Add initial config cmdlets --- ...Microsoft.PowerShell.SecretManagement.psd1 | 2 +- .../src/code/SecretManagement.cs | 79 +++++++++++++++++++ .../src/code/Utils.cs | 46 +++++++++-- 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 index dab5706..568faaa 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 @@ -63,7 +63,7 @@ FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( 'Register-SecretVault','Unregister-SecretVault','Get-SecretVault','Set-Secret','Remove-Secret','Get-Secret','Get-SecretInfo','Test-SecretVault', - 'Unlock-LocalStore','Update-LocalStorePassword') + 'Unlock-LocalStore','Update-LocalStorePassword','Get-LocalStoreConfiguration','Set-LocalStoreConfiguration') # Variables to export from this module VariablesToExport = '*' diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 539eac7..22254a4 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1357,13 +1357,92 @@ protected override void EndProcessing() #region Get-LocalStoreConfiguration + [Cmdlet(VerbsCommon.Get, "LocalStoreConfiguration")] + public sealed class GetLocalStoreConfiguration : PSCmdlet + { + #region Overrides + protected override void EndProcessing() + { + WriteObject( + LocalSecretStore.GetInstance(cmdlet: this).Configuration); + } + + #endregion + } #endregion #region Set-LocalStoreConfiguration + [Cmdlet(VerbsCommon.Set, "LocalStoreConfiguration")] + public sealed class SetLocalStoreConfiguration : PSCmdlet + { + #region Parameters + + [Parameter(Position=0)] + public SecureStoreScope Scope { get; set; } = SecureStoreScope.Local; + + [Parameter(Position=1)] + public SwitchParameter RequirePassword { get; set; } = true; + + [Parameter(Position=2)] + [ValidateRange(10, (Int32.MaxValue / 1000))] + public int PasswordTimeoutSeconds { get; set; } = 90000; + + [Parameter(Position=3)] + public SwitchParameter DoNotPrompt { get; set; } = false; + #endregion + + #region Overrides + + protected override void EndProcessing() + { + if (Scope == SecureStoreScope.Machine) + { + ThrowTerminatingError( + new ErrorRecord( + exception: new PSNotSupportedException("Machine scope is not yet supported."), + errorId: "LocalStoreConfigurationNotSupported", + errorCategory: ErrorCategory.NotEnabled, + this)); + } + + bool requirePassword = (RequirePassword.IsPresent) ? (bool)RequirePassword : true; + int passwordTimeoutMs = PasswordTimeoutSeconds * 1000; + var oldConfigData = LocalSecretStore.GetInstance(cmdlet: this).Configuration; + var newConfigData = new SecureStoreConfig( + scope: Scope, + passwordRequired: requirePassword, + passwordTimeout: passwordTimeoutMs, + doNotPrompt: DoNotPrompt); + + var errorMsg = ""; + if (!LocalSecretStore.GetInstance(cmdlet: this).UpdateConfiguration( + newConfigData, + ref errorMsg)) + { + ThrowTerminatingError( + new ErrorRecord( + exception: new PSInvalidOperationException(errorMsg), + errorId: "LocalStoreConfigurationUpdateFailed", + errorCategory: ErrorCategory.InvalidOperation, + this)); + } + + if (oldConfigData.PasswordRequired != newConfigData.PasswordRequired) + { + // TODO: Invoke UpdateLocalStorePasswordCommand + // a. For passwordrequired == false, Set password to null and update + // b. For passwordrequired == true, Prompt for new password (and verify), and update + } + + WriteObject(newConfigData); + } + + #endregion + } #endregion diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 3ca5572..b7a1480 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -393,7 +393,7 @@ private static byte[] GetDataFromSecureString(SecureString secureString) #endregion } - internal enum SecureStoreScope + public enum SecureStoreScope { Local = 1, Machine @@ -1103,15 +1103,26 @@ public bool UpdateConfigData( SecureStoreConfig configData, ref string errorMsg) { + SecureStoreConfig oldConfigData; lock (_syncObject) { + oldConfigData = _configData; _configData = configData; - return true; } - // TODO: Implement configuration helper method(s) that will: - // a. Update blob data to re-encrypt with/without password - // b. ??? + if (!SecureStoreFile.WriteConfigFile( + configData, + ref errorMsg)) + { + lock(_syncObject) + { + _configData = oldConfigData; + } + + return false; + } + + return true; } public void UpdateFromFile() @@ -1788,6 +1799,22 @@ internal sealed class LocalSecretStore #endregion + #region Properties + + public SecureStoreConfig Configuration + { + get + { + return new SecureStoreConfig( + scope: _secureStore.ConfigData.Scope, + passwordRequired: _secureStore.ConfigData.PasswordRequired, + passwordTimeout: _secureStore.ConfigData.PasswordTimeout, + doNotPrompt: _secureStore.ConfigData.DoNotPrompt); + } + } + + #endregion + #region Constructor private LocalSecretStore() @@ -2079,6 +2106,15 @@ public void UpdatePassword( oldPassword); } + public bool UpdateConfiguration( + SecureStoreConfig newConfigData, + ref string errorMsg) + { + return _secureStore.UpdateConfigData( + newConfigData, + ref errorMsg); + } + #endregion #region Private methods From a34e09e36fddfdf64dcc064d1de187fa6aa7416a Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 26 May 2020 16:07:14 -0700 Subject: [PATCH 08/19] Add more configuration functions. --- ...Microsoft.PowerShell.SecretManagement.psd1 | 2 +- .../src/code/SecretManagement.cs | 89 ++++++-- .../src/code/Utils.cs | 201 ++++++++++++++++-- 3 files changed, 260 insertions(+), 32 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 index 568faaa..29e005b 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.psd1 @@ -63,7 +63,7 @@ FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( 'Register-SecretVault','Unregister-SecretVault','Get-SecretVault','Set-Secret','Remove-Secret','Get-Secret','Get-SecretInfo','Test-SecretVault', - 'Unlock-LocalStore','Update-LocalStorePassword','Get-LocalStoreConfiguration','Set-LocalStoreConfiguration') + 'Unlock-LocalStore','Update-LocalStorePassword','Get-LocalStoreConfiguration','Set-LocalStoreConfiguration','Reset-LocalStore') # Variables to export from this module VariablesToExport = '*' diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 22254a4..2f05855 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1387,7 +1387,7 @@ public sealed class SetLocalStoreConfiguration : PSCmdlet public SwitchParameter RequirePassword { get; set; } = true; [Parameter(Position=2)] - [ValidateRange(10, (Int32.MaxValue / 1000))] + [ValidateRange(-1, (Int32.MaxValue / 1000))] public int PasswordTimeoutSeconds { get; set; } = 90000; [Parameter(Position=3)] @@ -1409,18 +1409,17 @@ protected override void EndProcessing() this)); } - bool requirePassword = (RequirePassword.IsPresent) ? (bool)RequirePassword : true; - int passwordTimeoutMs = PasswordTimeoutSeconds * 1000; - var oldConfigData = LocalSecretStore.GetInstance(cmdlet: this).Configuration; + int passwordTimeoutMs = (PasswordTimeoutSeconds > 0) ? PasswordTimeoutSeconds * 1000 : PasswordTimeoutSeconds; var newConfigData = new SecureStoreConfig( scope: Scope, - passwordRequired: requirePassword, + passwordRequired: RequirePassword, passwordTimeout: passwordTimeoutMs, doNotPrompt: DoNotPrompt); var errorMsg = ""; if (!LocalSecretStore.GetInstance(cmdlet: this).UpdateConfiguration( - newConfigData, + newConfigData: newConfigData, + cmdlet: this, ref errorMsg)) { ThrowTerminatingError( @@ -1431,13 +1430,6 @@ protected override void EndProcessing() this)); } - if (oldConfigData.PasswordRequired != newConfigData.PasswordRequired) - { - // TODO: Invoke UpdateLocalStorePasswordCommand - // a. For passwordrequired == false, Set password to null and update - // b. For passwordrequired == true, Prompt for new password (and verify), and update - } - WriteObject(newConfigData); } @@ -1448,7 +1440,78 @@ protected override void EndProcessing() #region Reset-LocalStore + [Cmdlet(VerbsCommon.Reset, "LocalStore", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public sealed class ResetLocalStoreCommand : PSCmdlet + { + #region Parmeters + + [Parameter(Position=0)] + public SecureStoreScope Scope { get; set; } = SecureStoreScope.Local; + + [Parameter(Position=1)] + public SwitchParameter RequirePassword { get; set; } = true; + [Parameter(Position=2)] + [ValidateRange(-1, (Int32.MaxValue / 1000))] + public int PasswordTimeoutSeconds { get; set; } = 90000; + + [Parameter(Position=3)] + public SwitchParameter DoNotPrompt { get; set; } = false; + + [Parameter] + public SwitchParameter Force { get; set; } + + #endregion + + #region Overrides + + protected override void BeginProcessing() + { + WriteWarning("This operation will completely remove all SecretManagement module local store secrets, and make any registered vault inoperable."); + } + + protected override void EndProcessing() + { + if (Force || ShouldProcess( + target: "SecretManagement module local store", + action: "Erase all secrets in the local store and reset the configuration settings")) + { + int passwordTimeoutMs = (PasswordTimeoutSeconds > 0) ? PasswordTimeoutSeconds * 1000 : PasswordTimeoutSeconds; + var newConfig = new SecureStoreConfig( + scope: Scope, + passwordRequired: RequirePassword, + passwordTimeout: passwordTimeoutMs, + doNotPrompt: DoNotPrompt); + + var errorMsg = ""; + if (!SecureStoreFile.RemoveStoreFile(ref errorMsg)) + { + ThrowTerminatingError( + new ErrorRecord( + exception: new PSInvalidOperationException(errorMsg), + errorId: "ResetLocalStoreCannotRemoveStoreFile", + errorCategory: ErrorCategory.InvalidOperation, + targetObject: this)); + } + + if (!SecureStoreFile.WriteConfigFile( + configData: newConfig, + ref errorMsg)) + { + ThrowTerminatingError( + new ErrorRecord( + exception: new PSInvalidOperationException(errorMsg), + errorId: "ResetLocalStoreCannotWriteConfigFile", + errorCategory: ErrorCategory.InvalidOperation, + targetObject: this)); + } + + LocalSecretStore.Reset(); + } + } + + #endregion + } #endregion diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index b7a1480..6a010f6 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -757,14 +757,14 @@ public SecureStorePasswordException(string msg) } // TODO: - // b. Add auto update - // c. Add support for SM local store configuration (password) + // d. Think about adding file back-up support - // e. Add Disposed check (?) // f. Add local store only tests // g. Create AzKeyVault extension vault for demo - // h. Add support for SM local store configuration (scope) + // h. Add support for SM machine scope store configuration // a. [DONE] Add support for SM local store password prompt + // b. [DONE] Add auto update + // c. [DONE] Add support for SM local store configuration (password) // i. [DONE] Integrate into LocalStore // j. [DONE] Test with current tests @@ -851,7 +851,10 @@ public void Dispose() /// public void SetPassword(SecureString password) { - VerifyPasswordRequired(); + if (password != null) + { + VerifyPasswordRequired(); + } int passwordTimeout; lock (_syncObject) @@ -882,9 +885,13 @@ public void SetPassword(SecureString password) /// public void UpdatePassword( SecureString newpassword, - SecureString oldPassword) + SecureString oldPassword, + bool skipPasswordRequiredCheck) { - VerifyPasswordRequired(); + if (!skipPasswordRequiredCheck) + { + VerifyPasswordRequired(); + } lock (_syncObject) { @@ -1100,18 +1107,19 @@ public bool DeleteBlob( } public bool UpdateConfigData( - SecureStoreConfig configData, + SecureStoreConfig newConfigData, + PSCmdlet cmdlet, ref string errorMsg) { + // First update the configuration information. SecureStoreConfig oldConfigData; lock (_syncObject) { oldConfigData = _configData; - _configData = configData; + _configData = newConfigData; } - if (!SecureStoreFile.WriteConfigFile( - configData, + newConfigData, ref errorMsg)) { lock(_syncObject) @@ -1122,6 +1130,78 @@ public bool UpdateConfigData( return false; } + // If password requirement changed, then change password encryption as needed. + if (oldConfigData.PasswordRequired != newConfigData.PasswordRequired) + { + bool success; + try + { + SecureString oldPassword; + SecureString newPassword; + if (newConfigData.PasswordRequired) + { + // Prompt for new password + oldPassword = null; + newPassword = Utils.PromptForPassword( + cmdlet: cmdlet, + verifyPassword: true, + message: "A password is now required for the local store configuration.\nTo complete the change please provide new password."); + + if (newPassword == null) + { + throw new PSInvalidOperationException("New password was not provided."); + } + } + else + { + // Prompt for old password + newPassword = null; + oldPassword = Utils.PromptForPassword( + cmdlet: cmdlet, + verifyPassword: false, + message: "A password is no longer required for the local store configuration.\nTo complete the change please provide the current password."); + + if (oldPassword == null) + { + throw new PSInvalidOperationException("Old password was not provided."); + } + } + + UpdatePassword( + newPassword, + oldPassword, + skipPasswordRequiredCheck: true); + + success = true; + } + catch (Exception ex) + { + errorMsg = string.Format(CultureInfo.InvariantCulture, + @"Unable to update local store data from configuration change with error: {0}", + ex.Message); + success = false; + } + + if (!success) + { + // Attempt to revert back to original configuration. + lock(_syncObject) + { + _configData = oldConfigData; + } + + SecureStoreFile.WriteConfigFile( + oldConfigData, + ref errorMsg); + + return false; + } + } + + // Write out actions taken for user + // a. Password timeout and DoNotPrompt will be in effect after new session start. + // b. If password required change, then secrets will be re-encrypted per above. + return true; } @@ -1322,7 +1402,8 @@ private void VerifyPasswordRequired() #region Static methods - public static SecureStore GetDefault() + public static SecureStore GetDefault( + SecureStoreConfig configData) { var data = new SecureStoreData() { @@ -1333,7 +1414,7 @@ public static SecureStore GetDefault() return new SecureStore( data: data, - configData: SecureStoreConfig.GetDefault()); + configData: configData); } public static SecureStore GetStore( @@ -1395,7 +1476,7 @@ public static SecureStore GetStore( // If no file, create a default store if (errorMsg.Equals("NoFile", StringComparison.OrdinalIgnoreCase)) { - var secureStore = GetDefault(); + var secureStore = GetDefault(configData); if (!SecureStoreFile.WriteFile( password: password, data: secureStore.Data, @@ -1477,6 +1558,24 @@ public static DateTime LastWriteTime } } + public static bool ConfigAllowsPrompting + { + get + { + // Try to read the local store configuration file. + string errorMsg = ""; + if (ReadConfigFile( + configData: out SecureStoreConfig configData, + ref errorMsg)) + { + return !configData.DoNotPrompt; + } + + // Default behavior is to allow password prompting. + return true; + } + } + #endregion #region Public methods @@ -1736,6 +1835,41 @@ public static bool ReadConfigFile( return false; } + public static bool RemoveStoreFile(ref string errorMsg) + { + var count = 0; + Exception exFail = null; + do + { + try + { + File.Delete(LocalStoreFilePath); + return true; + } + catch (IOException exIO) + { + // Make up to four attempts. + exFail = exIO; + } + catch (Exception ex) + { + // Unexpected error. + exFail = ex; + break; + } + + System.Threading.Thread.Sleep(250); + + } while (++count < 4); + + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"Unable to remove the local store file with error: {0}", + exFail.Message); + + return false; + } + public static bool StoreFileExists() { return File.Exists(LocalStoreFilePath); @@ -1845,13 +1979,14 @@ public static LocalSecretStore GetInstance( SecureString password = null, PSCmdlet cmdlet = null) { - if (LocalStore == null) { lock (SyncObject) { if (LocalStore == null) { + bool storeFileExists = SecureStoreFile.StoreFileExists(); + try { LocalStore = new LocalSecretStore( @@ -1859,12 +1994,30 @@ public static LocalSecretStore GetInstance( } catch (SecureStorePasswordException) { - if (cmdlet != null) + if ((cmdlet != null) && SecureStoreFile.ConfigAllowsPrompting) { - password = Utils.PromptForPassword(cmdlet); + if (SecureStoreFile.StoreFileExists()) + { + // Prompt for existing local store file. + password = Utils.PromptForPassword(cmdlet); + } + else + { + // Prompt for creation of new store file. + password = Utils.PromptForPassword( + cmdlet: cmdlet, + verifyPassword: true, + message: "Creating new store file. A password is required by the current store configuration."); + } + LocalStore = new LocalSecretStore( SecureStore.GetStore(password)); + + return LocalStore; } + + // Cannot access store without password. + throw; } } } @@ -1873,6 +2026,14 @@ public static LocalSecretStore GetInstance( return LocalStore; } + public static void Reset() + { + lock (SyncObject) + { + LocalStore = null; + } + } + #endregion #region Public methods @@ -2087,6 +2248,7 @@ public bool DeleteObject( public void UnlockLocalStore(SecureString password) { _secureStore.SetPassword(password); + try { _secureStore.UpdateFromFile(); @@ -2103,15 +2265,18 @@ public void UpdatePassword( { _secureStore.UpdatePassword( newPassword, - oldPassword); + oldPassword, + skipPasswordRequiredCheck: false); } public bool UpdateConfiguration( SecureStoreConfig newConfigData, + PSCmdlet cmdlet, ref string errorMsg) { return _secureStore.UpdateConfigData( newConfigData, + cmdlet, ref errorMsg); } From cd63211c6df532e99dc819efb07744b56a839695 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 27 May 2020 16:28:22 -0700 Subject: [PATCH 09/19] Add more password management --- .../src/code/SecretManagement.cs | 17 +- .../src/code/Utils.cs | 202 +++++++++++++++--- ...soft.PowerShell.SecretManagement.Tests.ps1 | 4 + 3 files changed, 184 insertions(+), 39 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 2f05855..7071ea8 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -403,7 +403,8 @@ private void StoreVaultParameters( string errorMsg = ""; if (!LocalSecretStore.GetInstance(cmdlet: this).WriteObject( name: parametersName, - parameters, + objectToWrite: parameters, + cmdlet: this, ref errorMsg)) { var msg = string.Format( @@ -541,7 +542,10 @@ private void RemoveParamSecrets( if (!string.IsNullOrEmpty(parametersName)) { string errorMsg = ""; - if (!LocalSecretStore.GetInstance(cmdlet: this).DeleteObject(parametersName, ref errorMsg)) + if (!LocalSecretStore.GetInstance(cmdlet: this).DeleteObject( + name: parametersName, + cmdlet: this, + ref errorMsg)) { var msg = string.Format(CultureInfo.InvariantCulture, "Removal of vault info script parameters {0} failed with error {1}", parametersName, errorMsg); @@ -766,6 +770,7 @@ private void SearchLocalStore(string name) if (LocalSecretStore.GetInstance(cmdlet: this).EnumerateObjectInfo( filter: Name, outSecretInfo: out SecretInformation[] outSecretInfo, + cmdlet: this, errorMsg: ref errorMsg)) { WriteResults( @@ -941,6 +946,7 @@ private bool SearchLocalStore(string name) if (LocalSecretStore.GetInstance(cmdlet: this).ReadObject( name: name, outObject: out object outObject, + cmdlet: this, ref errorMsg)) { WriteSecret(outObject); @@ -1069,7 +1075,8 @@ protected override void EndProcessing() { if (LocalSecretStore.GetInstance(cmdlet: this).ReadObject( name: Name, - out object _, + outObject: out object _, + cmdlet: this, ref errorMsg)) { ThrowTerminatingError( @@ -1085,6 +1092,7 @@ protected override void EndProcessing() if (!LocalSecretStore.GetInstance(cmdlet: this).WriteObject( name: Name, objectToWrite: secretToWrite, + cmdlet: this, ref errorMsg)) { var msg = string.Format(CultureInfo.InvariantCulture, @@ -1147,6 +1155,7 @@ protected override void ProcessRecord() string errorMsg = ""; if (!LocalSecretStore.GetInstance(cmdlet: this).DeleteObject( name: Name, + cmdlet: this, errorMsg: ref errorMsg)) { var msg = string.Format(CultureInfo.InvariantCulture, @@ -1388,7 +1397,7 @@ public sealed class SetLocalStoreConfiguration : PSCmdlet [Parameter(Position=2)] [ValidateRange(-1, (Int32.MaxValue / 1000))] - public int PasswordTimeoutSeconds { get; set; } = 90000; + public int PasswordTimeoutSeconds { get; set; } = 900; [Parameter(Position=3)] public SwitchParameter DoNotPrompt { get; set; } = false; diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 6a010f6..ae7adc9 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -24,13 +24,8 @@ internal static class Utils { #region Members -#if UNIX - private static readonly string LocalLocation = Environment.GetEnvironmentVariable("HOME"); -#else - private static readonly string LocalLocation = Environment.GetEnvironmentVariable("USERPROFILE"); -#endif - private static readonly string SMLocalPath = Path.Combine(LocalLocation, ".secretmanagement"); - + private static readonly string LocalLocation; + private static readonly string SMLocalPath; private const string ConvertJsonToHashtableScript = @" param ( [string] $json @@ -74,6 +69,27 @@ function ConvertToHash #endregion + #region Constructor + + static Utils() + { + var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + System.Runtime.InteropServices.OSPlatform.Windows); + + if (isWindows) + { + LocalLocation = Environment.GetEnvironmentVariable("USERPROFILE"); + } + else + { + LocalLocation = Environment.GetEnvironmentVariable("HOME"); + } + + SMLocalPath = Path.Combine(LocalLocation, ".secretmanagement"); + } + + #endregion + #region Properties public static string SecretManagementLocalPath @@ -856,14 +872,25 @@ public void SetPassword(SecureString password) VerifyPasswordRequired(); } - int passwordTimeout; lock (_syncObject) { _password = password; - passwordTimeout = _configData.PasswordTimeout; + if (password != null) + { + SetPasswordTimer(_configData.PasswordTimeout); + } } + } - if (passwordTimeout > 0) + private void SetPasswordTimer(int timeoutMSec) + { + if (_passwordTimer != null) + { + _passwordTimer.Dispose(); + _passwordTimer = null; + } + + if (timeoutMSec > 0) { _passwordTimer = new Timer( callback: (_) => @@ -874,7 +901,7 @@ public void SetPassword(SecureString password) } }, state: null, - dueTime: passwordTimeout, + dueTime: timeoutMSec, period: Timeout.Infinite); } } @@ -1197,10 +1224,10 @@ public bool UpdateConfigData( return false; } } - - // Write out actions taken for user - // a. Password timeout and DoNotPrompt will be in effect after new session start. - // b. If password required change, then secrets will be re-encrypted per above. + else if ((oldConfigData.PasswordTimeout != newConfigData.PasswordTimeout) && (_password != null)) + { + SetPasswordTimer(newConfigData.PasswordTimeout); + } return true; } @@ -1912,7 +1939,7 @@ public FileUpdateEventArgs(FileSystemEventArgs args) /// /// Default local secret store /// - internal sealed class LocalSecretStore + internal sealed class LocalSecretStore : IDisposable { #region Members @@ -1973,6 +2000,18 @@ static LocalSecretStore() #endregion + #region IDisposable + + public void Dispose() + { + if (_secureStore != null) + { + _secureStore.Dispose(); + } + } + + #endregion + #region Public static public static LocalSecretStore GetInstance( @@ -2030,6 +2069,7 @@ public static void Reset() { lock (SyncObject) { + LocalStore.Dispose(); LocalStore = null; } } @@ -2041,12 +2081,32 @@ public static void Reset() public bool WriteObject( string name, T objectToWrite, + PSCmdlet cmdlet, ref string errorMsg) { - return WriteObjectImpl( - PrependTag(name), - objectToWrite, - ref errorMsg); + var count = 0; + do + { + try + { + return WriteObjectImpl( + PrependTag(name), + objectToWrite, + ref errorMsg); + } + catch (SecureStorePasswordException) + { + if (_secureStore.ConfigData.DoNotPrompt || cmdlet == null) + { + throw; + } + + _secureStore.SetPassword( + Utils.PromptForPassword(cmdlet: cmdlet)); + } + } while (count++ < 1); + + return false; } private bool WriteObjectImpl( @@ -2095,12 +2155,33 @@ private bool WriteObjectImpl( public bool ReadObject( string name, out object outObject, + PSCmdlet cmdlet, ref string errorMsg) { - return ReadObjectImpl( - PrependTag(name), - out outObject, - ref errorMsg); + var count = 0; + do + { + try + { + return ReadObjectImpl( + PrependTag(name), + out outObject, + ref errorMsg); + } + catch (SecureStorePasswordException) + { + if (_secureStore.ConfigData.DoNotPrompt || cmdlet == null) + { + throw; + } + + _secureStore.SetPassword( + Utils.PromptForPassword(cmdlet: cmdlet)); + } + } while (count++ < 1); + + outObject = null; + return false; } private bool ReadObjectImpl( @@ -2154,12 +2235,37 @@ private bool ReadObjectImpl( public bool EnumerateObjectInfo( string filter, out SecretInformation[] outSecretInfo, + PSCmdlet cmdlet, ref string errorMsg) { - if (!EnumerateBlobs( - PrependTag(filter), - out EnumeratedBlob[] outBlobs, - ref errorMsg)) + var count = 0; + EnumeratedBlob[] outBlobs = null; + do + { + try + { + if (!EnumerateBlobs( + PrependTag(filter), + out outBlobs, + ref errorMsg)) + { + outSecretInfo = null; + return false; + } + } + catch (SecureStorePasswordException) + { + if (_secureStore.ConfigData.DoNotPrompt || cmdlet == null) + { + throw; + } + + _secureStore.SetPassword( + Utils.PromptForPassword(cmdlet: cmdlet)); + } + } while (count++ < 1); + + if (outBlobs == null) { outSecretInfo = null; return false; @@ -2218,13 +2324,37 @@ public bool EnumerateObjectInfo( public bool DeleteObject( string name, + PSCmdlet cmdlet, ref string errorMsg) { - // Hash tables are complex and require special processing. - if (!ReadObject( - name, - out object outObject, - ref errorMsg)) + var count = 0; + object outObject = null; + do + { + try + { + if (!ReadObject( + name: name, + outObject: out outObject, + cmdlet: null, + ref errorMsg)) + { + return false; + } + } + catch (SecureStorePasswordException) + { + if (_secureStore.ConfigData.DoNotPrompt || cmdlet == null) + { + throw; + } + + _secureStore.SetPassword( + Utils.PromptForPassword(cmdlet: cmdlet)); + } + } while (count++ < 1); + + if (outObject == null) { return false; } @@ -3737,6 +3867,7 @@ private Hashtable GetAdditionalParams() if (LocalSecretStore.GetInstance().ReadObject( name: VaultParametersName, outObject: out object outObject, + cmdlet: null, ref errorMsg)) { if (outObject is Hashtable hashtable) @@ -3755,8 +3886,9 @@ private static IReadOnlyDictionary GetParamsFromStore(string par { string errorMsg = ""; if (LocalSecretStore.GetInstance().ReadObject( - paramsName, - out object outObject, + name: paramsName, + outObject: out object outObject, + cmdlet: null, ref errorMsg)) { var hashtable = outObject as Hashtable; diff --git a/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 b/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 index 5f092a9..0687df2 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 @@ -10,6 +10,10 @@ Describe "Test Microsoft.PowerShell.SecretManagement module" -tags CI { Import-Module -Name Microsoft.PowerShell.SecretManagement } + # Reset the local store and configure it for no-password access + # TODO: This deletes all local store data!! + Reset-LocalStore -Scope Local -RequirePassword:$false -PasswordTimeoutSeconds -1 -DoNotPrompt -Force + # Binary extension module $classImplementation = @' using Microsoft.PowerShell.SecretManagement; From 4336c90a33cb3703dea0a95b2e13555d538cfab8 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Thu, 28 May 2020 15:47:47 -0700 Subject: [PATCH 10/19] Add config upate event and made various fixes. --- .../src/code/SecretManagement.cs | 6 +- .../src/code/Utils.cs | 215 ++++++++++++++---- 2 files changed, 169 insertions(+), 52 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 7071ea8..c54715e 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1049,7 +1049,7 @@ protected override void EndProcessing() if (result != null) { var msg = string.Format(CultureInfo.InvariantCulture, - "A secret with name {0} already exists in vault {1}", Name, Vault); + "A secret with name {0} already exists in vault {1}.", Name, Vault); ThrowTerminatingError( new ErrorRecord( new PSInvalidOperationException(msg), @@ -1079,9 +1079,11 @@ protected override void EndProcessing() cmdlet: this, ref errorMsg)) { + var msg = string.Format(CultureInfo.InvariantCulture, + "A secret with name {0} already exists.", Name); ThrowTerminatingError( new ErrorRecord( - new PSInvalidOperationException(errorMsg), + new PSInvalidOperationException(msg), "AddSecretAlreadyExists", ErrorCategory.ResourceExists, this)); diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index ae7adc9..dcb657b 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -246,9 +246,15 @@ public static SecureString PromptForPassword( bool verifyPassword = false, string message = null) { + if (cmdlet.Host == null || cmdlet.Host.UI == null) + { + throw new PSInvalidOperationException( + "Cannot prompt for password. No host available."); + } + SecureString password = null; - cmdlet.WriteObject( + cmdlet.Host.UI.WriteLine( string.IsNullOrEmpty(message) ? "A password is required for Secret Management module local store" : message); @@ -257,20 +263,20 @@ public static SecureString PromptForPassword( do { // Initial prompt - cmdlet.WriteObject("Enter password:"); + cmdlet.Host.UI.WriteLine("Enter password:"); password = cmdlet.Host.UI.ReadLineAsSecureString(); if (verifyPassword) { // Verification prompt - cmdlet.WriteObject("Enter password again for verification:"); + cmdlet.Host.UI.WriteLine("Enter password again for verification:"); var passwordVerified = cmdlet.Host.UI.ReadLineAsSecureString(); isVerified = ComparePasswords(password, passwordVerified); if (!isVerified) { - cmdlet.WriteObject("\nThe two entered passwords do not match. Please re-enter the passwords.\n"); + cmdlet.Host.UI.WriteLine("\nThe two entered passwords do not match. Please re-enter the passwords.\n"); } } } while (!isVerified); @@ -690,6 +696,20 @@ public void Clear() #endregion + #region Static methods + + public static SecureStoreData CreateEmpty() + { + return new SecureStoreData() + { + Key = CryptoUtils.GenerateKey(), + Blob = new byte[0], + MetaData = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + }; + } + + #endregion + #region Private methods // Example meta data json @@ -772,18 +792,6 @@ public SecureStorePasswordException(string msg) #endregion } - // TODO: - - // d. Think about adding file back-up support - // f. Add local store only tests - // g. Create AzKeyVault extension vault for demo - // h. Add support for SM machine scope store configuration - // a. [DONE] Add support for SM local store password prompt - // b. [DONE] Add auto update - // c. [DONE] Add support for SM local store configuration (password) - // i. [DONE] Integrate into LocalStore - // j. [DONE] Test with current tests - internal sealed class SecureStore : IDisposable { #region Members @@ -793,7 +801,7 @@ internal sealed class SecureStore : IDisposable private SecureStoreConfig _configData; private Timer _passwordTimer; private readonly object _syncObject = new object(); - private static TimeSpan _updateDelay = TimeSpan.FromSeconds(15); + private static TimeSpan _updateDelay = TimeSpan.FromSeconds(5); #endregion @@ -844,7 +852,21 @@ public SecureStore( _configData = configData; SetPassword(password); - SecureStoreFile.FileUpdated += (sender, args) => HandleFileUpdateEvent(sender, args); + SecureStoreFile.DataUpdated += (sender, args) => HandleDataUpdateEvent(sender, args); + SecureStoreFile.ConfigUpdated += (sender, args) => HandleConfigUpdateEvent(sender, args); + } + + #endregion + + #region Events + + public event EventHandler StoreConfigUpdated; + private void RaiseStoreConfigUpdatedEvent() + { + if (StoreConfigUpdated != null) + { + StoreConfigUpdated.Invoke(this, null); + } } #endregion @@ -962,6 +984,12 @@ public void UpdatePassword( _data = newData; SetPassword(newpassword); + + // Password change is considered a configuration change. + // Induce a configuration change event by writing to the config file. + SecureStoreFile.WriteConfigFile( + configData: _configData, + ref errorMsg); } } @@ -1232,15 +1260,16 @@ public bool UpdateConfigData( return true; } - public void UpdateFromFile() + public void UpdateDataFromFile() { var errorMsg = ""; + SecureStoreData data; if (!SecureStoreFile.ReadFile( password: Password, - data: out SecureStoreData data, + data: out data, ref errorMsg)) { - throw new PSInvalidOperationException(errorMsg); + data = SecureStoreData.CreateEmpty(); } lock (_syncObject) @@ -1253,17 +1282,48 @@ public void UpdateFromFile() #region Private methods - private void HandleFileUpdateEvent(object sender, FileUpdateEventArgs args) + private void UpdateConfigFromFile() + { + var errorMsg = ""; + if (!SecureStoreFile.ReadConfigFile( + configData: out SecureStoreConfig configData, + ref errorMsg)) + { + throw new PSInvalidOperationException(errorMsg); + } + + lock (_syncObject) + { + _configData = configData; + } + + // Refresh secret data + UpdateDataFromFile(); + } + + private void HandleConfigUpdateEvent(object sender, FileUpdateEventArgs args) + { + try + { + if ((args.FileChangedTime - SecureStoreFile.LastWriteTime) > _updateDelay) + { + UpdateConfigFromFile(); + } + + RaiseStoreConfigUpdatedEvent(); + } + catch + { + } + } + + private void HandleDataUpdateEvent(object sender, FileUpdateEventArgs args) { - // This is a 'best effort' intent to keep the current session data in sync - // with external changes. - // Only update if the reported change is after the latest write from this session. try { - var fileChangeTime = System.IO.File.GetLastWriteTime(args.FileChangedArgs.FullPath); - if ((fileChangeTime - SecureStoreFile.LastWriteTime) > _updateDelay) + if ((args.FileChangedTime - SecureStoreFile.LastWriteTime) > _updateDelay) { - UpdateFromFile(); + UpdateDataFromFile(); } } catch @@ -1429,15 +1489,10 @@ private void VerifyPasswordRequired() #region Static methods - public static SecureStore GetDefault( + private static SecureStore GetDefault( SecureStoreConfig configData) { - var data = new SecureStoreData() - { - Key = CryptoUtils.GenerateKey(), - Blob = new byte[0], - MetaData = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - }; + var data = SecureStoreData.CreateEmpty(); return new SecureStore( data: data, @@ -1528,13 +1583,18 @@ internal static class SecureStoreFile { #region Members + private const string StoreFileName = "storefile"; + private const string StoreConfigName = "storeconfig"; + private static readonly string LocalStorePath = Path.Combine(Utils.SecretManagementLocalPath, ".localstore"); - private static readonly string LocalStoreFilePath = Path.Combine(LocalStorePath, "storefile"); - private static readonly string LocalConfigFilePath = Path.Combine(LocalStorePath, "storeconfig"); + private static readonly string LocalStoreFilePath = Path.Combine(LocalStorePath, StoreFileName); + private static readonly string LocalConfigFilePath = Path.Combine(LocalStorePath, StoreConfigName); private static readonly FileSystemWatcher _storeFileWatcher; + private static readonly Timer _updateEventTimer; + private static readonly object _syncObject; private static DateTime _lastWriteTime; - private static object _syncObject; + private static DateTime _lastFileChange; #endregion @@ -1549,24 +1609,50 @@ static SecureStoreFile() _storeFileWatcher = new FileSystemWatcher(LocalStorePath); _storeFileWatcher.NotifyFilter = NotifyFilters.LastWrite; - _storeFileWatcher.Filter = "storefile"; + _storeFileWatcher.Filter = "store*"; // storefile, storeconfig _storeFileWatcher.EnableRaisingEvents = true; _storeFileWatcher.Changed += (sender, args) => { UpdateData(args); }; _syncObject = new object(); _lastWriteTime = DateTime.MinValue; + _updateEventTimer = new Timer( + (state) => { + try + { + DateTime fileChangeTime; + lock (_syncObject) + { + fileChangeTime = _lastFileChange; + } + + RaiseDataUpdatedEvent( + new FileUpdateEventArgs(fileChangeTime)); + } + catch + { + } + }); } #endregion #region Events - public static event EventHandler FileUpdated; - private static void RaiseFileUpdatedEvent(FileUpdateEventArgs args) + public static event EventHandler DataUpdated; + private static void RaiseDataUpdatedEvent(FileUpdateEventArgs args) + { + if (DataUpdated != null) + { + DataUpdated.Invoke(null, args); + } + } + + public static event EventHandler ConfigUpdated; + private static void RaiseConfigUpdatedEvent(FileUpdateEventArgs args) { - if (FileUpdated != null) + if (ConfigUpdated != null) { - FileUpdated.Invoke(null, args); + ConfigUpdated.Invoke(null, args); } } @@ -1908,7 +1994,32 @@ public static bool StoreFileExists() private static void UpdateData(FileSystemEventArgs args) { - RaiseFileUpdatedEvent(new FileUpdateEventArgs(args)); + + try + { + var lastFileChange = System.IO.File.GetLastWriteTime(args.FullPath); + var fileName = System.IO.Path.GetFileNameWithoutExtension(args.FullPath); + if (fileName.Equals(StoreFileName)) + { + lock (_syncObject) + { + // Set/reset event callback timer for each file change event. + // This is to smooth out multiple file changes into a single update event. + _lastFileChange = lastFileChange; + _updateEventTimer.Change( + dueTime: 5000, // 5 second delay + period: Timeout.Infinite); + } + } + else if (fileName.Equals(StoreConfigName)) + { + RaiseConfigUpdatedEvent( + new FileUpdateEventArgs(lastFileChange)); + } + } + catch + { + } } #endregion @@ -1918,15 +2029,15 @@ private static void UpdateData(FileSystemEventArgs args) internal sealed class FileUpdateEventArgs : EventArgs { - public FileSystemEventArgs FileChangedArgs + public DateTime FileChangedTime { - private set; get; + private set; } - public FileUpdateEventArgs(FileSystemEventArgs args) + public FileUpdateEventArgs(DateTime fileChangedTime) { - FileChangedArgs = args; + FileChangedTime = fileChangedTime; } } @@ -1986,6 +2097,10 @@ public LocalSecretStore( SecureStore secureStore) { _secureStore = secureStore; + _secureStore.StoreConfigUpdated += (sender, args) => { + // If the local store configuration changed, then reload the store from file. + LocalSecretStore.Reset(); + }; } static LocalSecretStore() @@ -2069,7 +2184,7 @@ public static void Reset() { lock (SyncObject) { - LocalStore.Dispose(); + LocalStore?.Dispose(); LocalStore = null; } } @@ -2381,7 +2496,7 @@ public void UnlockLocalStore(SecureString password) try { - _secureStore.UpdateFromFile(); + _secureStore.UpdateDataFromFile(); } catch (SecureStorePasswordException) { From 47189c56e45d2543c658557c5002c04e91601b43 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Thu, 28 May 2020 17:07:17 -0700 Subject: [PATCH 11/19] Make timeout value be in seconds --- ....PowerShell.SecretManagement.format.ps1xml | 41 +++++++++++++++++++ .../src/code/SecretManagement.cs | 8 ++-- .../src/code/Utils.cs | 30 +++++++------- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml index 211b304..b91f042 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml @@ -35,5 +35,46 @@ + + VaultInfo + + Microsoft.PowerShell.SecretManagement.SecureStoreConfig + + + + + + + + + + + + + + + + + + + + + + Scope + + + PasswordRequired + + + PasswordTimeoutSecs + + + DoNotPrompt + + + + + + diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index c54715e..0a9e856 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1420,11 +1420,10 @@ protected override void EndProcessing() this)); } - int passwordTimeoutMs = (PasswordTimeoutSeconds > 0) ? PasswordTimeoutSeconds * 1000 : PasswordTimeoutSeconds; var newConfigData = new SecureStoreConfig( scope: Scope, passwordRequired: RequirePassword, - passwordTimeout: passwordTimeoutMs, + passwordTimeoutSecs: PasswordTimeoutSeconds, doNotPrompt: DoNotPrompt); var errorMsg = ""; @@ -1464,7 +1463,7 @@ public sealed class ResetLocalStoreCommand : PSCmdlet [Parameter(Position=2)] [ValidateRange(-1, (Int32.MaxValue / 1000))] - public int PasswordTimeoutSeconds { get; set; } = 90000; + public int PasswordTimeoutSeconds { get; set; } = 900; [Parameter(Position=3)] public SwitchParameter DoNotPrompt { get; set; } = false; @@ -1487,11 +1486,10 @@ protected override void EndProcessing() target: "SecretManagement module local store", action: "Erase all secrets in the local store and reset the configuration settings")) { - int passwordTimeoutMs = (PasswordTimeoutSeconds > 0) ? PasswordTimeoutSeconds * 1000 : PasswordTimeoutSeconds; var newConfig = new SecureStoreConfig( scope: Scope, passwordRequired: RequirePassword, - passwordTimeout: passwordTimeoutMs, + passwordTimeoutSecs: PasswordTimeoutSeconds, doNotPrompt: DoNotPrompt); var errorMsg = ""; diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index dcb657b..f645c93 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -437,7 +437,7 @@ public bool PasswordRequired private set; } - public int PasswordTimeout + public int PasswordTimeoutSecs { get; private set; @@ -460,12 +460,12 @@ private SecureStoreConfig() public SecureStoreConfig( SecureStoreScope scope, bool passwordRequired, - int passwordTimeout, + int passwordTimeoutSecs, bool doNotPrompt) { Scope = scope; PasswordRequired = passwordRequired; - PasswordTimeout = passwordTimeout; + PasswordTimeoutSecs = passwordTimeoutSecs; DoNotPrompt = doNotPrompt; } @@ -490,8 +490,8 @@ public string ConvertToJson() key: "PasswordRequired", value: PasswordRequired); configHashtable.Add( - key: "PasswordTimeout", - value: PasswordTimeout); + key: "PasswordTimeoutSecs", + value: PasswordTimeoutSecs); configHashtable.Add( key: "DoNotPrompt", value: DoNotPrompt); @@ -513,7 +513,7 @@ private void ConvertFromJson(string json) dynamic configDataObj = (Utils.ConvertJsonToPSObject(json)); Scope = (SecureStoreScope) configDataObj.ConfigData.StoreScope; PasswordRequired = (bool) configDataObj.ConfigData.PasswordRequired; - PasswordTimeout = (int) configDataObj.ConfigData.PasswordTimeout; + PasswordTimeoutSecs = (int) configDataObj.ConfigData.PasswordTimeoutSecs; DoNotPrompt = (bool) configDataObj.ConfigData.DoNotPrompt; } @@ -526,7 +526,7 @@ public static SecureStoreConfig GetDefault() return new SecureStoreConfig( scope: SecureStoreScope.Local, passwordRequired: true, - passwordTimeout: 900000, // 15 minute timeout + passwordTimeoutSecs: 900, // 15 minute timeout doNotPrompt: false); } @@ -629,7 +629,7 @@ public SecureStoreData( @{ StoreScope='LocalScope' PasswordRequired=$true - PasswordTimeout=-1, + PasswordTimeoutSecs=-1, DoNotPrompt=$false } MetaData = @@ -899,12 +899,12 @@ public void SetPassword(SecureString password) _password = password; if (password != null) { - SetPasswordTimer(_configData.PasswordTimeout); + SetPasswordTimer(_configData.PasswordTimeoutSecs); } } } - private void SetPasswordTimer(int timeoutMSec) + private void SetPasswordTimer(int timeoutSecs) { if (_passwordTimer != null) { @@ -912,7 +912,7 @@ private void SetPasswordTimer(int timeoutMSec) _passwordTimer = null; } - if (timeoutMSec > 0) + if (timeoutSecs > 0) { _passwordTimer = new Timer( callback: (_) => @@ -923,7 +923,7 @@ private void SetPasswordTimer(int timeoutMSec) } }, state: null, - dueTime: timeoutMSec, + dueTime: timeoutSecs * 1000, period: Timeout.Infinite); } } @@ -1252,9 +1252,9 @@ public bool UpdateConfigData( return false; } } - else if ((oldConfigData.PasswordTimeout != newConfigData.PasswordTimeout) && (_password != null)) + else if ((oldConfigData.PasswordTimeoutSecs != newConfigData.PasswordTimeoutSecs) && (_password != null)) { - SetPasswordTimer(newConfigData.PasswordTimeout); + SetPasswordTimer(newConfigData.PasswordTimeoutSecs); } return true; @@ -2080,7 +2080,7 @@ public SecureStoreConfig Configuration return new SecureStoreConfig( scope: _secureStore.ConfigData.Scope, passwordRequired: _secureStore.ConfigData.PasswordRequired, - passwordTimeout: _secureStore.ConfigData.PasswordTimeout, + passwordTimeoutSecs: _secureStore.ConfigData.PasswordTimeoutSecs, doNotPrompt: _secureStore.ConfigData.DoNotPrompt); } } From 823647fa35388bdcbb47b919dc9b98f8b89dac0b Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 29 May 2020 08:23:26 -0700 Subject: [PATCH 12/19] Update Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs Co-authored-by: Ilya --- .../src/code/SecretManagement.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 0a9e856..5fc93c0 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1248,8 +1248,7 @@ protected override void EndProcessing() /// Password will remain in effect for the session until the timeout expires. /// The password timeout is set in the local store configuration. /// - [Cmdlet(VerbsCommon.Unlock, "LocalStore", - DefaultParameterSetName = SecureStringParameterSet)] + [Cmdlet(VerbsCommon.Unlock, "LocalStore", DefaultParameterSetName = SecureStringParameterSet)] public sealed class UnlockLocalStoreCommand : PSCmdlet { #region Members From 08e3babf06961edff18dce3a43acba3f5fc6c166 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 29 May 2020 08:24:47 -0700 Subject: [PATCH 13/19] Update Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs Co-authored-by: Ilya --- .../src/code/Utils.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index f645c93..f8a7b98 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -807,13 +807,7 @@ internal sealed class SecureStore : IDisposable #region Properties - public SecureStoreData Data - { - get - { - return _data; - } - } + public SecureStoreData Data => _data; public SecureStoreConfig ConfigData { From 52a1b74181bdc53b3f06e36cd92592660656a7c2 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 29 May 2020 08:24:57 -0700 Subject: [PATCH 14/19] Update Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs Co-authored-by: Ilya --- .../src/code/Utils.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index f8a7b98..8f21fbc 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -809,13 +809,7 @@ internal sealed class SecureStore : IDisposable public SecureStoreData Data => _data; - public SecureStoreConfig ConfigData - { - get - { - return _configData; - } - } + public SecureStoreConfig ConfigData => _configData; internal SecureString Password { From c1edc0add14c0c6cae768b0f2d938d205597d3db Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 29 May 2020 08:25:46 -0700 Subject: [PATCH 15/19] Update Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs Co-authored-by: Ilya --- Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 8f21fbc..f25dc2c 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -1575,7 +1575,7 @@ internal static class SecureStoreFile private const string StoreConfigName = "storeconfig"; private static readonly string LocalStorePath = Path.Combine(Utils.SecretManagementLocalPath, ".localstore"); - private static readonly string LocalStoreFilePath = Path.Combine(LocalStorePath, StoreFileName); + private static readonly string LocalStoreFilePath = Path.Join(LocalStorePath, StoreFileName); private static readonly string LocalConfigFilePath = Path.Combine(LocalStorePath, StoreConfigName); private static readonly FileSystemWatcher _storeFileWatcher; From 6964b1f05ae7d99782ffd1648c9a1cf652820ce8 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 29 May 2020 08:25:53 -0700 Subject: [PATCH 16/19] Update Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs Co-authored-by: Ilya --- Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index f25dc2c..4d19f8c 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -1576,7 +1576,7 @@ internal static class SecureStoreFile private static readonly string LocalStorePath = Path.Combine(Utils.SecretManagementLocalPath, ".localstore"); private static readonly string LocalStoreFilePath = Path.Join(LocalStorePath, StoreFileName); - private static readonly string LocalConfigFilePath = Path.Combine(LocalStorePath, StoreConfigName); + private static readonly string LocalConfigFilePath = Path.Join(LocalStorePath, StoreConfigName); private static readonly FileSystemWatcher _storeFileWatcher; private static readonly Timer _updateEventTimer; From 33407783cdc7e558058d0f116879bb056f037ce6 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 29 May 2020 08:26:04 -0700 Subject: [PATCH 17/19] Update Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs Co-authored-by: Ilya --- Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 4d19f8c..4c8698b 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -1574,7 +1574,7 @@ internal static class SecureStoreFile private const string StoreFileName = "storefile"; private const string StoreConfigName = "storeconfig"; - private static readonly string LocalStorePath = Path.Combine(Utils.SecretManagementLocalPath, ".localstore"); + private static readonly string LocalStorePath = Path.Join(Utils.SecretManagementLocalPath, ".localstore"); private static readonly string LocalStoreFilePath = Path.Join(LocalStorePath, StoreFileName); private static readonly string LocalConfigFilePath = Path.Join(LocalStorePath, StoreConfigName); From f9148e6d2f14aca062d1ead93fbcbd1208afd3f9 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Fri, 29 May 2020 15:02:44 -0700 Subject: [PATCH 18/19] Implement store cmdlet feedback --- ....PowerShell.SecretManagement.format.ps1xml | 41 ---- .../src/code/SecretManagement.cs | 231 +++++++++--------- .../src/code/Utils.cs | 57 +++-- ...soft.PowerShell.SecretManagement.Tests.ps1 | 2 +- 4 files changed, 151 insertions(+), 180 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml index b91f042..211b304 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/Microsoft.PowerShell.SecretManagement.format.ps1xml @@ -35,46 +35,5 @@ - - VaultInfo - - Microsoft.PowerShell.SecretManagement.SecureStoreConfig - - - - - - - - - - - - - - - - - - - - - - Scope - - - PasswordRequired - - - PasswordTimeoutSecs - - - DoNotPrompt - - - - - - diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 5fc93c0..0b51d4b 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1248,7 +1248,8 @@ protected override void EndProcessing() /// Password will remain in effect for the session until the timeout expires. /// The password timeout is set in the local store configuration. /// - [Cmdlet(VerbsCommon.Unlock, "LocalStore", DefaultParameterSetName = SecureStringParameterSet)] + [Cmdlet(VerbsCommon.Unlock, "LocalStore", + DefaultParameterSetName = SecureStringParameterSet)] public sealed class UnlockLocalStoreCommand : PSCmdlet { #region Members @@ -1263,15 +1264,18 @@ public sealed class UnlockLocalStoreCommand : PSCmdlet /// /// Gets or sets a plain text password. /// - [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=StringParameterSet)] + [Parameter(ParameterSetName=StringParameterSet)] public string Password { get; set; } /// /// Gets or sets a SecureString password. /// - [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=SecureStringParameterSet)] + [Parameter(Mandatory=true, ValueFromPipeline=true, ValueFromPipelineByPropertyName=true, ParameterSetName=SecureStringParameterSet)] public SecureString SecureStringPassword { get; set; } + [Parameter] + public int PasswordTimeout { get; set; } + #endregion #region Overrides @@ -1279,7 +1283,9 @@ public sealed class UnlockLocalStoreCommand : PSCmdlet protected override void EndProcessing() { var passwordToSet = (ParameterSetName == StringParameterSet) ? Utils.ConvertToSecureString(Password) : SecureStringPassword; - LocalSecretStore.GetInstance(password: passwordToSet).UnlockLocalStore(passwordToSet); + LocalSecretStore.GetInstance(password: passwordToSet).UnlockLocalStore( + password: passwordToSet, + passwordTimeout: MyInvocation.BoundParameters.ContainsKey(nameof(PasswordTimeout)) ? (int?)PasswordTimeout : null); } #endregion @@ -1292,68 +1298,23 @@ protected override void EndProcessing() /// /// Updates the local store password to the new password provided. /// - [Cmdlet(VerbsData.Update, "LocalStorePassword", - DefaultParameterSetName = NoParameterSet)] + [Cmdlet(VerbsData.Update, "LocalStorePassword")] public sealed class UpdateLocalStorePasswordCommand : PSCmdlet { - #region Members - - private const string NoParameterSet = "NoParameterSet"; - private const string StringParameterSet = "StringParameterSet"; - private const string SecureStringParameterSet = "SecureStringParameterSet"; - - #endregion - - #region Parameters - - [Parameter(ParameterSetName=NoParameterSet)] - public SwitchParameter PromptForPassword { get; set; } = true; - - [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=StringParameterSet)] - public string NewPassword { get; set; } - - [Parameter(Position=1, Mandatory=true, ParameterSetName=StringParameterSet)] - public string OldPassword { get; set; } - - [Parameter(Position=0, Mandatory=true, ValueFromPipeline=true, ParameterSetName=SecureStringParameterSet)] - public SecureString NewSecureStringPassword { get; set; } - - [Parameter(Position=1, Mandatory=true, ParameterSetName=SecureStringParameterSet)] - public SecureString OldSecureStringPassword { get; set; } - - #endregion - #region Overrides protected override void EndProcessing() { SecureString newPassword; SecureString oldPassword; - switch (ParameterSetName) - { - case StringParameterSet: - newPassword = Utils.ConvertToSecureString(NewPassword); - oldPassword = Utils.ConvertToSecureString(OldPassword); - break; - - case SecureStringParameterSet: - newPassword = NewSecureStringPassword; - oldPassword = OldSecureStringPassword; - break; - - default: - // NoParameterSet - if (!PromptForPassword) { return; } - oldPassword = Utils.PromptForPassword( - cmdlet: this, - verifyPassword: false, - message: "Old password"); - newPassword = Utils.PromptForPassword( - cmdlet: this, - verifyPassword: true, - message: "New password"); - break; - } + oldPassword = Utils.PromptForPassword( + cmdlet: this, + verifyPassword: false, + message: "Old password"); + newPassword = Utils.PromptForPassword( + cmdlet: this, + verifyPassword: true, + message: "New password"); LocalSecretStore.GetInstance(password: oldPassword).UpdatePassword( newPassword, @@ -1385,23 +1346,37 @@ protected override void EndProcessing() #region Set-LocalStoreConfiguration - [Cmdlet(VerbsCommon.Set, "LocalStoreConfiguration")] + [Cmdlet(VerbsCommon.Set, "LocalStoreConfiguration", DefaultParameterSetName = ParameterSet, + SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] public sealed class SetLocalStoreConfiguration : PSCmdlet { + #region Members + + private const string ParameterSet = "ParameterSet"; + private const string DefaultParameterSet = "DefaultParameterSet"; + + #endregion + #region Parameters - [Parameter(Position=0)] - public SecureStoreScope Scope { get; set; } = SecureStoreScope.Local; + [Parameter(ParameterSetName = ParameterSet)] + public SecureStoreScope Scope { get; set; } - [Parameter(Position=1)] - public SwitchParameter RequirePassword { get; set; } = true; + [Parameter(ParameterSetName = ParameterSet)] + public SwitchParameter PasswordRequired { get; set; } - [Parameter(Position=2)] + [Parameter(ParameterSetName = ParameterSet)] [ValidateRange(-1, (Int32.MaxValue / 1000))] - public int PasswordTimeoutSeconds { get; set; } = 900; + public int PasswordTimeout { get; set; } - [Parameter(Position=3)] - public SwitchParameter DoNotPrompt { get; set; } = false; + [Parameter(ParameterSetName = ParameterSet)] + public SwitchParameter DoNotPrompt { get; set; } + + [Parameter(ParameterSetName = DefaultParameterSet)] + public SwitchParameter Default { get; set; } + + [Parameter] + public SwitchParameter Force { get; set; } #endregion @@ -1409,21 +1384,37 @@ public sealed class SetLocalStoreConfiguration : PSCmdlet protected override void EndProcessing() { - if (Scope == SecureStoreScope.Machine) + if (Scope == SecureStoreScope.AllUsers) { ThrowTerminatingError( new ErrorRecord( - exception: new PSNotSupportedException("Machine scope is not yet supported."), + exception: new PSNotSupportedException("AllUsers scope is not yet supported."), errorId: "LocalStoreConfigurationNotSupported", errorCategory: ErrorCategory.NotEnabled, this)); } - var newConfigData = new SecureStoreConfig( - scope: Scope, - passwordRequired: RequirePassword, - passwordTimeoutSecs: PasswordTimeoutSeconds, - doNotPrompt: DoNotPrompt); + if (!Force && !ShouldProcess( + target: "SecretManagement module local store", + action: "Changes local store configuration")) + { + return; + } + + var oldConfigData = LocalSecretStore.GetInstance(cmdlet: this).Configuration; + SecureStoreConfig newConfigData; + if (ParameterSetName == ParameterSet) + { + newConfigData = new SecureStoreConfig( + scope: MyInvocation.BoundParameters.ContainsKey(nameof(Scope)) ? Scope : oldConfigData.Scope, + passwordRequired: MyInvocation.BoundParameters.ContainsKey(nameof(PasswordRequired)) ? (bool)PasswordRequired : oldConfigData.PasswordRequired, + passwordTimeout: MyInvocation.BoundParameters.ContainsKey(nameof(PasswordTimeout)) ? PasswordTimeout : oldConfigData.PasswordTimeout, + doNotPrompt: MyInvocation.BoundParameters.ContainsKey(nameof(DoNotPrompt)) ? (bool)DoNotPrompt : oldConfigData.DoNotPrompt); + } + else + { + newConfigData = SecureStoreConfig.GetDefault(); + } var errorMsg = ""; if (!LocalSecretStore.GetInstance(cmdlet: this).UpdateConfiguration( @@ -1449,23 +1440,23 @@ protected override void EndProcessing() #region Reset-LocalStore - [Cmdlet(VerbsCommon.Reset, "LocalStore", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [Cmdlet(VerbsCommon.Reset, "LocalStore", + SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] public sealed class ResetLocalStoreCommand : PSCmdlet { #region Parmeters - [Parameter(Position=0)] - public SecureStoreScope Scope { get; set; } = SecureStoreScope.Local; + [Parameter] + public SecureStoreScope Scope { get; set; } - [Parameter(Position=1)] - public SwitchParameter RequirePassword { get; set; } = true; + [Parameter] + public SwitchParameter PasswordRequired { get; set; } - [Parameter(Position=2)] - [ValidateRange(-1, (Int32.MaxValue / 1000))] - public int PasswordTimeoutSeconds { get; set; } = 900; + [Parameter] + public int PasswordTimeout { get; set; } - [Parameter(Position=3)] - public SwitchParameter DoNotPrompt { get; set; } = false; + [Parameter] + public SwitchParameter DoNotPrompt { get; set; } [Parameter] public SwitchParameter Force { get; set; } @@ -1476,46 +1467,58 @@ public sealed class ResetLocalStoreCommand : PSCmdlet protected override void BeginProcessing() { - WriteWarning("This operation will completely remove all SecretManagement module local store secrets, and make any registered vault inoperable."); + WriteWarning("This operation will completely remove all SecretManagement module local store secrets and configuration settings, making any registered vault inoperable."); } protected override void EndProcessing() { - if (Force || ShouldProcess( + if (!Force && !ShouldProcess( target: "SecretManagement module local store", action: "Erase all secrets in the local store and reset the configuration settings")) { - var newConfig = new SecureStoreConfig( - scope: Scope, - passwordRequired: RequirePassword, - passwordTimeoutSecs: PasswordTimeoutSeconds, - doNotPrompt: DoNotPrompt); - - var errorMsg = ""; - if (!SecureStoreFile.RemoveStoreFile(ref errorMsg)) - { - ThrowTerminatingError( - new ErrorRecord( - exception: new PSInvalidOperationException(errorMsg), - errorId: "ResetLocalStoreCannotRemoveStoreFile", - errorCategory: ErrorCategory.InvalidOperation, - targetObject: this)); - } + return; + } - if (!SecureStoreFile.WriteConfigFile( - configData: newConfig, - ref errorMsg)) - { - ThrowTerminatingError( - new ErrorRecord( - exception: new PSInvalidOperationException(errorMsg), - errorId: "ResetLocalStoreCannotWriteConfigFile", - errorCategory: ErrorCategory.InvalidOperation, - targetObject: this)); - } + var errorMsg = ""; + SecureStoreConfig oldConfigData; + if (!SecureStoreFile.ReadConfigFile( + configData: out oldConfigData, + ref errorMsg)) + { + oldConfigData = SecureStoreConfig.GetDefault(); + } + + var newConfigData = new SecureStoreConfig( + scope: MyInvocation.BoundParameters.ContainsKey(nameof(Scope)) ? Scope : oldConfigData.Scope, + passwordRequired: MyInvocation.BoundParameters.ContainsKey(nameof(PasswordRequired)) ? (bool)PasswordRequired : oldConfigData.PasswordRequired, + passwordTimeout: MyInvocation.BoundParameters.ContainsKey(nameof(PasswordTimeout)) ? PasswordTimeout : oldConfigData.PasswordTimeout, + doNotPrompt: MyInvocation.BoundParameters.ContainsKey(nameof(DoNotPrompt)) ? (bool)DoNotPrompt : oldConfigData.DoNotPrompt); - LocalSecretStore.Reset(); + if (!SecureStoreFile.RemoveStoreFile(ref errorMsg)) + { + ThrowTerminatingError( + new ErrorRecord( + exception: new PSInvalidOperationException(errorMsg), + errorId: "ResetLocalStoreCannotRemoveStoreFile", + errorCategory: ErrorCategory.InvalidOperation, + targetObject: this)); } + + if (!SecureStoreFile.WriteConfigFile( + configData: newConfigData, + ref errorMsg)) + { + ThrowTerminatingError( + new ErrorRecord( + exception: new PSInvalidOperationException(errorMsg), + errorId: "ResetLocalStoreCannotWriteConfigFile", + errorCategory: ErrorCategory.InvalidOperation, + targetObject: this)); + } + + LocalSecretStore.Reset(); + + WriteObject(newConfigData); } #endregion diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 4c8698b..511af61 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -63,7 +63,7 @@ function ConvertToHash $output } - $customObject = ConvertFrom-Json -InputObject $json -Depth 5 + $customObject = ConvertFrom-Json -InputObject $json return ConvertToHash $customObject "; @@ -114,7 +114,7 @@ public static Hashtable ConvertJsonToHashtable(string json) public static PSObject ConvertJsonToPSObject(string json) { var results = PowerShellInvoker.InvokeScriptCommon( - script: @"param ([string] $json) ConvertFrom-Json -InputObject $json -Depth 5", + script: @"param ([string] $json) ConvertFrom-Json -InputObject $json", args: new object[] { json }, error: out ErrorRecord _); @@ -124,7 +124,7 @@ public static PSObject ConvertJsonToPSObject(string json) public static string ConvertHashtableToJson(Hashtable hashtable) { var results = PowerShellInvoker.InvokeScriptCommon( - script: @"param ([hashtable] $hashtable) ConvertTo-Json -InputObject $hashtable -Depth 5", + script: @"param ([hashtable] $hashtable) ConvertTo-Json -InputObject $hashtable", args: new object[] { hashtable }, error: out ErrorRecord _); @@ -417,8 +417,8 @@ private static byte[] GetDataFromSecureString(SecureString secureString) public enum SecureStoreScope { - Local = 1, - Machine + CurrentUser = 1, + AllUsers } internal sealed class SecureStoreConfig @@ -437,7 +437,10 @@ public bool PasswordRequired private set; } - public int PasswordTimeoutSecs + /// + /// Password timeout time in seconds + /// + public int PasswordTimeout { get; private set; @@ -460,12 +463,12 @@ private SecureStoreConfig() public SecureStoreConfig( SecureStoreScope scope, bool passwordRequired, - int passwordTimeoutSecs, + int passwordTimeout, bool doNotPrompt) { Scope = scope; PasswordRequired = passwordRequired; - PasswordTimeoutSecs = passwordTimeoutSecs; + PasswordTimeout = passwordTimeout; DoNotPrompt = doNotPrompt; } @@ -490,8 +493,8 @@ public string ConvertToJson() key: "PasswordRequired", value: PasswordRequired); configHashtable.Add( - key: "PasswordTimeoutSecs", - value: PasswordTimeoutSecs); + key: "PasswordTimeout", + value: PasswordTimeout); configHashtable.Add( key: "DoNotPrompt", value: DoNotPrompt); @@ -513,7 +516,7 @@ private void ConvertFromJson(string json) dynamic configDataObj = (Utils.ConvertJsonToPSObject(json)); Scope = (SecureStoreScope) configDataObj.ConfigData.StoreScope; PasswordRequired = (bool) configDataObj.ConfigData.PasswordRequired; - PasswordTimeoutSecs = (int) configDataObj.ConfigData.PasswordTimeoutSecs; + PasswordTimeout = (int) configDataObj.ConfigData.PasswordTimeout; DoNotPrompt = (bool) configDataObj.ConfigData.DoNotPrompt; } @@ -524,9 +527,9 @@ private void ConvertFromJson(string json) public static SecureStoreConfig GetDefault() { return new SecureStoreConfig( - scope: SecureStoreScope.Local, + scope: SecureStoreScope.CurrentUser, passwordRequired: true, - passwordTimeoutSecs: 900, // 15 minute timeout + passwordTimeout: 900, doNotPrompt: false); } @@ -629,7 +632,7 @@ public SecureStoreData( @{ StoreScope='LocalScope' PasswordRequired=$true - PasswordTimeoutSecs=-1, + PasswordTimeout=-1, DoNotPrompt=$false } MetaData = @@ -887,12 +890,12 @@ public void SetPassword(SecureString password) _password = password; if (password != null) { - SetPasswordTimer(_configData.PasswordTimeoutSecs); + SetPasswordTimer(_configData.PasswordTimeout); } } } - private void SetPasswordTimer(int timeoutSecs) + public void SetPasswordTimer(int timeoutSecs) { if (_passwordTimer != null) { @@ -1240,9 +1243,9 @@ public bool UpdateConfigData( return false; } } - else if ((oldConfigData.PasswordTimeoutSecs != newConfigData.PasswordTimeoutSecs) && (_password != null)) + else if ((oldConfigData.PasswordTimeout != newConfigData.PasswordTimeout) && (_password != null)) { - SetPasswordTimer(newConfigData.PasswordTimeoutSecs); + SetPasswordTimer(newConfigData.PasswordTimeout); } return true; @@ -1574,9 +1577,9 @@ internal static class SecureStoreFile private const string StoreFileName = "storefile"; private const string StoreConfigName = "storeconfig"; - private static readonly string LocalStorePath = Path.Join(Utils.SecretManagementLocalPath, ".localstore"); - private static readonly string LocalStoreFilePath = Path.Join(LocalStorePath, StoreFileName); - private static readonly string LocalConfigFilePath = Path.Join(LocalStorePath, StoreConfigName); + private static readonly string LocalStorePath = Path.Combine(Utils.SecretManagementLocalPath, ".localstore"); + private static readonly string LocalStoreFilePath = Path.Combine(LocalStorePath, StoreFileName); + private static readonly string LocalConfigFilePath = Path.Combine(LocalStorePath, StoreConfigName); private static readonly FileSystemWatcher _storeFileWatcher; private static readonly Timer _updateEventTimer; @@ -2068,7 +2071,7 @@ public SecureStoreConfig Configuration return new SecureStoreConfig( scope: _secureStore.ConfigData.Scope, passwordRequired: _secureStore.ConfigData.PasswordRequired, - passwordTimeoutSecs: _secureStore.ConfigData.PasswordTimeoutSecs, + passwordTimeout: _secureStore.ConfigData.PasswordTimeout, doNotPrompt: _secureStore.ConfigData.DoNotPrompt); } } @@ -2478,7 +2481,9 @@ public bool DeleteObject( } } - public void UnlockLocalStore(SecureString password) + public void UnlockLocalStore( + SecureString password, + int? passwordTimeout = null) { _secureStore.SetPassword(password); @@ -2490,6 +2495,11 @@ public void UnlockLocalStore(SecureString password) { throw new SecureStorePasswordException("Unable to unlock local store. Password is invalid."); } + + if (passwordTimeout.HasValue) + { + _secureStore.SetPasswordTimer(passwordTimeout.Value); + } } public void UpdatePassword( @@ -4352,7 +4362,6 @@ public static Collection InvokeScriptCommon( finally { _powershell.Commands.Clear(); - _powershell.Runspace.ResetRunspaceState(); } return results; diff --git a/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 b/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 index 0687df2..ec4cba3 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 +++ b/Modules/Microsoft.PowerShell.SecretManagement/test/Microsoft.PowerShell.SecretManagement.Tests.ps1 @@ -12,7 +12,7 @@ Describe "Test Microsoft.PowerShell.SecretManagement module" -tags CI { # Reset the local store and configure it for no-password access # TODO: This deletes all local store data!! - Reset-LocalStore -Scope Local -RequirePassword:$false -PasswordTimeoutSeconds -1 -DoNotPrompt -Force + Reset-LocalStore -PasswordRequired:$false -DoNotPrompt -Force # Binary extension module $classImplementation = @' From 79298dddcb4267b65bab05a4648c6669fdf9a454 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 3 Jun 2020 14:55:53 -0700 Subject: [PATCH 19/19] Various fixes --- .../src/code/SecretManagement.cs | 10 +++ .../src/code/Utils.cs | 87 ++++++++++--------- ...soft.PowerShell.SecretManagement.Tests.ps1 | 21 ++++- 3 files changed, 75 insertions(+), 43 deletions(-) diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs index 0b51d4b..611ede3 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/SecretManagement.cs @@ -1467,6 +1467,16 @@ public sealed class ResetLocalStoreCommand : PSCmdlet protected override void BeginProcessing() { + if (Scope == SecureStoreScope.AllUsers) + { + ThrowTerminatingError( + new ErrorRecord( + exception: new PSNotSupportedException("AllUsers scope is not yet supported."), + errorId: "LocalStoreConfigurationNotSupported", + errorCategory: ErrorCategory.NotEnabled, + this)); + } + WriteWarning("This operation will completely remove all SecretManagement module local store secrets and configuration settings, making any registered vault inoperable."); } diff --git a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs index 511af61..75f6d07 100644 --- a/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs +++ b/Modules/Microsoft.PowerShell.SecretManagement/src/code/Utils.cs @@ -24,8 +24,6 @@ internal static class Utils { #region Members - private static readonly string LocalLocation; - private static readonly string SMLocalPath; private const string ConvertJsonToHashtableScript = @" param ( [string] $json @@ -73,19 +71,19 @@ function ConvertToHash static Utils() { - var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( System.Runtime.InteropServices.OSPlatform.Windows); - if (isWindows) + if (IsWindows) { - LocalLocation = Environment.GetEnvironmentVariable("USERPROFILE"); + var locationPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + SecretManagementLocalPath = Path.Combine(locationPath, "Microsoft", "PowerShell", "secretmanagement"); } else { - LocalLocation = Environment.GetEnvironmentVariable("HOME"); + var locationPath = Environment.GetEnvironmentVariable("HOME"); + SecretManagementLocalPath = Path.Combine(locationPath, ".secretmanagement"); } - - SMLocalPath = Path.Combine(LocalLocation, ".secretmanagement"); } #endregion @@ -94,7 +92,14 @@ static Utils() public static string SecretManagementLocalPath { - get { return SMLocalPath; } + get; + private set; + } + + public static bool IsWindows + { + get; + private set; } #endregion @@ -193,52 +198,44 @@ private static bool ComparePasswords( SecureString password1, SecureString password2) { - byte[] data1 = null; - byte[] data2 = null; - var passwordEqual = false; - try + if (password1.Length != password2.Length) { - if (!GetDataFromSecureString( - password1, - out data1)) - { - return false; - } - - if (!GetDataFromSecureString( - password2, - out data2)) - { - return false; - } + return false; + } - passwordEqual = (data1.Length == data2.Length); - if (passwordEqual) + IntPtr ptrPassword1 = IntPtr.Zero; + IntPtr ptrPassword2 = IntPtr.Zero; + try + { + ptrPassword1 = Marshal.SecureStringToCoTaskMemUnicode(password1); + ptrPassword2 = Marshal.SecureStringToCoTaskMemUnicode(password2); + if (ptrPassword1 != IntPtr.Zero && ptrPassword2 != IntPtr.Zero) { - for (int i=0; i