Skip to content

Commit a51bfec

Browse files
committed
Add decryptionKey to a snapshot
1 parent 7430899 commit a51bfec

File tree

5 files changed

+48
-31
lines changed

5 files changed

+48
-31
lines changed

src/WebJobs.Script.WebHost/Properties/Resources.Designer.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/WebJobs.Script.WebHost/Properties/Resources.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,10 @@
280280
<value>Master key {0}</value>
281281
</data>
282282
<data name="TraceNonDecryptedFunctionSecretRefresh" xml:space="preserve">
283-
<value>Non-decryptable function ('{0}') secrets detected. Refreshing secrets.</value>
283+
<value>Non-decryptable function ('{0}') secrets detected. Refreshing secrets. Exception: {1}.</value>
284284
</data>
285285
<data name="TraceNonDecryptedHostSecretRefresh" xml:space="preserve">
286-
<value>Non-decryptable host secrets detected. Refreshing secrets.</value>
286+
<value>Non-decryptable host secrets detected. Refreshing secrets. Exception: {0}.</value>
287287
</data>
288288
<data name="TraceSecretDeleted" xml:space="preserve">
289289
<value>{0} secret '{1}' deleted.</value>

src/WebJobs.Script.WebHost/Security/KeyManagement/ScriptSecrets.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections;
65
using System.Collections.Generic;
7-
using System.Globalization;
86
using System.Linq;
9-
using System.Text;
10-
using System.Threading.Tasks;
117
using Microsoft.Azure.WebJobs.Script.Config;
128
using Newtonsoft.Json;
139

@@ -37,6 +33,9 @@ protected ScriptSecrets()
3733
[JsonProperty(PropertyName = "source")]
3834
public string Source { get; set; }
3935

36+
[JsonProperty(PropertyName = "decryptionKeyId")]
37+
public string DecryptionKeyId { get; set; }
38+
4039
protected abstract ICollection<Key> GetKeys(string keyScope);
4140

4241
public abstract ScriptSecrets Refresh(IKeyValueConverterFactory factory);

src/WebJobs.Script.WebHost/Security/KeyManagement/SecretManager.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.IO;
99
using System.Linq;
1010
using System.Security.Cryptography;
11+
using System.Text;
1112
using System.Threading;
1213
using System.Threading.Tasks;
1314
using Microsoft.Azure.KeyVault.Models;
@@ -98,9 +99,10 @@ public async virtual Task<HostSecretsInfo> GetHostSecretsAsync()
9899
// so we read the secrets running them through the appropriate readers
99100
hostSecrets = ReadHostSecrets(hostSecrets);
100101
}
101-
catch (CryptographicException)
102+
catch (CryptographicException ex)
102103
{
103-
_logger?.LogDebug(Resources.TraceNonDecryptedHostSecretRefresh);
104+
string message = string.Format(Resources.TraceNonDecryptedHostSecretRefresh, ex);
105+
_logger?.LogDebug(message);
104106
await PersistSecretsAsync(hostSecrets, null, true);
105107
hostSecrets = GenerateHostSecrets(hostSecrets);
106108
await RefreshSecretsAsync(hostSecrets);
@@ -162,9 +164,9 @@ public async virtual Task<IDictionary<string, string>> GetFunctionSecretsAsync(s
162164
// Read all secrets, which will run the keys through the appropriate readers
163165
secrets.Keys = secrets.Keys.Select(k => _keyValueConverterFactory.ReadKey(k)).ToList();
164166
}
165-
catch (CryptographicException)
167+
catch (CryptographicException ex)
166168
{
167-
string message = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName);
169+
string message = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName, ex);
168170
_logger?.LogDebug(message);
169171
await PersistSecretsAsync(secrets, functionName, true);
170172
secrets = GenerateFunctionSecrets(secrets);
@@ -451,6 +453,14 @@ private async Task PersistSecretsAsync<T>(T secrets, string keyScope = null, boo
451453
ScriptSecretsType secretsType = secrets.SecretsType;
452454
if (isNonDecryptable)
453455
{
456+
string decryptionKey = SystemEnvironment.Instance.GetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey);
457+
if (!string.IsNullOrEmpty(decryptionKey))
458+
{
459+
SHA256Managed hash = new SHA256Managed();
460+
byte[] hashBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(decryptionKey));
461+
secrets.DecryptionKeyId = Convert.ToBase64String(hashBytes);
462+
}
463+
454464
string[] secretBackups = await _repository.GetSecretSnapshots(secrets.SecretsType, keyScope);
455465

456466
if (secretBackups.Length >= ScriptConstants.MaximumSecretBackupCount)

test/WebJobs.Script.Tests/Security/SecretManagerTests.cs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,15 @@ public async Task GetHostSecrets_WhenNonDecryptedHostSecrets_SavesAndRefreshes()
507507
[Fact]
508508
public async Task GetFunctiontSecrets_WhenNonDecryptedSecrets_SavesAndRefreshes()
509509
{
510-
using (var directory = new TempDirectory())
510+
string key = TestHelpers.GenerateKeyHexString();
511+
using (new TestScopedEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, key))
511512
{
512-
string functionName = "testfunction";
513-
string expectedTraceMessage = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName);
514-
string functionSecretsJson =
515-
@"{
513+
using (var directory = new TempDirectory())
514+
{
515+
string functionName = "testfunction";
516+
string expectedTraceMessage = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName, string.Empty);
517+
string functionSecretsJson =
518+
@"{
516519
'keys': [
517520
{
518521
'name': 'Key1',
@@ -526,21 +529,27 @@ public async Task GetFunctiontSecrets_WhenNonDecryptedSecrets_SavesAndRefreshes(
526529
}
527530
]
528531
}";
529-
File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson);
530-
Mock<IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(true, false);
531-
IDictionary<string, string> functionSecrets;
532-
ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path);
532+
File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson);
533+
Mock<IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(true, false);
534+
IDictionary<string, string> functionSecrets;
535+
ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path);
533536

534-
using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, null, new TestMetricsLogger()))
535-
{
536-
functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName);
537-
}
537+
using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, null, new TestMetricsLogger()))
538+
{
539+
functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName);
540+
}
538541

539-
Assert.NotNull(functionSecrets);
540-
Assert.NotEqual(functionSecrets["Key1"], "cryptoError");
541-
var result = JsonConvert.DeserializeObject<FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, functionName + ".json")));
542-
Assert.Equal(result.GetFunctionKey("Key1", functionName).Value, "!" + functionSecrets["Key1"]);
543-
Assert.Equal(1, Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*").Length);
542+
Assert.NotNull(functionSecrets);
543+
Assert.NotEqual(functionSecrets["Key1"], "cryptoError");
544+
var result = JsonConvert.DeserializeObject<FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, functionName + ".json")));
545+
Assert.Equal(result.GetFunctionKey("Key1", functionName).Value, "!" + functionSecrets["Key1"]);
546+
Assert.Equal(1, Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*").Length);
547+
548+
result = JsonConvert.DeserializeObject<FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, functionName + ".json")));
549+
string snapShotFileName = Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*")[0];
550+
result = JsonConvert.DeserializeObject<FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, snapShotFileName)));
551+
Assert.NotEqual(result.DecryptionKeyId, key);
552+
}
544553
}
545554
}
546555

@@ -780,7 +789,6 @@ public async Task GetFunctiontSecrets_AddsMetrics()
780789
using (var directory = new TempDirectory())
781790
{
782791
string functionName = "testfunction";
783-
string expectedTraceMessage = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName);
784792
string functionSecretsJson =
785793
@"{
786794
'keys': [

0 commit comments

Comments
 (0)