diff --git a/LNURL/BoltCardHelper.cs b/LNURL/BoltCardHelper.cs new file mode 100644 index 0000000..35d541a --- /dev/null +++ b/LNURL/BoltCardHelper.cs @@ -0,0 +1,229 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using NBitcoin.DataEncoders; + +namespace LNURL +{ + public class BoltCardHelper + { + private const int AES_BLOCK_SIZE = 16; + + /// + /// Extracts BoltCard information from a given request URI. + /// + /// The URI containing BoltCard data. + /// The AES key for decryption. + /// Outputs an error string if extraction fails. + /// A tuple containing the UID and counter if successful; null otherwise. + public static (string uid, uint counter)? ExtractBoltCardFromRequest(Uri requestUri, byte[] aesKey, + out string error) + { + var query = requestUri.ParseQueryString(); + + var pParam = query.Get("p"); + if (pParam is null) + { + error = "p parameter is missing"; + return null; + } + + var cParam = query.Get("c"); + + if (cParam is null) + { + error = "c parameter is missing"; + 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.PKCS7; + + 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); + } + + private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] data) + { + using MemoryStream ms = new MemoryStream(); + using var aes = Aes.Create(); + + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + + using var cs = new CryptoStream(ms, aes.CreateEncryptor(key, iv), CryptoStreamMode.Write); + cs.Write(data, 0, data.Length); + cs.FlushFinalBlock(); + + return ms.ToArray(); + } + + private static byte[] RotateLeft(byte[] b) + { + byte[] r = new byte[b.Length]; + byte carry = 0; + + for (int i = b.Length - 1; i >= 0; i--) + { + ushort u = (ushort) (b[i] << 1); + r[i] = (byte) ((u & 0xff) + carry); + carry = (byte) ((u & 0xff00) >> 8); + } + + return r; + } + + private static byte[] AesCmac(byte[] key, byte[] data) + { + // SubKey generation + // step 1, AES-128 with key K is applied to an all-zero input block. + byte[] L = AesEncrypt(key, new byte[16], new byte[16]); + + // step 2, K1 is derived through the following operation: + byte[] + FirstSubkey = + RotateLeft(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit. + if ((L[0] & 0x80) == 0x80) + FirstSubkey[15] ^= + 0x87; // Otherwise, K1 is the exclusive-OR of const_Rb and the left-shift of L by 1 bit. + + // step 3, K2 is derived through the following operation: + byte[] + SecondSubkey = + RotateLeft(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit. + if ((FirstSubkey[0] & 0x80) == 0x80) + SecondSubkey[15] ^= + 0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit. + + // MAC computing + if (((data.Length != 0) && (data.Length % 16 == 0)) == true) + { + // If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits), + // the last block shall be exclusive-OR'ed with K1 before processing + for (int j = 0; j < FirstSubkey.Length; j++) + data[data.Length - 16 + j] ^= FirstSubkey[j]; + } + else + { + // Otherwise, the last block shall be padded with 10^i + byte[] padding = new byte[16 - data.Length % 16]; + padding[0] = 0x80; + + data = data.Concat(padding.AsEnumerable()).ToArray(); + + // and exclusive-OR'ed with K2 + for (int j = 0; j < SecondSubkey.Length; j++) + data[data.Length - 16 + j] ^= SecondSubkey[j]; + } + + // The result of the previous process will be the input of the last encryption. + byte[] encResult = AesEncrypt(key, new byte[16], data); + + byte[] HashValue = new byte[16]; + Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length); + + return HashValue; + } + + /// + /// Verifies the CMAC for given UID, counter, key, and CMAC data. + /// + /// The user ID. + /// The counter data. + /// The CMAC key. + /// The CMAC data to verify against. + /// Outputs an error string if verification fails. + /// True if CMAC verification is successful, otherwise false. + public static bool CheckCmac(byte[] uid, byte[] ctr, byte[] k2CmacKey, byte[] cmac, out string error) + { + if (uid.Length != 7 || ctr.Length != 3 || k2CmacKey.Length != AES_BLOCK_SIZE) + { + error = "Invalid input lengths."; + return false; + } + + byte[] sv2 = new byte[AES_BLOCK_SIZE] + { + 0x3c, 0xc3, 0x00, 0x01, 0x00, 0x80, + uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6], + ctr[0], ctr[1], ctr[2] + }; + + try + { + byte[] computedCmac = AesCmac(k2CmacKey, sv2); + + if (computedCmac.Length != cmac.Length) + { + error = "Computed CMAC length mismatch."; + return false; + } + + for (int i = 0; i < computedCmac.Length; i++) + { + if (computedCmac[i] != cmac[i]) + { + error = "CMAC verification failed."; + return false; + } + } + + error = null; + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + } +} \ No newline at end of file