diff --git a/Beacon.Sdk.Tests/Beacon.Sdk.Tests.csproj b/Beacon.Sdk.Tests/Beacon.Sdk.Tests.csproj index ad1198d..37dec69 100644 --- a/Beacon.Sdk.Tests/Beacon.Sdk.Tests.csproj +++ b/Beacon.Sdk.Tests/Beacon.Sdk.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/Beacon.Sdk.Tests/CryptoTests.cs b/Beacon.Sdk.Tests/CryptoTests.cs index 12b927f..6235e9e 100644 --- a/Beacon.Sdk.Tests/CryptoTests.cs +++ b/Beacon.Sdk.Tests/CryptoTests.cs @@ -1,21 +1,24 @@ namespace Beacon.Sdk.Tests { + using System.Text; + using System; using Core.Infrastructure.Cryptography; using Core.Infrastructure.Cryptography.NaCl; using Core.Infrastructure.Cryptography.BouncyCastle; using Microsoft.VisualStudio.TestTools.UnitTesting; - using System.Text; [TestClass] public class CryptoTests { - private byte[] randomPubKeyBytes { get; } = + const int SeedSize = 32; + + private byte[] RandomPubKeyBytes { get; } = { 0x58, 0x96, 0xB4, 0xDE, 0x36, 0x93, 0x5F, 0x5, 0x79, 0x4E, 0xB8, 0x7, 0x3F, 0xB1, 0x7F, 0xE6, 0xAD, 0x64, 0xFF, 0xE3, 0x48, 0x60, 0x81, 0x1C, 0x9D, 0xDF, 0xB9, 0xE4, 0xF3, 0x2F, 0xF7, 0x5B, }; - private byte[] convertedPubKeyBytes { get; } = + private byte[] ConvertedPubKeyBytes { get; } = { 0x50, 0x94, 0xD3, 0xF3, 0x88, 0xFD, 0x19, 0xF3, 0x3D, 0xBF, 0xA7, 0x3A, 0x6C, 0x9E, 0xCD, 0x80, 0x7C, 0x1C, 0x92, 0x74, 0xE3, 0xA0, 0x71, 0x8F, 0xEC, 0xB8, 0xDD, 0x8D, 0x3E, 0x78, 0x66, 0x15 @@ -24,23 +27,24 @@ public class CryptoTests [TestMethod] public void TestSodiumConvertEd25519PublicKeyToCurve25519PublicKey() { - var actual = PublicKeyAuth.ConvertEd25519PublicKeyToCurve25519PublicKey(randomPubKeyBytes); - CollectionAssert.AreEqual(convertedPubKeyBytes, actual); + var actual = PublicKeyAuth.ConvertEd25519PublicKeyToCurve25519PublicKey(RandomPubKeyBytes); + CollectionAssert.AreEqual(ConvertedPubKeyBytes, actual); } [TestMethod] public void TestConvertEd25519PublicKeyToCurve25519PublicKey() { var actual = new byte[32]; - MontgomeryCurve25519.EdwardsToMontgomery(actual, randomPubKeyBytes); - CollectionAssert.AreEqual(convertedPubKeyBytes, actual); + MontgomeryCurve25519.EdwardsToMontgomery(actual, RandomPubKeyBytes); + CollectionAssert.AreEqual(ConvertedPubKeyBytes, actual); } [TestMethod] public void CanEncryptAndDecryptBySecretBox() { - var key = SecureRandom.GetRandomBytes(32); - var nonce = SecureRandom.GetRandomBytes(24); + var random = new Random(Seed:0); + var key = random.GetBytes(32); + var nonce = random.GetBytes(24); var message = Encoding.UTF8.GetBytes("Test message for secret box"); var cipher = SecretBox.Create(message, nonce, key); @@ -53,7 +57,8 @@ public void CanEncryptAndDecryptBySecretBox() [TestMethod] public void CanEncryptAndDecryptBySealedSecretBox() { - var seed = SecureRandom.GetRandomBytes(32); + var random = new Random(Seed: 0); + var seed = random.GetBytes(SeedSize); var ed25519keyPair = PublicKeyAuth.GenerateKeyPair(seed); @@ -68,5 +73,214 @@ public void CanEncryptAndDecryptBySealedSecretBox() CollectionAssert.AreEqual(message, decrypted); } + + [TestMethod] + public void CanGenerateKeyPairLikeSodium() + { + Sodium.Initialize(); + var random = new Random(Seed: 0); + + const int Iterations = 10000; + + for (var i = 0; i < Iterations; ++i) + { + var seed = random.GetBytes(SeedSize); + + var keyPair = PublicKeyAuth.GenerateKeyPair(seed); + + Assert.IsNotNull(keyPair); + + var sodiumPublicKey = new byte[Sodium.crypto_sign_ed25519_PUBLICKEYBYTES]; + var sodiumPrivateKey = new byte[Sodium.crypto_sign_ed25519_SECRETKEYBYTES]; + + var _ = Sodium.crypto_sign_ed25519_seed_keypair(sodiumPublicKey, sodiumPrivateKey, seed); + + CollectionAssert.AreEqual(sodiumPrivateKey, keyPair.PrivateKey); + CollectionAssert.AreEqual(sodiumPublicKey, keyPair.PublicKey); + } + } + + [TestMethod] + public void CanConvertEd25519PublicKeyToCurve25519PublicKeyLikeSodium() + { + Sodium.Initialize(); + var random = new Random(Seed: 0); + + const int Iterations = 10000; + + for (var i = 0; i < Iterations; ++i) + { + var seed = random.GetBytes(SeedSize); + var keyPair = PublicKeyAuth.GenerateKeyPair(seed); + var curve25519PublicKey = Ed25519Extensions.ConvertEd25519PublicKeyToCurve25519PublicKey(keyPair.PublicKey); + + Assert.IsNotNull(curve25519PublicKey); + + var buffer = new byte[Sodium.crypto_scalarmult_curve25519_BYTES]; + var _ = Sodium.crypto_sign_ed25519_pk_to_curve25519(buffer, keyPair.PublicKey); + + CollectionAssert.AreEqual(buffer, curve25519PublicKey); + } + } + + [TestMethod] + public void CanConvertEd25519PrivateKeyToCurve25519PrivateKeyLikeSodium() + { + Sodium.Initialize(); + var random = new Random(Seed: 0); + + const int Iterations = 10000; + + for (var i = 0; i < Iterations; ++i) + { + var seed = random.GetBytes(SeedSize); + var keyPair = PublicKeyAuth.GenerateKeyPair(seed); + var curve25519PrivateKey = Ed25519Extensions.ConvertEd25519SecretKeyToCurve25519SecretKey(keyPair.PrivateKey); + + Assert.IsNotNull(curve25519PrivateKey); + + var buffer = new byte[Sodium.crypto_scalarmult_curve25519_SCALARBYTES]; + var _ = Sodium.crypto_sign_ed25519_sk_to_curve25519(buffer, keyPair.PrivateKey); + + CollectionAssert.AreEqual(buffer, curve25519PrivateKey); + } + } + + [TestMethod] + public void CanCalculateGenericHashLikeSodium() + { + Sodium.Initialize(); + var random = new Random(Seed: 0); + + for (var messageSizeInBytes = 0; messageSizeInBytes < 1024; ++messageSizeInBytes) + { + for (var hashSizeInBytes = 1; hashSizeInBytes < 32; ++hashSizeInBytes) + { + var message = random.GetBytes(messageSizeInBytes); + var hash = GenericHash.Hash(message, hashSizeInBytes); + + Assert.IsNotNull(hash); + + var buffer = new byte[hashSizeInBytes]; + var _ = Sodium.crypto_generichash(buffer, hashSizeInBytes, message, (ulong)messageSizeInBytes, Array.Empty(), 0); + + CollectionAssert.AreEqual(buffer, hash); + } + } + } + + [TestMethod] + public void CanCreateClientSessionKeyPairLikeSodium() + { + Sodium.Initialize(); + var random = new Random(Seed: 0); + + const int Iterations = 10000; + + for (var i = 0; i < Iterations; ++i) + { + var serverSeed = random.GetBytes(SeedSize); + var clientSeed = random.GetBytes(SeedSize); + + var serverKeyPair = PublicKeyAuth.GenerateKeyPair(serverSeed); + var serverPublicKey = Ed25519Extensions.ConvertEd25519PublicKeyToCurve25519PublicKey(serverKeyPair.PublicKey); + //var serverPrivateKey = Ed25519Extensions.ConvertEd25519SecretKeyToCurve25519SecretKey(serverKeyPair.PrivateKey); + + var clientKeyPair = PublicKeyAuth.GenerateKeyPair(clientSeed); + var clientPublicKey = Ed25519Extensions.ConvertEd25519PublicKeyToCurve25519PublicKey(clientKeyPair.PublicKey); + var clientPrivateKey = Ed25519Extensions.ConvertEd25519SecretKeyToCurve25519SecretKey(clientKeyPair.PrivateKey); + + var sessionKeyPair = KeyExchange.CreateClientSessionKeyPair(clientPublicKey, clientPrivateKey, serverPublicKey); + + var rx = new byte[32]; + var tx = new byte[32]; + + var _ = Sodium.crypto_kx_client_session_keys(rx, tx, clientPublicKey, clientPrivateKey, serverPublicKey); + + var sodiumSessionKeyPair = new SessionKeyPair(rx, tx); + + CollectionAssert.AreEqual(sodiumSessionKeyPair.Rx, sessionKeyPair.Rx); + CollectionAssert.AreEqual(sodiumSessionKeyPair.Tx, sessionKeyPair.Tx); + } + } + + [TestMethod] + public void CanCreateServerSessionKeyPairLikeSodium() + { + Sodium.Initialize(); + var random = new Random(Seed: 0); + + const int Iterations = 10000; + + for (var i = 0; i < Iterations; ++i) + { + var serverSeed = random.GetBytes(SeedSize); + var clientSeed = random.GetBytes(SeedSize); + + var serverKeyPair = PublicKeyAuth.GenerateKeyPair(serverSeed); + var serverPublicKey = Ed25519Extensions.ConvertEd25519PublicKeyToCurve25519PublicKey(serverKeyPair.PublicKey); + var serverPrivateKey = Ed25519Extensions.ConvertEd25519SecretKeyToCurve25519SecretKey(serverKeyPair.PrivateKey); + + var clientKeyPair = PublicKeyAuth.GenerateKeyPair(clientSeed); + var clientPublicKey = Ed25519Extensions.ConvertEd25519PublicKeyToCurve25519PublicKey(clientKeyPair.PublicKey); + //var clientPrivateKey = Ed25519Extensions.ConvertEd25519SecretKeyToCurve25519SecretKey(clientKeyPair.PrivateKey); + + var sessionKeyPair = KeyExchange.CreateServerSessionKeyPair(serverPublicKey, serverPrivateKey, clientPublicKey); + + var rx = new byte[32]; + var tx = new byte[32]; + + var _ = Sodium.crypto_kx_server_session_keys(rx, tx, serverPublicKey, serverPrivateKey, clientPublicKey); + + var sodiumSessionKeyPair = new SessionKeyPair(rx, tx); + + CollectionAssert.AreEqual(sodiumSessionKeyPair.Rx, sessionKeyPair.Rx); + CollectionAssert.AreEqual(sodiumSessionKeyPair.Tx, sessionKeyPair.Tx); + } + } + + [TestMethod] + public void CanCreateAndOpenSealedPublicKeyBoxLikeSodium() + { + const int PublicKeyBytes = 32; + const int MacBytes = 16; + + Sodium.Initialize(); + var random = new Random(Seed: 0); + + const int Iterations = 10; + + for (var i = 0; i < Iterations; ++i) + { + var seed = random.GetBytes(SeedSize); + var keyPair = PublicKeyAuth.GenerateKeyPair(seed); + var publicKey = Ed25519Extensions.ConvertEd25519PublicKeyToCurve25519PublicKey(keyPair.PublicKey); + var privateKey = Ed25519Extensions.ConvertEd25519SecretKeyToCurve25519SecretKey(keyPair.PrivateKey); + + for (var messageSizeInBytes = 0; messageSizeInBytes < 1024; ++messageSizeInBytes) + { + var message = random.GetBytes(messageSizeInBytes); + + // create box + var cipher = SealedPublicKeyBox.Create(message, publicKey); + Assert.IsNotNull(cipher); + + var sodiumCipher = new byte[messageSizeInBytes + PublicKeyBytes + MacBytes]; + _ = Sodium.crypto_box_seal(sodiumCipher, message, (ulong)message.Length, publicKey); + + // cross open box + + // try open sodium cipher by NaCl implementation + var openedMessage = SealedPublicKeyBox.Open(sodiumCipher, privateKey, publicKey); + + // try open NaCl cipher by sodium + var sodiumOpenedMessage = new byte[sodiumCipher.Length - PublicKeyBytes - MacBytes]; + _ = Sodium.crypto_box_seal_open(sodiumOpenedMessage, cipher, (ulong)cipher.Length, publicKey, privateKey); + + CollectionAssert.AreEqual(message, openedMessage); + CollectionAssert.AreEqual(sodiumOpenedMessage, openedMessage); + } + } + } } } \ No newline at end of file diff --git a/Beacon.Sdk.Tests/RandomExtensions.cs b/Beacon.Sdk.Tests/RandomExtensions.cs new file mode 100644 index 0000000..0ca23fb --- /dev/null +++ b/Beacon.Sdk.Tests/RandomExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace Beacon.Sdk.Tests +{ + internal static class RandomExtensions + { + internal static byte[] GetBytes(this Random random, int count) + { + var buffer = new byte[count]; + + random.NextBytes(buffer); + + return buffer; + } + } +} \ No newline at end of file diff --git a/Beacon.Sdk.Tests/Sodium.cs b/Beacon.Sdk.Tests/Sodium.cs new file mode 100644 index 0000000..586e2ae --- /dev/null +++ b/Beacon.Sdk.Tests/Sodium.cs @@ -0,0 +1,235 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Beacon.Sdk.Tests +{ + internal static class Sodium + { + internal const int SODIUM_LIBRARY_VERSION_MAJOR = 10; + internal const int SODIUM_LIBRARY_VERSION_MINOR = 3; + internal const string SODIUM_VERSION_STRING = "1.0.18"; + internal const int crypto_sign_ed25519_BYTES = 64; + internal const int crypto_sign_ed25519_PUBLICKEYBYTES = 32; + internal const int crypto_sign_ed25519_SECRETKEYBYTES = 32 + 32; + internal const int crypto_sign_ed25519_SEEDBYTES = 32; + internal const int crypto_scalarmult_curve25519_BYTES = 32; + internal const int crypto_scalarmult_curve25519_SCALARBYTES = 32; + internal const int crypto_generichash_blake2b_BYTES_MAX = 64; + internal const int crypto_generichash_blake2b_BYTES_MIN = 16; + internal const int crypto_generichash_blake2b_KEYBYTES_MAX = 64; + internal const int crypto_generichash_blake2b_KEYBYTES_MIN = 16; + internal const int crypto_secretbox_xsalsa20poly1305_KEYBYTES = 32; + internal const int crypto_secretbox_xsalsa20poly1305_MACBYTES = 16; + internal const int crypto_secretbox_xsalsa20poly1305_NONCEBYTES = 24; + internal const int crypto_box_curve25519xsalsa20poly1305_MACBYTES = 16; + internal const int crypto_box_curve25519xsalsa20poly1305_PUBLICKEYBYTES = 32; + internal const int crypto_box_curve25519xsalsa20poly1305_SECRETKEYBYTES = 32; + internal const string Library = "libsodium"; + + private static readonly Action s_misuseHandler = new(InternalError); + + private static int s_initialized; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Initialize() + { + if (s_initialized == 0) + { + InitializeCore(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void InitializeCore() + { + try + { + if (sodium_library_version_major() != SODIUM_LIBRARY_VERSION_MAJOR || + sodium_library_version_minor() != SODIUM_LIBRARY_VERSION_MINOR) + { + string version = Marshal.PtrToStringAnsi(sodium_version_string()); + + throw (version != null && version != SODIUM_VERSION_STRING) + ? new NotSupportedException($"An error occurred while initializing cryptographic primitives. (Expected libsodium {SODIUM_VERSION_STRING} but found {version}.)") + : new NotSupportedException("An error occurred while initializing cryptographic primitives."); + } + + if (sodium_set_misuse_handler(s_misuseHandler) != 0) + { + throw new NotSupportedException("An error occurred while initializing cryptographic primitives."); + } + + // sodium_init() returns 0 on success, -1 on failure, and 1 if the library had already been initialized. + if (sodium_init() < 0) + { + throw new NotSupportedException("An error occurred while initializing cryptographic primitives."); + } + } + catch (DllNotFoundException e) + { + throw new PlatformNotSupportedException("Could not initialize platform-specific components. libsodium-core may not be supported on this platform. See https://github.com/ektrah/libsodium-core/blob/master/INSTALL.md for more information.", e); + } + catch (BadImageFormatException e) + { + throw new PlatformNotSupportedException("Could not initialize platform-specific components. libsodium-core may not be supported on this platform. See https://github.com/ektrah/libsodium-core/blob/master/INSTALL.md for more information.", e); + } + + Interlocked.Exchange(ref s_initialized, 1); + } + + private static void InternalError() + { + throw new NotSupportedException("An internal error occurred."); + } + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int sodium_init(); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int sodium_set_misuse_handler(Action handler); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int sodium_library_version_major(); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int sodium_library_version_minor(); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr sodium_version_string(); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519( + byte[] sm, + ref ulong smlen_p, + byte[] m, + ulong mlen, + byte[] sk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_detached( + byte[] sig, + ref ulong siglen_p, + byte[] m, + ulong mlen, + byte[] sk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_keypair( + byte[] pk, + byte[] sk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_open( + byte[] m, + ref ulong mlen_p, + byte[] sm, + ulong smlen, + byte[] pk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_pk_to_curve25519( + byte[] curve25519_pk, + byte[] ed25519_pk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_seed_keypair( + byte[] pk, + byte[] sk, + byte[] seed); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_sk_to_curve25519( + byte[] curve25519_sk, + byte[] ed25519_sk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_sk_to_pk( + byte[] pk, + byte[] sk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_sk_to_seed( + byte[] seed, + byte[] sk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_sign_ed25519_verify_detached( + byte[] sig, + byte[] m, + ulong mlen, + byte[] pk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_generichash_blake2b( + byte[] @out, + int outlen, + byte[] @in, + ulong inlen, + byte[] key, + int keylen); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_secretbox_easy( + byte[] c, + byte[] m, + ulong mlen, + byte[] n, + byte[] k); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_secretbox_open_easy( + byte[] m, + byte[] c, + ulong clen, + byte[] n, + byte[] k); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_box_seal( + byte[] c, + byte[] m, + ulong mlen, + byte[] pk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_box_seal_open( + byte[] m, + byte[] c, + ulong clen, + byte[] pk, + byte[] sk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_kx_client_session_keys( + byte[] rx, + byte[] tx, + byte[] client_pk, + byte[] client_sk, + byte[] server_pk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_kx_server_session_keys( + byte[] rx, + byte[] tx, + byte[] server_pk, + byte[] server_sk, + byte[] client_pk); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_box_macbytes(); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_box_noncebytes(); + + [DllImport(Library, CallingConvention = CallingConvention.Cdecl)] + internal static extern int crypto_generichash( + byte[] buffer, + int bufferLength, + byte[] message, + ulong messageLength, + byte[] key, + int keyLength); + } +} \ No newline at end of file diff --git a/Beacon.Sdk/Core/Infrastructure/Cryptography/NaCl/MontgomeryCurve25519.cs b/Beacon.Sdk/Core/Infrastructure/Cryptography/NaCl/MontgomeryCurve25519.cs index 51e453f..d3f7d7a 100644 --- a/Beacon.Sdk/Core/Infrastructure/Cryptography/NaCl/MontgomeryCurve25519.cs +++ b/Beacon.Sdk/Core/Infrastructure/Cryptography/NaCl/MontgomeryCurve25519.cs @@ -1,6 +1,5 @@ using System; - namespace Beacon.Sdk.Core.Infrastructure.Cryptography.NaCl { using Internal.Ed25519Ref10; @@ -14,7 +13,11 @@ public static void EdwardsToMontgomery(ArraySegment montgomery, ArraySegme FieldOperations.fe_1(out edwardsZ); EdwardsToMontgomeryX(out montgomeryX, ref edwardsY, ref edwardsZ); FieldOperations.fe_tobytes(montgomery.Array, montgomery.Offset, ref montgomeryX); - montgomery.Array[montgomery.Offset + 31] |= (byte)(edwards.Array[edwards.Offset + 31] & 0x80); // copy sign + + // Sign copying removed to match Libsodium implementation: + // https://github.com/jedisct1/libsodium/blob/b7aebe5a1ef46bbb1345e8570fd2e8cea64e587f/src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L64 + // + //montgomery.Array[montgomery.Offset + 31] |= (byte)(edwards.Array[edwards.Offset + 31] & 0x80); // copy sign } private static void EdwardsToMontgomeryX(out FieldElement montgomeryX, ref FieldElement edwardsY,