Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion BitPay/KeyUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public static async Task SaveEcKey(EcKey ecKey)
{
Directory.CreateDirectory(Path.GetDirectoryName(PrivateKeyFile)!);
}
using (var fs = new FileStream(PrivateKeyFile!, FileMode.Create, FileAccess.Write))
using (var fs = CreateOwnerOnlyFile(PrivateKeyFile!))
{
#pragma warning disable CA1835
await fs.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
Expand Down Expand Up @@ -327,5 +327,36 @@ private static string GetKeyStringFromFile(string filename)
return line;
}
}

/// <summary>
/// Creates (or overwrites) a file that only its owner can read and write.
/// </summary>
/// <remarks>
/// On Linux and macOS the file gets mode 0600. An existing file is changed to 0600 before it is
/// overwritten, because the create mode only applies to new files. On Windows, and on .NET
/// Framework, the file keeps the permissions it inherits from its folder.
/// </remarks>
private static FileStream CreateOwnerOnlyFile(string path)
{
#if NET7_0_OR_GREATER
if (!OperatingSystem.IsWindows())
{
const UnixFileMode ownerOnly = UnixFileMode.UserRead | UnixFileMode.UserWrite;

if (File.Exists(path))
{
File.SetUnixFileMode(path, ownerOnly);
}

return new FileStream(path, new FileStreamOptions
{
Mode = FileMode.Create,
Access = FileAccess.Write,
UnixCreateMode = ownerOnly,
});
}
#endif
return new FileStream(path, FileMode.Create, FileAccess.Write);
}
}
}
34 changes: 33 additions & 1 deletion BitPaySetup/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -828,13 +828,45 @@ private static void GenerateConfFile(string newConfFilePath = @"BitPay.config.js
}

// serialize JSON directly to a new config file
using (var file = File.CreateText(newConfFilePath))
using (var file = new StreamWriter(CreateOwnerOnlyFile(newConfFilePath)))
{
var serializer = new JsonSerializer {Formatting = Formatting.Indented};
serializer.Serialize(file, appConfig);
}

confFilePath = newConfFilePath;
}

/// <summary>
/// Creates (or overwrites) a file that only its owner can read and write. The config file
/// holds the API tokens, and with the "plain text" option it also holds the private key.
/// </summary>
/// <remarks>
/// On Linux and macOS the file gets mode 0600. An existing file is changed to 0600 before it is
/// overwritten, because the create mode only applies to new files. On Windows, and on .NET
/// Framework, the file keeps the permissions it inherits from its folder.
/// </remarks>
private static FileStream CreateOwnerOnlyFile(string path)
{
#if NET7_0_OR_GREATER
if (!OperatingSystem.IsWindows())
{
const UnixFileMode ownerOnly = UnixFileMode.UserRead | UnixFileMode.UserWrite;

if (File.Exists(path))
{
File.SetUnixFileMode(path, ownerOnly);
}

return new FileStream(path, new FileStreamOptions
{
Mode = FileMode.Create,
Access = FileAccess.Write,
UnixCreateMode = ownerOnly,
});
}
#endif
return new FileStream(path, FileMode.Create, FileAccess.Write);
}
}
}
1 change: 1 addition & 0 deletions BitPayUnitTest/ClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

namespace BitPayUnitTest
{
[Collection(KeyUtilsTest.StaticStateCollection)]
public class ClientTest
{
const string Identity = "someIdentity";
Expand Down
69 changes: 69 additions & 0 deletions BitPayUnitTest/KeyUtilsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2019 BitPay.
// All rights reserved.

using BitPay;

namespace BitPayUnitTest
{
// KeyUtils keeps the key path in a static field, and ClientTest sets it too.
// Tests in the same collection never run in parallel.
[Collection(StaticStateCollection)]
public class KeyUtilsTest : IDisposable
{
public const string StaticStateCollection = "KeyUtils static state";

private readonly string _directory;

public KeyUtilsTest()
{
_directory = Path.Combine(Path.GetTempPath(), "bitpay-keyutils-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_directory);
}

public void Dispose()
{
Directory.Delete(_directory, true);
GC.SuppressFinalize(this);
}

#if NET7_0_OR_GREATER
private const UnixFileMode OwnerOnly = UnixFileMode.UserRead | UnixFileMode.UserWrite;

[Fact]
public async Task it_should_save_the_key_file_readable_only_by_its_owner()
{
if (OperatingSystem.IsWindows())
{
return; // POSIX permissions do not apply on Windows.
}

var path = Path.Combine(_directory, "bitpay_private_test.key");
var key = KeyUtils.CreateEcKey();

KeyUtils.PrivateKeyExists(path);
await KeyUtils.SaveEcKey(key);

Assert.Equal(OwnerOnly, File.GetUnixFileMode(path));
Assert.Equal(key.ToAsn1(), KeyUtils.LoadEcKey().ToAsn1());
}

[Fact]
public async Task it_should_tighten_an_existing_key_file()
{
if (OperatingSystem.IsWindows())
{
return; // POSIX permissions do not apply on Windows.
}

var path = Path.Combine(_directory, "bitpay_private_test.key");
File.WriteAllText(path, "old key");
File.SetUnixFileMode(path, OwnerOnly | UnixFileMode.GroupRead | UnixFileMode.OtherRead);

KeyUtils.PrivateKeyExists(path);
await KeyUtils.SaveEcKey(KeyUtils.CreateEcKey());

Assert.Equal(OwnerOnly, File.GetUnixFileMode(path));
}
#endif
}
}
Loading