diff --git a/tests/session/testSessionClean.cs b/tests/session/testSessionClean.cs new file mode 100644 index 0000000..3c49292 --- /dev/null +++ b/tests/session/testSessionClean.cs @@ -0,0 +1,75 @@ +using NUnit.Framework; +using System; +using System.Linq; +using System.Threading; +using chestcrypto.session; +using chestcrypto.exceptions; +using Sodium; + +namespace sessionPrivateTestsCleaning +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + public long getFutureTime(int seconds){return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (long) seconds;} + + [Test] + public void TestSessionCleanPrivate(){ + byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; + byte[] newK = PublicKeyBox.GenerateKeyPair().PrivateKey; + Session session = new Session(privateK, publicK, true, 5); + session.setMinimumKeyExpireSeconds(1); + session.setMessageDelay((long) 1); + session.addPrivate(newK, getFutureTime(2)); + bool atLeastOneLoop = false; + while(true){ + try{ + if (Enumerable.SequenceEqual(session.getLatestPrivateKey(), newK)){ + Thread.Sleep(25); // ms + atLeastOneLoop = true; // key should not be deleted instantly + continue; + } + } + catch(System.ArgumentOutOfRangeException){ + break; + } + session.cleanPrivate(); + } + Assert.IsTrue(atLeastOneLoop); + } + + + [Test] + public void TestSessionCleanPublic(){ + byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; + byte[] newK = PublicKeyBox.GenerateKeyPair().PublicKey; + Session session = new Session(privateK, publicK, true, 5); + session.setMinimumKeyExpireSeconds(1); + session.setMessageDelay((long) 1); + session.addPublic(newK, getFutureTime(2)); + bool atLeastOneLoop = false; + while(true){ + try{ + if (Enumerable.SequenceEqual(session.getLatestPublicKey(), newK)){ + Thread.Sleep(25); // ms + atLeastOneLoop = true; // key should not be deleted instantly + continue; + } + } + catch(System.ArgumentOutOfRangeException){ + break; + } + session.cleanPublic(); + } + Assert.IsTrue(atLeastOneLoop); + } + + + } +} \ No newline at end of file diff --git a/tests/session/testSessionEncrypt.cs b/tests/session/testSessionEncrypt.cs new file mode 100644 index 0000000..863a333 --- /dev/null +++ b/tests/session/testSessionEncrypt.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; +using System; +using System.Linq; +using System.Threading; +using chestcrypto.session; +using chestcrypto.exceptions; +using Sodium; + +namespace sessionTestEncrypt +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + public long getFutureTime(int seconds){return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (long) seconds;} + + [Test] + public void TestEncrypt(){ + byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; + byte[] newK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte message = "" + Session session = new Session(privateK, publicK, true, 5); + SessionCrypto sessionCrypto = new SessionCrypto(session); + session.setMinimumKeyExpireSeconds(1); + session.setMessageDelay((long) 1); + session.addPublic(newK, getFutureTime(9)); + sessionCrypto.encrypt() + } + + } +} \ No newline at end of file diff --git a/tests/session/testSessionPrivate.cs b/tests/session/testSessionPrivate.cs new file mode 100644 index 0000000..e6c972a --- /dev/null +++ b/tests/session/testSessionPrivate.cs @@ -0,0 +1,55 @@ +using NUnit.Framework; +using System; +using System.Linq; +using chestcrypto.session; +using chestcrypto.exceptions; +using Sodium; + +namespace sessionPrivateTests +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + public long getFutureTime(int seconds){return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (long) seconds;} + + [Test] + public void TestSessionAddValidPrivate(){ + byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; + byte[] newK = PublicKeyBox.GenerateKeyPair().PrivateKey; + Session session = new Session(privateK, publicK, true, 5); + session.addPrivate(newK, getFutureTime(670)); + Assert.IsTrue(Enumerable.SequenceEqual(newK, session.getLatestPrivateKey())); + } + + [Test] + public void TestSessionAddInvalidPrivateTime(){ + byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; + byte[] newK = PublicKeyBox.GenerateKeyPair().PrivateKey; + Session session = new Session(privateK, publicK, true, 5); + try{ + session.addPrivate(newK, getFutureTime(1)); + } + catch(System.ArgumentOutOfRangeException){return;} + Assert.Fail(); + } + [Test] + public void TestSessionAddInvalidPrivate(){ + byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; + byte[] newK = {5, 3, 2, 1}; + Session session = new Session(privateK, publicK, true, 5); + try{ + session.addPrivate(newK, getFutureTime(7010)); + } + catch(InvalidKeyLength){return;} + Assert.Fail(); + } + + } +} \ No newline at end of file diff --git a/tests/session/testSession.cs b/tests/session/testSessionPublic.cs similarity index 72% rename from tests/session/testSession.cs rename to tests/session/testSessionPublic.cs index 99517ed..27b5864 100644 --- a/tests/session/testSession.cs +++ b/tests/session/testSessionPublic.cs @@ -14,8 +14,19 @@ namespace sessionTests { } - public long getFutureTime(int seconds){ - return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (long) seconds; + public long getFutureTime(int seconds){return DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (long) seconds;} + + [Test] + public void TestSessionGetLatestPublic(){ + byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; + byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; + byte[] newK = PublicKeyBox.GenerateKeyPair().PublicKey; + Session session = new Session(privateK, publicK, true, 5); + for (int i = 0; i < 5; i++){ + session.addPublic(PublicKeyBox.GenerateKeyPair().PublicKey, getFutureTime(630)); + } + session.addPublic(newK, getFutureTime(650)); + Assert.IsTrue(Enumerable.SequenceEqual(newK, session.getLatestPublicKey())); } [Test] @@ -23,8 +34,8 @@ namespace sessionTests byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; byte[] newK = PublicKeyBox.GenerateKeyPair().PublicKey; - Session session = new Session(privateK, publicK, true); - session.addPublic(newK, getFutureTime(61)); + Session session = new Session(privateK, publicK, true, 5); + session.addPublic(newK, getFutureTime(610)); Assert.IsTrue(Enumerable.SequenceEqual(newK, session.getLatestPublicKey())); } @@ -33,10 +44,10 @@ namespace sessionTests byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; byte[] newK = PublicKeyBox.GenerateKeyPair().PublicKey; - Session session = new Session(privateK, publicK, true); - session.addPublic(newK, getFutureTime(61)); + Session session = new Session(privateK, publicK, true, 5); + session.addPublic(newK, getFutureTime(615)); try{ - session.addPublic(newK, getFutureTime(61)); + session.addPublic(newK, getFutureTime(615)); } catch(DuplicatePublicKey){return;} Assert.Fail(); @@ -47,7 +58,7 @@ namespace sessionTests byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; byte[] newK = {3, 5}; - Session session = new Session(privateK, publicK, true); + Session session = new Session(privateK, publicK, true, 5); try{ session.addPublic(newK, getFutureTime(61)); } @@ -62,7 +73,7 @@ namespace sessionTests byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; byte[] newK = PublicKeyBox.GenerateKeyPair().PublicKey; - Session session = new Session(privateK, publicK, true); + Session session = new Session(privateK, publicK, true, 5); try{ session.addPublic(newK, getFutureTime(-1)); } @@ -77,11 +88,11 @@ namespace sessionTests { byte[] publicK = PublicKeyBox.GenerateKeyPair().PublicKey; byte[] privateK = PublicKeyBox.GenerateKeyPair().PrivateKey; - Session session = new Session(privateK, publicK, true); + Session session = new Session(privateK, publicK, true, 5); byte[] invalid = {0, 0, 0}; try{ - new Session(invalid, publicK, true); + new Session(invalid, publicK, true, 5); } catch(InvalidKeyLength){ goto secondAssert; @@ -89,7 +100,7 @@ namespace sessionTests Assert.Fail(); secondAssert: try{ - new Session(privateK, invalid, true); + new Session(privateK, invalid, true, 5); } catch(InvalidKeyLength){ return; diff --git a/treasurechest/chestcrypto/session/crypto/encrypt.cs b/treasurechest/chestcrypto/session/crypto/encrypt.cs new file mode 100644 index 0000000..c0d636e --- /dev/null +++ b/treasurechest/chestcrypto/session/crypto/encrypt.cs @@ -0,0 +1,18 @@ +using Sodium; + +using chestcrypto.session; +using chestcrypto; + +namespace chestcrypto.session.crypto{ + + internal class SessionEncrypt{ + + public static byte[] Encrypt(Session activeSession, byte[] message){ + byte[] publicKey = activeSession.getLatestPublicKey(); + byte[] privateKey = activeSession.getLatestPrivateKey(); + return Curve25519.encrypt(privateKey, publicKey, message); + } + + } + +} \ No newline at end of file diff --git a/treasurechest/chestcrypto/session/exceptions.cs b/treasurechest/chestcrypto/session/exceptions.cs index 1016897..60710e7 100644 --- a/treasurechest/chestcrypto/session/exceptions.cs +++ b/treasurechest/chestcrypto/session/exceptions.cs @@ -18,6 +18,40 @@ namespace chestcrypto{ { } } + public class DuplicatePrivateKey : Exception + { + public DuplicatePrivateKey() + { + } + + public DuplicatePrivateKey(string message) + : base(message) + { + } + + public DuplicatePrivateKey(string message, Exception inner) + : base(message, inner) + { + } + } + + public class NoSessionKeyAvailable : Exception + { + public NoSessionKeyAvailable() + { + } + + public NoSessionKeyAvailable(string message) + : base(message) + { + } + + public NoSessionKeyAvailable(string message, Exception inner) + : base(message, inner) + { + } + } + } } \ No newline at end of file diff --git a/treasurechest/chestcrypto/session/message.cs b/treasurechest/chestcrypto/session/message.cs deleted file mode 100644 index e69de29..0000000 diff --git a/treasurechest/chestcrypto/session/session.cs b/treasurechest/chestcrypto/session/session.cs index 1587d10..55e7998 100644 --- a/treasurechest/chestcrypto/session/session.cs +++ b/treasurechest/chestcrypto/session/session.cs @@ -7,7 +7,7 @@ namespace chestcrypto{ namespace session{ - internal class Session{ + public class Session{ // Create List of tuples(time, byte[]) // Where the tuple contains a time stamp for expiry and a ed25519 key @@ -17,16 +17,29 @@ namespace chestcrypto{ private byte[] ourMasterPrivateKey; private byte[] theirMasterPublicKey; private bool strictMode; - private const int minimumKeyExpireSeconds = 60; - private void validateKey(byte[] key){ + private long messageDelay = 25; + + private int minimumKeyExpireSeconds = 600; + + private void validateKeyLength(byte[] key){ if (key.Length != 32){ throw new InvalidKeyLength(); } } + private long getEpoch(){ + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + private void validateTimestamp(long ts){ + if (ts < getEpoch() + minimumKeyExpireSeconds){ + throw new ArgumentOutOfRangeException(); + } + } + private bool publicKeyExists(byte[] key){ - foreach( (int, byte[]) k in theirPublicKeys){ + foreach((int, byte[]) k in theirPublicKeys){ if (Enumerable.SequenceEqual(k.Item2, key)){ return true; } @@ -34,27 +47,88 @@ namespace chestcrypto{ return false; } - public Session(byte[] masterPrivate, byte[] masterPublic, bool strictMode){ - validateKey(masterPrivate); - validateKey(masterPublic); + private bool privateKeyExists(byte[] key){ + foreach((int, byte[]) k in ourPrivateKeys){ + if (Enumerable.SequenceEqual(k.Item2, key)){ + return true; + } + } + return false; + } + + public Session(byte[] masterPrivate, byte[] masterPublic, bool strictMode, long messageDelay){ + validateKeyLength(masterPrivate); + validateKeyLength(masterPublic); ourMasterPrivateKey = masterPrivate; theirMasterPublicKey = masterPublic; this.strictMode = strictMode; + this.messageDelay = messageDelay; ourPrivateKeys = new List<(long, byte[])>(); theirPublicKeys = new List<(long, byte[])>(); } + public void setMinimumKeyExpireSeconds(int newSeconds){minimumKeyExpireSeconds = newSeconds;} + public void setMessageDelay(long newDelay){ + messageDelay = newDelay; + } + public void addPublic(byte[] publicKey, long timestamp){ - validateKey(publicKey); + timestamp -= messageDelay; // Subtract some time from the specified timestamp because we don't want to use it close to expiry + validateKeyLength(publicKey); + validateTimestamp(timestamp); if (publicKeyExists(publicKey)){throw new DuplicatePublicKey();} - if (timestamp < DateTimeOffset.UtcNow.ToUnixTimeSeconds() + minimumKeyExpireSeconds){ - throw new ArgumentOutOfRangeException(); - } theirPublicKeys.Add((timestamp, publicKey)); } - public byte[] getLatestPublicKey(){return theirPublicKeys[theirPublicKeys.Count - 1].Item2;} + public byte[] getLatestPublicKey(){ + if (theirPublicKeys.Count == 0 && strictMode) + throw new NoSessionKeyAvailable(); + var key = theirPublicKeys[theirPublicKeys.Count - 1]; + validateTimestamp(key.Item1); + return key.Item2; + } + public byte[] getLatestPrivateKey(){ + if (ourPrivateKeys.Count == 0 && strictMode) + throw new NoSessionKeyAvailable(); + var key = ourPrivateKeys[ourPrivateKeys.Count -1]; + validateTimestamp(key.Item1); + return key.Item2; + } + public void addPrivate(byte[] privateKey, long timestamp){ + validateKeyLength(privateKey); + validateTimestamp(timestamp); + if (privateKeyExists(privateKey)){throw new DuplicatePrivateKey();} + ourPrivateKeys.Add((timestamp, privateKey)); + } + + public void cleanPublic(){ + long epoch = getEpoch(); + bool expired((long, byte[]) k){ + if (k.Item1 > epoch){ + return true; + } + return false; + } + + theirPublicKeys.RemoveAll(expired); // remove all keys who are truthy with expired() + } + + public void cleanPrivate(){ + // Can't use predicate approach because we want to zero out private keys + List remove = new List(); + + for (int i = 0; i < ourPrivateKeys.Count; i++){ + if (ourPrivateKeys[i].Item1 > getEpoch()){ + remove.Add(i); + // We manually clear memory to reduce attack surface a tiny bit (GC may take too long) + Array.Clear(ourPrivateKeys[i].Item2, 0, ourPrivateKeys[i].Item2.Length); + } + } + foreach(int i in remove){ + ourPrivateKeys.RemoveAt((int) i); + } + } }