diff --git a/src/Lyn.Protocol.Tests/Bolt2/AcceptChannelMessageServiceTests.cs b/src/Lyn.Protocol.Tests/Bolt2/AcceptChannelMessageServiceTests.cs index c8c1e35..087fcbf 100644 --- a/src/Lyn.Protocol.Tests/Bolt2/AcceptChannelMessageServiceTests.cs +++ b/src/Lyn.Protocol.Tests/Bolt2/AcceptChannelMessageServiceTests.cs @@ -9,7 +9,7 @@ using Lyn.Protocol.Bolt9; using Lyn.Protocol.Common; using Lyn.Protocol.Common.Blockchain; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Protocol.Common.Messages; using Lyn.Protocol.Connection; using Lyn.Types; diff --git a/src/Lyn.Protocol.Tests/Bolt2/ChannelEstablishment/FullChannelEstablishmentTest.cs b/src/Lyn.Protocol.Tests/Bolt2/ChannelEstablishment/FullChannelEstablishmentTest.cs index 777a6d0..8129b4d 100644 --- a/src/Lyn.Protocol.Tests/Bolt2/ChannelEstablishment/FullChannelEstablishmentTest.cs +++ b/src/Lyn.Protocol.Tests/Bolt2/ChannelEstablishment/FullChannelEstablishmentTest.cs @@ -12,7 +12,7 @@ using Lyn.Protocol.Bolt9; using Lyn.Protocol.Common; using Lyn.Protocol.Common.Blockchain; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Protocol.Common.Messages; using Lyn.Protocol.Connection; using Lyn.Types; diff --git a/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTestContext.cs b/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTestContext.cs index 9443320..2b6fdd5 100644 --- a/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTestContext.cs +++ b/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTestContext.cs @@ -5,7 +5,7 @@ using Lyn.Protocol.Bolt3; using Lyn.Protocol.Bolt3.Types; using Lyn.Protocol.Common; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Protocol.Common.Messages; using Lyn.Types; using Lyn.Types.Bitcoin; diff --git a/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTests.cs b/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTests.cs index 1ff779d..bb42f18 100644 --- a/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTests.cs +++ b/src/Lyn.Protocol.Tests/Bolt3/Bolt3CommitmentTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Lyn.Protocol.Bolt3.Types; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Types; using Lyn.Types.Bitcoin; using Lyn.Types.Fundamental; diff --git a/src/Lyn.Protocol.Tests/Bolt4/Data/SphinxReferenceVectors.cs b/src/Lyn.Protocol.Tests/Bolt4/Data/SphinxReferenceVectors.cs new file mode 100644 index 0000000..3db8b43 --- /dev/null +++ b/src/Lyn.Protocol.Tests/Bolt4/Data/SphinxReferenceVectors.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Lyn.Types.Fundamental; + +namespace Lyn.Protocol.Tests.Bolt4.Data +{ + public static class SphinxReferenceVectors + { + public static byte[] InvalidVersionPayload => new[] { (byte)0x00 }; + public static List ExpectedEphemeralKeys = new List() { + Convert.FromHexString("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), + Convert.FromHexString("028f9438bfbf7feac2e108d677e3a82da596be706cc1cf342b75c7b7e22bf4e6e2"), + Convert.FromHexString("03bfd8225241ea71cd0843db7709f4c222f62ff2d4516fd38b39914ab6b83e0da0"), + Convert.FromHexString("031dde6926381289671300239ea8e57ffaf9bebd05b9a5b95beaf07af05cd43595"), + Convert.FromHexString("03a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f4") + }; + + public static List ExpectedEphemeralSecrets = new List() { + Convert.FromHexString("53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66"), + Convert.FromHexString("a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae"), + Convert.FromHexString("3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc"), + Convert.FromHexString("21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d"), + Convert.FromHexString("b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328") + }; + + public static (byte[], int)[] PaylodLengths = new (byte[], int)[] { + (Convert.FromHexString("01"), 34), + (Convert.FromHexString("08"), 41), + (Convert.FromHexString("fc"), 285), + (Convert.FromHexString("fd00fd"), 288), + (Convert.FromHexString("fdffff"), 65570) + }; + + public static PrivateKey SessionKey = new PrivateKey(Convert.FromHexString("4141414141414141414141414141414141414141414141414141414141414141")); + + public static List PrivateKeys = new List() { + new PrivateKey(Convert.FromHexString("4141414141414141414141414141414141414141414141414141414141414141")), + new PrivateKey(Convert.FromHexString("4242424242424242424242424242424242424242424242424242424242424242")), + new PrivateKey(Convert.FromHexString("4343434343434343434343434343434343434343434343434343434343434343")), + new PrivateKey(Convert.FromHexString("4444444444444444444444444444444444444444444444444444444444444444")), + new PrivateKey(Convert.FromHexString("4545454545454545454545454545454545454545454545454545454545454545")) + }; + + public static List PublicKeys = new List() { + new PublicKey(Convert.FromHexString("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")), + new PublicKey(Convert.FromHexString("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c")), + new PublicKey(Convert.FromHexString("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")), + new PublicKey(Convert.FromHexString("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991")), + new PublicKey(Convert.FromHexString("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145")) + }; + + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + public static List ReferencePaymentPayloads = new List() { + Convert.FromHexString("1202023a98040205dc06080000000000000001"), + Convert.FromHexString("52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f"), + Convert.FromHexString("12020230d4040204e206080000000000000003"), + Convert.FromHexString("1202022710040203e806080000000000000004"), + Convert.FromHexString("fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a") + }; + + // This test vector uses multiple payloads to fill the whole onion packet. + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + public static List PaymentPayloadsFull = new List() { + Convert.FromHexString("8b09000000000000000030000000000000000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000250000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000"), + Convert.FromHexString("fd012a08000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000"), + Convert.FromHexString("620800000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + Convert.FromHexString("fc120000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000"), + Convert.FromHexString("fd01582200000000000000000000000000000000000000000022000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000") + }; + + public static List OneHopPaymentPayload = new List() { + Convert.FromHexString("fd04f16500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + }; + + public static byte[] AssociatedData = Convert.FromHexString("4242424242424242424242424242424242424242424242424242424242424242"); + + public static byte[] GenerateFiller_ExpectedFiller = Convert.FromHexString("51c30cc8f20da0153ca3839b850bcbc8fefc7fd84802f3e78cb35a660e747b57aa5b0de555cbcf1e6f044a718cc34219b96597f3684eee7a0232e1754f638006cb15a14788217abdf1bdd67910dc1ca74a05dcce8b5ad841b0f939fca8935f6a3ff660e0efb409f1a24ce4aa16fc7dc074cd84422c10cc4dd4fc150dd6d1e4f50b36ce10fef29248dd0cec85c72eb3e4b2f4a7c03b5c9e0c9dd12976553ede3d0e295f842187b33ff743e6d685075e98e1bcab8a46bff0102ca8b2098ae91798d370b01ca7076d3d626952a03663fe8dc700d1358263b73ba30e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab06"); + } +} diff --git a/src/Lyn.Protocol.Tests/Bolt4/FailureMessageSerializerTests.cs b/src/Lyn.Protocol.Tests/Bolt4/FailureMessageSerializerTests.cs new file mode 100644 index 0000000..afc0895 --- /dev/null +++ b/src/Lyn.Protocol.Tests/Bolt4/FailureMessageSerializerTests.cs @@ -0,0 +1,49 @@ +using Lyn.Protocol.Bolt4; +using Lyn.Protocol.Bolt4.Entities; +using Lyn.Protocol.Bolt4.Messages; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Lyn.Protocol.Tests.Bolt4 +{ + // need to implement tests so we have parity with FailureMessageCodecsSpec.scala from eclair + public class FailureMessageSerializerTests + { + + [Fact] + public void EncodeAllFailureMessages() + { + var failureMessages = new List() { + new InvalidRealmMessage(), + new TemporaryNodeFailureMessage(), + new PermanentNodeFailureMessage(), + new RequiredNodeFeatureMissingMessage() + }; + + var serializer = new FailureMessageSerializer(); + // loop through all failure messages, serialize them, and then deserialize them and compare the result + foreach (var failureMessage in failureMessages) + { + var bufferWriter = new ArrayBufferWriter(256); + int bytesWritten = serializer.Serialize(failureMessage, bufferWriter, null); + var failureMessageBytes = bufferWriter.WrittenSpan.ToArray(); + + Assert.Equal(bytesWritten, failureMessageBytes.Length); + + var failureMessaegSequence = new ReadOnlySequence(failureMessageBytes); + var failureMessageSequenceReader = new SequenceReader(failureMessaegSequence); + + var failureMessageDeserialized = serializer.Deserialize(ref failureMessageSequenceReader, null); + + Assert.Equal(failureMessage, failureMessageDeserialized); + } + } + + } +} diff --git a/src/Lyn.Protocol.Tests/Bolt4/OnionRoutingPacketSerializerTests.cs b/src/Lyn.Protocol.Tests/Bolt4/OnionRoutingPacketSerializerTests.cs new file mode 100644 index 0000000..e4c5350 --- /dev/null +++ b/src/Lyn.Protocol.Tests/Bolt4/OnionRoutingPacketSerializerTests.cs @@ -0,0 +1,37 @@ +using Lyn.Protocol.Bolt4.Entities; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Lyn.Protocol.Tests.Bolt4 +{ + public class OnionRoutingPacketSerializerTests + { + + [Fact] + public void SmallOnion_Serialization() + { + var onionToSerialize = new OnionRoutingPacket(); + onionToSerialize.Version = 1; + onionToSerialize.EphemeralKey = Convert.FromHexString("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"); + onionToSerialize.PayloadData = Convert.FromHexString("0012345679abcdef"); + onionToSerialize.Hmac = Convert.FromHexString("0000111122223333444455556666777788889999aaaabbbbccccddddeeee0000"); + + var bufferWriter = new ArrayBufferWriter(256); + + var serializer = new OnionRoutingPacketSerializer(); + int bytesWritten = serializer.Serialize(onionToSerialize, bufferWriter, null); + var onionBytes = bufferWriter.WrittenSpan.ToArray(); + + Assert.Equal(bytesWritten, onionBytes.Length); + // todo: figure out what to do about varint length here?? + Assert.Equal(Convert.FromHexString("004a01032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910012345679abcdef0000111122223333444455556666777788889999aaaabbbbccccddddeeee0000"), onionBytes); + } + + } +} diff --git a/src/Lyn.Protocol.Tests/Bolt4/RouteBlindingTests.cs b/src/Lyn.Protocol.Tests/Bolt4/RouteBlindingTests.cs new file mode 100644 index 0000000..e11cd32 --- /dev/null +++ b/src/Lyn.Protocol.Tests/Bolt4/RouteBlindingTests.cs @@ -0,0 +1,135 @@ +using Lyn.Protocol.Bolt4; +using Lyn.Protocol.Common.Crypto; +using System; +using System.Linq; +using Xunit; + +using Lyn.Protocol.Tests.Bolt4.Data; +using Lyn.Protocol.Bolt4.Entities; +using System.Collections.Generic; +using Lyn.Types.Fundamental; +using Lyn.Types.Onion; +using Lyn.Protocol.Bolt3; +using System.Diagnostics; + +namespace Lyn.Protocol.Tests.Bolt4 +{ + public class RouteBlindingTests + { + + [Fact] + public void RouteBlinding_CanCreatedBlindedRoute_ReferenceTestVector() + { + // note: this is really ugly rn, will clean up once e2e works + + var lightningKeyDerivation = new LightningKeyDerivation(); + + var alice = new PrivateKey(Convert.FromHexString("4141414141414141414141414141414141414141414141414141414141414141")); + var alicePubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(alice); + var bob = new PrivateKey(Convert.FromHexString("4242424242424242424242424242424242424242424242424242424242424242")); + var bobPubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(bob); + var bobPayload = Convert.FromHexString("01200000000000000000000000000000000000000000000000000000000000000000020800000000000000010a0800320000000027100c05000b7246320e00"); + var carol = new PrivateKey(Convert.FromHexString("4343434343434343434343434343434343434343434343434343434343434343")); + var carolPubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(carol); + var carolPayload = Convert.FromHexString("020800000000000000020821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a07004b00000096640c05000b7214320e00"); + var dave = new PrivateKey(Convert.FromHexString("4444444444444444444444444444444444444444444444444444444444444444")); + var davePubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(dave); + var davePayload = Convert.FromHexString("012200000000000000000000000000000000000000000000000000000000000000000000020800000000000000030a060019000000640c05000b71c9320e00"); + var eve = new PrivateKey(Convert.FromHexString("4545454545454545454545454545454545454545454545454545454545454545")); + var evePubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(eve); + var evePayload = Convert.FromHexString("011c000000000000000000000000000000000000000000000000000000000616c9cf92f45ade68345bc20ae672e2012f4af487ed44150c05000b71b0320e00"); + + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var routeBlinding = new RouteBlinding(sphinx, lightningKeyDerivation, curveActions); + + // Eve creates a blinded route to herself through Dave + var eveSessionKey = new PrivateKey(Convert.FromHexString("0101010101010101010101010101010101010101010101010101010101010101")); + var daveEveHops = new List<(PublicKey PublicKey, byte[] Payload)>() { + (PublicKey: davePubKey, Payload: davePayload), + (PublicKey: evePubKey, Payload: evePayload) + }; + var (blindedRouteEnd, lastBlinding) = routeBlinding.Create(eveSessionKey, daveEveHops); + Assert.Equal(new PublicKey(Convert.FromHexString("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")), blindedRouteEnd.BlindingKey); + Assert.Equal(new PublicKey(Convert.FromHexString("03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a")), lastBlinding); + + // Save a blinding override for later + var blindingOverride = blindedRouteEnd.BlindingKey; + + // Bob also wants to use route blinding: + var bobSessionKey = new PrivateKey(Convert.FromHexString("0202020202020202020202020202020202020202020202020202020202020202")); + var bobCarolHops = new List<(PublicKey PublicKey, byte[] Payload)>() { + (PublicKey: lightningKeyDerivation.PublicKeyFromPrivateKey(bob), Payload: bobPayload), + (PublicKey: lightningKeyDerivation.PublicKeyFromPrivateKey(carol), Payload: carolPayload) + }; + var blindedRouteStart = routeBlinding.Create(bobSessionKey, bobCarolHops).Route; + Assert.Equal(new PublicKey(Convert.FromHexString("024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766")), blindedRouteStart.BlindingKey); + + // We now have a blinded route Bob -> Carol -> Dave -> Eve + var blindedRoute = new BlindedRoute(bobPubKey, + blindedRouteStart.BlindingKey, + blindedRouteStart.BlindedNodes.Concat(blindedRouteEnd.BlindedNodes).ToArray()); + + Assert.Equal(new List() { + new PublicKey(Convert.FromHexString("03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25")), + new PublicKey(Convert.FromHexString("02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7")), + new PublicKey(Convert.FromHexString("036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf")), + new PublicKey(Convert.FromHexString("021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae")), + }, blindedRoute.BlindedNodeIds); + + Assert.Equal(new List() { + Convert.FromHexString("cd7b00ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c499a2888b49f2e72b19446f7e60a818aa2938d8c625415b992b8928a7321edb8f7cea40de362bed082ad51acc6156dca5532fb68"), + Convert.FromHexString("cc0f16524fd7f8bb0f4e8d40ad71709ef140174c76faa574cac401bb8992fef76c4d004aa485dd599ed1cf2715f570f656a5aaecaf1ee8dc9d0fa1d424759be1932a8f29fac08bc2d2a1ed7159f28b"), + Convert.FromHexString("0fa1a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8db3e561d9e49895fd4bcebf6f58d6f61a6d41a9bf5aa4b0453437856632e8255c351873143ddf2bb2b0832b091e1b4"), + Convert.FromHexString("da1c7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60722a63c688768042ade22f2c22f5724767d171fd221d3e579e43b354cc72e3ef146ada91a892d95fc48662f5b158add0af457da") + }, blindedRoute.EncryptedPayloads.ToList()); + + // After generating the blinded route, Eve is able to derive the private key corresponding to her blinded payload + var eveBlindedPrivKey = routeBlinding.DerivePrivateKey(eve, lastBlinding); + var eveBlindedPubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(eveBlindedPrivKey); + Assert.Equal(eveBlindedPubKey, blindedRoute.BlindedNodeIds.LastOrDefault()); + + // Every node in the route is able to decrypt its payload and extract the blinding point for the next node: + { + // Bob (the introduction point) is able to decrypt its encrypted payload and obtain the next ephemeral public key + var (payload0, ephKey1) = routeBlinding.DecryptPayload(bob, blindedRoute.BlindingKey, blindedRoute.EncryptedPayloads[0]); + Assert.Equal(bobPayload, payload0); + Assert.Equal(new PublicKey(Convert.FromHexString("034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0")), ephKey1); + + // Carol can derive the private key used to unwrap the onion and decrypt its payload + var carolBlindedPrivKey = routeBlinding.DerivePrivateKey(carol, ephKey1); + var carolBlindedPubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(carolBlindedPrivKey); + Assert.Equal(carolBlindedPubKey, blindedRoute.BlindedNodeIds[1]); + var (payload1, ephKey2) = routeBlinding.DecryptPayload(carol, ephKey1, blindedRoute.EncryptedPayloads[1]); + Assert.Equal(carolPayload, payload1); + Assert.Equal(new PublicKey(Convert.FromHexString("03af5ccc91851cb294e3a364ce63347709a08cdffa58c672e9a5c587ddd1bbca60")), ephKey2); + // NB: Carol finds a blinding override and will transmit that instead of ephKey2 to the next node. + // HACK: Really ugly way to check if the payload contains the blinding override + var payload1Str = Convert.ToHexString(payload1); + var blindingOverrideStr = Convert.ToHexString(blindingOverride.GetSpan().ToArray()); + Assert.True(payload1Str.Contains(blindingOverrideStr)); + + // Dave must be given the blinding override to derive the private key used to unwrap the onion and decrypt its payload + // TODO: This should probably be a specific exception + Assert.ThrowsAny(() => routeBlinding.DecryptPayload(dave, ephKey2, blindedRoute.EncryptedPayloads[2])); + var overridePrivKey = routeBlinding.DerivePrivateKey(dave, blindingOverride); + var overridePubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(overridePrivKey); + Assert.Equal(overridePubKey, blindedRoute.BlindedNodeIds[2]); + var (payload2, ephKey3) = routeBlinding.DecryptPayload(dave, blindingOverride, blindedRoute.EncryptedPayloads[2]); + Assert.Equal(davePayload, payload2); + Assert.Equal(new PublicKey(Convert.FromHexString("03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a")), ephKey3); + Assert.Equal(lastBlinding, ephKey3); + + // Eve is able to derive the private key used to unwrap the onion and decrypt its payload + var eveFinalBlindedPrivKey = routeBlinding.DerivePrivateKey(eve, ephKey3); + var eveFinalBlindedPubKey = lightningKeyDerivation.PublicKeyFromPrivateKey(eveFinalBlindedPrivKey); + Assert.Equal(eveFinalBlindedPubKey, blindedRoute.BlindedNodeIds[3]); + var (payload4, ephKey5) = routeBlinding.DecryptPayload(eve, ephKey3, blindedRoute.EncryptedPayloads[3]); + Assert.Equal(evePayload, payload4); + Assert.Equal(new PublicKey(Convert.FromHexString("038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589")), ephKey5); + } + } + + } +} \ No newline at end of file diff --git a/src/Lyn.Protocol.Tests/Bolt4/SphinxTests.cs b/src/Lyn.Protocol.Tests/Bolt4/SphinxTests.cs new file mode 100644 index 0000000..4093e66 --- /dev/null +++ b/src/Lyn.Protocol.Tests/Bolt4/SphinxTests.cs @@ -0,0 +1,255 @@ +using Lyn.Protocol.Bolt4; +using Lyn.Protocol.Common.Crypto; +using System; +using System.Linq; +using Xunit; + +using Lyn.Protocol.Tests.Bolt4.Data; +using Lyn.Protocol.Bolt4.Entities; +using System.Collections.Generic; +using Lyn.Types.Fundamental; +using Lyn.Types.Onion; + +namespace Lyn.Protocol.Tests.Bolt4 +{ + public class SphinxTests + { + + [Fact] + public void ReferenceTestVector_GeneratesEphemeralKeysAndSecrets() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var (ephemeralKeys, sharedSecrets) = sphinx.ComputeEphemeralPublicKeysAndSharedSecrets(SphinxReferenceVectors.SessionKey, + SphinxReferenceVectors.PublicKeys); + + Assert.NotNull(ephemeralKeys); + Assert.NotNull(sharedSecrets); + Assert.Equal(5, sharedSecrets.Count); + + var keyList = ephemeralKeys.ToList(); + var secretList = sharedSecrets.ToList(); + + for (var i = 0; i < sharedSecrets.Count; i++) + { + Assert.Equal(SphinxReferenceVectors.ExpectedEphemeralKeys[i], keyList.ElementAt(i).GetSpan().ToArray()); + Assert.Equal(SphinxReferenceVectors.ExpectedEphemeralSecrets[i], secretList.ElementAt(i)); + } + } + + [Fact] + public void GenerateFiller() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var (_, sharedSecrets) = sphinx.ComputeEphemeralPublicKeysAndSharedSecrets(SphinxReferenceVectors.SessionKey, + SphinxReferenceVectors.PublicKeys); + var filler = sphinx.GenerateFiller("rho", + 1300, + sharedSecrets.SkipLast(1), + SphinxReferenceVectors.ReferencePaymentPayloads.SkipLast(1)).ToArray(); + + Assert.Equal(SphinxReferenceVectors.GenerateFiller_ExpectedFiller, filler); + } + + [Fact] + public void PeekPerHopPayloadLength() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + foreach (var (payload, expected) in SphinxReferenceVectors.PaylodLengths) + { + var actual = sphinx.PeekPayloadLength(payload); + Assert.Equal(expected, actual); + } + + Assert.Throws(() => sphinx.PeekPayloadLength(SphinxReferenceVectors.InvalidVersionPayload)); + } + + [Fact] + public void IsLastPacket() + { + var publicKeys = SphinxReferenceVectors.PublicKeys; + var testCases = new[] + { + // Bolt 1.0 payloads use the next packet's hmac to signal termination. + (true, new DecryptedOnionPacket() { Payload = new [] {(byte)0x00}, + NextPacket = new OnionRoutingPacket() { Version = 0, + EphemeralKey = publicKeys[0], + PayloadData = new byte[32], + Hmac = ByteVector32.Zeroes }, + SharedSecret = ByteVector32.One + } + ), + (false, new DecryptedOnionPacket() { Payload = new [] {(byte)0x00}, + NextPacket = new OnionRoutingPacket() { Version = 0, + EphemeralKey = publicKeys[0], + PayloadData = new byte[32], + Hmac = ByteVector32.One }, + SharedSecret = ByteVector32.One + } + ), + // Bolt 1.1 payloads currently also use the next packet's hmac to signal termination. + (true, new DecryptedOnionPacket() { Payload = Convert.FromHexString("0101"), + NextPacket = new OnionRoutingPacket() { Version = 0, + EphemeralKey = publicKeys[0], + PayloadData = new byte[32], + Hmac = ByteVector32.Zeroes }, + SharedSecret = ByteVector32.One + } + ), + (false, new DecryptedOnionPacket() { Payload = Convert.FromHexString("0101"), + NextPacket = new OnionRoutingPacket() { Version = 0, + EphemeralKey = publicKeys[0], + PayloadData = new byte[32], + Hmac = ByteVector32.One }, + SharedSecret = ByteVector32.One + } + ), + (false, new DecryptedOnionPacket() { Payload = Convert.FromHexString("0100"), + NextPacket = new OnionRoutingPacket() { Version = 0, + EphemeralKey = publicKeys[0], + PayloadData = new byte[32], + Hmac = ByteVector32.One }, + SharedSecret = ByteVector32.One + } + ) + }; + + foreach (var (expected, packet) in testCases) + { + Assert.Equal(expected, packet.IsLastPacket); + } + } + + [Fact] + public void Peel_BadOnion_BadVersion_Throws() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var packet = new OnionRoutingPacket() + { + Version = 1, + EphemeralKey = SphinxReferenceVectors.PublicKeys[0], + PayloadData = ByteVector.Fill(65, 0x01), + Hmac = Convert.FromHexString("C908EE9582217D3B58D75FAC05CD5DBBEF91C1841A5B59D521283F9F4C43B643") + }; + + Assert.Throws(() => sphinx.PeelOnion(SphinxReferenceVectors.PrivateKeys[0], SphinxReferenceVectors.AssociatedData, packet)); + } + + [Fact] + public void Peel_BadOnion_InvalidPublicKey_Throws() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var packet = new OnionRoutingPacket() + { + Version = 0, + EphemeralKey = new byte[33], + PayloadData = ByteVector.Fill(65, 0x01), + Hmac = Convert.FromHexString("C908EE9582217D3B58D75FAC05CD5DBBEF91C1841A5B59D521283F9F4C43B643") + }; + + Assert.Throws(() => sphinx.PeelOnion(SphinxReferenceVectors.PrivateKeys[0], SphinxReferenceVectors.AssociatedData, packet)); + } + + [Fact] + public void Peel_BadOnion_InvalidHmac_Throws() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var packet = new OnionRoutingPacket() + { + Version = 0, + EphemeralKey = SphinxReferenceVectors.PublicKeys[0], + PayloadData = ByteVector.Fill(65, 0x01), + Hmac = Convert.FromHexString("2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a") + }; + + Assert.Throws(() => sphinx.PeelOnion(SphinxReferenceVectors.PrivateKeys[0], SphinxReferenceVectors.AssociatedData, packet)); + } + + [Fact] + public void CreatePaymentPacket_ReferenceTestVector() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var encryptedOnion = sphinx.CreateOnion(SphinxReferenceVectors.SessionKey, 1300, SphinxReferenceVectors.PublicKeys, SphinxReferenceVectors.ReferencePaymentPayloads, SphinxReferenceVectors.AssociatedData); + var sharedSecrets = encryptedOnion.SharedSecrets.ToArray(); + + OnionRoutingPacket currentPacket = encryptedOnion.Packet; + for (var i = 0; i < sharedSecrets.Length; i++) + { + var decrypted = sphinx.PeelOnion(SphinxReferenceVectors.PrivateKeys[i], SphinxReferenceVectors.AssociatedData, currentPacket); + Assert.Equal(SphinxReferenceVectors.ReferencePaymentPayloads[i], decrypted.Payload); + var (secret, _) = sharedSecrets[i]; + Assert.Equal(secret, decrypted.SharedSecret); + currentPacket = decrypted.NextPacket; + } + } + + [Fact] + public void CreatePaymentPacket_FullPayloads() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var encryptedOnion = sphinx.CreateOnion(SphinxReferenceVectors.SessionKey, 1300, SphinxReferenceVectors.PublicKeys, SphinxReferenceVectors.PaymentPayloadsFull, SphinxReferenceVectors.AssociatedData); + var sharedSecrets = encryptedOnion.SharedSecrets.ToArray(); + + OnionRoutingPacket currentPacket = encryptedOnion.Packet; + for (var i = 0; i < sharedSecrets.Length; i++) + { + var decrypted = sphinx.PeelOnion(SphinxReferenceVectors.PrivateKeys[i], SphinxReferenceVectors.AssociatedData, currentPacket); + Assert.Equal(SphinxReferenceVectors.PaymentPayloadsFull[i], decrypted.Payload); + var (secret, _) = sharedSecrets[i]; + Assert.Equal(secret, decrypted.SharedSecret); + currentPacket = decrypted.NextPacket; + } + } + + [Fact] + public void CreatePaymentPacket_SinglePayloadFillsOnion() + { + var curveActions = new EllipticCurveActions(); + var sphinx = new Sphinx(curveActions); + + var encryptedOnion = sphinx.CreateOnion(SphinxReferenceVectors.SessionKey, 1300, SphinxReferenceVectors.PublicKeys.Take(1), SphinxReferenceVectors.OneHopPaymentPayload, SphinxReferenceVectors.AssociatedData); + + OnionRoutingPacket currentPacket = encryptedOnion.Packet; + var decrypted = sphinx.PeelOnion(SphinxReferenceVectors.PrivateKeys[0], SphinxReferenceVectors.AssociatedData, currentPacket); + + var payload = decrypted.Payload; + var nextPacket = decrypted.NextPacket; + + Assert.Equal(payload, SphinxReferenceVectors.OneHopPaymentPayload[0]); + Assert.Equal(nextPacket.Hmac, new byte[32]); + } + + } + + public static class ByteVector32 + { + public static byte[] Zeroes = Enumerable.Repeat((byte)0, 32).ToArray(); + public static byte[] One = Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001"); + + } + + public static class ByteVector + { + public static byte[] Fill(int length, byte value) + { + return Enumerable.Repeat(value, length).ToArray(); + } + } + + +} diff --git a/src/Lyn.Protocol.Tests/Bolt8/Bolt8InitiatedNoiseProtocolTests.cs b/src/Lyn.Protocol.Tests/Bolt8/Bolt8InitiatedNoiseProtocolTests.cs index 85baefa..c3f1e99 100644 --- a/src/Lyn.Protocol.Tests/Bolt8/Bolt8InitiatedNoiseProtocolTests.cs +++ b/src/Lyn.Protocol.Tests/Bolt8/Bolt8InitiatedNoiseProtocolTests.cs @@ -1,4 +1,5 @@ using Lyn.Protocol.Bolt8; +using Lyn.Protocol.Common.Crypto; using Microsoft.Extensions.Logging; using Moq; diff --git a/src/Lyn.Protocol.Tests/Bolt8/HandshakeServiceTests.cs b/src/Lyn.Protocol.Tests/Bolt8/HandshakeServiceTests.cs index f10ad5c..63cc1b9 100644 --- a/src/Lyn.Protocol.Tests/Bolt8/HandshakeServiceTests.cs +++ b/src/Lyn.Protocol.Tests/Bolt8/HandshakeServiceTests.cs @@ -1,5 +1,6 @@ using System.Buffers; using Lyn.Protocol.Bolt8; +using Lyn.Protocol.Common.Crypto; using Lyn.Types; using Microsoft.Extensions.Logging; using Moq; diff --git a/src/Lyn.Protocol/Bolt1/Messages/Features.cs b/src/Lyn.Protocol/Bolt1/Messages/Features.cs index 1db20ae..1398b7c 100644 --- a/src/Lyn.Protocol/Bolt1/Messages/Features.cs +++ b/src/Lyn.Protocol/Bolt1/Messages/Features.cs @@ -29,5 +29,7 @@ public enum Features : ulong OptionAnchorOutputs = 1 << 21, OptionAnchorsZeroFeeHtlcTxRequired = 1 << 22, OptionAnchorsZeroFeeHtlcTx = 1 << 23, + OptionOnionMessagesRequired = 1 << 38, + OptionOnionMessages = 1 << 39 } } \ No newline at end of file diff --git a/src/Lyn.Protocol/Bolt2/ChannelEstablishment/AcceptChannelMessageService.cs b/src/Lyn.Protocol/Bolt2/ChannelEstablishment/AcceptChannelMessageService.cs index 31c515c..42d5360 100644 --- a/src/Lyn.Protocol/Bolt2/ChannelEstablishment/AcceptChannelMessageService.cs +++ b/src/Lyn.Protocol/Bolt2/ChannelEstablishment/AcceptChannelMessageService.cs @@ -16,7 +16,7 @@ using Lyn.Protocol.Bolt1.Messages; using Lyn.Protocol.Bolt2.Wallet; using Lyn.Protocol.Bolt9; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Types; using Lyn.Types.Fundamental; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Lyn.Protocol/Bolt3/LightningKeyDerivation.cs b/src/Lyn.Protocol/Bolt3/LightningKeyDerivation.cs index 58b1bb2..f2d7751 100644 --- a/src/Lyn.Protocol/Bolt3/LightningKeyDerivation.cs +++ b/src/Lyn.Protocol/Bolt3/LightningKeyDerivation.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Lyn.Protocol.Bolt3.Types; -using Lyn.Protocol.Common.Hashing; using Lyn.Types.Bitcoin; using Lyn.Types.Fundamental; using NBitcoin; diff --git a/src/Lyn.Protocol/Bolt3/LightningScripts.cs b/src/Lyn.Protocol/Bolt3/LightningScripts.cs index 2bf9d7d..5c67a1d 100644 --- a/src/Lyn.Protocol/Bolt3/LightningScripts.cs +++ b/src/Lyn.Protocol/Bolt3/LightningScripts.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using Lyn.Protocol.Bolt3.Types; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Types.Bitcoin; using Lyn.Types.Fundamental; using NBitcoin; diff --git a/src/Lyn.Protocol/Bolt3/Shachain/Shachain.cs b/src/Lyn.Protocol/Bolt3/Shachain/Shachain.cs index 9d2f975..79b4e41 100644 --- a/src/Lyn.Protocol/Bolt3/Shachain/Shachain.cs +++ b/src/Lyn.Protocol/Bolt3/Shachain/Shachain.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Types; using Lyn.Types.Bitcoin; diff --git a/src/Lyn.Protocol/Bolt4/Entities/DecryptedOnionPacket.cs b/src/Lyn.Protocol/Bolt4/Entities/DecryptedOnionPacket.cs new file mode 100644 index 0000000..d92d5dc --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Entities/DecryptedOnionPacket.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4.Entities +{ + public class DecryptedOnionPacket + { + + public byte[] Payload { get; set; } + + public OnionRoutingPacket NextPacket { get; set; } + + public byte[] SharedSecret { get; set; } + + public bool IsLastPacket => (NextPacket?.Hmac == null || NextPacket?.Hmac.Length == 0 || NextPacket?.Hmac.SequenceEqual(new byte[32]) == true); + + } +} diff --git a/src/Lyn.Protocol/Bolt4/Entities/OnionRoutingPacket.cs b/src/Lyn.Protocol/Bolt4/Entities/OnionRoutingPacket.cs new file mode 100644 index 0000000..e545634 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Entities/OnionRoutingPacket.cs @@ -0,0 +1,43 @@ +using Lyn.Types.Fundamental; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4.Entities +{ + public class OnionRoutingPacket + { + + public byte Version { get; set; } + + public PublicKey EphemeralKey { get; set; } + + public byte[] PayloadData { get; set; } + + public byte[] Hmac { get; set; } + + public static implicit operator ReadOnlySpan(OnionRoutingPacket packet) + { + var packetBytes = new List() { packet.Version }; + packetBytes.AddRange(packet.EphemeralKey.GetSpan().ToArray()); + packetBytes.AddRange(packet.PayloadData); + packetBytes.AddRange(packet.Hmac); + return packetBytes.ToArray(); + } + + public static implicit operator OnionRoutingPacket(ReadOnlySpan packetData) + { + var parsedPacket = new OnionRoutingPacket(); + // note: fixed sphinx header sizes + var payloadLength = packetData.Length - 66; + parsedPacket.Version = packetData[0]; + parsedPacket.EphemeralKey = new PublicKey(packetData.Slice(1, 32).ToArray()); + parsedPacket.PayloadData = packetData.Slice(33, payloadLength).ToArray(); + parsedPacket.Hmac = packetData.Slice(payloadLength, 32).ToArray(); + return parsedPacket; + } + } +} diff --git a/src/Lyn.Protocol/Bolt4/Entities/OnionRoutingPacketSerializer.cs b/src/Lyn.Protocol/Bolt4/Entities/OnionRoutingPacketSerializer.cs new file mode 100644 index 0000000..59b11c8 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Entities/OnionRoutingPacketSerializer.cs @@ -0,0 +1,49 @@ +using Lyn.Types.Fundamental; +using Lyn.Types.Serialization; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4.Entities +{ + public class OnionRoutingPacketSerializer : IProtocolTypeSerializer + { + public OnionRoutingPacket Deserialize(ref SequenceReader reader, ProtocolTypeSerializerOptions? options = null) + { + var packet = new OnionRoutingPacket(); + + var verByte = reader.ReadByte(); + packet.Version = verByte; + + var onionKeyBytes = reader.ReadBytes(33); + packet.EphemeralKey = new PublicKey(onionKeyBytes.ToArray()); + + // note: maybe this is a safe cast? need to confirm in spec + // The Sphinx packet header contains a version (1 byte), a public key (33 bytes) and a mac (32 bytes) -> total 66 bytes + var payloadLength = (int)reader.Length - 66; + var payloadBytes = reader.ReadBytes(payloadLength); + + packet.PayloadData = payloadBytes.ToArray(); + + var hmacBytes = reader.ReadBytes(32); + packet.Hmac = hmacBytes.ToArray(); + + return packet; + } + + public int Serialize(OnionRoutingPacket typeInstance, IBufferWriter writer, ProtocolTypeSerializerOptions? options = null) + { + int bytesWritten = 0; + + bytesWritten += writer.WriteByte(typeInstance.Version); + bytesWritten += writer.WriteBytes(typeInstance.EphemeralKey); + bytesWritten += writer.WriteBytes(typeInstance.PayloadData); + bytesWritten += writer.WriteBytes(typeInstance.Hmac); + + return bytesWritten; + } + } +} diff --git a/src/Lyn.Protocol/Bolt4/Entities/PacketAndSecrets.cs b/src/Lyn.Protocol/Bolt4/Entities/PacketAndSecrets.cs new file mode 100644 index 0000000..6c9c313 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Entities/PacketAndSecrets.cs @@ -0,0 +1,15 @@ +using Lyn.Types.Fundamental; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4.Entities +{ + public struct PacketAndSecrets + { + public OnionRoutingPacket Packet { get; set; } + public IEnumerable<(byte[], PublicKey)> SharedSecrets { get; set; } + } +} diff --git a/src/Lyn.Protocol/Bolt4/FailurePacket.cs b/src/Lyn.Protocol/Bolt4/FailurePacket.cs new file mode 100644 index 0000000..e48bb2a --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/FailurePacket.cs @@ -0,0 +1,56 @@ +using System.Buffers; +using System.Linq; +using Lyn.Protocol.Bolt4.Messages; +using Lyn.Protocol.Common.Crypto; + +namespace Lyn.Protocol.Bolt4 +{ + + // todo: this class doesn't feel very C#-y. It's a mix of static and instance methods, + // and it's not clear what the purpose of the instance methods are. + public class FailurePacket + { + public const int MaxPayloadLength = 256; + public const int PacketLength = 32 + MaxPayloadLength + 2 + 2; + + private readonly ISphinx sphinx; + + // todo: Do not love this Sphinx layering, need to resolve + public FailurePacket(ISphinx sphinx) + { + this.sphinx = sphinx; + } + + public byte[] Create(byte[] sharedSecret, FailureMessage failureMessage) + { + var um = sphinx.GenerateSphinxKey("um", sharedSecret); + var messageSerializer = new FailureMessageSerializer(); + + // create a bufferwriter to write the failure message to + var bufferWriter = new ArrayBufferWriter(); + int bytesWritten = messageSerializer.Serialize(failureMessage, bufferWriter); + + // var failureOnion = new FailureOnion(); + + return null; + } + + public byte[] Wrap(byte[] packet, byte[] sharedSecret) + { + if (packet.Length != PacketLength) + { + // this is a warn in eclair + // throw new ArgumentException($"Invalid error packet length {packet.Length}, must be {PacketLength} (malicious or buggy downstream node)"); + } + + var key = sphinx.GenerateSphinxKey("ammag", sharedSecret); + var stream = sphinx.GenerateStream(key, packet.Length); + + var paddedPacket = new byte[packet.Length].Concat(packet).ToArray(); + var result = sphinx.ExclusiveOR(paddedPacket, stream).ToArray(); + return result; + } + + } + +} \ No newline at end of file diff --git a/src/Lyn.Protocol/Bolt4/ISphinx.cs b/src/Lyn.Protocol/Bolt4/ISphinx.cs new file mode 100644 index 0000000..71373d3 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/ISphinx.cs @@ -0,0 +1,25 @@ +using Lyn.Protocol.Bolt4.Entities; +using Lyn.Types.Fundamental; +using System; +using System.Collections.Generic; + +namespace Lyn.Protocol.Bolt4 +{ + public interface ISphinx + { + PublicKey BlindKey(PublicKey pubKey, IEnumerable blindingFactors); + PublicKey BlindKey(PublicKey pubKey, ReadOnlySpan blindingFactor); + ReadOnlySpan ComputeBlindingFactor(PublicKey pubKey, ReadOnlySpan secret); + (IList, IList) ComputeEphemeralPublicKeysAndSharedSecrets(PrivateKey sessionKey, IEnumerable publicKeys); + (IList, IList) ComputeEphemeralPublicKeysAndSharedSecrets(PrivateKey sessionKey, IEnumerable publicKeys, IList ephemeralPublicKeys, IList blindingFactors, IList sharedSecrets); + ReadOnlySpan ComputeSharedSecret(PublicKey publicKey, PrivateKey secret); + PrivateKey DeriveBlindedPrivateKey(PrivateKey privateKey, PublicKey blindingEphemeralKey); + ReadOnlySpan ExclusiveOR(ReadOnlySpan left, ReadOnlySpan right); + ReadOnlySpan GenerateSphinxKey(byte[] keyType, ReadOnlySpan secret); + ReadOnlySpan GenerateSphinxKey(string keyType, ReadOnlySpan secret); + ReadOnlySpan GenerateStream(ReadOnlySpan keyData, int streamLength); + ReadOnlySpan GenerateFiller(string keyType, int packetPayloadLength, IEnumerable sharedSecrets, IEnumerable payloads); + DecryptedOnionPacket PeelOnion(PrivateKey privateKey, byte[]? associatedData, OnionRoutingPacket packet); + PacketAndSecrets CreateOnion(PrivateKey sessionKey, int packetPayloadLength, IEnumerable publicKeys, IEnumerable payloads, byte[]? associatedData); + } +} \ No newline at end of file diff --git a/src/Lyn.Protocol/Bolt4/Messages/FailureMessage.cs b/src/Lyn.Protocol/Bolt4/Messages/FailureMessage.cs new file mode 100644 index 0000000..a6c1805 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Messages/FailureMessage.cs @@ -0,0 +1,101 @@ +using System; +using System.Buffers; +using System.Linq; +using Lyn.Types.Serialization; + +namespace Lyn.Protocol.Bolt4 +{ + + [Flags] + public enum FailureMessageFlags : ushort + { + // 0x8000 (BADONION) - the onion was invalid. + BadOnion = 0x8000, + // 0x4000 (PERM) - the failure is permanent. + Permenant = 0x4000, + // 0x2000 (NODE) - the failure was at the final node. + Node = 0x2000, + // 0x1000 (UPDATE) - there is a new channel_update enclosed. + Update = 0x1000 + } + + public record FailureMessage(string Message, ushort Code) + { + // todo: is this the best way to do this? i prolly also need deserialize... + public virtual int Serialize(IBufferWriter writer, ProtocolTypeSerializerOptions? options = null) + { + return writer.WriteUShort(Code); + } + } + + public record PermenantFailureMessage(string Message, ushort Code) : FailureMessage(Message, Code); + + // note: all onion failures are permenant as far as I can tell... + // note: this is the closest we can get to the scala Perm and BadOnion traits + public record BadOnionMessage(string Message, ushort Code, byte[] OnionHash) : PermenantFailureMessage(Message, Code) + { + public override int Serialize(IBufferWriter writer, ProtocolTypeSerializerOptions? options = null) + { + var bytesWritten = base.Serialize(writer, options); + bytesWritten += writer.WriteBytes(OnionHash); + return bytesWritten; + } + } + + public record NodeFailureMessage(string Message, ushort Code) : FailureMessage(Message, Code); + + public record PermenantNodeFailureMessage(string Message, ushort Code) : PermenantFailureMessage(Message, Code); + + // TODO: Add channel_update + // public record ChannelUpdateFailureMessage(string Message, ushort Code, ChannelUpdate update) : FailureMessage(Message, Code); + + public record InvalidRealmMessage() : PermenantFailureMessage("realm was not understood by the processing node", ((ushort)FailureMessageFlags.Permenant | 1)); + + public record TemporaryNodeFailureMessage() : NodeFailureMessage("general temporary failure of the processing node", ((ushort)FailureMessageFlags.Node | 2)); + + public record PermanentNodeFailureMessage() : PermenantNodeFailureMessage("general permanent failure of the processing node", ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Node | 2)); + + public record RequiredNodeFeatureMissingMessage() : PermenantNodeFailureMessage("the processing node does not support the required feature bit", ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Node | 3)); + + public record InvalidOnionVersionMessage(byte[] OnionHash) : BadOnionMessage("onion version was not understood by the processing node", ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 4), OnionHash); + + public record InvalidOnionHmacMessage(byte[] OnionHash) : BadOnionMessage("onion HMAC was incorrect when it reached the processing node", ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 5), OnionHash); + + public record InvalidOnionKeyMessage(byte[] OnionHash) : BadOnionMessage("onion key was unparsable by the processing node", ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 6), OnionHash); + + public record InvalidOnionBlindingMessage(byte[] OnionHash) : BadOnionMessage("the blinded onion didn't match the processing node's requirements", ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 7), OnionHash); + + // todo: we need to add the channel_update logic to Lyn before we can implement this + // public record TemporaryChannelFailureMessage(ChannelUpdate update) : ChannelUpdateFailureMessage("channel ${update.shortChannelId} is currently unavailable", ((ushort)FailureMessageFlags.Update | 8), update); + + public record PermanentChannelFailureMessage() : PermenantFailureMessage("channel is permanently unavailable", ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Update | 9)); + + public record RequiredChannelFeatureMissingMessage() : PermenantFailureMessage("channel requires features not present in the onion", ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Update | 10)); + + public record UnknownNextPeerMessage() : PermenantFailureMessage("the next peer in the route was not known", ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Update | 11)); + + // todo: we need to add the channel_update logic to Lyn before we can implement this + // public record AmountBelowMinimumMessage(MilliSatoshis amount, ChannelUpdate update) : ChannelUpdateFailureMessage("amount is below the minimum amount allowed", ((ushort)FailureMessageFlags.Update | 12), update); + // public record FeeInsufficientMessage(MilliSatoshis amount, ChannelUpdate update) : ChannelUpdateFailureMessage("fee is insufficient", ((ushort)FailureMessageFlags.Update | 13), update); + + public record TrampolineFeeInsufficientMessage() : PermenantFailureMessage("payment fee was below the minimum required by the trampoline node", ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Update | 14)); + + // public record ChannelUpdateFailureMessage() : PermenantFailureMessage("channel is currently disabled", ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Channel | 15)); + + // public record IncorrectCltvExpiryMessage(CltvExpiry expiry, ChannelUpdate update) : ChannelUpdateFailureMessage("incorrect CLTV expiry", ((ushort)FailureMessageFlags.Update | 16), update); + + // public record ExpiryTooSoonMessage(CltvExpiry expiry, ChannelUpdate update) : ChannelUpdateFailureMessage("expiry is too close to the current block height for safe handling by the relaying node", ((ushort)FailureMessageFlags.Update | 17), update); + + public record TrampolineExpiryTooSoonMessage() : NodeFailureMessage("expiry is too close to the current block height for safe handling by the relaying node", 18); + + // public record FinalIncorrectCltvExpiryMessage(CltvExpiry expiry) : NodeFailureMessage("payment expiry doesn't match the value in the onion", 19); + + // public record FinalIncorrectHtlcAmountMessage(MiliSatoshi amount) : NodeFailureMessage("payment amount doesn't match the value in the onion", 20); + + public record ExpiryTooFarMessage() : NodeFailureMessage("payment expiry is too fari nt he future", 21); + + public record InvaldOnionPayloadMessage(ulong Tag, ushort Offset) : PermenantFailureMessage("nion per-hop payload is invalid", ((ushort)FailureMessageFlags.Permenant | 22)); + + public record PaymentTimeoutMessage() : FailureMessage("the complete payment amount was not received within a reasonable time", 23); + +} \ No newline at end of file diff --git a/src/Lyn.Protocol/Bolt4/Messages/FailureMessageSerializer.cs b/src/Lyn.Protocol/Bolt4/Messages/FailureMessageSerializer.cs new file mode 100644 index 0000000..47d4266 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Messages/FailureMessageSerializer.cs @@ -0,0 +1,49 @@ +using Lyn.Types.Serialization; +using System; +using System.Buffers; + +namespace Lyn.Protocol.Bolt4.Messages +{ + public class FailureMessageSerializer : IProtocolTypeSerializer + { + + public int Serialize(FailureMessage typeInstance, IBufferWriter writer, ProtocolTypeSerializerOptions? options = null) + { + int bytesWritten = 0; + bytesWritten += typeInstance.Serialize(writer); + return bytesWritten; + } + + public FailureMessage Deserialize(ref SequenceReader reader, ProtocolTypeSerializerOptions? options = null) + { + + var failureMessageType = reader.ReadUShort(); + + // todo: this seems so clunky? + switch (failureMessageType) + { + case ((ushort)FailureMessageFlags.Permenant | 1): + return new InvalidRealmMessage(); + case ((ushort)FailureMessageFlags.Node | 2): + return new TemporaryNodeFailureMessage(); + case ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Node | 2): + return new PermanentNodeFailureMessage(); + case ((ushort)FailureMessageFlags.Permenant | (ushort)FailureMessageFlags.Node | 3): + return new RequiredNodeFeatureMissingMessage(); + case ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 4): + return new InvalidOnionVersionMessage(reader.ReadBytes(32).ToArray()); + case ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 5): + return new InvalidOnionHmacMessage(reader.ReadBytes(32).ToArray()); + case ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 6): + return new InvalidOnionKeyMessage(reader.ReadBytes(32).ToArray()); + case ((ushort)FailureMessageFlags.BadOnion | (ushort)FailureMessageFlags.Permenant | 7): + return new InvalidOnionBlindingMessage(reader.ReadBytes(32).ToArray()); + default: + // todo: return UnknownFailureMessage + throw new NotImplementedException(); + } + } + + } + +} \ No newline at end of file diff --git a/src/Lyn.Protocol/Bolt4/Messages/FailurePacketSerializer.cs b/src/Lyn.Protocol/Bolt4/Messages/FailurePacketSerializer.cs new file mode 100644 index 0000000..27b1ce3 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Messages/FailurePacketSerializer.cs @@ -0,0 +1,57 @@ +using Lyn.Types.Serialization; +using System; +using System.Buffers; + +namespace Lyn.Protocol.Bolt4.Messages +{ + + /** + * An onion-encrypted failure from an intermediate node: + * {{{ + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * }}} + * with failure message length + pad length = 256 + */ + public record FailureOnion(byte[] Hmac, ushort FailureMessageLength, byte[] FailureMessage, ushort PadLength, byte[] Pad); + + public class FailureOnionSerializer : IProtocolTypeSerializer + { + + public FailureOnionSerializer() + { + } + + public int Serialize(FailureOnion typeInstance, IBufferWriter writer, ProtocolTypeSerializerOptions? options = null) + { + int bytesWritten = 0; + bytesWritten += writer.WriteBytes(typeInstance.Hmac); + bytesWritten += writer.WriteUShort(typeInstance.FailureMessageLength); + bytesWritten += writer.WriteBytes(typeInstance.FailureMessage); + bytesWritten += writer.WriteUShort(typeInstance.PadLength); + bytesWritten += writer.WriteBytes(typeInstance.Pad); + return bytesWritten; + } + + public FailureOnion Deserialize(ref SequenceReader reader, ProtocolTypeSerializerOptions? options = null) + { + // readi in the hmac first + var hmac = reader.ReadBytes(32).ToArray(); + + // next read the failure message length + var failureMessageLength = reader.ReadUShort(); + + // next read the failure message + var failureMessageBytes = reader.ReadBytes(failureMessageLength).ToArray(); + + // next read the pad length + var padLength = reader.ReadUShort(); + + // next read the pad + var pad = reader.ReadBytes(padLength).ToArray(); + + return new FailureOnion(hmac, failureMessageLength, failureMessageBytes, padLength, pad); + } + } +} \ No newline at end of file diff --git a/src/Lyn.Protocol/Bolt4/Messages/OnionMessage.cs b/src/Lyn.Protocol/Bolt4/Messages/OnionMessage.cs new file mode 100644 index 0000000..421bebe --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Messages/OnionMessage.cs @@ -0,0 +1,26 @@ +using Lyn.Protocol.Bolt4.Entities; +using Lyn.Protocol.Common.Messages; +using Lyn.Types.Fundamental; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4.Messages +{ + public class OnionMessage : MessagePayload + { + public override MessageType MessageType => MessageType.OnionMessage; + + public PublicKey BlindingKey { get; set; } + + public OnionRoutingPacket OnionPacket { get; set; } + + public OnionMessage() + { + OnionPacket = new OnionRoutingPacket(); + } + + } +} diff --git a/src/Lyn.Protocol/Bolt4/Messages/OnionMessageSerializer.cs b/src/Lyn.Protocol/Bolt4/Messages/OnionMessageSerializer.cs new file mode 100644 index 0000000..ffd7031 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Messages/OnionMessageSerializer.cs @@ -0,0 +1,45 @@ +using Lyn.Protocol.Bolt4.Entities; +using Lyn.Types.Fundamental; +using Lyn.Types.Serialization; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4.Messages +{ + public class OnionMessageSerializer : IProtocolTypeSerializer + { + private readonly IProtocolTypeSerializer packetSerializer; + + public OnionMessageSerializer(IProtocolTypeSerializer _packetSerializer) + { + this.packetSerializer = _packetSerializer; + } + + public OnionMessage Deserialize(ref SequenceReader reader, ProtocolTypeSerializerOptions? options = null) + { + var onionMessage = new OnionMessage(); + + var blindingKeyBytes = reader.ReadBytes(33); + onionMessage.BlindingKey = new PublicKey(blindingKeyBytes.ToArray()); + + // read the onion packet data from the message + onionMessage.OnionPacket = packetSerializer.Deserialize(ref reader, options); + + return onionMessage; + } + + public int Serialize(OnionMessage typeInstance, IBufferWriter writer, ProtocolTypeSerializerOptions? options = null) + { + int bytesWritten = 0; + + bytesWritten += writer.WriteBytes(typeInstance.BlindingKey); + bytesWritten += packetSerializer.Serialize(typeInstance.OnionPacket, writer, options); + + return bytesWritten; + } + } +} diff --git a/src/Lyn.Protocol/Bolt4/Messages/TlvRecords/OnionMessageTlv.cs b/src/Lyn.Protocol/Bolt4/Messages/TlvRecords/OnionMessageTlv.cs new file mode 100644 index 0000000..c459af5 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Messages/TlvRecords/OnionMessageTlv.cs @@ -0,0 +1,13 @@ +using Lyn.Protocol.Common.Messages; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4.Messages.TlvRecords +{ + public class OnionMessageTlv : TlvRecord + { + } +} diff --git a/src/Lyn.Protocol/Bolt4/OnionMessageService.cs b/src/Lyn.Protocol/Bolt4/OnionMessageService.cs new file mode 100644 index 0000000..49a3c04 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/OnionMessageService.cs @@ -0,0 +1,60 @@ +using Lyn.Protocol.Bolt1.Messages; +using Lyn.Protocol.Bolt4.Entities; +using Lyn.Protocol.Bolt4.Messages; +using Lyn.Protocol.Bolt8; +using Lyn.Protocol.Bolt9; +using Lyn.Protocol.Common.Messages; +using Lyn.Protocol.Connection; +using Lyn.Types.Fundamental; +using NaCl.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NBitcoin.Secp256k1; +using Lyn.Protocol.Common.Crypto; +using System.Buffers; +using Lyn.Types.Serialization; + +namespace Lyn.Protocol.Bolt4 +{ + public class OnionMessageService : IBoltMessageService + { + private readonly IBoltFeatures _boltFeatures; + private readonly IEllipticCurveActions _ellipticCurveActions; + private readonly ICipherFunction _cipherFunctions; + private readonly ISphinx _sphinx; + + // todo: need an actual way to surface this! + private PrivateKey _nodePrivatekey; + + public OnionMessageService(IBoltFeatures boltFeatures, + IEllipticCurveActions ellipticCurveActions, + ICipherFunction cipherFunctions, + ISphinx sphinx) + { + _boltFeatures = boltFeatures; + _ellipticCurveActions = ellipticCurveActions; + _cipherFunctions = cipherFunctions; + _sphinx = sphinx; + } + + public Task ProcessMessageAsync(PeerMessage message) + { + if (_boltFeatures.SupportedFeatures.HasFlag(Features.OptionOnionMessagesRequired)) + { + // todo: verify payload bytes aren't too large + + // else, if payload is correct sized + var blindedPrivateKey = _sphinx.DeriveBlindedPrivateKey(_nodePrivatekey, message.MessagePayload.BlindingKey); + + // peel the onion + _sphinx.PeelOnion(blindedPrivateKey, null, message.MessagePayload.OnionPacket); + } + + throw new NotImplementedException(); + } + } +} diff --git a/src/Lyn.Protocol/Bolt4/RouteBlinding.cs b/src/Lyn.Protocol/Bolt4/RouteBlinding.cs new file mode 100644 index 0000000..bcf0b07 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/RouteBlinding.cs @@ -0,0 +1,141 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Lyn.Protocol.Bolt3; +using Lyn.Protocol.Common.Crypto; +using Lyn.Types.Fundamental; +using NaCl.Core; + +namespace Lyn.Protocol.Bolt4 +{ + + public record IntroductionNode(PublicKey PublicKey, PublicKey BlindedPublicKey, PublicKey BlindingEphemeralKey, byte[] EncryptedPayload); + + public record BlindedNode(PublicKey BlindedPublicKey, byte[] EncryptedPayload); + + public record BlindedRoute + { + + // todo: Are these two properties needed? The other properties contain the same information? + public PublicKey IntroductionNodeId { get; init; } + public PublicKey BlindingKey { get; init; } + + public IntroductionNode IntroductionNode { get; init; } + public BlindedNode[] SubsequentNodes { get; private set; } + public PublicKey[] BlindedNodeIds { get; private set; } + public byte[][] EncryptedPayloads { get; private set; } + + + public BlindedNode[] BlindedNodes { get; init; } + + public BlindedRoute(PublicKey introductionNodeId, PublicKey blindingKey, BlindedNode[] blindedNodes) + { + if (blindedNodes.Length == 0) + { + throw new ArgumentException(nameof(blindedNodes), "blinded route must not be empty"); + } + + IntroductionNodeId = introductionNodeId; + BlindingKey = blindingKey; + BlindedNodes = blindedNodes; + + IntroductionNode = new IntroductionNode(IntroductionNodeId, BlindedNodes.First().BlindedPublicKey, BlindingKey, BlindedNodes.First().EncryptedPayload); + SubsequentNodes = BlindedNodes.Skip(1).ToArray(); + BlindedNodeIds = BlindedNodes.Select(x => x.BlindedPublicKey).ToArray(); + EncryptedPayloads = BlindedNodes.Select(x => x.EncryptedPayload).ToArray(); + } + } + + public record BlindedRouteDetails(BlindedRoute Route, PublicKey LastBlinding); + + public class RouteBlinding + { + + private readonly ISphinx _sphinx = null; + private readonly ILightningKeyDerivation _lightningKeyDerivation = null; + private readonly IEllipticCurveActions _ellipticCureActions = null; + + public RouteBlinding(ISphinx sphinx, ILightningKeyDerivation lightningKeyDerivation, IEllipticCurveActions ellipticCurveActions) + { + this._sphinx = sphinx; + this._lightningKeyDerivation = lightningKeyDerivation; + this._ellipticCureActions = ellipticCurveActions; + } + + // note: eclair has separate lists of public keys and encrypted payloads, yet asserts that they are the same length + // todo: should we do the same? i feel this enumerable of tuples is more elegant but i wonder if there's a 'muh security' reason for the separate lists + public BlindedRouteDetails Create(PrivateKey sessionKey, IEnumerable<(PublicKey PublicKey, byte[] Payload)> hops) + { + var e = sessionKey; + var blindedHopsAndKeys = hops.Select((hop) => + { + var (publicKey, payload) = hop; + + // Compute a shared secret, use it to derive ablinding key and rho for the ChaCha20Poly1305 cipher + var sharedSecret = _sphinx.ComputeSharedSecret(publicKey, e); + var blindingKey = _lightningKeyDerivation.PublicKeyFromPrivateKey(e); + var blindedPublicKey = _sphinx.BlindKey(publicKey, _sphinx.GenerateSphinxKey("blinded_node_id", sharedSecret)); + var rho = _sphinx.GenerateSphinxKey("rho", sharedSecret); + var cipher = new ChaCha20Poly1305(rho.ToArray()); + + // Next, allocate some buffers and encrypt the payload for this hop + // note: i didn't use the ChaCha20Poly1305CipherFunction bc the nonce increments by 1 for each call, and this requires a nonce of 0(?) + Span encryptedPayload = stackalloc byte[payload.Length + 16]; + Span ciphertext = stackalloc byte[payload.Length]; + Span mac = stackalloc byte[16]; + + cipher.Encrypt(new byte[12], payload.AsSpan(), ciphertext, mac, new byte[0]); + + ciphertext.CopyTo(encryptedPayload); + mac.CopyTo(encryptedPayload.Slice(payload.Length)); + + // Before we move onto the next hop we need to derive the next hop's e value + // todo: is blindingKey length a fixed 32 bytes? + Span bytesToHash = stackalloc byte[blindingKey.GetSpan().Length + sharedSecret.Length]; + blindingKey.GetSpan().CopyTo(bytesToHash); + sharedSecret.CopyTo(bytesToHash.Slice(blindingKey.GetSpan().Length)); + var newPrivKey = new PrivateKey(HashGenerator.Sha256(bytesToHash).ToArray()); + e = new PrivateKey(_ellipticCureActions.MultiplyWithPrivateKey(newPrivKey, e).ToArray()); + return (blindedHop: new BlindedNode(blindedPublicKey, encryptedPayload.ToArray()), blindingKey: blindingKey); + }).ToList(); + + return new BlindedRouteDetails(new BlindedRoute(hops.First().PublicKey, + blindedHopsAndKeys.First().blindingKey, + blindedHopsAndKeys.Select(x => x.blindedHop).ToArray()), + blindedHopsAndKeys.Last().blindingKey + ); + } + + public PrivateKey DerivePrivateKey(PrivateKey privateKey, PublicKey blindingEphemeralKey) + { + var sharedSecret = _sphinx.ComputeSharedSecret(blindingEphemeralKey, privateKey); + var generatedKey = new PrivateKey(_sphinx.GenerateSphinxKey("blinded_node_id", sharedSecret).ToArray()); + return new PrivateKey(_ellipticCureActions.MultiplyWithPrivateKey(generatedKey, privateKey).ToArray()); + } + + public (byte[], PublicKey) DecryptPayload(PrivateKey privateKey, PublicKey blindingEphemeralKey, byte[] encryptedPayload) + { + // Derive rho from our shared secret and instantiate a ChaCha20Poly1305 cipher + var sharedSecret = _sphinx.ComputeSharedSecret(blindingEphemeralKey, privateKey); + var rho = _sphinx.GenerateSphinxKey("rho", sharedSecret); + var cipher = new ChaCha20Poly1305(rho.ToArray()); + + // Create some buffers to handle the decryption + Span decryptedPayload = stackalloc byte[encryptedPayload.Length - 16]; + Span ciphertext = stackalloc byte[encryptedPayload.Length - 16]; + Span mac = stackalloc byte[16]; + + // Split the encrypted payload into the ciphertext and the MAC for decryption + encryptedPayload.AsSpan().Slice(0, encryptedPayload.Length - 16).CopyTo(ciphertext); + encryptedPayload.AsSpan().Slice(encryptedPayload.Length - 16).CopyTo(mac); + + // Decrypt the payload + // Note: Should figure out what the right error handling is here + cipher.Decrypt(new byte[12], ciphertext, mac, decryptedPayload, new byte[0]); + var nextBlindingEphemeralKey = _sphinx.BlindKey(blindingEphemeralKey, _sphinx.ComputeBlindingFactor(blindingEphemeralKey, sharedSecret)); + return (decryptedPayload.ToArray(), nextBlindingEphemeralKey); + } + } +} diff --git a/src/Lyn.Protocol/Bolt4/Sphinx.cs b/src/Lyn.Protocol/Bolt4/Sphinx.cs new file mode 100644 index 0000000..26cbac2 --- /dev/null +++ b/src/Lyn.Protocol/Bolt4/Sphinx.cs @@ -0,0 +1,371 @@ +using Lyn.Protocol.Bolt4.Entities; +using Lyn.Protocol.Common.Crypto; +using Lyn.Types.Fundamental; +using Lyn.Types.Onion; +using Lyn.Types.Serialization; +using NaCl.Core; +using NBitcoin.Secp256k1; +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lyn.Protocol.Bolt4 +{ + public class Sphinx : ISphinx + { + //todo: move somewhere better + private const int SPHINX_VERSION = 0; + private const int MAC_LENGTH = 32; + + private readonly IEllipticCurveActions _ellipticCurveActions; + + public Sphinx(IEllipticCurveActions ellipticCurveActions) + { + _ellipticCurveActions = ellipticCurveActions; + } + + + public ReadOnlySpan ComputeSharedSecret(PublicKey publicKey, PrivateKey secret) + { + return HashGenerator.Sha256(_ellipticCurveActions.MultiplyPubKey(secret, publicKey)); + } + + public ReadOnlySpan GenerateSphinxKey(byte[] keyType, ReadOnlySpan secret) + { + return HashGenerator.HmacSha256(keyType, secret); + } + + public ReadOnlySpan GenerateSphinxKey(string keyType, ReadOnlySpan secret) + { + var keyTypeBytes = Encoding.UTF8.GetBytes(keyType); + return GenerateSphinxKey(keyTypeBytes, secret); + } + + public PrivateKey DeriveBlindedPrivateKey(PrivateKey privateKey, PublicKey blindingEphemeralKey) + { + var sharedSecret = ComputeSharedSecret(blindingEphemeralKey, privateKey); + var generatedKey = GenerateSphinxKey("blinded_node_id", sharedSecret); + var newKeyBytes = _ellipticCurveActions.MultiplyPubKey(privateKey, generatedKey); + return new PrivateKey(newKeyBytes.ToArray()); + } + + // todo: util/helper + public ReadOnlySpan ExclusiveOR(ReadOnlySpan left, ReadOnlySpan right) + { + if (left.Length != right.Length) + { + throw new ArgumentException("inputs must be same length"); + } + + byte[] result = new byte[left.Length]; + + for (int i = 0; i < left.Length; i++) + { + result[i] = (byte)(left[i] ^ right[i]); + } + + return result; + } + + public ReadOnlySpan GenerateStream(ReadOnlySpan keyData, int streamLength) + { + // This is an unfortunate allocation, but NaCl seems to require a heap allocation for the key? + var cipher = new ChaCha20(new ReadOnlyMemory(keyData.ToArray()), 0); + var emptyPlainText = Enumerable.Range(0, streamLength).Select(x => 0x00).ToArray(); + var nonce = Enumerable.Range(0, 12).Select(x => 0x00).ToArray(); + return cipher.Encrypt(emptyPlainText, nonce); + } + + public ReadOnlySpan ComputeBlindingFactor(PublicKey pubKey, ReadOnlySpan secret) + { + // create an array to hold the concatenated data + var concatenatedData = new byte[pubKey.GetSpan().Length + secret.Length]; + + // copy the pubKey and secret data into the concatenated data array + pubKey.GetSpan().CopyTo(concatenatedData); + secret.CopyTo(concatenatedData.AsSpan(pubKey.GetSpan().Length)); + + // compute the SHA-256 hash of the concatenated data + return HashGenerator.Sha256(concatenatedData); + } + + public PublicKey BlindKey(PublicKey pubKey, ReadOnlySpan blindingFactor) + { + var blindingFactorArray = new byte[blindingFactor.Length]; + blindingFactor.CopyTo(blindingFactorArray); + + var blindKeyBytes = _ellipticCurveActions.MultiplyPubKey(new PrivateKey(blindingFactorArray), pubKey); + return new PublicKey(blindKeyBytes.ToArray()); + } + + public PublicKey BlindKey(PublicKey pubKey, IEnumerable blindingFactors) + { + return blindingFactors.Aggregate(pubKey, (key, blindingFactor) => BlindKey(key, blindingFactor)); + } + + public (IList, IList) ComputeEphemeralPublicKeysAndSharedSecrets(PrivateKey sessionKey, + IEnumerable publicKeys) + { + // this seems inelegant as fuck? + var key = new ECPubKey(EC.G, null); + var ephemeralPublicKey0 = BlindKey(new PublicKey(key.ToBytes()), sessionKey); + var secret0 = ComputeSharedSecret(publicKeys.First(), sessionKey); + var blindingFactor0 = ComputeBlindingFactor(ephemeralPublicKey0, secret0); + + IList ephemeralPublicKeys = new List { ephemeralPublicKey0 }; + IList blindingFactors = new List { blindingFactor0.ToArray() }; + IList sharedSecrets = new List() { secret0.ToArray() }; + + return ComputeEphemeralPublicKeysAndSharedSecrets(sessionKey, + publicKeys.Skip(1).ToList(), + ephemeralPublicKeys, + blindingFactors, + sharedSecrets); + } + + public (IList, IList) ComputeEphemeralPublicKeysAndSharedSecrets(PrivateKey sessionKey, + IEnumerable publicKeys, + IList ephemeralPublicKeys, + IList blindingFactors, + IList sharedSecrets) + { + if (!publicKeys.Any()) + { + return (ephemeralPublicKeys, sharedSecrets); + } + else + { + var ephemeralPublicKey = BlindKey(ephemeralPublicKeys.Last(), blindingFactors.Last()); + var secret = ComputeSharedSecret(BlindKey(publicKeys.First(), blindingFactors), sessionKey); + var blindingFactor = ComputeBlindingFactor(ephemeralPublicKey, secret); + + ephemeralPublicKeys.Add(ephemeralPublicKey); + blindingFactors.Add(blindingFactor.ToArray()); + sharedSecrets.Add(secret.ToArray()); + + return ComputeEphemeralPublicKeysAndSharedSecrets(sessionKey, + publicKeys.Skip(1).ToList(), + ephemeralPublicKeys, + blindingFactors, + sharedSecrets); + } + } + + public int PeekPayloadLength(byte[] payloadData) + { + var sequence = new ReadOnlySequence(new ReadOnlyMemory(payloadData)); + var binReader = new SequenceReader(sequence); + + if (binReader.TryPeek(out var firstByte)) + { + if (firstByte == 0x00) + { + // todo: consider re-throwing in Peel? + throw new InvalidOnionVersionException(); + } + + // safe to truncate because a packet will never be larger than 64KB? + int perHopPayloadLength = (int)binReader.ReadBigSize(); + perHopPayloadLength += (int)binReader.Consumed; //offset the length by the number of bytes consoomed + perHopPayloadLength += MAC_LENGTH; + return perHopPayloadLength; + } + else + { + throw new ArgumentException("payloadData is empty"); + } + } + + public ReadOnlySpan GenerateFiller(string keyType, + int packetPayloadLength, + IEnumerable sharedSecrets, + IEnumerable payloads) + { + // todo: asserts + var secretsAndPayloads = sharedSecrets.Zip(payloads); + var padding = new List(); + var filler = secretsAndPayloads.Aggregate(padding, (padding, secretAndPayload) => + { + var (secret, perHopPayload) = secretAndPayload; + + // todo: decide how the hmac comes into play... + var perHopPayloadLength = PeekPayloadLength(perHopPayload); + + if (perHopPayloadLength != perHopPayload.Length + MAC_LENGTH) + { + throw new Exception("invalid length"); + } + + // assert payload length + var fillerKey = GenerateSphinxKey(Encoding.ASCII.GetBytes(keyType), secret); + // todo: byte array matching payload length + padding.AddRange(Enumerable.Range(0, perHopPayloadLength).Select(x => 0x00)); + var stream = GenerateStream(fillerKey, packetPayloadLength + perHopPayloadLength).ToArray().TakeLast(padding.Count).ToArray(); + var filler = ExclusiveOR(padding.ToArray(), stream).ToArray(); + return filler.ToList(); + }); + + return filler.ToArray(); + } + + // TODO: return DecryptedOnionPacket? + public DecryptedOnionPacket PeelOnion(PrivateKey privateKey, byte[]? associatedData, OnionRoutingPacket packet) + { + if (packet.Version != 0) + { + // todo: this needs to contain the hash of the packet + throw new InvalidOnionVersionException(); + } + + var sharedSecret = ComputeSharedSecret(packet.EphemeralKey, privateKey); + var mu = GenerateSphinxKey("mu", sharedSecret); + var payloadToSign = associatedData != null ? packet.PayloadData.Concat(associatedData).ToArray() : packet.PayloadData; + var computedHmac = HashGenerator.HmacSha256(mu.ToArray(), payloadToSign); + Debug.WriteLine($"computedHmac: {Convert.ToHexString(computedHmac)}"); + Debug.WriteLine($"packet.Hmac: {Convert.ToHexString(packet.Hmac)}"); + if (computedHmac.SequenceEqual(packet.Hmac)) + { + var rho = GenerateSphinxKey("rho", sharedSecret); + var cipherStream = GenerateStream(rho.ToArray(), 2 * packet.PayloadData.Length); + // todo: better variable name here + var paddedPayload = packet.PayloadData.Concat(new byte[packet.PayloadData.Length]).ToArray(); + var binData = ExclusiveOR(paddedPayload, cipherStream).ToArray(); + + var perHopPayloadLength = PeekPayloadLength(binData); + + var sequence = new ReadOnlySequence(new ReadOnlyMemory(binData)); + var binReader = new SequenceReader(sequence); + + // todo: extract payload bytes from xor'd byte stream using payload length and hmac + var perHopPayload = binReader.ReadBytes(perHopPayloadLength - MAC_LENGTH); + var hopHMAC = binReader.ReadBytes(MAC_LENGTH); + + // truncated'd again but its safe? + var nextOnionPayload = binReader.ReadBytes((int)packet.PayloadData.Length); + var nextPublicKey = BlindKey(packet.EphemeralKey, ComputeBlindingFactor(packet.EphemeralKey, sharedSecret)); + + return new DecryptedOnionPacket() + { + Payload = perHopPayload.ToArray(), + NextPacket = new OnionRoutingPacket() + { + Version = SPHINX_VERSION, + EphemeralKey = nextPublicKey, + PayloadData = nextOnionPayload.ToArray(), + Hmac = hopHMAC.ToArray() + }, + SharedSecret = sharedSecret.ToArray(), + }; + + } + else + { + throw new InvalidOnionHmacException(); + } + } + + private OnionRoutingPacket WrapOnion(ReadOnlySpan payload, + PublicKey ephemeralPublicKey, + ReadOnlySpan sharedSecret, + T packet, + byte[]? associatedData, + byte[]? filler = null) + { + // todo: verify size + int packetPayloadLength = 0; + byte[] currentHmac = null; + byte[] currentPayload = null; + if (packet is OnionRoutingPacket onionPacket) + { + packetPayloadLength = onionPacket.PayloadData.Length; + currentPayload = onionPacket.PayloadData; + currentHmac = onionPacket.Hmac; + } + else if (packet is byte[] arr) + { + packetPayloadLength = arr.Length; + currentPayload = arr; + currentHmac = Enumerable.Range(0, MAC_LENGTH).Select(x => 0x00).ToArray(); + } + else + { + throw new Exception("unsupported packet type specified for T"); + } + + // onionPayload1 + var nextOnionPayload = payload.ToArray().Concat(currentHmac).Concat(currentPayload.SkipLast(payload.Length + MAC_LENGTH)).ToArray(); + var secondPayloadStream = GenerateStream(GenerateSphinxKey("rho", sharedSecret), packetPayloadLength); + nextOnionPayload = ExclusiveOR(nextOnionPayload, secondPayloadStream).ToArray(); + + if (filler != null) + { + nextOnionPayload = nextOnionPayload.SkipLast(filler.Length).Concat(filler).ToArray(); + } + + var hmacBytes = nextOnionPayload; + if (associatedData != null) + { + hmacBytes = nextOnionPayload.Concat(associatedData).ToArray(); + } + + var nextHmac = HashGenerator.HmacSha256(GenerateSphinxKey("mu", sharedSecret).ToArray(), hmacBytes); + Debug.WriteLine($"Computed HMAC: {Convert.ToHexString(nextHmac)}"); + var nextPacket = new OnionRoutingPacket() + { + Version = SPHINX_VERSION, + EphemeralKey = ephemeralPublicKey, + PayloadData = nextOnionPayload, + Hmac = nextHmac.ToArray() + }; + return nextPacket; + } + + private OnionRoutingPacket RecursivelyCreateOnion(IEnumerable payloads, + IEnumerable publicKeys, + IEnumerable sharedSecrets, + OnionRoutingPacket packet, + byte[]? associatedData) + { + if (!payloads.Any()) + { + return packet; + } + else + { + var nextPacket = WrapOnion(payloads.Last(), publicKeys.Last(), sharedSecrets.Last(), packet, associatedData); + return RecursivelyCreateOnion(payloads.SkipLast(1), publicKeys.SkipLast(1), sharedSecrets.SkipLast(1), nextPacket, associatedData); + } + } + + public PacketAndSecrets CreateOnion(PrivateKey sessionKey, + int packetPayloadLength, + IEnumerable publicKeys, + IEnumerable payloads, + byte[]? associatedData) + { + // todo: verify size of inputs to make sure nothing is too long + var (ephemeralPublicKeys, sharedSecrets) = ComputeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys); + var filler = GenerateFiller("rho", + packetPayloadLength, + sharedSecrets.SkipLast(1), + payloads.SkipLast(1)).ToArray(); + + // generate the last packet of the route + var startingBytes = GenerateStream(GenerateSphinxKey("pad", sessionKey), packetPayloadLength).ToArray(); + var lastPacket = WrapOnion(payloads.Last(), ephemeralPublicKeys.Last(), sharedSecrets.Last(), startingBytes, associatedData, filler); + + var onionPacket = RecursivelyCreateOnion(payloads.SkipLast(1), ephemeralPublicKeys.SkipLast(1), sharedSecrets.SkipLast(1), lastPacket, associatedData); + + return new PacketAndSecrets() + { + Packet = onionPacket, + SharedSecrets = sharedSecrets.Zip(ephemeralPublicKeys) + }; + } + } +} diff --git a/src/Lyn.Protocol/Bolt7/AnnouncementSignaturesValidator.cs b/src/Lyn.Protocol/Bolt7/AnnouncementSignaturesValidator.cs index 7e3daf5..9ef2217 100644 --- a/src/Lyn.Protocol/Bolt7/AnnouncementSignaturesValidator.cs +++ b/src/Lyn.Protocol/Bolt7/AnnouncementSignaturesValidator.cs @@ -1,11 +1,11 @@ using Lyn.Protocol.Bolt7.Messages; using Lyn.Protocol.Common; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Protocol.Common.Messages; namespace Lyn.Protocol.Bolt7 { - public class AnnouncementSignaturesValidator : IMessageValidator + public class AnnouncementSignaturesValidator : IMessageValidator { private readonly ISerializationFactory _serializationFactory; private readonly IGossipRepository _repository; diff --git a/src/Lyn.Protocol/Bolt7/ChannelAnnouncementValidator.cs b/src/Lyn.Protocol/Bolt7/ChannelAnnouncementValidator.cs index 4e7a3e9..bd9fe2e 100644 --- a/src/Lyn.Protocol/Bolt7/ChannelAnnouncementValidator.cs +++ b/src/Lyn.Protocol/Bolt7/ChannelAnnouncementValidator.cs @@ -1,7 +1,7 @@ using System.Linq; using Lyn.Protocol.Bolt7.Messages; using Lyn.Protocol.Common; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Protocol.Common.Messages; using Lyn.Types; using Lyn.Types.Fundamental; diff --git a/src/Lyn.Protocol/Bolt7/NodeAnnouncementValidator.cs b/src/Lyn.Protocol/Bolt7/NodeAnnouncementValidator.cs index 80a45f6..094c3f1 100644 --- a/src/Lyn.Protocol/Bolt7/NodeAnnouncementValidator.cs +++ b/src/Lyn.Protocol/Bolt7/NodeAnnouncementValidator.cs @@ -1,6 +1,6 @@ using Lyn.Protocol.Bolt7.Messages; using Lyn.Protocol.Common; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Protocol.Common.Messages; using Lyn.Types.Fundamental; diff --git a/src/Lyn.Protocol/Bolt8/EllipticCurveActions.cs b/src/Lyn.Protocol/Bolt8/EllipticCurveActions.cs deleted file mode 100644 index ac40a15..0000000 --- a/src/Lyn.Protocol/Bolt8/EllipticCurveActions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using NBitcoin; - -namespace Lyn.Protocol.Bolt8 -{ - public class EllipticCurveActions : IEllipticCurveActions - { - public ReadOnlySpan Multiply(byte[] privateKey, ReadOnlySpan publicKey) - => new PubKey(publicKey.ToArray()) - .GetSharedSecret(new Key(privateKey)); - } -} \ No newline at end of file diff --git a/src/Lyn.Protocol/Bolt8/HandshakeService.cs b/src/Lyn.Protocol/Bolt8/HandshakeService.cs index 48591cc..0e432c5 100644 --- a/src/Lyn.Protocol/Bolt8/HandshakeService.cs +++ b/src/Lyn.Protocol/Bolt8/HandshakeService.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using Lyn.Protocol.Common.Crypto; using Microsoft.Extensions.Logging; namespace Lyn.Protocol.Bolt8 diff --git a/src/Lyn.Protocol/Bolt8/IEllipticCurveActions.cs b/src/Lyn.Protocol/Bolt8/IEllipticCurveActions.cs deleted file mode 100644 index d5196a3..0000000 --- a/src/Lyn.Protocol/Bolt8/IEllipticCurveActions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Lyn.Protocol.Bolt8 -{ - public interface IEllipticCurveActions - { - ReadOnlySpan Multiply(byte[] privateKey, ReadOnlySpan publicKey); - } -} \ No newline at end of file diff --git a/src/Lyn.Protocol/Common/Crypto/EllipticCurveActions.cs b/src/Lyn.Protocol/Common/Crypto/EllipticCurveActions.cs new file mode 100644 index 0000000..96fb851 --- /dev/null +++ b/src/Lyn.Protocol/Common/Crypto/EllipticCurveActions.cs @@ -0,0 +1,26 @@ +using System; +using Lyn.Types.Fundamental; +using NBitcoin; + +namespace Lyn.Protocol.Common.Crypto +{ + public class EllipticCurveActions : IEllipticCurveActions + { + public ReadOnlySpan Multiply(byte[] privateKey, ReadOnlySpan publicKey) + => new PubKey(publicKey.ToArray()) + .GetSharedSecret(new Key(privateKey)); + + public ReadOnlySpan MultiplyWithPrivateKey(byte[] privateKey, ReadOnlySpan blindingKey) + { + NBitcoin.Secp256k1.ECPrivKey.TryCreate(privateKey, out var privKey); + byte[] result = new byte[32]; + privKey.TweakMul(blindingKey).WriteToSpan(result.AsSpan()); + + return result; + } + + public ReadOnlySpan MultiplyPubKey(byte[] privateKey, ReadOnlySpan publicKey) + => new PubKey(publicKey.ToArray()) + .GetSharedPubkey(new Key(privateKey)).ToBytes(); + } +} \ No newline at end of file diff --git a/src/Lyn.Protocol/Common/Hashing/HashGenerator.cs b/src/Lyn.Protocol/Common/Crypto/HashGenerator.cs similarity index 85% rename from src/Lyn.Protocol/Common/Hashing/HashGenerator.cs rename to src/Lyn.Protocol/Common/Crypto/HashGenerator.cs index ec0be89..5c6544b 100644 --- a/src/Lyn.Protocol/Common/Hashing/HashGenerator.cs +++ b/src/Lyn.Protocol/Common/Crypto/HashGenerator.cs @@ -3,7 +3,7 @@ using System.Security.Cryptography; using Lyn.Types.Bitcoin; -namespace Lyn.Protocol.Common.Hashing +namespace Lyn.Protocol.Common.Crypto { public static partial class HashGenerator { @@ -57,6 +57,14 @@ public static UInt256 DoubleSha512AsUInt256(ReadOnlySpan data) return new UInt256(result.Slice(0, 32)); } + public static ReadOnlySpan HmacSha256(ReadOnlySpan key, ReadOnlySpan data) + { + using var hmac = new HMACSHA256(key.ToArray()); + Span result = new byte[32]; + if (!hmac.TryComputeHash(data, result, out _)) throw new HashGeneratorException($"Failed to perform {nameof(HmacSha256)}"); + return result; + } + [DoesNotReturn] public static void ThrowHashGeneratorException(string message) { diff --git a/src/Lyn.Protocol/Common/Hashing/HashGeneratorException.cs b/src/Lyn.Protocol/Common/Crypto/HashGeneratorException.cs similarity index 93% rename from src/Lyn.Protocol/Common/Hashing/HashGeneratorException.cs rename to src/Lyn.Protocol/Common/Crypto/HashGeneratorException.cs index 9a14772..bb5064d 100644 --- a/src/Lyn.Protocol/Common/Hashing/HashGeneratorException.cs +++ b/src/Lyn.Protocol/Common/Crypto/HashGeneratorException.cs @@ -1,6 +1,6 @@ using System; -namespace Lyn.Protocol.Common.Hashing +namespace Lyn.Protocol.Common.Crypto { [Serializable] public class HashGeneratorException : Exception diff --git a/src/Lyn.Protocol/Common/Crypto/IEllipticCurveActions.cs b/src/Lyn.Protocol/Common/Crypto/IEllipticCurveActions.cs new file mode 100644 index 0000000..2eece23 --- /dev/null +++ b/src/Lyn.Protocol/Common/Crypto/IEllipticCurveActions.cs @@ -0,0 +1,11 @@ +using System; + +namespace Lyn.Protocol.Common.Crypto +{ + public interface IEllipticCurveActions + { + ReadOnlySpan Multiply(byte[] privateKey, ReadOnlySpan publicKey); + ReadOnlySpan MultiplyWithPrivateKey(byte[] privateKey, ReadOnlySpan blindingKey); + ReadOnlySpan MultiplyPubKey(byte[] privateKey, ReadOnlySpan publicKey); + } +} \ No newline at end of file diff --git a/src/Lyn.Protocol/Common/Hashing/ITransactionHashCalculator.cs b/src/Lyn.Protocol/Common/Crypto/ITransactionHashCalculator.cs similarity index 84% rename from src/Lyn.Protocol/Common/Hashing/ITransactionHashCalculator.cs rename to src/Lyn.Protocol/Common/Crypto/ITransactionHashCalculator.cs index e7f7bbb..22f4a1e 100644 --- a/src/Lyn.Protocol/Common/Hashing/ITransactionHashCalculator.cs +++ b/src/Lyn.Protocol/Common/Crypto/ITransactionHashCalculator.cs @@ -1,6 +1,6 @@ using Lyn.Types.Bitcoin; -namespace Lyn.Protocol.Common.Hashing +namespace Lyn.Protocol.Common.Crypto { public interface ITransactionHashCalculator { diff --git a/src/Lyn.Protocol/Common/Hashing/TransactionHashCalculator.cs b/src/Lyn.Protocol/Common/Crypto/TransactionHashCalculator.cs similarity index 94% rename from src/Lyn.Protocol/Common/Hashing/TransactionHashCalculator.cs rename to src/Lyn.Protocol/Common/Crypto/TransactionHashCalculator.cs index cf9c012..b157368 100644 --- a/src/Lyn.Protocol/Common/Hashing/TransactionHashCalculator.cs +++ b/src/Lyn.Protocol/Common/Crypto/TransactionHashCalculator.cs @@ -1,8 +1,9 @@ -using System.Buffers; +using System; +using System.Buffers; using Lyn.Types.Bitcoin; using Lyn.Types.Serialization; -namespace Lyn.Protocol.Common.Hashing +namespace Lyn.Protocol.Common.Crypto { public class TransactionHashCalculator : ITransactionHashCalculator { diff --git a/src/Lyn.Protocol/Common/DefaultIoCRegistrations.cs b/src/Lyn.Protocol/Common/DefaultIoCRegistrations.cs index 6b9e83e..6248901 100644 --- a/src/Lyn.Protocol/Common/DefaultIoCRegistrations.cs +++ b/src/Lyn.Protocol/Common/DefaultIoCRegistrations.cs @@ -16,7 +16,7 @@ using Lyn.Protocol.Bolt8; using Lyn.Protocol.Bolt9; using Lyn.Protocol.Common.Blockchain; -using Lyn.Protocol.Common.Hashing; +using Lyn.Protocol.Common.Crypto; using Lyn.Protocol.Common.Messages; using Lyn.Protocol.Connection; using Lyn.Types.Serialization; diff --git a/src/Lyn.Protocol/Common/Messages/MessageType.cs b/src/Lyn.Protocol/Common/Messages/MessageType.cs index 1532253..f152e63 100644 --- a/src/Lyn.Protocol/Common/Messages/MessageType.cs +++ b/src/Lyn.Protocol/Common/Messages/MessageType.cs @@ -35,5 +35,8 @@ public enum MessageType : ushort QueryShortChannelIds = 261, QueryChannelRange = 263, GossipTimestampFilter = 265, + + // Sphinx/Onion + OnionMessage = 513 } } \ No newline at end of file diff --git a/src/Lyn.Types/Onion/InvalidOnionHmacException.cs b/src/Lyn.Types/Onion/InvalidOnionHmacException.cs new file mode 100644 index 0000000..988d3e1 --- /dev/null +++ b/src/Lyn.Types/Onion/InvalidOnionHmacException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Lyn.Types.Onion +{ + [Serializable] + public class InvalidOnionHmacException : Exception + { + public InvalidOnionHmacException() : + base("Onion has an invalid HMAC") + { + + } + } +} diff --git a/src/Lyn.Types/Onion/InvalidOnionVersionException.cs b/src/Lyn.Types/Onion/InvalidOnionVersionException.cs new file mode 100644 index 0000000..cdad249 --- /dev/null +++ b/src/Lyn.Types/Onion/InvalidOnionVersionException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Lyn.Types.Onion +{ + [Serializable] + public class InvalidOnionVersionException : Exception + { + public InvalidOnionVersionException() : + base("Legacy Onion Format is not supported anymore") + { + + } + } +}