Skip to content

Commit

Permalink
Add virtual bolt card support
Browse files Browse the repository at this point in the history
  • Loading branch information
Kukks committed Sep 26, 2023
1 parent 0a55b43 commit ff6930a
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 43 deletions.
2 changes: 2 additions & 0 deletions LNURL.Tests/LNURL.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NBitcoin" Version="7.0.30" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.20" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
62 changes: 62 additions & 0 deletions LNURL.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.Altcoins.Elements;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
Expand Down Expand Up @@ -185,6 +188,65 @@ public async Task CanUseBoltCardHelper()
Assert.Equal("04996c6a926980", result.Value.uid);
Assert.True(BoltCardHelper.CheckCmac(result.Value.rawUid, result.Value.rawCtr, cmacKey, result.Value.c,
out error));

var manualP = BoltCardHelper.CreatePValue(key, result.Value.counter, result.Value.uid);
var manualPResult = BoltCardHelper.ExtractUidAndCounterFromP(manualP, key, out error);
Assert.Null(error);
Assert.NotNull(manualPResult);
Assert.Equal((uint) 3, manualPResult.Value.counter);
Assert.Equal("04996c6a926980", manualPResult.Value.uid);

var manualC = BoltCardHelper.CreateCValue(result.Value.rawUid, result.Value.rawCtr, cmacKey);
Assert.Equal(result.Value.c, manualC);
}

[Fact]
public async Task DeterministicCards()

Check warning on line 204 in LNURL.Tests/UnitTest1.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var masterSeed = RandomUtils.GetBytes(64);
var masterSeedSlip21 = Slip21Node.FromSeed(masterSeed);

var i = Random.Shared.Next(0, 10000);
var k1 = masterSeedSlip21.DeriveChild(i + "k1").Key.ToBytes().Take(16).ToArray();
var k2 = masterSeedSlip21.DeriveChild(i + "k2").Key.ToBytes().Take(16).ToArray();

var counter = (uint) Random.Shared.Next(0, 1000);
var uid = Convert.ToHexString(RandomUtils.GetBytes(7));
var pParam = Convert.ToHexString(BoltCardHelper.CreatePValue(k1, counter, uid));
var cParam = Convert.ToHexString(BoltCardHelper.CreateCValue(uid, counter, k2));
var lnurlw = $"https://test.com?p={pParam}&c={cParam}";

var result = BoltCardHelper.ExtractBoltCardFromRequest(new Uri(lnurlw), k1, out var error);
Assert.Null(error);
Assert.NotNull(result);
Assert.Equal(uid.ToLowerInvariant(), result.Value.uid.ToLowerInvariant());
Assert.Equal(counter, result.Value.counter);
Assert.True(BoltCardHelper.CheckCmac(result.Value.rawUid, result.Value.rawCtr, k2, result.Value.c,
out error));
Assert.Null(error);


for (int j = 0; j <= 10000; j++)
{
var brutek1 = masterSeedSlip21.DeriveChild(j + "k1").Key.ToBytes().Take(16).ToArray();
var brutek2 = masterSeedSlip21.DeriveChild(j + "k2").Key.ToBytes().Take(16).ToArray();
try
{
var bruteResult = BoltCardHelper.ExtractBoltCardFromRequest(new Uri(lnurlw), brutek1, out error);
Assert.Null(error);
Assert.NotNull(bruteResult);
Assert.Equal(uid.ToLowerInvariant(), bruteResult.Value.uid.ToLowerInvariant());
Assert.Equal(counter, bruteResult.Value.counter);
Assert.True(BoltCardHelper.CheckCmac(bruteResult.Value.rawUid, bruteResult.Value.rawCtr, brutek2,
bruteResult.Value.c, out error));
Assert.Null(error);

break;
}
catch (Exception e)

Check warning on line 246 in LNURL.Tests/UnitTest1.cs

View workflow job for this annotation

GitHub Actions / build

The variable 'e' is declared but never used
{
}
}
}
}
}
173 changes: 131 additions & 42 deletions LNURL/BoltCardHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,65 @@ public class BoltCardHelper
{
private const int AES_BLOCK_SIZE = 16;


public static (string uid, uint counter, byte[] rawUid, byte[] rawCtr)? ExtractUidAndCounterFromP(string pHex,
byte[] aesKey, out string? error)

Check warning on line 16 in LNURL/BoltCardHelper.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 16 in LNURL/BoltCardHelper.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 16 in LNURL/BoltCardHelper.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (!HexEncoder.IsWellFormed(pHex))
{
error = "p parameter is not hex";
return null;
}

return ExtractUidAndCounterFromP(Convert.FromHexString(pHex), aesKey, out error);
}

public static (string uid, uint counter, byte[] rawUid, byte[] rawCtr)? ExtractUidAndCounterFromP(byte[] p,
byte[] aesKey, out string? error)

Check warning on line 28 in LNURL/BoltCardHelper.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 28 in LNURL/BoltCardHelper.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 28 in LNURL/BoltCardHelper.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (p.Length != 16)
{
error = "p parameter length not valid";
return null;
}

using var aes = Aes.Create();
aes.Key = aesKey;
aes.IV = new byte[16]; // assuming IV is zeros. Adjust if needed.
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;

var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

using var memoryStream = new System.IO.MemoryStream(p);
using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
using var reader = new System.IO.BinaryReader(cryptoStream);
var decryptedPData = reader.ReadBytes(p.Length);
if (decryptedPData[0] != 0xC7)
{
error = "decrypted data not starting with 0xC7";
return null;
}

var uid = decryptedPData[1..8];
var ctr = decryptedPData[8..11];

var c = (uint) (ctr[2] << 16 | ctr[1] << 8 | ctr[0]);
var uidStr = BitConverter.ToString(uid).Replace("-", "").ToLower();
error = null;

return (uidStr, c, uid, ctr);
}

/// <summary>
/// Extracts BoltCard information from a given request URI.
/// </summary>
/// <param name="requestUri">The URI containing BoltCard data.</param>
/// <param name="aesKey">The AES key for decryption.</param>
/// <param name="error">Outputs an error string if extraction fails.</param>
/// <returns>A tuple containing the UID and counter if successful; null otherwise.</returns>
public static (string uid, uint counter, byte[] rawUid, byte[] rawCtr, byte[] c)? ExtractBoltCardFromRequest(Uri requestUri, byte[] aesKey,
public static (string uid, uint counter, byte[] rawUid, byte[] rawCtr, byte[] c)? ExtractBoltCardFromRequest(
Uri requestUri, byte[] aesKey,
out string error)
{
var query = requestUri.ParseQueryString();
Expand All @@ -30,6 +81,12 @@ public static (string uid, uint counter, byte[] rawUid, byte[] rawCtr, byte[] c)
return null;
}

var pResult = ExtractUidAndCounterFromP(pParam, aesKey, out error);
if (error is not null || pResult is null)
{
return null;
}

var cParam = query.Get("c");

if (cParam is null)
Expand All @@ -38,57 +95,22 @@ public static (string uid, uint counter, byte[] rawUid, byte[] rawCtr, byte[] c)
return null;
}

if (!HexEncoder.IsWellFormed(pParam))
{
error = "p parameter is not hex";
return null;
}

if (!HexEncoder.IsWellFormed(cParam))
{
error = "c parameter is not hex";
return null;
}

var pRaw = Convert.FromHexString(pParam);
var cRaw = Convert.FromHexString(cParam);
if (pRaw.Length != 16)
{
error = "p parameter length not valid";
return null;
}

if (cRaw.Length != 8)
{
error = "c parameter length not valid";
return null;
}

using var aes = Aes.Create();
aes.Key = aesKey;
aes.IV = new byte[16]; // assuming IV is zeros. Adjust if needed.
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;

var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

using var memoryStream = new System.IO.MemoryStream(pRaw);
using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
using var reader = new System.IO.BinaryReader(cryptoStream);
var decryptedPData = reader.ReadBytes(pRaw.Length);
if (decryptedPData[0] != 0xC7)
{
error = "decrypted data not starting with 0xC7";
return null;
}

var uid = decryptedPData[1..8];
var ctr = decryptedPData[8..11];

var c = (uint) (ctr[2] << 16 | ctr[1] << 8 | ctr[0]);
var uidStr = BitConverter.ToString(uid).Replace("-", "").ToLower();
error = null;
return (uidStr, c, uid, ctr, cRaw );
return (pResult.Value.uid, pResult.Value.counter, pResult.Value.rawUid, pResult.Value.rawCtr, cRaw);
}

private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] data)
Expand Down Expand Up @@ -172,13 +194,14 @@ private static byte[] AesCmac(byte[] key, byte[] data)

return HashValue;
}

private static byte[] GetSunMac(byte[] key, byte[] sv2) {

private static byte[] GetSunMac(byte[] key, byte[] sv2)
{
var cmac1 = AesCmac(key, sv2);
var cmac2 = AesCmac(cmac1, Array.Empty<byte>());

var halfMac = new byte[cmac2.Length / 2];
for (var i = 1; i < cmac2.Length; i += 2)
var halfMac = new byte[cmac2.Length / 2];
for (var i = 1; i < cmac2.Length; i += 2)
{
halfMac[i >> 1] = cmac2[i];
}
Expand All @@ -203,7 +226,7 @@ public static bool CheckCmac(byte[] uid, byte[] ctr, byte[] k2CmacKey, byte[] cm
return false;
}

byte[] sv2 = new byte[AES_BLOCK_SIZE]
byte[] sv2 = new byte[]
{
0x3c, 0xc3, 0x00, 0x01, 0x00, 0x80,
uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6],
Expand Down Expand Up @@ -235,5 +258,71 @@ public static bool CheckCmac(byte[] uid, byte[] ctr, byte[] k2CmacKey, byte[] cm
return false;
}
}

public static byte[] CreateCValue(string uid, uint counter, byte[] k2CmacKey)
{
var ctr = new byte[3];
ctr[2] = (byte) (counter >> 16);
ctr[1] = (byte) (counter >> 8);
ctr[0] = (byte) (counter);

var uidBytes = Convert.FromHexString(uid);
return CreateCValue(uidBytes, ctr, k2CmacKey);
}

public static byte[] CreateCValue(byte[] uid, byte[] counter, byte[] k2CmacKey)
{
if (uid.Length != 7 || counter.Length != 3 || k2CmacKey.Length != AES_BLOCK_SIZE)
{
throw new ArgumentException("Invalid input lengths.");
}

byte[] sv2 =
{
0x3c, 0xc3, 0x00, 0x01, 0x00, 0x80,
uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6],
counter[0], counter[1], counter[2]
};

var computedCmac = GetSunMac(k2CmacKey, sv2);

return computedCmac;
}

public static byte[] CreatePValue(byte[] aesKey, uint counter, string uid)
{
using var aes = Aes.Create();
aes.Key = aesKey;
aes.IV = new byte[16]; // assuming IV is zeros. Adjust if needed.
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;

// Constructing the 16-byte array to be encrypted
byte[] toEncrypt = new byte[16];
toEncrypt[0] = 0xC7; // First byte is 0xC7

var uidBytes = Convert.FromHexString(uid);
Array.Copy(uidBytes, 0, toEncrypt, 1, uidBytes.Length);

// Counter
toEncrypt[8] = (byte) (counter & 0xFF); // least-significant byte
toEncrypt[9] = (byte) ((counter >> 8) & 0xFF);
toEncrypt[10] = (byte) ((counter >> 16) & 0xFF);

// Encryption
var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] encryptedData;
using var memoryStream = new System.IO.MemoryStream();
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
{
cryptoStream.Write(toEncrypt, 0, toEncrypt.Length);
}

encryptedData = memoryStream.ToArray();

var result = ExtractUidAndCounterFromP(encryptedData, aesKey, out var error);

return encryptedData;
}
}
}
2 changes: 1 addition & 1 deletion LNURL/LNURL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>LNURL protocol implementation in .NET Core</Description>
<PackageTags>bitcoin lightning lnurl</PackageTags>
<PackageVersion>0.0.33</PackageVersion>
<PackageVersion>0.0.34</PackageVersion>
<RepositoryUrl>https://github.com/Kukks/LNURL.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down

0 comments on commit ff6930a

Please sign in to comment.