API key protection
This commit is contained in:
parent
cd72b7b9aa
commit
38bb7e40ef
7 changed files with 143 additions and 19 deletions
|
|
@ -3,11 +3,14 @@ set -e
|
|||
|
||||
# compiler tout
|
||||
dotnet build -c Release
|
||||
dotnet publish -c Release -f netcoreapp2.1 src/isnd
|
||||
dotnet publish -c Release src/isnd
|
||||
dotnet publish -c Release src/isn
|
||||
# MAJ du serveur
|
||||
sudo systemctl stop isnd
|
||||
sudo cp -a src/isnd/bin/Release/netcoreapp2.1/publish/* /srv/www/isnd
|
||||
sudo cp -a src/isnd/bin/Release/net10.0/publish/* /srv/www/isnd
|
||||
sudo systemctl start isnd
|
||||
# MAJ du client
|
||||
sudo cp -a src/isn/bin/Release/netcoreapp2.1/* /usr/local/lib/isn
|
||||
sudo chown -R root.root /usr/local/lib/isn
|
||||
sudo cp -a src/isn/bin/Release/net10.0/* /usr/local/lib/isn
|
||||
sudo chown -R root:root /usr/local/lib/isn
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Isn
|
|||
|
||||
public static void LoadConfig()
|
||||
{
|
||||
FileInfo cfgSettingIf = new FileInfo(_configFileName);
|
||||
FileInfo cfgSettingIf = new FileInfo(ConfigFileName);
|
||||
if (cfgSettingIf.Exists)
|
||||
{
|
||||
var json = File.ReadAllText(cfgSettingIf.FullName);
|
||||
|
|
@ -29,12 +29,13 @@ namespace Isn
|
|||
{ "h|help", "show this message and exit", h => shouldShowPushHelp = h != null },
|
||||
};
|
||||
|
||||
private static readonly string _configFileName =
|
||||
Path.Combine(
|
||||
Path.Combine(Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.UserProfile), ".isn"),
|
||||
"config.json")
|
||||
;
|
||||
private static string ConfigDirectory =>
|
||||
Environment.GetEnvironmentVariable("ISN_CONFIG_DIR")
|
||||
?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
".isn");
|
||||
|
||||
private static string ConfigFileName => Path.Combine(ConfigDirectory, "config.json");
|
||||
|
||||
public const string push = "push";
|
||||
static readonly OptionSet options = new OptionSet {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ namespace Isn
|
|||
}
|
||||
public static void SaveConfig()
|
||||
{
|
||||
FileInfo cfgSettingIf = new FileInfo(_configFileName);
|
||||
FileInfo cfgSettingIf = new FileInfo(ConfigFileName);
|
||||
if (!cfgSettingIf.Directory.Exists) cfgSettingIf.Directory.Create();
|
||||
File.WriteAllText(
|
||||
cfgSettingIf.FullName,
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ namespace isnd.Controllers
|
|||
try
|
||||
{
|
||||
var clientVersionId = Request.Headers["X-NuGet-Client-Version"];
|
||||
string apiKey = Request.Headers["X-NuGet-ApiKey"][0];
|
||||
string apiKey = Request.Headers["X-NuGet-ApiKey"].FirstOrDefault() ?? string.Empty;
|
||||
ViewData["versionId"] = typeof(PackagesController).Assembly.FullName;
|
||||
var files = new List<string>();
|
||||
ViewData["files"] = files;
|
||||
|
||||
var clearkey = protector.Unprotect(apiKey);
|
||||
var clearkey = ApiKeyProtector.UnprotectWithFallback(protector, apiKey);
|
||||
var apikey = dbContext.ApiKeys.SingleOrDefault(k => k.Id == clearkey);
|
||||
if (apikey == null)
|
||||
{
|
||||
|
|
|
|||
50
src/isnd/Helpers/ApiKeyProtector.cs
Normal file
50
src/isnd/Helpers/ApiKeyProtector.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace isnd.Helpers
|
||||
{
|
||||
public static class ApiKeyProtector
|
||||
{
|
||||
private const byte LegacyDelta = 145;
|
||||
|
||||
public static string UnprotectWithFallback(IDataProtector protector, string protectedValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(protectedValue))
|
||||
{
|
||||
return protectedValue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return protector.Unprotect(protectedValue);
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
return UnprotectLegacy(protectedValue);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return UnprotectLegacy(protectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UnprotectLegacy(string protectedValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(protectedValue))
|
||||
{
|
||||
return protectedValue;
|
||||
}
|
||||
|
||||
var bytes = Convert.FromBase64String(protectedValue);
|
||||
var unprotectedBytes = new byte[bytes.Length];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
unprotectedBytes[index] = (byte)(bytes[index] ^ LegacyDelta);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(unprotectedBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ namespace Isn.tests
|
|||
{
|
||||
private readonly HttpListener listener;
|
||||
private readonly Task listenerTask;
|
||||
private readonly string configDirectory;
|
||||
private readonly string originalConfigDirectory;
|
||||
|
||||
public LocalSourceFixture()
|
||||
{
|
||||
|
|
@ -64,6 +66,11 @@ namespace Isn.tests
|
|||
}
|
||||
});
|
||||
|
||||
configDirectory = Path.Combine(Path.GetTempPath(), $"isn-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(configDirectory);
|
||||
originalConfigDirectory = Environment.GetEnvironmentVariable("ISN_CONFIG_DIR");
|
||||
Environment.SetEnvironmentVariable("ISN_CONFIG_DIR", configDirectory);
|
||||
|
||||
SourceUrl = prefix + "index.json";
|
||||
ConfigureIsnSettings(SourceUrl);
|
||||
Program.LoadConfig();
|
||||
|
|
@ -84,6 +91,12 @@ namespace Isn.tests
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("ISN_CONFIG_DIR", originalConfigDirectory);
|
||||
if (Directory.Exists(configDirectory))
|
||||
{
|
||||
Directory.Delete(configDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
|
|
@ -95,12 +108,8 @@ namespace Isn.tests
|
|||
return port;
|
||||
}
|
||||
|
||||
private static void ConfigureIsnSettings(string sourceUrl)
|
||||
private void ConfigureIsnSettings(string sourceUrl)
|
||||
{
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var configDirectory = Path.Combine(home, ".isn");
|
||||
Directory.CreateDirectory(configDirectory);
|
||||
|
||||
var configPath = Path.Combine(configDirectory, "config.json");
|
||||
var settings = new Settings
|
||||
{
|
||||
|
|
@ -125,6 +134,44 @@ namespace Isn.tests
|
|||
this.fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsIsolatedConfigDirectoryOverride()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"isn-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var originalConfigDir = Environment.GetEnvironmentVariable("ISN_CONFIG_DIR");
|
||||
var configFile = Path.Combine(tempDir, "config.json");
|
||||
var expectedSource = "https://example.test/index.json";
|
||||
var settings = new Settings
|
||||
{
|
||||
DataProtectionTitle = "isn",
|
||||
Sources = new Dictionary<string, SourceSettings>
|
||||
{
|
||||
[expectedSource] = new SourceSettings { Alias = "override" }
|
||||
},
|
||||
DefaultSourceKey = expectedSource
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ISN_CONFIG_DIR", tempDir);
|
||||
File.WriteAllText(configFile, JsonConvert.SerializeObject(settings, Formatting.Indented));
|
||||
|
||||
Program.LoadConfig();
|
||||
|
||||
Assert.Equal(expectedSource, Program.Settings.DefaultSourceKey);
|
||||
Assert.True(Program.Settings.Sources.ContainsKey(expectedSource));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ISN_CONFIG_DIR", originalConfigDir);
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaveADefaultDataProtector()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ using System.Threading.Tasks;
|
|||
using NuGet.Protocol.Core.Types;
|
||||
using NuGet.Common;
|
||||
using Isn.Abstract;
|
||||
using isnd.Helpers;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace isnd.host.tests
|
||||
{
|
||||
|
|
@ -49,6 +52,17 @@ namespace isnd.host.tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyApiKeyProtectorCanUnprotectValuesFromOlderClients()
|
||||
{
|
||||
const string clearValue = "legacy-api-key";
|
||||
var legacyProtectedValue = new Isn.DefaultDataProtector().Protect(clearValue);
|
||||
|
||||
var unprotected = ApiKeyProtector.UnprotectWithFallback(new ThrowingProtector(), legacyProtectedValue);
|
||||
|
||||
Assert.Equal(clearValue, unprotected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
void TestDropUser()
|
||||
{
|
||||
|
|
@ -117,5 +131,14 @@ namespace isnd.host.tests
|
|||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private sealed class ThrowingProtector : IDataProtector
|
||||
{
|
||||
public byte[] Protect(byte[] plaintext) => throw new CryptographicException("unexpected protect call");
|
||||
|
||||
public byte[] Unprotect(byte[] protectedData) => throw new CryptographicException("unexpected unprotect call");
|
||||
|
||||
public IDataProtector CreateProtector(string purpose) => this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue