diff --git a/BitPay/KeyUtils.cs b/BitPay/KeyUtils.cs
index b4b1054..1efe3e3 100644
--- a/BitPay/KeyUtils.cs
+++ b/BitPay/KeyUtils.cs
@@ -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);
@@ -327,5 +327,36 @@ private static string GetKeyStringFromFile(string filename)
return line;
}
}
+
+ ///
+ /// Creates (or overwrites) a file that only its owner can read and write.
+ ///
+ ///
+ /// 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.
+ ///
+ 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);
+ }
}
}
\ No newline at end of file
diff --git a/BitPaySetup/Program.cs b/BitPaySetup/Program.cs
index 95d8287..659aecc 100644
--- a/BitPaySetup/Program.cs
+++ b/BitPaySetup/Program.cs
@@ -828,7 +828,7 @@ 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);
@@ -836,5 +836,37 @@ private static void GenerateConfFile(string newConfFilePath = @"BitPay.config.js
confFilePath = newConfFilePath;
}
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ 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);
+ }
}
}
\ No newline at end of file
diff --git a/BitPayUnitTest/ClientTest.cs b/BitPayUnitTest/ClientTest.cs
index 27c56e6..761a5d7 100644
--- a/BitPayUnitTest/ClientTest.cs
+++ b/BitPayUnitTest/ClientTest.cs
@@ -22,6 +22,7 @@
namespace BitPayUnitTest
{
+ [Collection(KeyUtilsTest.StaticStateCollection)]
public class ClientTest
{
const string Identity = "someIdentity";
diff --git a/BitPayUnitTest/KeyUtilsTest.cs b/BitPayUnitTest/KeyUtilsTest.cs
new file mode 100644
index 0000000..1ab1d0c
--- /dev/null
+++ b/BitPayUnitTest/KeyUtilsTest.cs
@@ -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
+ }
+}