Skip to content

Commit

Permalink
src: Add counter parameter to (X)ChaCha20.
Browse files Browse the repository at this point in the history
Useful for implementing (X)ChaCha20-Poly1305.
  • Loading branch information
samuel-lucas6 committed Aug 29, 2022
1 parent f86b759 commit f5d6729
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 16 deletions.
39 changes: 38 additions & 1 deletion src/Geralt.Tests/ChaCha20Tests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Geralt.Tests;
Expand All @@ -12,7 +13,13 @@ public class ChaCha20Tests
private static readonly byte[] Ciphertext = Convert.FromHexString("76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586");
private static readonly byte[] Nonce = Convert.FromHexString("000000000000000000000000");
private static readonly byte[] Key = Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000000");

private static readonly byte[] CounterPlaintext = Convert.FromHexString("416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f");
private static readonly byte[] CounterCiphertext = Convert.FromHexString("a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221");
private static readonly byte[] CounterNonce = Convert.FromHexString("000000000000000000000002");
private static readonly byte[] CounterKey = Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001");
private const uint Counter = 1;
private const uint OverflowCounter = uint.MaxValue - 5;

[TestMethod]
public void Encrypt_ValidInputs()
{
Expand Down Expand Up @@ -63,6 +70,14 @@ public void Encrypt_DifferentKey()
Assert.IsFalse(ciphertext.SequenceEqual(Ciphertext));
}

[TestMethod]
public void Encrypt_DifferentCounter()
{
Span<byte> ciphertext = stackalloc byte[CounterPlaintext.Length];
ChaCha20.Encrypt(ciphertext, CounterPlaintext, CounterNonce, CounterKey, Counter);
Assert.IsTrue(ciphertext.SequenceEqual(CounterCiphertext));
}

[TestMethod]
public void Encrypt_InvalidCiphertext()
{
Expand Down Expand Up @@ -100,6 +115,13 @@ public void Encrypt_InvalidKey()
Assert.ThrowsException<ArgumentOutOfRangeException>(() => ChaCha20.Encrypt(ciphertext, Plaintext, Nonce, key));
}

[TestMethod]
public void Encrypt_CounterOverflow()
{
var ciphertext = new byte[CounterPlaintext.Length];
Assert.ThrowsException<CryptographicException>(() => ChaCha20.Encrypt(ciphertext, CounterPlaintext, CounterNonce, CounterKey, OverflowCounter));
}

[TestMethod]
public void Decrypt_ValidInputs()
{
Expand Down Expand Up @@ -138,6 +160,14 @@ public void Decrypt_DifferentKey()
Assert.IsFalse(plaintext.SequenceEqual(Plaintext));
}

[TestMethod]
public void Decrypt_DifferentCounter()
{
Span<byte> plaintext = stackalloc byte[CounterCiphertext.Length];
ChaCha20.Decrypt(plaintext, CounterCiphertext, CounterNonce, CounterKey, Counter);
Assert.IsTrue(plaintext.SequenceEqual(CounterPlaintext));
}

[TestMethod]
public void Decrypt_InvalidPlaintext()
{
Expand Down Expand Up @@ -175,4 +205,11 @@ public void Decrypt_InvalidKey()
key = new byte[ChaCha20.KeySize + 1];
Assert.ThrowsException<ArgumentOutOfRangeException>(() => ChaCha20.Decrypt(plaintext, Ciphertext, Nonce, key));
}

[TestMethod]
public void Decrypt_CounterOverflow()
{
var plaintext = new byte[CounterCiphertext.Length];
Assert.ThrowsException<CryptographicException>(() => ChaCha20.Decrypt(plaintext, CounterCiphertext, CounterNonce, CounterKey, OverflowCounter));
}
}
20 changes: 19 additions & 1 deletion src/Geralt.Tests/XChaCha20Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ namespace Geralt.Tests;
[TestClass]
public class XChaCha20Tests
{
// draft-irtf-cfrg-xchacha Section A.3.2.1: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.3.2
// draft-irtf-cfrg-xchacha Section A.3.2: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.3.2
private static readonly byte[] Plaintext = Convert.FromHexString("5468652064686f6c65202870726f6e6f756e6365642022646f6c65222920697320616c736f206b6e6f776e2061732074686520417369617469632077696c6420646f672c2072656420646f672c20616e642077686973746c696e6720646f672e2049742069732061626f7574207468652073697a65206f662061204765726d616e20736865706865726420627574206c6f6f6b73206d6f7265206c696b652061206c6f6e672d6c656767656420666f782e205468697320686967686c7920656c757369766520616e6420736b696c6c6564206a756d70657220697320636c6173736966696564207769746820776f6c7665732c20636f796f7465732c206a61636b616c732c20616e6420666f78657320696e20746865207461786f6e6f6d69632066616d696c792043616e696461652e");
private static readonly byte[] Ciphertext = Convert.FromHexString("4559abba4e48c16102e8bb2c05e6947f50a786de162f9b0b7e592a9b53d0d4e98d8d6410d540a1a6375b26d80dace4fab52384c731acbf16a5923c0c48d3575d4d0d2c673b666faa731061277701093a6bf7a158a8864292a41c48e3a9b4c0daece0f8d98d0d7e05b37a307bbb66333164ec9e1b24ea0d6c3ffddcec4f68e7443056193a03c810e11344ca06d8ed8a2bfb1e8d48cfa6bc0eb4e2464b748142407c9f431aee769960e15ba8b96890466ef2457599852385c661f752ce20f9da0c09ab6b19df74e76a95967446f8d0fd415e7bee2a12a114c20eb5292ae7a349ae577820d5520a1f3fb62a17ce6a7e68fa7c79111d8860920bc048ef43fe84486ccb87c25f0ae045f0cce1e7989a9aa220a28bdd4827e751a24a6d5c62d790a66393b93111c1a55dd7421a10184974c7c5");
private static readonly byte[] Nonce = Convert.FromHexString("404142434445464748494a4b4c4d4e4f5051525354555658");
private static readonly byte[] Key = Convert.FromHexString("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");
private static readonly byte[] CounterCiphertext = Convert.FromHexString("7d0a2e6b7f7c65a236542630294e063b7ab9b555a5d5149aa21e4ae1e4fbce87ecc8e08a8b5e350abe622b2ffa617b202cfad72032a3037e76ffdcdc4376ee053a190d7e46ca1de04144850381b9cb29f051915386b8a710b8ac4d027b8b050f7cba5854e028d564e453b8a968824173fc16488b8970cac828f11ae53cabd20112f87107df24ee6183d2274fe4c8b1485534ef2c5fbc1ec24bfc3663efaa08bc047d29d25043532db8391a8a3d776bf4372a6955827ccb0cdd4af403a7ce4c63d595c75a43e045f0cce1f29c8b93bd65afc5974922f214a40b7c402cdb91ae73c0b63615cdad0480680f16515a7ace9d39236464328a37743ffc28f4ddb324f4d0f5bbdc270c65b1749a6efff1fbaa09536175ccd29fb9e6057b307320d316838a9c71f70b5b5907a66f7ea49aadc409");
private const ulong Counter = 1;

[TestMethod]
public void Encrypt_ValidInputs()
Expand Down Expand Up @@ -63,6 +65,14 @@ public void Encrypt_DifferentKey()
Assert.IsFalse(ciphertext.SequenceEqual(Ciphertext));
}

[TestMethod]
public void Encrypt_DifferentCounter()
{
Span<byte> ciphertext = stackalloc byte[Plaintext.Length];
XChaCha20.Encrypt(ciphertext, Plaintext, Nonce, Key, Counter);
Assert.IsTrue(ciphertext.SequenceEqual(CounterCiphertext));
}

[TestMethod]
public void Encrypt_InvalidCiphertext()
{
Expand Down Expand Up @@ -137,6 +147,14 @@ public void Decrypt_DifferentKey()
XChaCha20.Decrypt(plaintext, Ciphertext, Nonce, key);
Assert.IsFalse(plaintext.SequenceEqual(Plaintext));
}

[TestMethod]
public void Decrypt_DifferentCounter()
{
Span<byte> plaintext = stackalloc byte[CounterCiphertext.Length];
XChaCha20.Decrypt(plaintext, CounterCiphertext, Nonce, Key, Counter);
Assert.IsTrue(plaintext.SequenceEqual(Plaintext));
}

[TestMethod]
public void Decrypt_InvalidPlaintext()
Expand Down
21 changes: 16 additions & 5 deletions src/Geralt/Crypto/ChaCha20.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,41 @@ public static class ChaCha20
{
public const int KeySize = crypto_stream_chacha20_ietf_KEYBYTES;
public const int NonceSize = crypto_stream_chacha20_ietf_NONCEBYTES;

public static unsafe void Encrypt(Span<byte> ciphertext, ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key)
public const int BlockSize = 64;

public static unsafe void Encrypt(Span<byte> ciphertext, ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key, uint counter = 0)
{
Validation.EqualToSize(nameof(ciphertext), ciphertext.Length, plaintext.Length);
Validation.EqualToSize(nameof(nonce), nonce.Length, NonceSize);
Validation.EqualToSize(nameof(key), key.Length, KeySize);
CounterOverflow(plaintext.Length, counter);
Sodium.Initialise();
fixed (byte* c = ciphertext, p = plaintext, n = nonce, k = key)
{
int ret = crypto_stream_chacha20_ietf_xor(c, p, (ulong)plaintext.Length, n, k);
int ret = crypto_stream_chacha20_ietf_xor_ic(c, p, (ulong)plaintext.Length, n, counter, k);
if (ret != 0) { throw new CryptographicException("Error encrypting plaintext."); }
}
}

public static unsafe void Decrypt(Span<byte> plaintext, ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key)
public static unsafe void Decrypt(Span<byte> plaintext, ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key, uint counter = 0)
{
Validation.EqualToSize(nameof(plaintext), plaintext.Length, ciphertext.Length);
Validation.EqualToSize(nameof(nonce), nonce.Length, NonceSize);
Validation.EqualToSize(nameof(key), key.Length, KeySize);
CounterOverflow(ciphertext.Length, counter);
Sodium.Initialise();
fixed (byte* p = plaintext, c = ciphertext, n = nonce, k = key)
{
int ret = crypto_stream_chacha20_ietf_xor(p, c, (ulong)ciphertext.Length, n, k);
int ret = crypto_stream_chacha20_ietf_xor_ic(p, c, (ulong)ciphertext.Length, n, counter, k);
if (ret != 0) { throw new CryptographicException("Error decrypting ciphertext."); }
}
}

private static void CounterOverflow(int messageSize, uint counter)
{
if (counter <= 1) { return; }
long blockCount = (-1L + messageSize + BlockSize) / BlockSize;
if (counter + blockCount > uint.MaxValue)
throw new CryptographicException("Counter overflow prevented.");
}
}
9 changes: 5 additions & 4 deletions src/Geralt/Crypto/XChaCha20.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,30 @@ public static class XChaCha20
{
public const int KeySize = crypto_stream_xchacha20_KEYBYTES;
public const int NonceSize = crypto_stream_xchacha20_NONCEBYTES;
public const int BlockSize = 64;

public static unsafe void Encrypt(Span<byte> ciphertext, ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key)
public static unsafe void Encrypt(Span<byte> ciphertext, ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key, ulong counter = 0)
{
Validation.EqualToSize(nameof(ciphertext), ciphertext.Length, plaintext.Length);
Validation.EqualToSize(nameof(nonce), nonce.Length, NonceSize);
Validation.EqualToSize(nameof(key), key.Length, KeySize);
Sodium.Initialise();
fixed (byte* c = ciphertext, p = plaintext, n = nonce, k = key)
{
int ret = crypto_stream_xchacha20_xor(c, p, (ulong)plaintext.Length, n, k);
int ret = crypto_stream_xchacha20_xor_ic(c, p, (ulong)plaintext.Length, n, counter, k);
if (ret != 0) { throw new CryptographicException("Error encrypting plaintext."); }
}
}

public static unsafe void Decrypt(Span<byte> plaintext, ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key)
public static unsafe void Decrypt(Span<byte> plaintext, ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> key, ulong counter = 0)
{
Validation.EqualToSize(nameof(plaintext), plaintext.Length, ciphertext.Length);
Validation.EqualToSize(nameof(nonce), nonce.Length, NonceSize);
Validation.EqualToSize(nameof(key), key.Length, KeySize);
Sodium.Initialise();
fixed (byte* p = plaintext, c = ciphertext, n = nonce, k = key)
{
int ret = crypto_stream_xchacha20_xor(p, c, (ulong)ciphertext.Length, n, k);
int ret = crypto_stream_xchacha20_xor_ic(p, c, (ulong)ciphertext.Length, n, counter, k);
if (ret != 0) { throw new CryptographicException("Error decrypting ciphertext."); }
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Geralt/Geralt.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>1.0.3</Version>
<Version>1.1.0</Version>
<Authors>Samuel Lucas</Authors>
<Description>A modern cryptographic library for .NET based on libsodium.</Description>
<Copyright>Copyright (c) 2022 Samuel Lucas</Copyright>
Expand Down
4 changes: 2 additions & 2 deletions src/Geralt/Interop/Interop.ChaCha20.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ internal static partial class Libsodium
{
internal const int crypto_stream_chacha20_ietf_KEYBYTES = 32;
internal const int crypto_stream_chacha20_ietf_NONCEBYTES = 12;

[DllImport(DllName, CallingConvention = Convention)]
internal static extern unsafe int crypto_stream_chacha20_ietf_xor(byte* ciphertext, byte* plaintext, ulong plaintextLength, byte* nonce, byte* key);
internal static extern unsafe int crypto_stream_chacha20_ietf_xor_ic(byte* ciphertext, byte* plaintext, ulong plaintextLength, byte* nonce, uint counter, byte* key);
}
}
4 changes: 2 additions & 2 deletions src/Geralt/Interop/Interop.XChaCha20.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ internal static partial class Libsodium
{
internal const int crypto_stream_xchacha20_KEYBYTES = 32;
internal const int crypto_stream_xchacha20_NONCEBYTES = 24;

[DllImport(DllName, CallingConvention = Convention)]
internal static extern unsafe int crypto_stream_xchacha20_xor(byte* ciphertext, byte* plaintext, ulong plaintextLength, byte* nonce, byte* key);
internal static extern unsafe int crypto_stream_xchacha20_xor_ic(byte* ciphertext, byte* plaintext, ulong plaintextLength, byte* nonce, ulong counter, byte* key);
}
}

0 comments on commit f5d6729

Please sign in to comment.