From e692a81bd6fdf30905302ee5c54f44e1ca233fe2 Mon Sep 17 00:00:00 2001 From: Jin Lei <54836179+msJinLei@users.noreply.github.com> Date: Thu, 24 Nov 2022 20:05:39 +0800 Subject: [PATCH] Store Secrets to Encrypted Storage (#19929) * Store secrets to encrypted storage * Uses unprotected files in Linux * Update Changelog.md * Fix the pipeline issue * Update src/Accounts/Authentication/KeyStore/AzKeyStore.cs Co-authored-by: Beisi Zhou Co-authored-by: Beisi Zhou --- src/Accounts/Accounts.Test/AutosaveTests.cs | 18 +- .../Accounts.Test/ProfileCmdletTests.cs | 21 ++ .../Accounts/Account/ConnectAzureRmAccount.cs | 20 +- .../AutoSave/DisableAzureRmContextAutosave.cs | 5 + .../AutoSave/EnableAzureRmContextAutosave.cs | 6 + src/Accounts/Accounts/ChangeLog.md | 1 + .../Accounts/Properties/Resources.Designer.cs | 11 +- .../Accounts/Properties/Resources.resx | 3 + .../AzureRmProfile.cs | 31 ++- .../Authentication.Test/AzKeyStorageTest.cs | 165 +++++++++++++++ .../Authentication/AzureSessionInitializer.cs | 4 +- .../Authentication/ContextAutosaveSettings.cs | 5 + .../Factories/AuthenticationFactory.cs | 23 ++- .../Authentication/KeyStore/AzKeyStore.cs | 190 ++++++++++++++++-- .../Authentication/KeyStore/IStorage.cs | 32 +++ .../KeyStore/SecureStringConverter.cs | 42 ++++ .../KeyStore/ServicePrincipalKeyConverter.cs | 41 ++++ .../Authentication/KeyStore/StorageWrapper.cs | 138 +++++++++++++ .../Properties/Resources.Designer.cs | 11 +- .../Authentication/Properties/Resources.resx | 3 + 20 files changed, 733 insertions(+), 37 deletions(-) create mode 100644 src/Accounts/Authentication.Test/AzKeyStorageTest.cs create mode 100644 src/Accounts/Authentication/KeyStore/IStorage.cs create mode 100644 src/Accounts/Authentication/KeyStore/SecureStringConverter.cs create mode 100644 src/Accounts/Authentication/KeyStore/ServicePrincipalKeyConverter.cs create mode 100644 src/Accounts/Authentication/KeyStore/StorageWrapper.cs diff --git a/src/Accounts/Accounts.Test/AutosaveTests.cs b/src/Accounts/Accounts.Test/AutosaveTests.cs index 310c937b7d02..03ff5b6f11d1 100644 --- a/src/Accounts/Accounts.Test/AutosaveTests.cs +++ b/src/Accounts/Accounts.Test/AutosaveTests.cs @@ -22,10 +22,12 @@ using Xunit.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using System; +using System.Security; using Microsoft.Azure.Commands.Profile.Context; using Microsoft.Azure.Commands.ScenarioTest; using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.Azure.Commands.TestFx.Mocks; +using Moq; namespace Microsoft.Azure.Commands.Profile.Test { @@ -33,12 +35,25 @@ public class AutosaveTests { private MemoryDataStore dataStore; private MockCommandRuntime commandRuntimeMock; + private AzKeyStore keyStore; public AutosaveTests(ITestOutputHelper output) { XunitTracingInterceptor.AddToContext(new XunitTracingInterceptor(output)); commandRuntimeMock = new MockCommandRuntime(); dataStore = new MemoryDataStore(); - ResetState(); + keyStore = SetMockedAzKeyStore(); + } + + private AzKeyStore SetMockedAzKeyStore() + { + var storageMocker = new Mock(); + storageMocker.Setup(f => f.Create()).Returns(storageMocker.Object); + storageMocker.Setup(f => f.ReadData()).Returns(new byte[0]); + storageMocker.Setup(f => f.WriteData(It.IsAny())).Callback((byte[] s) => {}); + var keyStore = new AzKeyStore(AzureSession.Instance.ARMProfileDirectory, "keystore.cache", false, false, storageMocker.Object); + AzKeyStore.RegisterJsonConverter(typeof(ServicePrincipalKey), typeof(ServicePrincipalKey).Name); + AzKeyStore.RegisterJsonConverter(typeof(SecureString), typeof(SecureString).Name, new SecureStringConverter()); + return keyStore; } void ResetState() @@ -54,6 +69,7 @@ void ResetState() Environment.SetEnvironmentVariable("Azure_PS_Data_Collection", "false"); PowerShellTokenCacheProvider tokenProvider = new InMemoryTokenCacheProvider(); AzureSession.Instance.RegisterComponent(PowerShellTokenCacheProvider.PowerShellTokenCacheProviderKey, () => tokenProvider, true); + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); } [Fact] diff --git a/src/Accounts/Accounts.Test/ProfileCmdletTests.cs b/src/Accounts/Accounts.Test/ProfileCmdletTests.cs index 06f7425ef783..8d4f73af2035 100644 --- a/src/Accounts/Accounts.Test/ProfileCmdletTests.cs +++ b/src/Accounts/Accounts.Test/ProfileCmdletTests.cs @@ -15,6 +15,7 @@ using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Models; +using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.Azure.Commands.ScenarioTest; using Microsoft.Azure.Commands.TestFx.Mocks; using Microsoft.Azure.ServiceManagement.Common.Models; @@ -22,6 +23,7 @@ using Microsoft.WindowsAzure.Commands.ScenarioTest; using Microsoft.WindowsAzure.Commands.Test.Utilities.Common; using Microsoft.WindowsAzure.Commands.Utilities.Common; +using Moq; using System; using System.Linq; using System.Management.Automation; @@ -34,6 +36,7 @@ public class ProfileCmdletTests : RMTestBase { private MemoryDataStore dataStore; private MockCommandRuntime commandRuntimeMock; + private AzKeyStore keyStore; public ProfileCmdletTests(ITestOutputHelper output) { @@ -43,12 +46,25 @@ public ProfileCmdletTests(ITestOutputHelper output) AzureSession.Instance.DataStore = dataStore; commandRuntimeMock = new MockCommandRuntime(); AzureSession.Instance.AuthenticationFactory = new MockTokenAuthenticationFactory(); + keyStore = SetMockedAzKeyStore(); + } + + private AzKeyStore SetMockedAzKeyStore() + { + var storageMocker = new Mock(); + storageMocker.Setup(f => f.Create()).Returns(storageMocker.Object); + storageMocker.Setup(f => f.ReadData()).Returns(new byte[0]); + storageMocker.Setup(f => f.WriteData(It.IsAny())).Callback((byte[] s) => { }); + var keyStore = new AzKeyStore(AzureSession.Instance.ARMProfileDirectory, "keystore.cache", false, false, storageMocker.Object); + return keyStore; } [Fact] [Trait(Category.AcceptanceType, Category.CheckIn)] public void SelectAzureProfileInMemory() { + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); + var profile = new AzureRmProfile { DefaultContext = new AzureContext() }; var env = new AzureEnvironment(AzureEnvironment.PublicEnvironments.Values.FirstOrDefault()); env.Name = "foo"; @@ -71,6 +87,7 @@ public void SelectAzureProfileInMemory() [Trait(Category.AcceptanceType, Category.CheckIn)] public void SelectAzureProfileBadPath() { + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); #pragma warning disable CS0618 // Suppress obsolescence warning: cmdlet name is changing ImportAzureRMContextCommand cmdlt = new ImportAzureRMContextCommand(); #pragma warning restore CS0618 // Suppress obsolescence warning: cmdlet name is changing @@ -88,6 +105,7 @@ public void SelectAzureProfileBadPath() [Trait(Category.AcceptanceType, Category.CheckIn)] public void SelectAzureProfileFromDisk() { + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); var profile = new AzureRmProfile(); profile.EnvironmentTable.Add("foo", new AzureEnvironment(new AzureEnvironment( AzureEnvironment.PublicEnvironments.Values.FirstOrDefault()))); profile.EnvironmentTable["foo"].Name = "foo"; @@ -110,6 +128,7 @@ public void SelectAzureProfileFromDisk() [Trait(Category.AcceptanceType, Category.CheckIn)] public void SaveAzureProfileInMemory() { + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); var profile = new AzureRmProfile(); profile.EnvironmentTable.Add("foo", new AzureEnvironment(AzureEnvironment.PublicEnvironments.Values.FirstOrDefault())); profile.EnvironmentTable["foo"].Name = "foo"; @@ -134,6 +153,7 @@ public void SaveAzureProfileInMemory() [Trait(Category.AcceptanceType, Category.CheckIn)] public void SaveAzureProfileNull() { + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); #pragma warning disable CS0618 // Suppress obsolescence warning: cmdlet name is changing SaveAzureRMContextCommand cmdlt = new SaveAzureRMContextCommand(); #pragma warning restore CS0618 // Suppress obsolescence warning: cmdlet name is changing @@ -150,6 +170,7 @@ public void SaveAzureProfileNull() [Trait(Category.AcceptanceType, Category.CheckIn)] public void SaveAzureProfileFromDefault() { + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); var profile = new AzureRmProfile(); profile.EnvironmentTable.Add("foo", new AzureEnvironment(AzureEnvironment.PublicEnvironments.Values.FirstOrDefault())); profile.DefaultContext = new AzureContext(new AzureSubscription(), new AzureAccount(), profile.EnvironmentTable["foo"]); diff --git a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs index f76e7db50fd7..cb9a6a9fe95d 100644 --- a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs +++ b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs @@ -41,7 +41,6 @@ using Microsoft.Azure.PowerShell.Common.Config; using Microsoft.Identity.Client; using Microsoft.WindowsAzure.Commands.Common; -using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; using Microsoft.WindowsAzure.Commands.Common.Utilities; using Microsoft.WindowsAzure.Commands.Utilities.Common; using Microsoft.Azure.PowerShell.Common.Share.Survey; @@ -426,7 +425,6 @@ public override void ExecuteCmdlet() azureAccount.SetProperty(AzureAccount.Property.CertificatePath, resolvedPath); if (CertificatePassword != null) { - azureAccount.SetProperty(AzureAccount.Property.CertificatePassword, CertificatePassword.ConvertToString()); keyStore?.SaveKey(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, azureAccount.Id, Tenant), CertificatePassword); } } @@ -449,7 +447,6 @@ public override void ExecuteCmdlet() if (azureAccount.Type == AzureAccount.AccountType.ServicePrincipal && password != null) { - azureAccount.SetProperty(AzureAccount.Property.ServicePrincipalSecret, password.ConvertToString()); keyStore?.SaveKey(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret ,azureAccount.Id, Tenant), password); if (GetContextModificationScope() == ContextModificationScope.CurrentUser) @@ -713,16 +710,23 @@ public void OnImport() WriteInitializationWarnings(Resources.FallbackContextSaveModeDueCacheCheckError.FormatInvariant(ex.Message)); } - if(!InitializeProfileProvider(autoSaveEnabled)) + AzKeyStore keyStore = null; + //AzureSession.Instance.KeyStoreFile + keyStore = new AzKeyStore(AzureSession.Instance.ARMProfileDirectory, "keystore.cache", false, autoSaveEnabled); + AzKeyStore.RegisterJsonConverter(typeof(ServicePrincipalKey), typeof(ServicePrincipalKey).Name); + AzKeyStore.RegisterJsonConverter(typeof(SecureString), typeof(SecureString).Name, new SecureStringConverter()); + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore); + + if (!InitializeProfileProvider(autoSaveEnabled)) { AzureSession.Instance.ARMContextSaveMode = ContextSaveMode.Process; autoSaveEnabled = false; } -#pragma warning disable CS0618 // Type or member is obsolete - var keyStore = new AzKeyStore(AzureRmProfileProvider.Instance.Profile); -#pragma warning restore CS0618 // Type or member is obsolete - AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore); + if (!keyStore.LoadStorage()) + { + WriteInitializationWarnings(Resources.KeyStoreLoadingError); + } IAuthenticatorBuilder builder = null; if (!AzureSession.Instance.TryGetComponent(AuthenticatorBuilder.AuthenticatorBuilderKey, out builder)) diff --git a/src/Accounts/Accounts/AutoSave/DisableAzureRmContextAutosave.cs b/src/Accounts/Accounts/AutoSave/DisableAzureRmContextAutosave.cs index 1d69aba78ce5..c9f768733cb7 100644 --- a/src/Accounts/Accounts/AutoSave/DisableAzureRmContextAutosave.cs +++ b/src/Accounts/Accounts/AutoSave/DisableAzureRmContextAutosave.cs @@ -92,6 +92,11 @@ void DisableAutosave(IAzureSession session, bool writeAutoSaveFile, out ContextA builder.Reset(); } + if (AzureSession.Instance.TryGetComponent(AzKeyStore.Name, out AzKeyStore keystore)) + { + keystore.DisableAutoSaving(); + } + if (writeAutoSaveFile) { FileUtilities.EnsureDirectoryExists(session.ProfileDirectory); diff --git a/src/Accounts/Accounts/AutoSave/EnableAzureRmContextAutosave.cs b/src/Accounts/Accounts/AutoSave/EnableAzureRmContextAutosave.cs index ff0644ce4685..fae6a7aed5b5 100644 --- a/src/Accounts/Accounts/AutoSave/EnableAzureRmContextAutosave.cs +++ b/src/Accounts/Accounts/AutoSave/EnableAzureRmContextAutosave.cs @@ -102,6 +102,12 @@ void EnableAutosave(IAzureSession session, bool writeAutoSaveFile, out ContextAu AzureSession.Instance.RegisterComponent(PowerShellTokenCacheProvider.PowerShellTokenCacheProviderKey, () => newCacheProvider, true); } + if (AzureSession.Instance.TryGetComponent(AzKeyStore.Name, out AzKeyStore keystore)) + { + keystore.Flush(); + keystore.DisableAutoSaving(); + } + if (writeAutoSaveFile) { diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index 4fb86a43ca8c..37727510f083 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -21,6 +21,7 @@ ## Upcoming Release * Enabled caching tokens when logging in with a service principal. This could reduce network traffic and improve performance. * Upgraded target framework of Microsoft.Identity.Client to net461 [#20189] +* Stored `ServicePrincipalSecret` and `CertificatePassword` into `AzKeyStore`. ## Version 2.10.3 * Updated `Get-AzSubscription` to retrieve subscription by Id rather than listed all the subscriptions from server if subscription Id is provided. [#19115] diff --git a/src/Accounts/Accounts/Properties/Resources.Designer.cs b/src/Accounts/Accounts/Properties/Resources.Designer.cs index 259c16ce4ef2..fc2bf7703218 100644 --- a/src/Accounts/Accounts/Properties/Resources.Designer.cs +++ b/src/Accounts/Accounts/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.Azure.Commands.Profile.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -690,6 +690,15 @@ internal static string InvalidSubscriptionId { } } + /// + /// Looks up a localized string similar to KeyStore cannot be loaded from storage. Please check the keystore file integrity or system compablity. The functions relate to context autosaving may be affected.. + /// + internal static string KeyStoreLoadingError { + get { + return ResourceManager.GetString("KeyStoreLoadingError", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} account in environment '{1}'. /// diff --git a/src/Accounts/Accounts/Properties/Resources.resx b/src/Accounts/Accounts/Properties/Resources.resx index 4e0927b64785..8d5c44e4b999 100644 --- a/src/Accounts/Accounts/Properties/Resources.resx +++ b/src/Accounts/Accounts/Properties/Resources.resx @@ -571,4 +571,7 @@ The command {0} is part of Azure PowerShell module "{1}" and it is not installed. Run "Install-Module {1}" to install it. 0: command being not found; 1: its module + + KeyStore cannot be loaded from storage. Please check the keystore file integrity or system compablity. The functions relate to context autosaving may be affected. + \ No newline at end of file diff --git a/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs b/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs index aff4970ab3ce..d6738055f6d6 100644 --- a/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs +++ b/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs @@ -25,6 +25,7 @@ using Microsoft.Azure.Commands.Common.Authentication.ResourceManager.Properties; using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.Azure.Commands.ResourceManager.Common.Serialization; +using Microsoft.WindowsAzure.Commands.Common; using Newtonsoft.Json; @@ -205,15 +206,39 @@ private void Initialize(AzureRmProfile profile) EnvironmentTable[environment.Key] = environment.Value; } + AzKeyStore keystore = null; + AzureSession.Instance.TryGetComponent(AzKeyStore.Name, out keystore); + foreach (var context in profile.Contexts) { - this.Contexts.Add(context.Key, context.Value); + this.Contexts.Add(context.Key, MigrateSecretToKeyStore(context.Value, keystore)); } DefaultContextKey = profile.DefaultContextKey ?? (profile.Contexts.Any() ? null : "Default"); } } + private IAzureContext MigrateSecretToKeyStore(IAzureContext context, AzKeyStore keystore) + { + if (keystore != null) + { + var account = context.Account; + if (account.IsPropertySet(AzureAccount.Property.ServicePrincipalSecret)) + { + keystore?.SaveKey(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, account.GetTenants().First()) + , account.ExtendedProperties.GetProperty(AzureAccount.Property.ServicePrincipalSecret).ConvertToSecureString()); + account.ExtendedProperties.Remove(AzureAccount.Property.ServicePrincipalSecret); + } + if (account.IsPropertySet(AzureAccount.Property.CertificatePassword)) + { + keystore?.SaveKey(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, account.Id, account.GetTenants().First()) + , account.ExtendedProperties.GetProperty(AzureAccount.Property.CertificatePassword).ConvertToSecureString()); + account.ExtendedProperties.Remove(AzureAccount.Property.CertificatePassword); + } + } + return context; + } + private void LoadImpl(string contents) { } @@ -311,6 +336,10 @@ public void Save(IFileProvider provider, bool serializeCache = true) // so that previous data is overwritten provider.Stream.SetLength(provider.Stream.Position); } + + AzKeyStore keystore = null; + AzureSession.Instance.TryGetComponent(AzKeyStore.Name, out keystore); + keystore?.Flush(); } finally { diff --git a/src/Accounts/Authentication.Test/AzKeyStorageTest.cs b/src/Accounts/Authentication.Test/AzKeyStorageTest.cs new file mode 100644 index 000000000000..bd0b64b8ebf9 --- /dev/null +++ b/src/Accounts/Authentication.Test/AzKeyStorageTest.cs @@ -0,0 +1,165 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- +using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.WindowsAzure.Commands.Common; +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using Moq; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Text; +using Xunit; + +namespace Common.Authenticators.Test +{ + public class AzKeyStorageTest + { + private Mock storageMocker = null; + private List storageChecker = null; + private string dummpyPath = "/home/dummy/.Azure"; + private string keyStoreFileName = "keystore.cache"; + + public AzKeyStorageTest() + { + storageChecker = new List(); + storageMocker = new Mock(); + storageMocker.Setup(f => f.Create()).Returns(storageMocker.Object); + storageMocker.Setup(f => f.WriteData(It.IsAny())).Callback((byte[] s) => { storageChecker.Clear(); storageChecker.AddRange(s); }); + } + + private static bool CompareJsonObjects(string expected, string acutal) + { + var expectedObjects = JsonConvert.DeserializeObject(expected, typeof(List)) as List; + var expectedStrings = expectedObjects.ConvertAll(x => x.ToString()); + expectedStrings.Sort(); + var objects = JsonConvert.DeserializeObject(acutal, typeof(List)) as List; + var acutalStrings = objects.ConvertAll(x => x.ToString()); + acutalStrings.Sort(); + return expectedStrings.SequenceEqual(acutalStrings); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void SaveKey() + { + using (var store = new AzKeyStore(dummpyPath, keyStoreFileName, false, true, storageMocker.Object)) + { + AzKeyStore.RegisterJsonConverter(typeof(ServicePrincipalKey), typeof(ServicePrincipalKey).Name); + AzKeyStore.RegisterJsonConverter(typeof(SecureString), typeof(SecureString).Name, new SecureStringConverter()); + + IKeyStoreKey servicePrincipalKey = new ServicePrincipalKey("ServicePrincipalSecret", "6c984d31-5b4f-4734-b548-e230a248e347", "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a"); + var secret = "secret".ConvertToSecureString(); + store.SaveKey(servicePrincipalKey, secret); + + IKeyStoreKey certificatePassword = new ServicePrincipalKey("CertificatePassword", "6c984d31-5b4f-4734-b548-e230a248e347", "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a"); + var passowrd = "password".ConvertToSecureString(); + store.SaveKey(certificatePassword, passowrd); + + store.Flush(); + } + storageMocker.Verify(); + var result = Encoding.UTF8.GetString(storageChecker.ToArray()); + const string EXPECTEDSTRING = @"[{""keyType"":""ServicePrincipalKey"",""keyStoreKey"":""{\""appId\"":\""6c984d31-5b4f-4734-b548-e230a248e347\"",\""tenantId\"":\""54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\"",\""name\"":\""CertificatePassword\""}"",""valueType"":""SecureString"",""keyStoreValue"":""\""password\""""},{""keyType"":""ServicePrincipalKey"",""keyStoreKey"":""{\""appId\"":\""6c984d31-5b4f-4734-b548-e230a248e347\"",\""tenantId\"":\""54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\"",\""name\"":\""ServicePrincipalSecret\""}"",""valueType"":""SecureString"",""keyStoreValue"":""\""secret\""""}]"; + Assert.True(CompareJsonObjects(EXPECTEDSTRING, result)); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void FindKey() + { + const string EXPECTED = @"[{""keyType"":""ServicePrincipalKey"",""keyStoreKey"":""{\""appId\"":\""6c984d31-5b4f-4734-b548-e230a248e347\"",\""tenantId\"":\""54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\"",\""name\"":\""ServicePrincipalSecret\""}"",""valueType"":""SecureString"",""keyStoreValue"":""\""secret\""""}]"; + storageChecker.AddRange(Encoding.UTF8.GetBytes(EXPECTED)); + using (var store = new AzKeyStore(dummpyPath, keyStoreFileName, false, true, storageMocker.Object)) + { + AzKeyStore.RegisterJsonConverter(typeof(ServicePrincipalKey), typeof(ServicePrincipalKey).Name); + AzKeyStore.RegisterJsonConverter(typeof(SecureString), typeof(SecureString).Name, new SecureStringConverter()); + storageMocker.Setup(f => f.ReadData()).Returns(storageChecker.ToArray()); + store.LoadStorage(); + + IKeyStoreKey servicePrincipalKey = new ServicePrincipalKey("ServicePrincipalSecret", "6c984d31-5b4f-4734-b548-e230a248e347", "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a"); + var secret = store.GetKey(servicePrincipalKey); + Assert.Equal("secret", secret.ConvertToString()); + } + storageMocker.Verify(); + } + + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void FindNoKey() + { + const string EXPECTED = @"[{""keyType"":""ServicePrincipalKey"",""keyStoreKey"":""{\""appId\"":\""6c984d31-5b4f-4734-b548-e230a248e347\"",\""tenantId\"":\""54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\"",\""name\"":\""ServicePrincipalSecret\""}"",""valueType"":""SecureString"",""keyStoreValue"":""\""secret\""""}]"; + storageChecker.AddRange(Encoding.UTF8.GetBytes(EXPECTED)); + using (var store = new AzKeyStore(dummpyPath, keyStoreFileName, false, true, storageMocker.Object)) + { + AzKeyStore.RegisterJsonConverter(typeof(ServicePrincipalKey), typeof(ServicePrincipalKey).Name); + AzKeyStore.RegisterJsonConverter(typeof(SecureString), typeof(SecureString).Name, new SecureStringConverter()); + storageMocker.Setup(f => f.ReadData()).Returns(storageChecker.ToArray()); + store.LoadStorage(); + + IKeyStoreKey servicePrincipalKey = new ServicePrincipalKey("CertificatePassword", "6c984d31-5b4f-4734-b548-e230a248e347", "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a"); + Assert.Throws(() => store.GetKey(servicePrincipalKey)); + } + storageMocker.Verify(); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void RemoveKey() + { + const string EXPECTED = @"[{""keyType"":""ServicePrincipalKey"",""keyStoreKey"":""{\""appId\"":\""6c984d31-5b4f-4734-b548-e230a248e347\"",\""tenantId\"":\""54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\"",\""name\"":\""ServicePrincipalSecret\""}"",""valueType"":""SecureString"",""keyStoreValue"":""\""secret\""""}]"; + storageChecker.AddRange(Encoding.UTF8.GetBytes(EXPECTED)); + using (var store = new AzKeyStore(dummpyPath, keyStoreFileName, false, true, storageMocker.Object)) + { + AzKeyStore.RegisterJsonConverter(typeof(ServicePrincipalKey), typeof(ServicePrincipalKey).Name); + AzKeyStore.RegisterJsonConverter(typeof(SecureString), typeof(SecureString).Name, new SecureStringConverter()); + storageMocker.Setup(f => f.ReadData()).Returns(storageChecker.ToArray()); + store.LoadStorage(); + + IKeyStoreKey servicePrincipalKey = new ServicePrincipalKey("ServicePrincipalSecret", "6c984d31-5b4f-4734-b548-e230a248e347", "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a"); + store.DeleteKey(servicePrincipalKey); + store.Flush(); + } + storageMocker.Verify(); + var result = Encoding.UTF8.GetString(storageChecker.ToArray()); + var objects = JsonConvert.DeserializeObject>(result); + Assert.Empty(objects); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void RemoveNoKey() + { + const string EXPECTED = @"[{""keyType"":""ServicePrincipalKey"",""keyStoreKey"":""{\""appId\"":\""6c984d31-5b4f-4734-b548-e230a248e347\"",\""tenantId\"":\""54826b22-38d6-4fb2-bad9-b7b93a3e9c5a\"",\""name\"":\""ServicePrincipalSecret\""}"",""valueType"":""SecureString"",""keyStoreValue"":""\""secret\""""}]"; + storageChecker.AddRange(Encoding.UTF8.GetBytes(EXPECTED)); + using (var store = new AzKeyStore(dummpyPath, keyStoreFileName, false, true, storageMocker.Object)) + { + AzKeyStore.RegisterJsonConverter(typeof(ServicePrincipalKey), typeof(ServicePrincipalKey).Name); + AzKeyStore.RegisterJsonConverter(typeof(SecureString), typeof(SecureString).Name, new SecureStringConverter()); + storageMocker.Setup(f => f.ReadData()).Returns(storageChecker.ToArray()); + store.LoadStorage(); + + IKeyStoreKey servicePrincipalKey = new ServicePrincipalKey("CertificatePassword", "6c984d31-5b4f-4734-b548-e230a248e347", "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a"); + store.DeleteKey(servicePrincipalKey); + store.Flush(); + } + storageMocker.Verify(); + var result = Encoding.UTF8.GetString(storageChecker.ToArray()); + var objects = JsonConvert.DeserializeObject>(result); + Assert.Single(objects); + } + } +} diff --git a/src/Accounts/Authentication/AzureSessionInitializer.cs b/src/Accounts/Authentication/AzureSessionInitializer.cs index d157682e2749..c677e99ff0f2 100644 --- a/src/Accounts/Authentication/AzureSessionInitializer.cs +++ b/src/Accounts/Authentication/AzureSessionInitializer.cs @@ -160,7 +160,8 @@ static ContextAutosaveSettings InitializeSessionSettings(IDataStore store, strin ContextDirectory = profileDirectory, Mode = ContextSaveMode.Process, CacheFile = "msal.cache", - ContextFile = "AzureRmContext.json" + ContextFile = "AzureRmContext.json", + KeyStoreFile = "keystore.cache" }; var settingsPath = Path.Combine(profileDirectory, settingsFile); @@ -176,6 +177,7 @@ static ContextAutosaveSettings InitializeSessionSettings(IDataStore store, strin result.ContextDirectory = migrated ? profileDirectory : settings.ContextDirectory ?? result.ContextDirectory; result.Mode = settings.Mode; result.ContextFile = settings.ContextFile ?? result.ContextFile; + result.KeyStoreFile = settings.KeyStoreFile ?? result.KeyStoreFile; result.Settings = settings.Settings; bool updateSettings = false; if (!settings.Settings.ContainsKey("InstallationId")) diff --git a/src/Accounts/Authentication/ContextAutosaveSettings.cs b/src/Accounts/Authentication/ContextAutosaveSettings.cs index 88a249f053a4..2e15743aef85 100644 --- a/src/Accounts/Authentication/ContextAutosaveSettings.cs +++ b/src/Accounts/Authentication/ContextAutosaveSettings.cs @@ -51,6 +51,11 @@ public class ContextAutosaveSettings : IExtensibleSettings /// public string CacheFile { get; set; } + /// + /// The name of the keystore file + /// + public string KeyStoreFile { get; set; } + /// /// Extensible settings for autosave diff --git a/src/Accounts/Authentication/Factories/AuthenticationFactory.cs b/src/Accounts/Authentication/Factories/AuthenticationFactory.cs index 623896306e5a..0d69336550bf 100644 --- a/src/Accounts/Authentication/Factories/AuthenticationFactory.cs +++ b/src/Accounts/Authentication/Factories/AuthenticationFactory.cs @@ -573,14 +573,29 @@ private AuthenticationParameters GetAuthenticationParameters( password = password ?? account.GetProperty(AzureAccount.Property.ServicePrincipalSecret)?.ConvertToSecureString(); if (password == null) { - password = KeyStore.GetKey(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret - , account.Id, tenant)); + try + { + password = KeyStore.GetKey(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret +, account.Id, tenant)); + } + catch + { + password = null; + } + } var certificatePassword = account.GetProperty(AzureAccount.Property.CertificatePassword)?.ConvertToSecureString(); if (certificatePassword == null) { - certificatePassword = KeyStore.GetKey(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword - , account.Id, tenant)); + try + { + certificatePassword = KeyStore.GetKey(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword + , account.Id, tenant)); + } + catch + { + certificatePassword = null; + } } return new ServicePrincipalParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.Id, account.GetProperty(AzureAccount.Property.CertificateThumbprint), account.GetProperty(AzureAccount.Property.CertificatePath), certificatePassword, password, sendCertificateChain); diff --git a/src/Accounts/Authentication/KeyStore/AzKeyStore.cs b/src/Accounts/Authentication/KeyStore/AzKeyStore.cs index 609eab21b1f0..4dc7ccf1a716 100644 --- a/src/Accounts/Authentication/KeyStore/AzKeyStore.cs +++ b/src/Accounts/Authentication/KeyStore/AzKeyStore.cs @@ -11,38 +11,140 @@ // See the License for the specific language governing permissions and // limitations under the License. // ---------------------------------------------------------------------------------- - -using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core; -using Microsoft.Azure.Commands.Common.Authentication.Abstractions; -using Microsoft.WindowsAzure.Commands.Common; +using Newtonsoft.Json; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Security; +using System.Text; namespace Microsoft.Azure.Commands.ResourceManager.Common { public class AzKeyStore : IDisposable { public const string Name = "AzKeyStore"; - private static readonly IDictionary _credentials = new Dictionary(); - [Obsolete("The constructor is deprecated. Will read key from encryted storage later.", false)] - public AzKeyStore(IAzureContextContainer profile) + internal class KeyStoreElement + { + public string keyType; + public string keyStoreKey; + public string valueType; + public string keyStoreValue; + } + + private static IDictionary _typeNameMap = new ConcurrentDictionary(); + + private static IDictionary _elementConverterMap = new ConcurrentDictionary(); + + public static void RegisterJsonConverter(Type type, string typeName, JsonConverter converter = null) + { + if (string.IsNullOrEmpty(typeName)) + { + throw new ArgumentNullException($"typeName cannot be empty."); + } + if (_typeNameMap.ContainsKey(type)) + { + if (string.Compare(_typeNameMap[type], typeName) != 0) + { + throw new ArgumentException($"{typeName} has conflict with {_typeNameMap[type]} with reference to {type}."); + } + } + else + { + _typeNameMap[type] = typeName; + } + if (converter != null) + { + _elementConverterMap[_typeNameMap[type]] = converter; + } + } + + private IDictionary _credentials = new ConcurrentDictionary(); + private IStorage _storage = null; + + private bool autoSave = true; + private Exception lastError = null; + + public IStorage Storage + { + get => _storage; + set => _storage = value; + } + + public AzKeyStore() + { + + } + + public AzKeyStore(string directory, string fileName, bool loadStorage = true, bool autoSaveEnabled = true, IStorage inputStorage = null) + { + autoSave = autoSaveEnabled; + Storage = inputStorage ?? new StorageWrapper() + { + FileName = fileName, + Directory = directory + }; + Storage.Create(); + + if (loadStorage&&!LoadStorage()) + { + throw new InvalidOperationException("Failed to load keystore from storage."); + } + } + + private object Deserialize(string typeName, string value) { - if (profile != null && profile.Accounts != null) + Type t = null; + t = _typeNameMap.FirstOrDefault(item => item.Value == typeName).Key; + + if (t != null) + { + if (_elementConverterMap.ContainsKey(typeName)) + { + return JsonConvert.DeserializeObject(value, t, _elementConverterMap[typeName]); + } + else + { + return JsonConvert.DeserializeObject(value, t); + } + } + return null; + } + + public bool LoadStorage() + { + try { - foreach (var account in profile.Accounts) + var data = Storage.ReadData(); + if (data != null && data.Length > 0) { - if (account != null && account.ExtendedProperties.ContainsKey(AzureAccount.Property.ServicePrincipalSecret)) + var rawJsonString = Encoding.UTF8.GetString(data); + var serializableKeyStore = JsonConvert.DeserializeObject(rawJsonString, typeof(List)) as List; + if (serializableKeyStore != null) { - IKeyStoreKey keyStoreKey = new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id - , account.GetTenants().FirstOrDefault()); - var servicePrincipalSecret = account.ExtendedProperties[AzureAccount.Property.ServicePrincipalSecret]; - _credentials[keyStoreKey] = servicePrincipalSecret.ConvertToSecureString(); + foreach (var item in serializableKeyStore) + { + IKeyStoreKey keyStoreKey = Deserialize(item.keyType, item.keyStoreKey) as IKeyStoreKey; + if (keyStoreKey == null) + { + throw new ArgumentException($"Cannot parse the keystore {item.keyStoreKey} with the type {item.keyType}."); + } + var keyStoreValue = Deserialize(item.valueType, item.keyStoreValue); + if (keyStoreValue == null) + { + throw new ArgumentException($"Cannot parse the keystore {item.keyStoreValue} with the type {item.valueType}."); + } + _credentials[keyStoreKey] = keyStoreValue; + } } } } + catch (Exception e) + { + lastError = e; + return false; + } + return true; } public void ClearCache() @@ -50,28 +152,76 @@ public void ClearCache() _credentials.Clear(); } + public void Flush() + { + IList serializableKeyStore = new List(); + foreach (var item in _credentials) + { + var keyType = _typeNameMap[item.Key.GetType()]; + var key = _elementConverterMap.ContainsKey(keyType) ? + JsonConvert.SerializeObject(item.Key, _elementConverterMap[keyType]) : JsonConvert.SerializeObject(item.Key); + if (!string.IsNullOrEmpty(key)) + { + var valueType = _typeNameMap[item.Value.GetType()]; + serializableKeyStore.Add(new KeyStoreElement() + { + keyType = keyType, + keyStoreKey = key, + valueType = valueType, + keyStoreValue = _elementConverterMap.ContainsKey(valueType) ? + JsonConvert.SerializeObject(item.Value, _elementConverterMap[valueType]) : JsonConvert.SerializeObject(item.Value), + }) ; + } + } + var JsonString = JsonConvert.SerializeObject(serializableKeyStore); + Storage.WriteData(Encoding.UTF8.GetBytes(JsonString)); + } + public void Dispose() { + if (autoSave) + { + Flush(); + } ClearCache(); } - public void SaveKey(IKeyStoreKey key, SecureString value) + public void SaveKey(IKeyStoreKey key, T value) { + if (!_typeNameMap.ContainsKey(key.GetType()) || !_typeNameMap.ContainsKey(value.GetType())) + { + throw new InvalidOperationException("Please register key & values type before save it."); + } _credentials[key] = value; } - public SecureString GetKey(IKeyStoreKey key) + public T GetKey(IKeyStoreKey key) { - if (_credentials.ContainsKey(key)) + if (!_credentials.ContainsKey(key)) { - return _credentials[key]; + throw new ArgumentException($"{key.ToString()} is not stored in AzKeyStore yet."); } - return null; + return (T)_credentials[key]; } public bool DeleteKey(IKeyStoreKey key) { return _credentials.Remove(key); } + + public void EnableAutoSaving() + { + autoSave = true; + } + + public void DisableAutoSaving() + { + autoSave = false; + } + + public Exception GetLastError() + { + return lastError; + } } } diff --git a/src/Accounts/Authentication/KeyStore/IStorage.cs b/src/Accounts/Authentication/KeyStore/IStorage.cs new file mode 100644 index 000000000000..146a6404752a --- /dev/null +++ b/src/Accounts/Authentication/KeyStore/IStorage.cs @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- +using System; + +namespace Microsoft.Azure.Commands.ResourceManager.Common +{ + public interface IStorage + { + IStorage Create(); + + void Clear(); + + byte[] ReadData(); + + void VerifyPersistence(); + + void WriteData(byte[] data); + + Exception GetLastError(); + } +} diff --git a/src/Accounts/Authentication/KeyStore/SecureStringConverter.cs b/src/Accounts/Authentication/KeyStore/SecureStringConverter.cs new file mode 100644 index 000000000000..c399dfacd424 --- /dev/null +++ b/src/Accounts/Authentication/KeyStore/SecureStringConverter.cs @@ -0,0 +1,42 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +using Microsoft.WindowsAzure.Commands.Common; +using Newtonsoft.Json; +using System; +using System.Security; + +namespace Microsoft.Azure.Commands.ResourceManager.Common +{ + public class SecureStringConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType.IsAssignableFrom(typeof(SecureString)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var plaintext = reader.Value as string; + return plaintext?.ConvertToSecureString(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var plaintext = (value as SecureString)?.ConvertToString(); + if (!string.IsNullOrEmpty(plaintext)) + { + serializer.Serialize(writer, plaintext); + } + } + } +} \ No newline at end of file diff --git a/src/Accounts/Authentication/KeyStore/ServicePrincipalKeyConverter.cs b/src/Accounts/Authentication/KeyStore/ServicePrincipalKeyConverter.cs new file mode 100644 index 000000000000..cb30a205d030 --- /dev/null +++ b/src/Accounts/Authentication/KeyStore/ServicePrincipalKeyConverter.cs @@ -0,0 +1,41 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace Microsoft.Azure.Commands.ResourceManager.Common +{ + public class ServicePrincipalKeyConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType.IsAssignableFrom(typeof(ServicePrincipalKey)); + } + + public override bool CanWrite => false; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jObject = serializer.Deserialize(reader); + var result = JsonConvert.DeserializeObject(jObject.ToString()); + return result; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Accounts/Authentication/KeyStore/StorageWrapper.cs b/src/Accounts/Authentication/KeyStore/StorageWrapper.cs new file mode 100644 index 000000000000..a2f9e5f80aff --- /dev/null +++ b/src/Accounts/Authentication/KeyStore/StorageWrapper.cs @@ -0,0 +1,138 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- +using Microsoft.Azure.Commands.Common.Authentication.Properties; +using Microsoft.Identity.Client.Extensions.Msal; +using System; +using System.Threading; + +namespace Microsoft.Azure.Commands.ResourceManager.Common +{ + class StorageWrapper : IStorage + { + private const string KeyChainServiceName = "Microsoft.Azure.PowerShell"; + + public string FileName { get; set; } + public string Directory { get; set; } + + private Exception _lastError; + + private Storage _storage = null; + + static ReaderWriterLockSlim storageLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + public StorageWrapper() + { + + } + + public IStorage Create() + { + StorageCreationPropertiesBuilder storageProperties = null; + if (!storageLock.TryEnterWriteLock(TimeSpan.Zero)) + { + throw new InvalidOperationException(Resources.StorageLockConflicts); + } + try + { + storageProperties = new StorageCreationPropertiesBuilder(FileName, Directory) + .WithMacKeyChain(KeyChainServiceName + ".other_secrets", FileName) + .WithLinuxUnprotectedFile(); + _storage = Storage.Create(storageProperties.Build()); + VerifyPersistence(); + } + catch (MsalCachePersistenceException e) + { + _lastError = e; + _storage.Clear(); + storageProperties = new StorageCreationPropertiesBuilder(FileName, Directory).WithUnprotectedFile(); + _storage = Storage.Create(storageProperties.Build()); + } + finally + { + storageLock.ExitWriteLock(); + } + return this; + } + + public void Clear() + { + if (!storageLock.TryEnterWriteLock(TimeSpan.Zero)) + { + throw new InvalidOperationException(Resources.StorageLockConflicts); + } + try + { + _storage.Clear(); + } + finally + { + storageLock.ExitWriteLock(); + } + } + + public byte[] ReadData() + { + if (!storageLock.TryEnterReadLock(TimeSpan.Zero)) + { + throw new InvalidOperationException(Resources.StorageLockConflicts); + } + try + { + return _storage.ReadData(); + } + finally + { + storageLock.ExitReadLock(); + } + } + + public void VerifyPersistence() + { + if (!storageLock.TryEnterWriteLock(TimeSpan.Zero)) + { + throw new InvalidOperationException(Resources.StorageLockConflicts); + } + try + { + _storage.VerifyPersistence(); + } + finally + { + storageLock.ExitWriteLock(); + } + } + + public void WriteData(byte[] data) + { + if (!storageLock.TryEnterWriteLock(TimeSpan.Zero)) + { + throw new InvalidOperationException(Resources.StorageLockConflicts); + } + + try + { + _storage.WriteData(data); + } + finally + { + storageLock.ExitWriteLock(); + } + } + + public Exception GetLastError() + { + return _lastError; + } + } +} diff --git a/src/Accounts/Authentication/Properties/Resources.Designer.cs b/src/Accounts/Authentication/Properties/Resources.Designer.cs index 25b28d44d420..d092662465e8 100644 --- a/src/Accounts/Authentication/Properties/Resources.Designer.cs +++ b/src/Accounts/Authentication/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.Azure.Commands.Common.Authentication.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -600,6 +600,15 @@ public static string SPNTokenExpirationCheckTrace { } } + /// + /// Looks up a localized string similar to AzKeyStore is locked. Please try again after a while.. + /// + public static string StorageLockConflicts { + get { + return ResourceManager.GetString("StorageLockConflicts", resourceCulture); + } + } + /// /// Looks up a localized string similar to The subscription id {0} doesn't exist.. /// diff --git a/src/Accounts/Authentication/Properties/Resources.resx b/src/Accounts/Authentication/Properties/Resources.resx index 73580b970175..2c428126f735 100644 --- a/src/Accounts/Authentication/Properties/Resources.resx +++ b/src/Accounts/Authentication/Properties/Resources.resx @@ -373,4 +373,7 @@ When enabled, Azure PowerShell cmdlets send telemetry data to Microsoft to improve the customer experience. For more information, see our privacy statement: https://aka.ms/privacy + + AzKeyStore is locked. Please try again after a while. + \ No newline at end of file