From d19f9c9e5bf41857d8c4f0e50f5f6e0f295f42c0 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 28 Mar 2022 01:13:24 +0200 Subject: [PATCH] Write out the high-level API I need. --- README.md | 9 ++ .../CryptoAeadXChaCha20Poly1305Ietf.cs | 112 ++++++++++++++++++ SpaceWizards.Sodium/CryptoBox.cs | 94 +++++++++++++++ SpaceWizards.Sodium/SodiumCore.cs | 26 ++++ SpaceWizards.Sodium/SodiumException.cs | 16 +++ SpaceWizards.Sodium/SodiumInitException.cs | 17 +++ .../SpaceWizards.Sodium.csproj | 11 ++ 7 files changed, 285 insertions(+) create mode 100644 SpaceWizards.Sodium/CryptoAeadXChaCha20Poly1305Ietf.cs create mode 100644 SpaceWizards.Sodium/CryptoBox.cs create mode 100644 SpaceWizards.Sodium/SodiumCore.cs create mode 100644 SpaceWizards.Sodium/SodiumException.cs create mode 100644 SpaceWizards.Sodium/SodiumInitException.cs diff --git a/README.md b/README.md index 507e5ea..c23102a 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,12 @@ A .NET [libsodium](https://libsodium.gitbook.io/doc/) binding that doesn't suck. Actually exposes the native API so you don't have to cry yourself to sleep in unnecessary allocations or OOP nonsense. Currently depends on the ["libsodium" NuGet package](https://www.nuget.org/packages/libsodium/) provided by [Sodium.Core](https://github.com/ektrah/libsodium-core) for the native library. They say you shouldn't depend on it directly but I am too lazy to compile them myself so deal with it. + +# API Shape + +There is a low-level API in `SpaceWizards.Sodium.Interop`. This is a raw P/Invoke binding, hope you like pointers. + +The high-level API in `SpaceWizards.Sodium` generally has two variants: span with return code, or `byte[]` with exceptions. Note that the former still throws exceptions if you pass spans that are too small, but otherwise failing return codes from libsodium are passed through so you can handle them. This is just what I decided to settle on to keep API size down. + + +Also, if it wasn't obvious, the only API wrapped is the ones I needed at the moment. PRs welcome I guess. diff --git a/SpaceWizards.Sodium/CryptoAeadXChaCha20Poly1305Ietf.cs b/SpaceWizards.Sodium/CryptoAeadXChaCha20Poly1305Ietf.cs new file mode 100644 index 0000000..5aa3718 --- /dev/null +++ b/SpaceWizards.Sodium/CryptoAeadXChaCha20Poly1305Ietf.cs @@ -0,0 +1,112 @@ +namespace SpaceWizards.Sodium; +using static Interop.Libsodium; + +/// +/// Wrappers around the crypto_aead_xchacha20poly1305_ietf_ APIs. +/// +public static class CryptoAeadXChaCha20Poly1305Ietf +{ + static CryptoAeadXChaCha20Poly1305Ietf() + { + SodiumCore.EnsureInit(); + } + + public const int NoncePublicBytes = (int)crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + public const int KeyBytes = (int)crypto_aead_xchacha20poly1305_ietf_KEYBYTES; + public const int AddBytes = (int)crypto_aead_xchacha20poly1305_ietf_ABYTES; + + public static unsafe void Keygen(Span key) + { + if (key.Length != KeyBytes) + throw new ArgumentException($"Key must be {nameof(KeyBytes)} bytes"); + + fixed (byte* k = key) + { + crypto_aead_xchacha20poly1305_ietf_keygen(k); + } + } + + public static byte[] Keygen() + { + var key = new byte[KeyBytes]; + Keygen(key); + return key; + } + + public static unsafe bool Encrypt( + Span cipher, + out int cipherLength, + ReadOnlySpan message, + ReadOnlySpan additionalData, + ReadOnlySpan noncePublic, + ReadOnlySpan key) + { + if (cipher.Length < checked(message.Length + AddBytes)) + throw new ArgumentException("Destination is too short"); + + if (key.Length != KeyBytes) + throw new ArgumentException($"Key must be {nameof(KeyBytes)} bytes"); + + if (noncePublic.Length != NoncePublicBytes) + throw new ArgumentException($"Nonce must be {nameof(NoncePublicBytes)} bytes"); + + fixed (byte* c = cipher) + fixed (byte* m = message) + fixed (byte* ad = additionalData) + fixed (byte* npub = noncePublic) + fixed (byte* k = key) + { + ulong clen; + var ret = crypto_aead_xchacha20poly1305_ietf_encrypt( + c, &clen, + m, (ulong)message.Length, + ad, (ulong)additionalData.Length, + null, + npub, + k); + + cipherLength = (int)clen; + return ret == 0; + } + } + + public static unsafe bool Decrypt( + Span message, + out int messageLength, + ReadOnlySpan cipher, + ReadOnlySpan additionalData, + ReadOnlySpan noncePublic, + ReadOnlySpan key) + { + if (cipher.Length < AddBytes) + throw new ArgumentException("Input is too short"); + + if (message.Length < cipher.Length - AddBytes) + throw new ArgumentException("Output is too short"); + + if (key.Length != KeyBytes) + throw new ArgumentException($"Key must be {nameof(KeyBytes)} bytes"); + + if (noncePublic.Length != NoncePublicBytes) + throw new ArgumentException($"Nonce must be {nameof(NoncePublicBytes)} bytes"); + + fixed (byte* c = cipher) + fixed (byte* m = message) + fixed (byte* ad = additionalData) + fixed (byte* npub = noncePublic) + fixed (byte* k = key) + { + ulong mlen; + var ret = crypto_aead_xchacha20poly1305_ietf_decrypt( + m, &mlen, + null, + c, (ulong)cipher.Length, + ad, (ulong)additionalData.Length, + npub, + k); + + messageLength = (int)mlen; + return ret == 0; + } + } +} diff --git a/SpaceWizards.Sodium/CryptoBox.cs b/SpaceWizards.Sodium/CryptoBox.cs new file mode 100644 index 0000000..5a39183 --- /dev/null +++ b/SpaceWizards.Sodium/CryptoBox.cs @@ -0,0 +1,94 @@ +namespace SpaceWizards.Sodium; + +using static Interop.Libsodium; + +/// +/// Wrappers around the crypto_box APIs. +/// +public static class CryptoBox +{ + static CryptoBox() + { + SodiumCore.EnsureInit(); + } + + public const int SealBytes = (int)crypto_box_SEALBYTES; + public const int PublicKeyBytes = (int)crypto_box_PUBLICKEYBYTES; + public const int SecretKeyBytes = (int)crypto_box_SECRETKEYBYTES; + + public static unsafe bool KeyPair(Span publicKey, Span secretKey) + { + if (publicKey.Length != PublicKeyBytes) + throw new ArgumentException($"Public key must be {nameof(PublicKeyBytes)} bytes."); + + if (secretKey.Length != SecretKeyBytes) + throw new ArgumentException($"Secret key must be {nameof(SecretKeyBytes)} bytes."); + + fixed (byte* pk = publicKey) + fixed (byte* sk = secretKey) + { + return crypto_box_keypair(pk, sk) == 0; + } + } + + public static unsafe bool Seal(Span cipher, ReadOnlySpan message, ReadOnlySpan publicKey) + { + if (cipher.Length < checked(message.Length + SealBytes)) + throw new ArgumentException("Destination is too short"); + + fixed (byte* c = cipher) + fixed (byte* m = message) + fixed (byte* pk = publicKey) + { + return crypto_box_seal(c, m, (ulong)message.Length, pk) == 0; + } + } + + public static byte[] Seal(ReadOnlySpan message, ReadOnlySpan publicKey) + { + var cipher = new byte[message.Length + SealBytes]; + if (!Seal(cipher, message, publicKey)) + throw new SodiumException("Seal failed"); + + return cipher; + } + + public static unsafe bool SealOpen( + Span message, + ReadOnlySpan cipher, + ReadOnlySpan publicKey, + ReadOnlySpan secretKey) + { + if (cipher.Length < SealBytes) + throw new ArgumentException("Input is too short"); + + if (message.Length < (cipher.Length - SealBytes)) + throw new ArgumentException("Destination is too short"); + + if (publicKey.Length != PublicKeyBytes) + throw new ArgumentException($"Public key must be {nameof(PublicKeyBytes)} bytes."); + + if (secretKey.Length != SecretKeyBytes) + throw new ArgumentException($"Secret key must be {nameof(SecretKeyBytes)} bytes."); + + fixed (byte* m = message) + fixed (byte* c = cipher) + fixed (byte* pk = publicKey) + fixed (byte* sk = secretKey) + { + return crypto_box_seal_open(m, c, (ulong)cipher.Length, pk, sk) == 0; + } + } + + public static byte[] SealOpen(ReadOnlySpan cipher, ReadOnlySpan publicKey, ReadOnlySpan secretKey) + { + if (cipher.Length < SealBytes) + throw new ArgumentException("Input is too short"); + + var message = new byte[cipher.Length - SealBytes]; + if (!SealOpen(message, cipher, publicKey, secretKey)) + throw new SodiumException("SealOpen failed"); + + return message; + } +} diff --git a/SpaceWizards.Sodium/SodiumCore.cs b/SpaceWizards.Sodium/SodiumCore.cs new file mode 100644 index 0000000..d08440e --- /dev/null +++ b/SpaceWizards.Sodium/SodiumCore.cs @@ -0,0 +1,26 @@ +using static SpaceWizards.Sodium.Interop.Libsodium; + +namespace SpaceWizards.Sodium; + +public static class SodiumCore +{ + /// + /// Directly call . + /// + /// 0 on success, 1 if already initialized, -1 on initialize failure. + /// + public static int Init() + { + return sodium_init(); + } + + /// + /// Try to ensure libsodium is initialized, throwing if it fails to initialize. + /// + /// Thrown if initialization of libsodium failed. + public static void EnsureInit() + { + if (Init() == -1) + throw new SodiumInitException("Failed to init libsodium!"); + } +} diff --git a/SpaceWizards.Sodium/SodiumException.cs b/SpaceWizards.Sodium/SodiumException.cs new file mode 100644 index 0000000..5fe7eac --- /dev/null +++ b/SpaceWizards.Sodium/SodiumException.cs @@ -0,0 +1,16 @@ +namespace SpaceWizards.Sodium; + +public sealed class SodiumException : Exception +{ + public SodiumException() + { + } + + public SodiumException(string message) : base(message) + { + } + + public SodiumException(string message, Exception inner) : base(message, inner) + { + } +} diff --git a/SpaceWizards.Sodium/SodiumInitException.cs b/SpaceWizards.Sodium/SodiumInitException.cs new file mode 100644 index 0000000..973310e --- /dev/null +++ b/SpaceWizards.Sodium/SodiumInitException.cs @@ -0,0 +1,17 @@ +namespace SpaceWizards.Sodium; + +[Serializable] +public sealed class SodiumInitException : Exception +{ + public SodiumInitException() + { + } + + public SodiumInitException(string message) : base(message) + { + } + + public SodiumInitException(string message, Exception inner) : base(message, inner) + { + } +} diff --git a/SpaceWizards.Sodium/SpaceWizards.Sodium.csproj b/SpaceWizards.Sodium/SpaceWizards.Sodium.csproj index 132c02c..9bac676 100644 --- a/SpaceWizards.Sodium/SpaceWizards.Sodium.csproj +++ b/SpaceWizards.Sodium/SpaceWizards.Sodium.csproj @@ -4,6 +4,17 @@ net6.0 enable enable + true + SpaceWizards.Sodium + 0.1.0 + true + Simple API binding for libsodium. + CS1591 + libsodium + + + +