diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b8b5a0a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,168 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 +space_before_self_closing = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf +dotnet_style_namespace_match_folder = true:suggestion + +[*.json] +indent_size = 2 + +[swagger*.json] +indent_size = 4 + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_within_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# only use var when it's obvious what the variable type is +csharp_style_var_for_built_in_types = false:none +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion + +# use language keywords instead of BCL types +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style + +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal + +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +dotnet_sort_system_directives_first = true +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_style_prefer_null_check_over_type_check = true:warning +csharp_prefer_simple_using_statement = true:warning +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_lambdas = true:silent + +# C++ Files + +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd, bat}] +end_of_line = crlf diff --git a/BTCPayServer.BoltCardTools.sln b/BTCPayServer.BoltCardTools.sln index 477f8af..f70e383 100644 --- a/BTCPayServer.BoltCardTools.sln +++ b/BTCPayServer.BoltCardTools.sln @@ -3,18 +3,22 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.NTag424", "src\BTCPayServer.NTag424\BTCPayServer.NTag424.csproj", "{1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.NTag424", "src\BTCPayServer.NTag424\BTCPayServer.NTag424.csproj", "{1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoltCardTools.Tests", "tests\BoltCardTools.Tests.csproj", "{4ABD12CC-3B7C-4C8C-8576-19F32C402584}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BoltCardTools.Tests", "tests\BoltCardTools.Tests.csproj", "{4ABD12CC-3B7C-4C8C-8576-19F32C402584}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{AE043F8E-1026-4502-8E7A-6F9C6C75EAF6}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitattributes = .gitattributes + .gitignore = .gitignore + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -25,4 +29,7 @@ Global {4ABD12CC-3B7C-4C8C-8576-19F32C402584}.Release|Any CPU.ActiveCfg = Release|Any CPU {4ABD12CC-3B7C-4C8C-8576-19F32C402584}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal diff --git a/src/BTCPayServer.NTag424/AESKey.cs b/src/BTCPayServer.NTag424/AESKey.cs index c9f8769..9be04c5 100644 --- a/src/BTCPayServer.NTag424/AESKey.cs +++ b/src/BTCPayServer.NTag424/AESKey.cs @@ -1,191 +1,189 @@ -using System; -using System.Diagnostics.Metrics; +using System; using System.IO; using System.Linq; using System.Security.Cryptography; using static BoltCardTools.Helpers; -namespace BoltCardTools +namespace BoltCardTools; + +public class AESKey { - public class AESKey - { - public const int BLOCK_SIZE = 16; - byte[] _bytes; - public byte[] ToBytes() => _bytes.ToArray(); - public static AESKey Parse(string hex) + public const int BLOCK_SIZE = 16; + byte[] _bytes; + public byte[] ToBytes() => _bytes.ToArray(); + public static AESKey Parse(string hex) + { + return new AESKey(hex.HexToBytes()); + } + AESKey(byte[] bytes) + { + AssertKeySize(bytes); + _bytes = bytes; + } + public AESKey(ReadOnlySpan bytes) + { + AssertKeySize(bytes); + _bytes = bytes.ToArray(); + } + + private static void AssertKeySize(ReadOnlySpan bytes) + { + if (bytes.Length != BLOCK_SIZE) + throw new ArgumentException($"AES key must be {BLOCK_SIZE} bytes long"); + } + + public AESKey Derive(byte[] input) + { + return new AESKey(CMac(input)); + } + public byte[] Decrypt(ReadOnlySpan cypherText, byte[]? iv = null) + { + iv ??= new byte[BLOCK_SIZE]; + using MemoryStream ms = new MemoryStream(cypherText.ToArray()); + using var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + using var cs = new CryptoStream(ms, aes.CreateDecryptor(_bytes, iv), CryptoStreamMode.Read); + var output = new byte[cypherText.Length]; + cs.Read(output); + return output; + } + public byte[] Encrypt(byte[] clearText, byte[]? iv = null, CipherMode mode = CipherMode.CBC) + { + return AesEncrypt(_bytes, iv ?? new byte[BLOCK_SIZE], clearText, mode); + } + public byte[] GetSunMac(PICCData piccData, byte[]? payload = null) + { + return GetSunMac(piccData.Uid, piccData.Counter, payload); + } + public byte[] GetSunMac(byte[]? uid, int? counter, byte[]? payload = null) + { + payload ??= Array.Empty(); + var derived = SesSDMFileReadMACKey(uid, counter); + var cmac = derived.CMac(payload); + return Truncate(cmac); + } + public PICCData DecryptSun(byte[] data) + { + return PICCData.Create(Decrypt(data)); + } + AESKey SesSDMFileReadMACKey(byte[]? uid, int? counter) + { + int i = 0; + var sv2 = new byte[16]; + sv2[i++] = 0x3c; + sv2[i++] = 0xc3; + sv2[i++] = 0x00; + sv2[i++] = 0x01; + sv2[i++] = 0x00; + sv2[i++] = 0x80; + if (uid is not null) { - return new AESKey(hex.HexToBytes()); + sv2[i++] = uid[0]; + sv2[i++] = uid[1]; + sv2[i++] = uid[2]; + sv2[i++] = uid[3]; + sv2[i++] = uid[4]; + sv2[i++] = uid[5]; + sv2[i++] = uid[6]; } - AESKey(byte[] bytes) - { - AssertKeySize(bytes); - _bytes = bytes; - } - public AESKey(ReadOnlySpan bytes) - { - AssertKeySize(bytes); - _bytes = bytes.ToArray(); - } + if (counter is int) + { + sv2[i++] = (byte)counter; + sv2[i++] = (byte)(counter >> 8); + sv2[i++] = (byte)(counter >> 16); + } + return Derive(sv2); + } + //AESKey SesSDMFileReadENCKey(AESKey key, byte[] uid, int counter) + //{ + // byte[] sv1 = + // { + // 0xc3, 0x3c, 0x00, 0x01, 0x00, 0x80, + // uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6], + // ((byte)counter), (byte)(counter >> 8), (byte)(counter >> 16) + // }; + // return key.Derive(sv1); + //} + private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] data, CipherMode mode = CipherMode.CBC) + { + using MemoryStream ms = new MemoryStream(); + using var aes = Aes.Create(); + aes.Mode = mode; + aes.Padding = PaddingMode.None; - private static void AssertKeySize(ReadOnlySpan bytes) - { - if (bytes.Length != BLOCK_SIZE) - throw new ArgumentException($"AES key must be {BLOCK_SIZE} bytes long"); - } + using var cs = new CryptoStream(ms, aes.CreateEncryptor(key, iv), CryptoStreamMode.Write); + cs.Write(data, 0, data.Length); + cs.FlushFinalBlock(); - public AESKey Derive(byte[] input) - { - return new AESKey(CMac(input)); - } - public byte[] Decrypt(ReadOnlySpan cypherText, byte[]? iv = null) - { - iv ??= new byte[BLOCK_SIZE]; - using MemoryStream ms = new MemoryStream(cypherText.ToArray()); - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; - using var cs = new CryptoStream(ms, aes.CreateDecryptor(_bytes, iv), CryptoStreamMode.Read); - var output = new byte[cypherText.Length]; - cs.Read(output); - return output; - } - public byte[] Encrypt(byte[] clearText, byte[]? iv = null, CipherMode mode = CipherMode.CBC) - { - return AesEncrypt(_bytes, iv ?? new byte[BLOCK_SIZE], clearText, mode); - } - public byte[] GetSunMac(PICCData piccData, byte[]? payload = null) - { - return GetSunMac(piccData.Uid, piccData.Counter, payload); - } - public byte[] GetSunMac(byte[]? uid, int? counter, byte[]? payload = null) - { - payload ??= Array.Empty(); - var derived = SesSDMFileReadMACKey(uid, counter); - var cmac = derived.CMac(payload); - return Truncate(cmac); - } - public PICCData DecryptSun(byte[] data) - { - return PICCData.Create(Decrypt(data)); - } - AESKey SesSDMFileReadMACKey(byte[]? uid, int? counter) - { - int i = 0; - var sv2 = new byte[16]; - sv2[i++] = 0x3c; - sv2[i++] = 0xc3; - sv2[i++] = 0x00; - sv2[i++] = 0x01; - sv2[i++] = 0x00; - sv2[i++] = 0x80; - if (uid is byte[]) - { - sv2[i++] = uid[0]; - sv2[i++] = uid[1]; - sv2[i++] = uid[2]; - sv2[i++] = uid[3]; - sv2[i++] = uid[4]; - sv2[i++] = uid[5]; - sv2[i++] = uid[6]; - } - if (counter is int) - { - sv2[i++] = (byte)counter; - sv2[i++] = (byte)(counter >> 8); - sv2[i++] = (byte)(counter >> 16); - } - return Derive(sv2); - } - //AESKey SesSDMFileReadENCKey(AESKey key, byte[] uid, int counter) - //{ - // byte[] sv1 = - // { - // 0xc3, 0x3c, 0x00, 0x01, 0x00, 0x80, - // uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6], - // ((byte)counter), (byte)(counter >> 8), (byte)(counter >> 16) - // }; - // return key.Derive(sv1); - //} - private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] data, CipherMode mode = CipherMode.CBC) - { - using MemoryStream ms = new MemoryStream(); - using var aes = Aes.Create(); - aes.Mode = mode; - aes.Padding = PaddingMode.None; + return ms.ToArray(); + } - using var cs = new CryptoStream(ms, aes.CreateEncryptor(key, iv), CryptoStreamMode.Write); - cs.Write(data, 0, data.Length); - cs.FlushFinalBlock(); + public byte[] CMac(byte[] data) + { + var key = _bytes; + // 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]); - return ms.ToArray(); - } + // 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. - public byte[] CMac(byte[] data) - { - var key = _bytes; - // 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 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. - // 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. + // 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; - // 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. + data = data.Concat(padding.AsEnumerable()).ToArray(); - // 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; + // and exclusive-OR'ed with K2 + for (int j = 0; j < SecondSubkey.Length; j++) + data[data.Length - 16 + j] ^= SecondSubkey[j]; + } - data = data.Concat(padding.AsEnumerable()).ToArray(); + // The result of the previous process will be the input of the last encryption. + byte[] encResult = AesEncrypt(key, new byte[16], data); - // and exclusive-OR'ed with K2 - for (int j = 0; j < SecondSubkey.Length; j++) - data[data.Length - 16 + j] ^= SecondSubkey[j]; - } + byte[] HashValue = new byte[16]; + Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length); - // The result of the previous process will be the input of the last encryption. - byte[] encResult = AesEncrypt(key, new byte[16], data); + return HashValue; + } - byte[] HashValue = new byte[16]; - Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length); + static byte[] RotateLeft(byte[] b) + { + byte[] r = new byte[b.Length]; + byte carry = 0; - return HashValue; - } + 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); + } - 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; - } - } + return r; + } } diff --git a/src/BTCPayServer.NTag424/AesCmac.cs b/src/BTCPayServer.NTag424/AesCmac.cs index b4245ae..ba1309f 100644 --- a/src/BTCPayServer.NTag424/AesCmac.cs +++ b/src/BTCPayServer.NTag424/AesCmac.cs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Frans van Dorsselaer +// SPDX-FileCopyrightText: 2022 Frans van Dorsselaer // // SPDX-License-Identifier: MIT // https://github.com/dorssel/dotnet-aes-extra/ @@ -6,272 +6,271 @@ using System; using System.Security.Cryptography; -namespace BoltCardTools +namespace BoltCardTools; + +/// +/// Computes a Cipher-based Message Authentication Code (CMAC) by using the symmetric key AES block cipher. +/// +public sealed class AesCmac + : KeyedHashAlgorithm { - /// - /// Computes a Cipher-based Message Authentication Code (CMAC) by using the symmetric key AES block cipher. - /// - public sealed class AesCmac - : KeyedHashAlgorithm - { - const int BLOCKSIZE = 16; // bytes + const int BLOCKSIZE = 16; // bytes - /// - /// This static override defaults to . - public static new KeyedHashAlgorithm Create() => new AesCmac(); + /// + /// This static override defaults to . + public static new KeyedHashAlgorithm Create() => new AesCmac(); - /// - public static new KeyedHashAlgorithm? Create(string algorithmName) - { - if (algorithmName == null) - { - throw new ArgumentNullException(nameof(algorithmName)); - } - return algorithmName == nameof(AesCmac) ? Create() : null; - } + /// + public static new KeyedHashAlgorithm? Create(string algorithmName) + { + if (algorithmName == null) + { + throw new ArgumentNullException(nameof(algorithmName)); + } + return algorithmName == nameof(AesCmac) ? Create() : null; + } - /// - /// Initializes a new instance of the class with a randomly generated key. - /// - public AesCmac() - { - AesEcb = Aes.Create(); - AesEcb.Mode = CipherMode.ECB; // DevSkim: ignore DS187371 - AesEcb.Padding = PaddingMode.None; - CryptoTransform = AesEcb.CreateEncryptor(); - HashSizeValue = BLOCKSIZE * 8; - } + /// + /// Initializes a new instance of the class with a randomly generated key. + /// + public AesCmac() + { + AesEcb = Aes.Create(); + AesEcb.Mode = CipherMode.ECB; // DevSkim: ignore DS187371 + AesEcb.Padding = PaddingMode.None; + CryptoTransform = AesEcb.CreateEncryptor(); + HashSizeValue = BLOCKSIZE * 8; + } - /// - /// Initializes a new instance of the class with the specified key data. - /// - /// The secret key for AES-CMAC algorithm. - public AesCmac(byte[] key) - : this() - { - Key = key; - } + /// + /// Initializes a new instance of the class with the specified key data. + /// + /// The secret key for AES-CMAC algorithm. + public AesCmac(byte[] key) + : this() + { + Key = key; + } - void ZeroizeState() - { - CryptographicOperations.ZeroMemory(C); - CryptographicOperations.ZeroMemory(Partial); - } + void ZeroizeState() + { + CryptographicOperations.ZeroMemory(C); + CryptographicOperations.ZeroMemory(Partial); + } - #region IDisposable - bool IsDisposed; + #region IDisposable + bool IsDisposed; - /// - protected override void Dispose(bool disposing) - { - if (!IsDisposed) - { - if (disposing) - { - CryptoTransform.Dispose(); - AesEcb.Dispose(); - ZeroizeState(); - } - IsDisposed = true; - } - base.Dispose(disposing); - } - #endregion + /// + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + CryptoTransform.Dispose(); + AesEcb.Dispose(); + ZeroizeState(); + } + IsDisposed = true; + } + base.Dispose(disposing); + } + #endregion - /// - public override byte[] Key - { - get => AesEcb.Key; - set - { - CryptoTransform.Dispose(); - AesEcb.Key = value; - CryptoTransform = AesEcb.CreateEncryptor(); - } - } + /// + public override byte[] Key + { + get => AesEcb.Key; + set + { + CryptoTransform.Dispose(); + AesEcb.Key = value; + CryptoTransform = AesEcb.CreateEncryptor(); + } + } - readonly Aes AesEcb; - ICryptoTransform CryptoTransform; + readonly Aes AesEcb; + ICryptoTransform CryptoTransform; - // See: NIST SP 800-38B, Section 6.2, Step 5 - readonly byte[] C = new byte[BLOCKSIZE]; + // See: NIST SP 800-38B, Section 6.2, Step 5 + readonly byte[] C = new byte[BLOCKSIZE]; - // See: NIST SP 800-38B, Section 4.2.2 - // - // In-place: X = CIPH_K(X) - void CIPH_K_InPlace(byte[] X_Base, int X_Offset = 0) - { - CryptoTransform.TransformBlock(X_Base, X_Offset, BLOCKSIZE, X_Base, X_Offset); - } + // See: NIST SP 800-38B, Section 4.2.2 + // + // In-place: X = CIPH_K(X) + void CIPH_K_InPlace(byte[] X_Base, int X_Offset = 0) + { + CryptoTransform.TransformBlock(X_Base, X_Offset, BLOCKSIZE, X_Base, X_Offset); + } - // See: NIST SP 800-38B, Section 6.1 - // - // Returns: first ? K1 : K2 - byte[] SUBK(bool first) - { - var X = new byte[BLOCKSIZE]; - // Step 1: X has the role of L - CIPH_K_InPlace(X); - // Step 2: X has the role of K1 - dbl_InPlace(X); - if (first) - { - // Step 4: return K1 - return X; - } - // Step 3: X has the role of K1 - dbl_InPlace(X); - // Step 4: return K2 - return X; - } + // See: NIST SP 800-38B, Section 6.1 + // + // Returns: first ? K1 : K2 + byte[] SUBK(bool first) + { + var X = new byte[BLOCKSIZE]; + // Step 1: X has the role of L + CIPH_K_InPlace(X); + // Step 2: X has the role of K1 + dbl_InPlace(X); + if (first) + { + // Step 4: return K1 + return X; + } + // Step 3: X has the role of K1 + dbl_InPlace(X); + // Step 4: return K2 + return X; + } - /// - public override void Initialize() - { - // See: NIST SP 800-38B, Section 6.2, Step 5 - ZeroizeState(); + /// + public override void Initialize() + { + // See: NIST SP 800-38B, Section 6.2, Step 5 + ZeroizeState(); - PartialLength = 0; - } + PartialLength = 0; + } - readonly byte[] Partial = new byte[BLOCKSIZE]; - int PartialLength; + readonly byte[] Partial = new byte[BLOCKSIZE]; + int PartialLength; - // See: NIST SP 800-38B, Section 6.2, Step 6 - void AddBlock(byte[] blockBase, int blockOffset = 0) - { - xor_InPlace(C, 0, blockBase, blockOffset, BLOCKSIZE); - CIPH_K_InPlace(C); - } + // See: NIST SP 800-38B, Section 6.2, Step 6 + void AddBlock(byte[] blockBase, int blockOffset = 0) + { + xor_InPlace(C, 0, blockBase, blockOffset, BLOCKSIZE); + CIPH_K_InPlace(C); + } - /// - protected override void HashCore(byte[] array, int ibStart, int cbSize) - { - if (cbSize == 0) - { - return; - } + /// + protected override void HashCore(byte[] array, int ibStart, int cbSize) + { + if (cbSize == 0) + { + return; + } - // If we have a non-empty && non-full Partial block already -> append to that first. - if ((0 < PartialLength) && (PartialLength < BLOCKSIZE)) - { - var count = Math.Min(cbSize, BLOCKSIZE - PartialLength); - Array.Copy(array, ibStart, Partial, PartialLength, count); - PartialLength += count; - if (count == cbSize) - { - // No more data supplied, we're done. Even if we filled up Partial completely, - // because we don't know if it will be the final block. - return; - } - ibStart += count; - cbSize -= count; - } + // If we have a non-empty && non-full Partial block already -> append to that first. + if ((0 < PartialLength) && (PartialLength < BLOCKSIZE)) + { + var count = Math.Min(cbSize, BLOCKSIZE - PartialLength); + Array.Copy(array, ibStart, Partial, PartialLength, count); + PartialLength += count; + if (count == cbSize) + { + // No more data supplied, we're done. Even if we filled up Partial completely, + // because we don't know if it will be the final block. + return; + } + ibStart += count; + cbSize -= count; + } - // We get here only if Partial is either empty or full (i.e. we are block-aligned) && there is more to "hash". - if (PartialLength == BLOCKSIZE) - { - // Since there is more to hash, this is not the final block. - // See: NIST SP 800-38B, Section 6.2, Steps 3 and 6 - AddBlock(Partial); - PartialLength = 0; - } + // We get here only if Partial is either empty or full (i.e. we are block-aligned) && there is more to "hash". + if (PartialLength == BLOCKSIZE) + { + // Since there is more to hash, this is not the final block. + // See: NIST SP 800-38B, Section 6.2, Steps 3 and 6 + AddBlock(Partial); + PartialLength = 0; + } - // We get here only if Partial is empty && there is more to "hash". - // Add complete, non-final blocks. Never add the last block given in this call since we don't know if that will be the final block. - for (int i = 0, nonFinalBlockCount = (cbSize - 1) / BLOCKSIZE; i < nonFinalBlockCount; i++) - { - // See: NIST SP 800-38B, Section 6.2, Steps 3 and 6 - AddBlock(array, ibStart); - ibStart += BLOCKSIZE; - cbSize -= BLOCKSIZE; - } + // We get here only if Partial is empty && there is more to "hash". + // Add complete, non-final blocks. Never add the last block given in this call since we don't know if that will be the final block. + for (int i = 0, nonFinalBlockCount = (cbSize - 1) / BLOCKSIZE; i < nonFinalBlockCount; i++) + { + // See: NIST SP 800-38B, Section 6.2, Steps 3 and 6 + AddBlock(array, ibStart); + ibStart += BLOCKSIZE; + cbSize -= BLOCKSIZE; + } - // Save what we have left (we always have some, by construction). - Array.Copy(array, ibStart, Partial, 0, cbSize); - PartialLength = cbSize; - } + // Save what we have left (we always have some, by construction). + Array.Copy(array, ibStart, Partial, 0, cbSize); + PartialLength = cbSize; + } - /// - protected override byte[] HashFinal() - { - // Partial now has the role of Mn* - if (PartialLength == BLOCKSIZE) - { - // See: NIST SP 800-38B, Section 6.2, Step 1: K1 - var K1 = SUBK(true); - xor_InPlace(Partial, 0, K1, 0, BLOCKSIZE); - // Partial now has the role of Mn - } - else - { - // Add padding - Partial[PartialLength] = 0x80; - for (var i = PartialLength + 1; i < BLOCKSIZE; ++i) - { - Partial[i] = 0x00; - } - // See: NIST SP 800-38B, Section 6.2, Step 1: K2 - var K2 = SUBK(false); - xor_InPlace(Partial, 0, K2, 0, BLOCKSIZE); - // Partial now has the role of Mn - } - // See: NIST SP 800-38B, Section 6.2, Steps 4 and 6 - AddBlock(Partial); - PartialLength = 0; + /// + protected override byte[] HashFinal() + { + // Partial now has the role of Mn* + if (PartialLength == BLOCKSIZE) + { + // See: NIST SP 800-38B, Section 6.2, Step 1: K1 + var K1 = SUBK(true); + xor_InPlace(Partial, 0, K1, 0, BLOCKSIZE); + // Partial now has the role of Mn + } + else + { + // Add padding + Partial[PartialLength] = 0x80; + for (var i = PartialLength + 1; i < BLOCKSIZE; ++i) + { + Partial[i] = 0x00; + } + // See: NIST SP 800-38B, Section 6.2, Step 1: K2 + var K2 = SUBK(false); + xor_InPlace(Partial, 0, K2, 0, BLOCKSIZE); + // Partial now has the role of Mn + } + // See: NIST SP 800-38B, Section 6.2, Steps 4 and 6 + AddBlock(Partial); + PartialLength = 0; - // NOTE: KeyedHashAlgorithm exposes the returned array reference as the - // Hash property, so we must *not* return C itself as it may be reused. - var cmac = new byte[BLOCKSIZE]; - C.CopyTo(cmac, 0); + // NOTE: KeyedHashAlgorithm exposes the returned array reference as the + // Hash property, so we must *not* return C itself as it may be reused. + var cmac = new byte[BLOCKSIZE]; + C.CopyTo(cmac, 0); - ZeroizeState(); + ZeroizeState(); - return cmac; - } + return cmac; + } - static void xor_InPlace(byte[] X_Base, int X_Offset, byte[] Y_Base, int Y_Offset, int count) - { - for (var i = 0; i < count; ++i) - { - X_Base[X_Offset + i] ^= Y_Base[Y_Offset + i]; - } - } - // See: NIST SP 800-38B, Section 6.1 - // See: RFC 5297, Section 2.1 - // - // In place: S = dbl(S) - static void dbl_InPlace(byte[] S) - { - // See: NIST SP 800-38B, Section 5.3 - // See: RFC 5297, Section 2.3 - const int Rb = 0b10000111; + static void xor_InPlace(byte[] X_Base, int X_Offset, byte[] Y_Base, int Y_Offset, int count) + { + for (var i = 0; i < count; ++i) + { + X_Base[X_Offset + i] ^= Y_Base[Y_Offset + i]; + } + } + // See: NIST SP 800-38B, Section 6.1 + // See: RFC 5297, Section 2.1 + // + // In place: S = dbl(S) + static void dbl_InPlace(byte[] S) + { + // See: NIST SP 800-38B, Section 5.3 + // See: RFC 5297, Section 2.3 + const int Rb = 0b10000111; - // See: NIST SP 800-38B, Section 6.1, Step 2/3 - if (LeftShiftOne_InPlace(S)) - { - S[BLOCKSIZE - 1] ^= Rb; - } - } + // See: NIST SP 800-38B, Section 6.1, Step 2/3 + if (LeftShiftOne_InPlace(S)) + { + S[BLOCKSIZE - 1] ^= Rb; + } + } - // See: NIST SP 800-38B, Section 4.2 - // - // In place: X = (X << 1) - // Returns final carry. - static bool LeftShiftOne_InPlace(byte[] X) - { - var carry = false; - for (var i = X.Length - 1; i >= 0; --i) - { - var nextCarry = (X[i] & 0x80) != 0; - _ = unchecked(X[i] <<= 1); - if (carry) - { - X[i] |= 1; - } - carry = nextCarry; - } - return carry; - } - } + // See: NIST SP 800-38B, Section 4.2 + // + // In place: X = (X << 1) + // Returns final carry. + static bool LeftShiftOne_InPlace(byte[] X) + { + var carry = false; + for (var i = X.Length - 1; i >= 0; --i) + { + var nextCarry = (X[i] & 0x80) != 0; + _ = unchecked(X[i] <<= 1); + if (carry) + { + X[i] |= 1; + } + carry = nextCarry; + } + return carry; + } } diff --git a/src/BTCPayServer.NTag424/BoltCard.cs b/src/BTCPayServer.NTag424/BoltCard.cs index d40c22d..d4bfa8d 100644 --- a/src/BTCPayServer.NTag424/BoltCard.cs +++ b/src/BTCPayServer.NTag424/BoltCard.cs @@ -1,32 +1,27 @@ -using NdefLibrary.Ndef; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using NdefLibrary.Ndef; -namespace BoltCardTools +namespace BoltCardTools; + +public class BoltCard { - public class BoltCard - { - public BoltCard(Ntag424 ntag) + public BoltCard(Ntag424 ntag) + { + Ntag = ntag; + } + public Ntag424 Ntag { get; } + + + private static byte[] CreateNDefMessage(string lnUrl) + { + var queryString = "p=00000000000000000000000000000000&c=0000000000000000"; + var templateUrl = lnUrl.Contains("?", StringComparison.OrdinalIgnoreCase) ? + $"{lnUrl}?{queryString}" : + $"{lnUrl}&{queryString}"; + var message = new NdefLibrary.Ndef.NdefMessage { - Ntag = ntag; - } - public Ntag424 Ntag { get; } - - - private static byte[] CreateNDefMessage(string lnUrl) - { - var queryString = "p=00000000000000000000000000000000&c=0000000000000000"; - var templateUrl = lnUrl.Contains("?", StringComparison.OrdinalIgnoreCase) ? - $"{lnUrl}?{queryString}" : - $"{lnUrl}&{queryString}"; - var message = new NdefLibrary.Ndef.NdefMessage - { - new NdefUriRecord() { Uri = templateUrl } - }; - return message.ToByteArray(); - } - } + new NdefUriRecord() { Uri = templateUrl } + }; + return message.ToByteArray(); + } } diff --git a/src/BTCPayServer.NTag424/Extensions.cs b/src/BTCPayServer.NTag424/Extensions.cs index bec2902..51e99b0 100644 --- a/src/BTCPayServer.NTag424/Extensions.cs +++ b/src/BTCPayServer.NTag424/Extensions.cs @@ -1,74 +1,68 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; -namespace BoltCardTools +namespace BoltCardTools; + +internal static class Extensions { - internal static class Extensions - { - public static bool IsSame(this byte[] arr1, byte[] arr2) - { - if (arr1.Length != arr2.Length) - return false; - for (int i = 0; i < arr1.Length; i++) - if (arr1[i] != arr2[i]) - return false; - return true; - } - public static string ToHex(this byte[] data) - { - return Convert.ToHexString(data, 0, data.Length).ToLowerInvariant(); - } - public static byte[] HexToBytes(this string hex) - { - if (hex == null) - throw new ArgumentNullException(nameof(hex)); - if (hex.Length % 2 == 1) - throw new FormatException("Invalid Hex String"); - if (hex.Length < (hex.Length >> 1)) - throw new ArgumentException("output should be bigger", nameof(hex)); - var output = new byte[hex.Length / 2]; - try - { - for (int i = 0, j = 0; i < hex.Length; i += 2, j++) - { - var a = IsDigitCore(hex[i]); - var b = IsDigitCore(hex[i + 1]); - if (a == 0xff || b == 0xff) - throw new FormatException("Invalid Hex String"); - output[j] = (byte)(((uint)a << 4) | (uint)b); - } - } - catch (IndexOutOfRangeException) { throw new FormatException("Invalid Hex String"); } - return output; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static byte IsDigitCore(char c) - { - return CharToHexLookup[c]; - } - static byte[] CharToHexLookup => new byte[] - { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 - 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 - 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 - 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 + public static bool IsSame(this byte[] arr1, byte[] arr2) + { + if (arr1.Length != arr2.Length) + return false; + for (int i = 0; i < arr1.Length; i++) + if (arr1[i] != arr2[i]) + return false; + return true; + } + public static string ToHex(this byte[] data) + { + return Convert.ToHexString(data, 0, data.Length).ToLowerInvariant(); + } + public static byte[] HexToBytes(this string hex) + { + if (hex == null) + throw new ArgumentNullException(nameof(hex)); + if (hex.Length % 2 == 1) + throw new FormatException("Invalid Hex String"); + if (hex.Length < (hex.Length >> 1)) + throw new ArgumentException("output should be bigger", nameof(hex)); + var output = new byte[hex.Length / 2]; + try + { + for (int i = 0, j = 0; i < hex.Length; i += 2, j++) + { + var a = IsDigitCore(hex[i]); + var b = IsDigitCore(hex[i + 1]); + if (a == 0xff || b == 0xff) + throw new FormatException("Invalid Hex String"); + output[j] = (byte)(((uint)a << 4) | (uint)b); + } + } + catch (IndexOutOfRangeException) { throw new FormatException("Invalid Hex String"); } + return output; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static byte IsDigitCore(char c) + { + return CharToHexLookup[c]; + } + static byte[] CharToHexLookup => new byte[] + { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 + 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 + 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 }; - } } diff --git a/src/BTCPayServer.NTag424/FileSettings.cs b/src/BTCPayServer.NTag424/FileSettings.cs index 7982c5a..dcbd9ea 100644 --- a/src/BTCPayServer.NTag424/FileSettings.cs +++ b/src/BTCPayServer.NTag424/FileSettings.cs @@ -1,265 +1,264 @@ -using System; +using System; using System.Collections.Generic; -namespace BoltCardTools +namespace BoltCardTools; + +public enum AccessCondition { - public enum AccessCondition - { - Key0 = 0, Key1 = 1, Key2 = 2, Key3 = 3, Key4 = 4, - Free = 0x0E, Never = 0x0F - } - public enum AccessRight - { - Read, - Write, - Change - } - - public record SDMAccessRights - { - public SDMAccessRights() - { - MetaRead = AccessCondition.Never; - FileRead = AccessCondition.Never; - CtrRet = AccessCondition.Never; - } - public SDMAccessRights(ReadOnlySpan conditions) - { - MetaRead = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4); - FileRead = (AccessCondition)(conditions[1] & 0b0000_1111); - CtrRet = (AccessCondition)(conditions[0] & 0b0000_1111); - } - public AccessCondition MetaRead { get; set; } - public AccessCondition FileRead { get; set; } - public AccessCondition CtrRet { get; set; } - public byte[] ToBytes() - { - var fileSettings = new byte[2]; - fileSettings[1] = (byte)(((byte)MetaRead << 4) | ((byte)FileRead & 0b0000_1111)); - fileSettings[0] = (byte)(((byte)CtrRet & 0b0000_1111) | (byte)0xf0); - return fileSettings; - } - } - public record AccessRights - { - public AccessRights(ReadOnlySpan conditions) - { - Read = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4); - Write = (AccessCondition)(conditions[1] & 0b0000_1111); - ReadWrite = (AccessCondition)((byte)(conditions[0] & 0b1111_0000) >> 4); - Change = (AccessCondition)(conditions[0] & 0b0000_1111); - } - public AccessRights() : this(DataFile.NDEF) - { - - } - public AccessRights(DataFile file) - { - if (file == DataFile.CC) - { - Read = AccessCondition.Free; - Write = AccessCondition.Key0; - ReadWrite = AccessCondition.Key0; - Change = AccessCondition.Key0; - } - else if (file == DataFile.NDEF) - { - Read = AccessCondition.Free; - Write = AccessCondition.Free; - ReadWrite = AccessCondition.Free; - Change = AccessCondition.Key0; - } - else - { - Read = AccessCondition.Key2; - Write = AccessCondition.Key3; - ReadWrite = AccessCondition.Key3; - Change = AccessCondition.Key0; - } - } - - public bool IsAllowed(int keyNo, AccessRight right) - { - var keyno = (AccessCondition)keyNo; - if (right == AccessRight.Change) - return Change == keyno || Change == AccessCondition.Free; - if (ReadWrite == keyno || ReadWrite == AccessCondition.Free) - return true; - if (right == AccessRight.Write) - return Write == keyno || Write == AccessCondition.Free; - if (right == AccessRight.Read) - return Read == keyno || Read == AccessCondition.Free; - return false; - } - - public AccessCondition Write { get; set; } - public AccessCondition ReadWrite { get; set; } - public AccessCondition Read { get; set; } - public AccessCondition Change { get; set; } - public byte[] ToBytes() - { - var fileSettings = new byte[2]; - fileSettings[1] = (byte)(((byte)Read << 4) | ((byte)Write & 0b0000_1111)); - fileSettings[0] = (byte)(((byte)ReadWrite << 4) | ((byte)Change & 0b0000_1111)); - return fileSettings; - } - } - - public record FileSettings - { - public FileSettings(DataFile file) - { - SDMMirroring = false; - CommMode = CommMode.Plain; - AccessRights = new AccessRights(file); - } - public FileSettings(byte[] fileSettings, bool update) - { - int i = 0; - if (!update) - { - i++; - } - SDMMirroring = (fileSettings[i] & 0b0100_0000) != 0; - CommMode = (fileSettings[i] & 0b0000_0011) switch - { - 0b01 => CommMode.MAC, - 0b11 => CommMode.Full, - _ => CommMode.Plain - }; - i++; - AccessRights = new AccessRights(fileSettings[i..(i + 2)]); - i += 2; - if (!update) - { - i += 3; // Size - } - if (!SDMMirroring) - return; - var sdmOptions = fileSettings[i]; - SDMUID = (0b1000_0000 & sdmOptions) != 0; - SDMReadCtr = (0b0100_0000 & sdmOptions) != 0; - SDMReadCtrLimit = (0b0010_0000 & sdmOptions) != 0; - SDMENCFileData = (0b0001_0000 & sdmOptions) != 0; - i++; - SDMAccessRights = new SDMAccessRights(fileSettings[i..(i + 2)]); - i += 2; - if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free) - { - UIDOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - } - if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free) - { - SDMReadCtrOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - } - if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never) - { - PICCDataOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - } - if (SDMAccessRights.FileRead != AccessCondition.Never) - { - SDMMACInputOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - } - if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData) - { - SDMENCOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - SDMENCLength = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - } - if (SDMAccessRights.FileRead != AccessCondition.Never) - { - SDMMACOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - } - if (SDMReadCtrLimit) - { - SDMReadCtrLimitValue = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); - i += 3; - } - } - - public bool SDMUID { get; set; } - public bool SDMReadCtr { get; set; } - public bool SDMReadCtrLimit { get; set; } - public int SDMReadCtrLimitValue { get; set; } - public bool SDMENCFileData { get; set; } - public SDMAccessRights SDMAccessRights { get; set; } = new SDMAccessRights(); - - public byte[] ToBytes() - { - List output = new List(); - byte fileOptions = CommMode switch - { - CommMode.MAC => 0b01, - CommMode.Full => 0b11, - _ => 0b00 - }; - if (SDMMirroring) - fileOptions |= 0b0100_0000; - output.Add(fileOptions); - output.AddRange(AccessRights.ToBytes()); - if (!SDMMirroring) - return output.ToArray(); - - var sdmOptions = 0x01; - if (SDMUID) - sdmOptions |= 0b1000_0000; - if (SDMReadCtr) - sdmOptions |= 0b0100_0000; - if (SDMReadCtrLimit) - sdmOptions |= 0b0010_0000; - if (SDMENCFileData) - sdmOptions |= 0b0001_0000; - output.Add((byte)sdmOptions); - output.AddRange(SDMAccessRights.ToBytes()); - if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free) - { - output.AddRange(Helpers.UIntTo3BytesLE(UIDOffset)); - } - if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free) - { - output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrOffset)); - } - if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never) - { - output.AddRange(Helpers.UIntTo3BytesLE(PICCDataOffset)); - } - if (SDMAccessRights.FileRead != AccessCondition.Never) - { - output.AddRange(Helpers.UIntTo3BytesLE(SDMMACInputOffset)); - } - if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData) - { - output.AddRange(Helpers.UIntTo3BytesLE(SDMENCOffset)); - output.AddRange(Helpers.UIntTo3BytesLE(SDMENCLength)); - } - if (SDMAccessRights.FileRead != AccessCondition.Never) - { - output.AddRange(Helpers.UIntTo3BytesLE(SDMMACOffset)); - } - if (SDMReadCtrLimit) - { - output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrLimitValue)); - } - return output.ToArray(); - } - public int SDMMACOffset { get; set; } - public int SDMMACInputOffset { get; set; } - public int SDMENCLength { get; set; } - public int SDMENCOffset { get; set; } - public int PICCDataOffset { get; set; } - public int SDMReadCtrOffset { get; set; } - public int UIDOffset { get; set; } - public AccessRights AccessRights { get; set; } - - public bool IsAllowed(int keyNo, AccessRight right) => AccessRights.IsAllowed(keyNo, right); - - public bool SDMMirroring { get; set; } - public CommMode CommMode { get; set; } - } + Key0 = 0, Key1 = 1, Key2 = 2, Key3 = 3, Key4 = 4, + Free = 0x0E, Never = 0x0F +} +public enum AccessRight +{ + Read, + Write, + Change +} + +public record SDMAccessRights +{ + public SDMAccessRights() + { + MetaRead = AccessCondition.Never; + FileRead = AccessCondition.Never; + CtrRet = AccessCondition.Never; + } + public SDMAccessRights(ReadOnlySpan conditions) + { + MetaRead = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4); + FileRead = (AccessCondition)(conditions[1] & 0b0000_1111); + CtrRet = (AccessCondition)(conditions[0] & 0b0000_1111); + } + public AccessCondition MetaRead { get; set; } + public AccessCondition FileRead { get; set; } + public AccessCondition CtrRet { get; set; } + public byte[] ToBytes() + { + var fileSettings = new byte[2]; + fileSettings[1] = (byte)(((byte)MetaRead << 4) | ((byte)FileRead & 0b0000_1111)); + fileSettings[0] = (byte)(((byte)CtrRet & 0b0000_1111) | (byte)0xf0); + return fileSettings; + } +} +public record AccessRights +{ + public AccessRights(ReadOnlySpan conditions) + { + Read = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4); + Write = (AccessCondition)(conditions[1] & 0b0000_1111); + ReadWrite = (AccessCondition)((byte)(conditions[0] & 0b1111_0000) >> 4); + Change = (AccessCondition)(conditions[0] & 0b0000_1111); + } + public AccessRights() : this(DataFile.NDEF) + { + + } + public AccessRights(DataFile file) + { + if (file == DataFile.CC) + { + Read = AccessCondition.Free; + Write = AccessCondition.Key0; + ReadWrite = AccessCondition.Key0; + Change = AccessCondition.Key0; + } + else if (file == DataFile.NDEF) + { + Read = AccessCondition.Free; + Write = AccessCondition.Free; + ReadWrite = AccessCondition.Free; + Change = AccessCondition.Key0; + } + else + { + Read = AccessCondition.Key2; + Write = AccessCondition.Key3; + ReadWrite = AccessCondition.Key3; + Change = AccessCondition.Key0; + } + } + + public bool IsAllowed(int keyNo, AccessRight right) + { + var keyno = (AccessCondition)keyNo; + if (right == AccessRight.Change) + return Change == keyno || Change == AccessCondition.Free; + if (ReadWrite == keyno || ReadWrite == AccessCondition.Free) + return true; + if (right == AccessRight.Write) + return Write == keyno || Write == AccessCondition.Free; + if (right == AccessRight.Read) + return Read == keyno || Read == AccessCondition.Free; + return false; + } + + public AccessCondition Write { get; set; } + public AccessCondition ReadWrite { get; set; } + public AccessCondition Read { get; set; } + public AccessCondition Change { get; set; } + public byte[] ToBytes() + { + var fileSettings = new byte[2]; + fileSettings[1] = (byte)(((byte)Read << 4) | ((byte)Write & 0b0000_1111)); + fileSettings[0] = (byte)(((byte)ReadWrite << 4) | ((byte)Change & 0b0000_1111)); + return fileSettings; + } +} + +public record FileSettings +{ + public FileSettings(DataFile file) + { + SDMMirroring = false; + CommMode = CommMode.Plain; + AccessRights = new AccessRights(file); + } + public FileSettings(byte[] fileSettings, bool update) + { + int i = 0; + if (!update) + { + i++; + } + SDMMirroring = (fileSettings[i] & 0b0100_0000) != 0; + CommMode = (fileSettings[i] & 0b0000_0011) switch + { + 0b01 => CommMode.MAC, + 0b11 => CommMode.Full, + _ => CommMode.Plain + }; + i++; + AccessRights = new AccessRights(fileSettings[i..(i + 2)]); + i += 2; + if (!update) + { + i += 3; // Size + } + if (!SDMMirroring) + return; + var sdmOptions = fileSettings[i]; + SDMUID = (0b1000_0000 & sdmOptions) != 0; + SDMReadCtr = (0b0100_0000 & sdmOptions) != 0; + SDMReadCtrLimit = (0b0010_0000 & sdmOptions) != 0; + SDMENCFileData = (0b0001_0000 & sdmOptions) != 0; + i++; + SDMAccessRights = new SDMAccessRights(fileSettings[i..(i + 2)]); + i += 2; + if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free) + { + UIDOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + } + if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free) + { + SDMReadCtrOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + } + if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never) + { + PICCDataOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + } + if (SDMAccessRights.FileRead != AccessCondition.Never) + { + SDMMACInputOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + } + if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData) + { + SDMENCOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + SDMENCLength = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + } + if (SDMAccessRights.FileRead != AccessCondition.Never) + { + SDMMACOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + } + if (SDMReadCtrLimit) + { + SDMReadCtrLimitValue = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]); + i += 3; + } + } + + public bool SDMUID { get; set; } + public bool SDMReadCtr { get; set; } + public bool SDMReadCtrLimit { get; set; } + public int SDMReadCtrLimitValue { get; set; } + public bool SDMENCFileData { get; set; } + public SDMAccessRights SDMAccessRights { get; set; } = new SDMAccessRights(); + + public byte[] ToBytes() + { + List output = new List(); + byte fileOptions = CommMode switch + { + CommMode.MAC => 0b01, + CommMode.Full => 0b11, + _ => 0b00 + }; + if (SDMMirroring) + fileOptions |= 0b0100_0000; + output.Add(fileOptions); + output.AddRange(AccessRights.ToBytes()); + if (!SDMMirroring) + return output.ToArray(); + + var sdmOptions = 0x01; + if (SDMUID) + sdmOptions |= 0b1000_0000; + if (SDMReadCtr) + sdmOptions |= 0b0100_0000; + if (SDMReadCtrLimit) + sdmOptions |= 0b0010_0000; + if (SDMENCFileData) + sdmOptions |= 0b0001_0000; + output.Add((byte)sdmOptions); + output.AddRange(SDMAccessRights.ToBytes()); + if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free) + { + output.AddRange(Helpers.UIntTo3BytesLE(UIDOffset)); + } + if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free) + { + output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrOffset)); + } + if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never) + { + output.AddRange(Helpers.UIntTo3BytesLE(PICCDataOffset)); + } + if (SDMAccessRights.FileRead != AccessCondition.Never) + { + output.AddRange(Helpers.UIntTo3BytesLE(SDMMACInputOffset)); + } + if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData) + { + output.AddRange(Helpers.UIntTo3BytesLE(SDMENCOffset)); + output.AddRange(Helpers.UIntTo3BytesLE(SDMENCLength)); + } + if (SDMAccessRights.FileRead != AccessCondition.Never) + { + output.AddRange(Helpers.UIntTo3BytesLE(SDMMACOffset)); + } + if (SDMReadCtrLimit) + { + output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrLimitValue)); + } + return output.ToArray(); + } + public int SDMMACOffset { get; set; } + public int SDMMACInputOffset { get; set; } + public int SDMENCLength { get; set; } + public int SDMENCOffset { get; set; } + public int PICCDataOffset { get; set; } + public int SDMReadCtrOffset { get; set; } + public int UIDOffset { get; set; } + public AccessRights AccessRights { get; set; } + + public bool IsAllowed(int keyNo, AccessRight right) => AccessRights.IsAllowed(keyNo, right); + + public bool SDMMirroring { get; set; } + public CommMode CommMode { get; set; } } diff --git a/src/BTCPayServer.NTag424/Helpers.cs b/src/BTCPayServer.NTag424/Helpers.cs index 827083d..6b683f9 100644 --- a/src/BTCPayServer.NTag424/Helpers.cs +++ b/src/BTCPayServer.NTag424/Helpers.cs @@ -1,123 +1,119 @@ -using System; -using System.Collections.Generic; +using System; using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace BoltCardTools +namespace BoltCardTools; + +internal class Helpers { - internal class Helpers - { - internal static byte[] Truncate(byte[] cmac) - { - var halfMac = new byte[cmac.Length / 2]; - for (var i = 1; i < cmac.Length; i += 2) - { - halfMac[i >> 1] = cmac[i]; - } - return halfMac; - } - public static byte[] RotateLeft(byte[] bytesArr) - { - byte first = bytesArr[0]; - byte[] rotatedArray = new byte[bytesArr.Length]; - Array.Copy(bytesArr, 1, rotatedArray, 0, bytesArr.Length - 1); - rotatedArray[bytesArr.Length - 1] = first; - return rotatedArray; - } - internal static byte[] XOR(byte[] a1, byte[] a2) - { - byte[] res = new byte[a1.Length]; - for (int i = 0; i < a1.Length; i++) - res[i] = (byte)(a1[i] ^ a2[i]); - return res; - } + internal static byte[] Truncate(byte[] cmac) + { + var halfMac = new byte[cmac.Length / 2]; + for (var i = 1; i < cmac.Length; i += 2) + { + halfMac[i >> 1] = cmac[i]; + } + return halfMac; + } + public static byte[] RotateLeft(byte[] bytesArr) + { + byte first = bytesArr[0]; + byte[] rotatedArray = new byte[bytesArr.Length]; + Array.Copy(bytesArr, 1, rotatedArray, 0, bytesArr.Length - 1); + rotatedArray[bytesArr.Length - 1] = first; + return rotatedArray; + } + internal static byte[] XOR(byte[] a1, byte[] a2) + { + byte[] res = new byte[a1.Length]; + for (int i = 0; i < a1.Length; i++) + res[i] = (byte)(a1[i] ^ a2[i]); + return res; + } - internal static uint BytesToUIntLE(ReadOnlySpan bytes) - { - return bytes[0] + ((uint)bytes[1] << 8) + ((uint)bytes[2] << 16) + ((uint)bytes[3] << 24); - } - internal static byte[] UIntToBytesLE(uint value) - { - return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff), (byte)((value >> 24) & 0xff) }; - } - public static int BytesToUIntLE(byte[] value) - { - return value[0] + ((int)value[1] << 8) + ((int)value[2] << 16); - } - public static byte[] UIntTo3BytesLE(int value) - { - return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff) }; - } - public static byte[] UIntTo3BytesBE(int value) - { - return new byte[] { (byte)((value >> 16) & 0xff), (byte)((value >> 8) & 0xff), (byte)(value & 0xff) }; - } - internal static byte[] UShortToBytesLE(int value) - { - if (value > ushort.MaxValue) - return new byte[] { 0xFF, 0xFF }; - return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff) }; - } + internal static uint BytesToUIntLE(ReadOnlySpan bytes) + { + return bytes[0] + ((uint)bytes[1] << 8) + ((uint)bytes[2] << 16) + ((uint)bytes[3] << 24); + } + internal static byte[] UIntToBytesLE(uint value) + { + return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff), (byte)((value >> 24) & 0xff) }; + } + public static int BytesToUIntLE(byte[] value) + { + return value[0] + ((int)value[1] << 8) + ((int)value[2] << 16); + } + public static byte[] UIntTo3BytesLE(int value) + { + return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff) }; + } + public static byte[] UIntTo3BytesBE(int value) + { + return new byte[] { (byte)((value >> 16) & 0xff), (byte)((value >> 8) & 0xff), (byte)(value & 0xff) }; + } + internal static byte[] UShortToBytesLE(int value) + { + if (value > ushort.MaxValue) + return new byte[] { 0xFF, 0xFF }; + return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff) }; + } - internal static byte[] Concat(params byte[]?[] arrays) - { - var res = new byte[arrays.Sum(a => a?.Length ?? 0)]; - int offset = 0; - foreach (var a in arrays) - { - if (a is null) - continue; - Array.Copy(a, 0, res, offset, a.Length); - offset += a.Length; - } - return res; - } + internal static byte[] Concat(params byte[]?[] arrays) + { + var res = new byte[arrays.Sum(a => a?.Length ?? 0)]; + int offset = 0; + foreach (var a in arrays) + { + if (a is null) + continue; + Array.Copy(a, 0, res, offset, a.Length); + offset += a.Length; + } + return res; + } - //https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crcjam.ts - - readonly static uint[] TABLE = new uint[]{ - 0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u, 0x9e6495a3u, - 0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu, 0xe7b82d07u, 0x90bf1d91u, - 0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du, 0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, - 0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu, 0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u, - 0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u, 0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, - 0x35b5a8fau, 0x42b2986cu, 0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, - 0x26d930acu, 0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu, - 0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu, 0xb6662d3du, - 0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu, 0x9fbfe4a5u, 0xe8b8d433u, - 0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu, 0x086d3d2du, 0x91646c97u, 0xe6635c01u, - 0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu, 0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, - 0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau, 0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u, - 0x4db26158u, 0x3ab551ceu, 0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, - 0x4369e96au, 0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u, - 0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u, 0xce61e49fu, - 0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u, 0xb7bd5c3bu, 0xc0ba6cadu, - 0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u, 0x9dd277afu, 0x04db2615u, 0x73dc1683u, - 0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u, 0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, - 0xf00f9344u, 0x8708a3d2u, 0x1e01f268u, 0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u, - 0xfed41b76u, 0x89d32be0u, 0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, - 0xd6d6a3e8u, 0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu, - 0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu, 0x4669be79u, - 0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u, 0x220216b9u, 0x5505262fu, - 0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u, 0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du, - 0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au, 0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, - 0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu, 0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, - 0x86d3d2d4u, 0xf1d4e242u, 0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u, - 0x88085ae6u, 0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u, - 0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du, 0x3e6e77dbu, - 0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u, 0x47b2cf7fu, 0x30b5ffe9u, - 0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u, 0xcdd70693u, 0x54de5729u, 0x23d967bfu, - 0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u, 0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du, - }; - public static uint CRCJam(byte[] data) - { - uint crc = 0xffffffff; - for (int index = 0; index < data.Length; index++) - { - crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8); - } - return crc; - } - } + //https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crcjam.ts + + readonly static uint[] TABLE = new uint[]{ + 0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u, 0x9e6495a3u, +0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu, 0xe7b82d07u, 0x90bf1d91u, +0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du, 0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, +0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu, 0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u, +0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u, 0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, +0x35b5a8fau, 0x42b2986cu, 0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, +0x26d930acu, 0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu, +0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu, 0xb6662d3du, +0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu, 0x9fbfe4a5u, 0xe8b8d433u, +0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu, 0x086d3d2du, 0x91646c97u, 0xe6635c01u, +0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu, 0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, +0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau, 0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u, +0x4db26158u, 0x3ab551ceu, 0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, +0x4369e96au, 0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u, +0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u, 0xce61e49fu, +0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u, 0xb7bd5c3bu, 0xc0ba6cadu, +0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u, 0x9dd277afu, 0x04db2615u, 0x73dc1683u, +0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u, 0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, +0xf00f9344u, 0x8708a3d2u, 0x1e01f268u, 0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u, +0xfed41b76u, 0x89d32be0u, 0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, +0xd6d6a3e8u, 0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu, +0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu, 0x4669be79u, +0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u, 0x220216b9u, 0x5505262fu, +0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u, 0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du, +0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au, 0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, +0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu, 0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, +0x86d3d2d4u, 0xf1d4e242u, 0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u, +0x88085ae6u, 0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u, +0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du, 0x3e6e77dbu, +0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u, 0x47b2cf7fu, 0x30b5ffe9u, +0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u, 0xcdd70693u, 0x54de5729u, 0x23d967bfu, +0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u, 0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du, + }; + public static uint CRCJam(byte[] data) + { + uint crc = 0xffffffff; + for (int index = 0; index < data.Length; index++) + { + crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8); + } + return crc; + } } diff --git a/src/BTCPayServer.NTag424/IAPDUTransport.cs b/src/BTCPayServer.NTag424/IAPDUTransport.cs index 50f94c9..ae717bc 100644 --- a/src/BTCPayServer.NTag424/IAPDUTransport.cs +++ b/src/BTCPayServer.NTag424/IAPDUTransport.cs @@ -1,14 +1,8 @@ -using PCSC.Iso7816; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -namespace BoltCardTools +namespace BoltCardTools; + +public interface IAPDUTransport { - public interface IAPDUTransport - { - Task SendAPDU(NTagCommand apdu); - } + Task SendAPDU(NTagCommand apdu); } diff --git a/src/BTCPayServer.NTag424/Ntag424.cs b/src/BTCPayServer.NTag424/Ntag424.cs index 87dae77..83c9d30 100644 --- a/src/BTCPayServer.NTag424/Ntag424.cs +++ b/src/BTCPayServer.NTag424/Ntag424.cs @@ -1,420 +1,409 @@ -using Microsoft.VisualBasic; -using NdefLibrary.Ndef; -using PCSC.Iso7816; using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Net; -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; using System.Security; using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; +using NdefLibrary.Ndef; using static BoltCardTools.Helpers; -using static System.Runtime.InteropServices.JavaScript.JSType; -namespace BoltCardTools +namespace BoltCardTools; + +public enum ISOLevel { - public enum ISOLevel - { - PICC, // MF - Application // DF - } - public enum DataFile - { - CC = 0xE103, - NDEF = 0xE104, - Proprietary = 0xE105 - } - - public class Ntag424 - { - public record Session(int KeyNo, AESKey ENCKey, AESKey MACKey, byte[] TransactionId) - { - public int Counter { get; set; } - - public byte[] EncryptCommand(byte[] data) - { - if (data.Length == 0) - return data; - data = PaddingForEnc(data); - var iv = ENCKey.Encrypt( - Concat( - "A55A".HexToBytes(), - TransactionId, - UShortToBytesLE(Counter), - "0000000000000000".HexToBytes() - ), null, CipherMode.ECB - ); - return ENCKey.Encrypt(data, iv); - } - - internal static byte[] PaddingForEnc(byte[] data) - { - var len = data.Length; - var paddingLen = 16 - (len % 16); - var padded = new byte[len + paddingLen]; - Array.Copy(data, padded, len); - padded[len] = 0x80; - return padded; - } - - - public byte[] DecryptResponse(byte rc, byte[] data, CommMode commMode) - { - if (commMode is CommMode.Plain || data.Length == 0) - return data; - var mac = data[^8..]; - data = data[..^8]; - var expectedMac = this.GetMac(rc, data); - if (!expectedMac.IsSame(mac)) - throw new UnexpectedResponseException("Invalid MAC"); - if (data.Length == 0 || commMode == CommMode.MAC) - return data; - var iv = ENCKey.Encrypt( - Concat( - "5AA5".HexToBytes(), - TransactionId, - UShortToBytesLE(Counter), - "0000000000000000".HexToBytes() - ), null, CipherMode.ECB - ); - var decrypted = ENCKey.Decrypt(data, iv); - var paddingStart = Array.LastIndexOf(decrypted, (byte)0x80); - return decrypted[0..paddingStart]; - } - - public byte[] GetMac(byte ins, byte[]? data) - { - var macValue = Concat( - new byte[] { ins }, - UShortToBytesLE(Counter), - TransactionId, - data ?? Array.Empty()); - var mac = MACKey.CMac(macValue); - return Truncate(mac); - } - } - readonly IAPDUTransport Transport; - public Ntag424(IAPDUTransport transport) - { - Transport = transport; - } - public Session? CurrentSession { get; private set; } - - public async Task IsoSelectFile(ISOLevel level) - { - await SendAPDU(NtagCommands.ISOSelectFile with - { - P1 = 0x04, - P2 = 0x00, - Data = (level switch - { - ISOLevel.PICC => "d2760000850100", - ISOLevel.Application => "d2760000850101", - _ => throw new ArgumentException(nameof(level)) - }).HexToBytes() - }); - } - public async Task IsoSelectFile(DataFile file) - { - await SendAPDU(NtagCommands.ISOSelectFile with - { - P1 = 0x00, - P2 = 0x00, - Data = new byte[] { (byte)((int)file >> 8), (byte)file }, - Le = 0 - }); - } - - public Task AuthenticateEV2NonFirst(int keyNo, AESKey key) - { - return AuthenticateEV2(keyNo, key, false); - } - public Task AuthenticateEV2First(int keyNo, AESKey key) - { - return AuthenticateEV2(keyNo, key, true); - } - public async Task AuthenticateEV2(int keyNo, AESKey key, bool first) - { - int sessionCounter = CurrentSession?.Counter ?? 0; - if (first) - { - await IsoSelectFile(ISOLevel.Application); - sessionCounter = 0; - } - else - { - if (CurrentSession is null) - throw new InvalidOperationException("Authentication required for AuthenticateEV2NonFirst"); - sessionCounter = CurrentSession.Counter; - } - - NtagResponse resp; - if (first) - { - resp = await SendAPDU(NtagCommands.AuthenticateEV2FirstPart1 with - { - Data = $"{(byte)keyNo:x2}03000000".HexToBytes() - }); - } - else - { - resp = await SendAPDU(NtagCommands.AuthenticateEV2NonFirstPart1 with - { - Data = new byte[] { (byte)keyNo } - }); - } - var rndB = key.Decrypt(resp.Data); - var rndBp = RotateLeft(rndB); - var rndA = RandomNumberGenerator.GetBytes(16); - var encRnd = key.Encrypt(Concat(rndA, rndBp)); - var secondPart = first ? NtagCommands.AuthenticateEV2FirstPart2 : NtagCommands.AuthenticateEV2NonFirstPart2; - resp = await SendAPDU(secondPart with - { - Data = encRnd - }); - - var data = key.Decrypt(resp.Data); - var rndAp = RotateLeft(rndA); - byte[] tid; - byte[] actualRndAp; - if (first) - { - tid = data[0..4]; - actualRndAp = data[4..20]; - } - else - { - tid = CurrentSession!.TransactionId; - actualRndAp = data[0..16]; - } - if (!rndAp.IsSame(actualRndAp)) - throw new UnexpectedResponseException("Invalid RndAp response"); - var rndMix = Concat( - rndA[0..2], - XOR(rndA[2..8], rndB[0..6]), - rndB[6..16], - rndA[8..16]); - var encKey = key.Derive(Concat( - "A55A00010080".HexToBytes(), - rndMix)); - var macKey = key.Derive(Concat( - "5AA500010080".HexToBytes(), - rndMix)); - var session = new Session(keyNo, encKey, macKey, tid) - { - Counter = sessionCounter - }; - CurrentSession = session; - return session; - } - - private async Task SendAPDU(NTagCommand command) - { - CommMode commandMode; - if (command.CommMode is CommMode m) - { - commandMode = m; - } - else - { - commandMode = CurrentSession is null ? CommMode.Plain : CommMode.Full; - command = command with - { - CommMode = commandMode - }; - } - if (commandMode is not CommMode.Plain) - { - if (CurrentSession is null) - throw new InvalidOperationException("Authentication required"); - command = command.Encode(CurrentSession); - } - if (CurrentSession is not null) - CurrentSession.Counter++; - - var resp = await Transport.SendAPDU(command); - command.ThrowIfUnexpected(resp); - if (commandMode is not CommMode.Plain && CurrentSession is not null) - { - resp = resp.Decode(CurrentSession, commandMode); - } - return resp; - } - - public async Task GetCardUID() - { - return (await SendAPDU(NtagCommands.GetCardUID)).Data; - } - - public async Task GetFileSettings(DataFile file = DataFile.NDEF) - { - return new FileSettings((await SendAPDU(NtagCommands.GetFileSettings with - { - Data = GetFileNo(file) - })).Data, false); - } - public async Task ChangeFileSettings(DataFile file = DataFile.NDEF, FileSettings? fileSettings = null) - { - fileSettings ??= new FileSettings(file); - await SendAPDU(NtagCommands.ChangeFileSettings with - { - Data = Concat( - GetFileNo(file), - fileSettings.ToBytes() - ) - }); - } - - public async Task ReadNDef() - { - await IsoSelectFile(ISOLevel.Application); - await IsoSelectFile(DataFile.NDEF); - var size = (await SendAPDU(NtagCommands.ISOReadBinary with - { - P1 = 0, - P2 = 0, - Le = 2 - })).Data[1]; - var data = (await SendAPDU(NtagCommands.ISOReadBinary with - { - P1 = 0, - P2 = 2, - Le = size - })).Data; - return NdefMessage.FromByteArray(data); - } - - public async Task ReadFile(DataFile file, int offset, int length) - { - var commMode = await GetCommMode(file, AccessRight.Read); - return (await SendAPDU(NtagCommands.ReadData with - { - CommMode = commMode, - CommandHeaderSize = 7, - Data = Concat( - GetFileNo(file), - UIntTo3BytesLE(offset), - UIntTo3BytesLE(length) - ) - })).Data; - } - - private async Task GetCommMode(DataFile file, AccessRight requiredRight) - { - if (CurrentSession is null) - return CommMode.Plain; - var settings = await GetFileSettings(file); - if (!settings.IsAllowed(CurrentSession.KeyNo, requiredRight)) - throw new SecurityException($"The key {CurrentSession.KeyNo} doesn't have the necessary permissions"); - return settings.CommMode; - } - - private static byte[] GetFileNo(DataFile file) - { - return new byte[] { file switch - { - DataFile.CC => 0x01, - DataFile.NDEF => 0x02, - DataFile.Proprietary => 0x03, - _ => throw new ArgumentException(nameof(file)) - } }; - } - - public async Task WriteNDef(NdefMessage message) - { - var ndefMessageBytes = message.ToByteArray(); - var content = new byte[220]; // Normally we have 256 bytes, but APDU has a size limit we need some margin - content[0] = (byte)(ndefMessageBytes.Length >> 8); - content[1] = (byte)ndefMessageBytes.Length; - Array.Copy(ndefMessageBytes, 0, content, 2, Math.Min(content.Length - 2, ndefMessageBytes.Length)); - await SendAPDU(NtagCommands.WriteData with - { - CommMode = await GetCommMode(DataFile.NDEF, AccessRight.Write), - Data = Concat( - GetFileNo(DataFile.NDEF), - new byte[] { 0x00, 0x00, 0x00 }, - UIntTo3BytesLE(content.Length), - content - ) - }); - } - - public async Task ChangeKey(int keyNo, AESKey newKey, AESKey? oldKey = null, int version = 0) - { - if (CurrentSession is null || CurrentSession.KeyNo != 0) - throw new InvalidOperationException("Authentication required with KeyNo 0"); - - byte[] data; - if (keyNo == 0) - { - data = Concat( - newKey.ToBytes(), - new byte[] { (byte)version } - ); - } - else - { - oldKey ??= new AESKey(new byte[16]); - data = Concat( - XOR(newKey.ToBytes(), oldKey.ToBytes()), - new byte[] { (byte)version }, - UIntToBytesLE(CRCJam(newKey.ToBytes()))); - } - - await SendAPDU(NtagCommands.ChangeKey with - { - Data = Concat( - new byte[] { (byte)keyNo }, - data - ) - }); - } - - public async Task SetupBoltcard(string lnurlw) - { - if (!lnurlw.Contains('?', StringComparison.OrdinalIgnoreCase)) - lnurlw += "?"; - else - lnurlw += "&"; - lnurlw += "p=00000000000000000000000000000000&c=0000000000000000"; - - var ndef = new NdefMessage - { - new NdefUriRecord() { Uri = lnurlw } - }; - await WriteNDef(ndef); - var ndefBytes = ndef.ToByteArray(); - var pIndex = Array.LastIndexOf(ndefBytes, (byte)'p') + 4; - var cIndex = Array.LastIndexOf(ndefBytes, (byte)'c') + 4; - - var settings = new FileSettings(DataFile.NDEF) - { - AccessRights = new () - { - ReadWrite = AccessCondition.Key0, - Change = AccessCondition.Key0, - Write = AccessCondition.Key0, - Read = AccessCondition.Free - }, - SDMMirroring = true, - SDMUID = true, - SDMReadCtr = true, - SDMAccessRights = new () - { - MetaRead = AccessCondition.Key1, - FileRead = AccessCondition.Key2, - CtrRet = AccessCondition.Never - }, - SDMMACInputOffset = cIndex, - SDMMACOffset = cIndex, - PICCDataOffset = pIndex - }; - await ChangeFileSettings(fileSettings: settings); - - } - } + PICC, // MF + Application // DF +} +public enum DataFile +{ + CC = 0xE103, + NDEF = 0xE104, + Proprietary = 0xE105 +} + +public class Ntag424 +{ + public record Session(int KeyNo, AESKey ENCKey, AESKey MACKey, byte[] TransactionId) + { + public int Counter { get; set; } + + public byte[] EncryptCommand(byte[] data) + { + if (data.Length == 0) + return data; + data = PaddingForEnc(data); + var iv = ENCKey.Encrypt( + Concat( + "A55A".HexToBytes(), + TransactionId, + UShortToBytesLE(Counter), + "0000000000000000".HexToBytes() + ), null, CipherMode.ECB + ); + return ENCKey.Encrypt(data, iv); + } + + internal static byte[] PaddingForEnc(byte[] data) + { + var len = data.Length; + var paddingLen = 16 - (len % 16); + var padded = new byte[len + paddingLen]; + Array.Copy(data, padded, len); + padded[len] = 0x80; + return padded; + } + + + public byte[] DecryptResponse(byte rc, byte[] data, CommMode commMode) + { + if (commMode is CommMode.Plain || data.Length == 0) + return data; + var mac = data[^8..]; + data = data[..^8]; + var expectedMac = this.GetMac(rc, data); + if (!expectedMac.IsSame(mac)) + throw new UnexpectedResponseException("Invalid MAC"); + if (data.Length == 0 || commMode == CommMode.MAC) + return data; + var iv = ENCKey.Encrypt( + Concat( + "5AA5".HexToBytes(), + TransactionId, + UShortToBytesLE(Counter), + "0000000000000000".HexToBytes() + ), null, CipherMode.ECB + ); + var decrypted = ENCKey.Decrypt(data, iv); + var paddingStart = Array.LastIndexOf(decrypted, (byte)0x80); + return decrypted[0..paddingStart]; + } + + public byte[] GetMac(byte ins, byte[]? data) + { + var macValue = Concat( + new byte[] { ins }, + UShortToBytesLE(Counter), + TransactionId, + data ?? Array.Empty()); + var mac = MACKey.CMac(macValue); + return Truncate(mac); + } + } + readonly IAPDUTransport Transport; + public Ntag424(IAPDUTransport transport) + { + Transport = transport; + } + public Session? CurrentSession { get; private set; } + + public async Task IsoSelectFile(ISOLevel level) + { + await SendAPDU(NtagCommands.ISOSelectFile with + { + P1 = 0x04, + P2 = 0x00, + Data = (level switch + { + ISOLevel.PICC => "d2760000850100", + ISOLevel.Application => "d2760000850101", + _ => throw new ArgumentException(nameof(level)) + }).HexToBytes() + }); + } + public async Task IsoSelectFile(DataFile file) + { + await SendAPDU(NtagCommands.ISOSelectFile with + { + P1 = 0x00, + P2 = 0x00, + Data = new byte[] { (byte)((int)file >> 8), (byte)file }, + Le = 0 + }); + } + + public Task AuthenticateEV2NonFirst(int keyNo, AESKey key) + { + return AuthenticateEV2(keyNo, key, false); + } + public Task AuthenticateEV2First(int keyNo, AESKey key) + { + return AuthenticateEV2(keyNo, key, true); + } + public async Task AuthenticateEV2(int keyNo, AESKey key, bool first) + { + int sessionCounter = CurrentSession?.Counter ?? 0; + if (first) + { + await IsoSelectFile(ISOLevel.Application); + sessionCounter = 0; + } + else + { + if (CurrentSession is null) + throw new InvalidOperationException("Authentication required for AuthenticateEV2NonFirst"); + sessionCounter = CurrentSession.Counter; + } + + NtagResponse resp; + if (first) + { + resp = await SendAPDU(NtagCommands.AuthenticateEV2FirstPart1 with + { + Data = $"{(byte)keyNo:x2}03000000".HexToBytes() + }); + } + else + { + resp = await SendAPDU(NtagCommands.AuthenticateEV2NonFirstPart1 with + { + Data = new byte[] { (byte)keyNo } + }); + } + var rndB = key.Decrypt(resp.Data); + var rndBp = RotateLeft(rndB); + var rndA = RandomNumberGenerator.GetBytes(16); + var encRnd = key.Encrypt(Concat(rndA, rndBp)); + var secondPart = first ? NtagCommands.AuthenticateEV2FirstPart2 : NtagCommands.AuthenticateEV2NonFirstPart2; + resp = await SendAPDU(secondPart with + { + Data = encRnd + }); + + var data = key.Decrypt(resp.Data); + var rndAp = RotateLeft(rndA); + byte[] tid; + byte[] actualRndAp; + if (first) + { + tid = data[0..4]; + actualRndAp = data[4..20]; + } + else + { + tid = CurrentSession!.TransactionId; + actualRndAp = data[0..16]; + } + if (!rndAp.IsSame(actualRndAp)) + throw new UnexpectedResponseException("Invalid RndAp response"); + var rndMix = Concat( + rndA[0..2], + XOR(rndA[2..8], rndB[0..6]), + rndB[6..16], + rndA[8..16]); + var encKey = key.Derive(Concat( + "A55A00010080".HexToBytes(), + rndMix)); + var macKey = key.Derive(Concat( + "5AA500010080".HexToBytes(), + rndMix)); + var session = new Session(keyNo, encKey, macKey, tid) + { + Counter = sessionCounter + }; + CurrentSession = session; + return session; + } + + private async Task SendAPDU(NTagCommand command) + { + CommMode commandMode; + if (command.CommMode is CommMode m) + { + commandMode = m; + } + else + { + commandMode = CurrentSession is null ? CommMode.Plain : CommMode.Full; + command = command with + { + CommMode = commandMode + }; + } + if (commandMode is not CommMode.Plain) + { + if (CurrentSession is null) + throw new InvalidOperationException("Authentication required"); + command = command.Encode(CurrentSession); + } + if (CurrentSession is not null) + CurrentSession.Counter++; + + var resp = await Transport.SendAPDU(command); + command.ThrowIfUnexpected(resp); + if (commandMode is not CommMode.Plain && CurrentSession is not null) + { + resp = resp.Decode(CurrentSession, commandMode); + } + return resp; + } + + public async Task GetCardUID() + { + return (await SendAPDU(NtagCommands.GetCardUID)).Data; + } + + public async Task GetFileSettings(DataFile file = DataFile.NDEF) + { + return new FileSettings((await SendAPDU(NtagCommands.GetFileSettings with + { + Data = GetFileNo(file) + })).Data, false); + } + public async Task ChangeFileSettings(DataFile file = DataFile.NDEF, FileSettings? fileSettings = null) + { + fileSettings ??= new FileSettings(file); + await SendAPDU(NtagCommands.ChangeFileSettings with + { + Data = Concat( + GetFileNo(file), + fileSettings.ToBytes() + ) + }); + } + + public async Task ReadNDef() + { + await IsoSelectFile(ISOLevel.Application); + await IsoSelectFile(DataFile.NDEF); + var size = (await SendAPDU(NtagCommands.ISOReadBinary with + { + P1 = 0, + P2 = 0, + Le = 2 + })).Data[1]; + var data = (await SendAPDU(NtagCommands.ISOReadBinary with + { + P1 = 0, + P2 = 2, + Le = size + })).Data; + return NdefMessage.FromByteArray(data); + } + + public async Task ReadFile(DataFile file, int offset, int length) + { + var commMode = await GetCommMode(file, AccessRight.Read); + return (await SendAPDU(NtagCommands.ReadData with + { + CommMode = commMode, + CommandHeaderSize = 7, + Data = Concat( + GetFileNo(file), + UIntTo3BytesLE(offset), + UIntTo3BytesLE(length) + ) + })).Data; + } + + private async Task GetCommMode(DataFile file, AccessRight requiredRight) + { + if (CurrentSession is null) + return CommMode.Plain; + var settings = await GetFileSettings(file); + if (!settings.IsAllowed(CurrentSession.KeyNo, requiredRight)) + throw new SecurityException($"The key {CurrentSession.KeyNo} doesn't have the necessary permissions"); + return settings.CommMode; + } + + private static byte[] GetFileNo(DataFile file) + { + return new byte[] { file switch + { + DataFile.CC => 0x01, + DataFile.NDEF => 0x02, + DataFile.Proprietary => 0x03, + _ => throw new ArgumentException(nameof(file)) + } }; + } + + public async Task WriteNDef(NdefMessage message) + { + var ndefMessageBytes = message.ToByteArray(); + var content = new byte[220]; // Normally we have 256 bytes, but APDU has a size limit we need some margin + content[0] = (byte)(ndefMessageBytes.Length >> 8); + content[1] = (byte)ndefMessageBytes.Length; + Array.Copy(ndefMessageBytes, 0, content, 2, Math.Min(content.Length - 2, ndefMessageBytes.Length)); + await SendAPDU(NtagCommands.WriteData with + { + CommMode = await GetCommMode(DataFile.NDEF, AccessRight.Write), + Data = Concat( + GetFileNo(DataFile.NDEF), + new byte[] { 0x00, 0x00, 0x00 }, + UIntTo3BytesLE(content.Length), + content + ) + }); + } + + public async Task ChangeKey(int keyNo, AESKey newKey, AESKey? oldKey = null, int version = 0) + { + if (CurrentSession is null || CurrentSession.KeyNo != 0) + throw new InvalidOperationException("Authentication required with KeyNo 0"); + + byte[] data; + if (keyNo == 0) + { + data = Concat( + newKey.ToBytes(), + new byte[] { (byte)version } + ); + } + else + { + oldKey ??= new AESKey(new byte[16]); + data = Concat( + XOR(newKey.ToBytes(), oldKey.ToBytes()), + new byte[] { (byte)version }, + UIntToBytesLE(CRCJam(newKey.ToBytes()))); + } + + await SendAPDU(NtagCommands.ChangeKey with + { + Data = Concat( + new byte[] { (byte)keyNo }, + data + ) + }); + } + + public async Task SetupBoltcard(string lnurlw) + { + if (!lnurlw.Contains('?', StringComparison.OrdinalIgnoreCase)) + lnurlw += "?"; + else + lnurlw += "&"; + lnurlw += "p=00000000000000000000000000000000&c=0000000000000000"; + + var ndef = new NdefMessage + { + new NdefUriRecord() { Uri = lnurlw } + }; + await WriteNDef(ndef); + var ndefBytes = ndef.ToByteArray(); + var pIndex = Array.LastIndexOf(ndefBytes, (byte)'p') + 4; + var cIndex = Array.LastIndexOf(ndefBytes, (byte)'c') + 4; + + var settings = new FileSettings(DataFile.NDEF) + { + AccessRights = new() + { + ReadWrite = AccessCondition.Key0, + Change = AccessCondition.Key0, + Write = AccessCondition.Key0, + Read = AccessCondition.Free + }, + SDMMirroring = true, + SDMUID = true, + SDMReadCtr = true, + SDMAccessRights = new() + { + MetaRead = AccessCondition.Key1, + FileRead = AccessCondition.Key2, + CtrRet = AccessCondition.Never + }, + SDMMACInputOffset = cIndex, + SDMMACOffset = cIndex, + PICCDataOffset = pIndex + }; + await ChangeFileSettings(fileSettings: settings); + + } } diff --git a/src/BTCPayServer.NTag424/NtagCommands.cs b/src/BTCPayServer.NTag424/NtagCommands.cs index 11025ec..434c6fd 100644 --- a/src/BTCPayServer.NTag424/NtagCommands.cs +++ b/src/BTCPayServer.NTag424/NtagCommands.cs @@ -1,191 +1,183 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics.Metrics; using System.Linq; -using System.Security.AccessControl; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; -using System.Transactions; using static BoltCardTools.Helpers; -using static System.Runtime.InteropServices.JavaScript.JSType; -namespace BoltCardTools +namespace BoltCardTools; + +public enum CommMode { - public enum CommMode - { - Plain, - Full, - MAC - } - public record NTagError(ushort sw1sw2, string Code, string Description) - { - public override string ToString() - { - return $"{Code} ({sw1sw2:x4}): {Description}"; - } - } - public record NTagCommand(string Name, byte CLA, byte INS, byte? P1, byte? P2, byte? Lc, byte[]? Data, byte? Le, ushort ExpectedStatus, CommMode? CommMode, int CommandHeaderSize = 0) - { - public List ErrorCodes = new List(); - static List DefaultErrorCodes = new List() - { - new NTagError(0x9100, "OPERATION_OK", "Successful operation."), - new NTagError(0x911C, "ILLEGAL_COMMAND_CODE", "Command code not supported."), - new NTagError(0x911E, "INTEGRITY_ERROR", "CRC or MAC does not match data. Padding bytes not valid."), - new NTagError(0x9140, "NO_SUCH_KEY", "Invalid key number specified."), - new NTagError(0x917E, "LENGTH_ERROR", "Length of command string invalid."), - new NTagError(0x919D, "PERMISSION_DENIED", "Current configuration / status does not allow the requested command."), - new NTagError(0x919E, "PARAMETER_ERROR", "Value of the parameter(s) invalid."), - new NTagError(0x91AD, "AUTHENTICATION_DELAY", "Currently not allowed to authenticate. Keep trying until full delay is spent."), - new NTagError(0x91AE, "AUTHENTICATION_ERROR", "Current authentication status does not allow the requested command."), - new NTagError(0x91AF, "ADDITIONAL_FRAME", "Additional data frame is expected to be sent."), - new NTagError(0x91BE, "BOUNDARY_ERROR", "Attempt to read/write data from/to beyond the file’s/record’s limits. Attempt to exceed the limits of a value file."), - new NTagError(0x91CA, "COMMAND_ABORTED", "Previous Command was not fully completed. Not all Frames were requested or provided by the PCD."), - new NTagError(0x91F0, "FILE_NOT_FOUND", "Specified file number does not exist."), - new NTagError(0x6700, "WRONG_LENGTH", "Wrong length; no further indication."), - new NTagError(0x6982, "SECURITY_STATUS_NOT_SATISFIED", "Security status not satisfied."), - new NTagError(0x6985, "CONDITIONS_OF_USE_NOT_SATISFIED", "Conditions of use not satisfied."), - new NTagError(0x6A80, "INCORRECT_PARAMETERS_IN_DATA_FIELD", "Incorrect parameters in the command data field."), - new NTagError(0x6A82, "FILE_OR_APPLICATION_NOT_FOUND", "File or application not found."), - new NTagError(0x6A86, "INCORRECT_PARAMETERS_P1_P2", "Incorrect parameters P1-P2."), - new NTagError(0x6A87, "LC_INCONSISTENT_WITH_PARAMETERS_P1_P2", "Lc inconsistent with parameters P1-P2."), - new NTagError(0x6C00, "WRONG_LE_FIELD", "Wrong Le field."), - new NTagError(0x6D00, "INSTRUCTION_CODE_NOT_SUPPORTED_OR_INVALID", "Instruction code not supported or invalid."), - new NTagError(0x6E00, "CLASS_NOT_SUPPORTED", "Class not supported."), - new NTagError(0x9000, "NORMAL_PROCESSING", "Normal processing (no further qualification).") - }; - internal void ThrowIfUnexpected(NtagResponse resp) - { - if (resp.sw1sw2 != ExpectedStatus) - { - var errorCode = ErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2); - errorCode ??= DefaultErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2); - if (errorCode is null) - throw new UnexpectedStatusException(Name, ExpectedStatus, resp.sw1sw2); - else - throw new UnexpectedStatusException(Name, ExpectedStatus, errorCode); - } - } - public byte[] ToBytes() - { - var list = new List - { - CLA, - INS - }; - if (!P1.HasValue) - throw new InvalidOperationException("P1 not provided"); - if (!P2.HasValue) - throw new InvalidOperationException("P2 not provided"); - - list.Add(P1.Value); - list.Add(P2.Value); - if (Data != null) - { - if (Lc.HasValue) - { - var realLc = Lc.Value; - if (CommMode is BoltCardTools.CommMode.Full) - { - var encDataSize = realLc - CommandHeaderSize; - realLc = (byte)CommandHeaderSize; - realLc += (byte)(16 - (encDataSize % 16)); // Padding - realLc += 8; // Add mac - } - if (CommMode is BoltCardTools.CommMode.MAC) - { - realLc += 8; // Add mac - } - - if (realLc != Data.Length) - throw new InvalidOperationException("Invalid Data length"); - } - list.Add((byte)(Data.Length)); - list.AddRange(Data); - } - if (Le.HasValue) - { - list.Add(Le.Value); - } - return list.ToArray(); - } - public override string ToString() - { - return ToBytes().ToHex(); - } - - internal NTagCommand Encode(Ntag424.Session currentSession) - { - if (CommMode is null) - throw new InvalidOperationException("CommMode isn't set"); - if (CommMode is BoltCardTools.CommMode.Plain) - return this; - var data = Data; - if (CommMode is BoltCardTools.CommMode.Full && data is not null) - { - var nonEncrypted = data[0..CommandHeaderSize]; - var encrypted = data[CommandHeaderSize..]; - data = Concat(nonEncrypted, currentSession.EncryptCommand(encrypted)); - } - var mac = currentSession.GetMac(INS, data); - data = Concat(data, mac); - return this with - { - Data = data - }; - } - } - internal class NtagCommands - { - internal readonly static NTagCommand AuthenticateEV2FirstPart1 = new(Name: "AuthenticateEV2FirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain); - internal readonly static NTagCommand AuthenticateEV2FirstPart2 = new(Name: "AuthenticateEV2FirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain); - internal readonly static NTagCommand AuthenticateEV2NonFirstPart1 = new(Name: "AuthenticateEV2NonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain); - internal readonly static NTagCommand AuthenticateEV2NonFirstPart2 = new(Name: "AuthenticateEV2NonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain); - internal readonly static NTagCommand AuthenticateLRPFirstPart1 = new(Name: "AuthenticateLRPFirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null); - internal readonly static NTagCommand AuthenticateLRPFirstPart2 = new(Name: "AuthenticateLRPFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null); - internal readonly static NTagCommand AuthenticateLRPNonFirstPart1 = new(Name: "AuthenticateLRPNonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null); - internal readonly static NTagCommand AuthenticateLRPNonFirstPart2 = new(Name: "AuthenticateLRPNonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null); - internal readonly static NTagCommand ChangeFileSettings = new(Name: "ChangeFileSettings", CLA: 0x90, INS: 0x5F, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1); - internal readonly static NTagCommand ChangeKey = new(Name: "ChangeKey", CLA: 0x90, INS: 0xC4, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1) - { - ErrorCodes = - { - new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."), - new NTagError(0x911E, "INTEGRITY_ERROR", "Integrity error in cryptogram or invalid secure messaging MAC (Secure Messaging)."), - new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."), - new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."), - new NTagError(0x9140, "NO_SUCH_KEY", "Targeted key does not exist."), - new NTagError(0x919D, "PERMISSION_DENIED", "At PICC level, targeting any OriginalityKey which cannot be changed."), - new NTagError(0x91AE, "AUTHENTICATION_ERROR", "At application level, missing active authentication with AppMasterKey while targeting any AppKey."), - new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.") - } - }; - internal readonly static NTagCommand GetCardUID = new(Name: "GetCardUID", CLA: 0x90, INS: 0x51, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full); - internal readonly static NTagCommand GetFileCounters = new(Name: "GetFileCounters", CLA: 0x90, INS: 0xF6, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full); - internal readonly static NTagCommand GetFileSettings = new(Name: "GetFileSettings", CLA: 0x90, INS: 0xF5, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC) - { - ErrorCodes = - { - new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."), - new NTagError(0x911E, "INTEGRITY_ERROR", "Invalid secure messaging MAC (only)."), - new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."), - new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."), - new NTagError(0x919D, "PERMISSION_DENIED", "PICC level (MF) is selected."), - new NTagError(0x91F0, "FILE_NOT_FOUND", "File with targeted FileNo does not exist for the targeted application."), - new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.") - } - }; - internal readonly static NTagCommand GetKeyVersion = new(Name: "GetKeyVersion", CLA: 0x90, INS: 0x64, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC); - internal readonly static NTagCommand GetVersionPart1 = new(Name: "GetVersionPart1", CLA: 0x90, INS: 0x60, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC); - internal readonly static NTagCommand GetVersionPart2 = new(Name: "GetVersionPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC); - internal readonly static NTagCommand GetVersionPart3 = new(Name: "GetVersionPart3", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC); - internal readonly static NTagCommand ISOReadBinary = new(Name: "ISOReadBinary", CLA: 0x00, INS: 0xB0, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain); - internal readonly static NTagCommand ReadData = new(Name: "ReadData", CLA: 0x90, INS: 0xAD, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7); - internal readonly static NTagCommand Read_Sig = new(Name: "Read_Sig", CLA: 0x90, INS: 0x3C, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full); - internal readonly static NTagCommand ISOSelectFile = new(Name: "ISOSelectFile", CLA: 0x00, INS: 0xA4, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain); - internal readonly static NTagCommand SetConfiguration = new(Name: "SetConfiguration", CLA: 0x90, INS: 0x5C, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1); - internal readonly static NTagCommand ISOUpdateBinary = new(Name: "ISOUpdateBinary", CLA: 0x00, INS: 0xD6, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain); - internal readonly static NTagCommand WriteData = new(Name: "WriteData", CLA: 0x90, INS: 0x8D, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7); - } + Plain, + Full, + MAC +} +public record NTagError(ushort sw1sw2, string Code, string Description) +{ + public override string ToString() + { + return $"{Code} ({sw1sw2:x4}): {Description}"; + } +} +public record NTagCommand(string Name, byte CLA, byte INS, byte? P1, byte? P2, byte? Lc, byte[]? Data, byte? Le, ushort ExpectedStatus, CommMode? CommMode, int CommandHeaderSize = 0) +{ + public List ErrorCodes = new List(); + static List DefaultErrorCodes = new List() + { + new NTagError(0x9100, "OPERATION_OK", "Successful operation."), + new NTagError(0x911C, "ILLEGAL_COMMAND_CODE", "Command code not supported."), + new NTagError(0x911E, "INTEGRITY_ERROR", "CRC or MAC does not match data. Padding bytes not valid."), + new NTagError(0x9140, "NO_SUCH_KEY", "Invalid key number specified."), + new NTagError(0x917E, "LENGTH_ERROR", "Length of command string invalid."), + new NTagError(0x919D, "PERMISSION_DENIED", "Current configuration / status does not allow the requested command."), + new NTagError(0x919E, "PARAMETER_ERROR", "Value of the parameter(s) invalid."), + new NTagError(0x91AD, "AUTHENTICATION_DELAY", "Currently not allowed to authenticate. Keep trying until full delay is spent."), + new NTagError(0x91AE, "AUTHENTICATION_ERROR", "Current authentication status does not allow the requested command."), + new NTagError(0x91AF, "ADDITIONAL_FRAME", "Additional data frame is expected to be sent."), + new NTagError(0x91BE, "BOUNDARY_ERROR", "Attempt to read/write data from/to beyond the file’s/record’s limits. Attempt to exceed the limits of a value file."), + new NTagError(0x91CA, "COMMAND_ABORTED", "Previous Command was not fully completed. Not all Frames were requested or provided by the PCD."), + new NTagError(0x91F0, "FILE_NOT_FOUND", "Specified file number does not exist."), + new NTagError(0x6700, "WRONG_LENGTH", "Wrong length; no further indication."), + new NTagError(0x6982, "SECURITY_STATUS_NOT_SATISFIED", "Security status not satisfied."), + new NTagError(0x6985, "CONDITIONS_OF_USE_NOT_SATISFIED", "Conditions of use not satisfied."), + new NTagError(0x6A80, "INCORRECT_PARAMETERS_IN_DATA_FIELD", "Incorrect parameters in the command data field."), + new NTagError(0x6A82, "FILE_OR_APPLICATION_NOT_FOUND", "File or application not found."), + new NTagError(0x6A86, "INCORRECT_PARAMETERS_P1_P2", "Incorrect parameters P1-P2."), + new NTagError(0x6A87, "LC_INCONSISTENT_WITH_PARAMETERS_P1_P2", "Lc inconsistent with parameters P1-P2."), + new NTagError(0x6C00, "WRONG_LE_FIELD", "Wrong Le field."), + new NTagError(0x6D00, "INSTRUCTION_CODE_NOT_SUPPORTED_OR_INVALID", "Instruction code not supported or invalid."), + new NTagError(0x6E00, "CLASS_NOT_SUPPORTED", "Class not supported."), + new NTagError(0x9000, "NORMAL_PROCESSING", "Normal processing (no further qualification).") + }; + internal void ThrowIfUnexpected(NtagResponse resp) + { + if (resp.sw1sw2 != ExpectedStatus) + { + var errorCode = ErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2); + errorCode ??= DefaultErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2); + if (errorCode is null) + throw new UnexpectedStatusException(Name, ExpectedStatus, resp.sw1sw2); + else + throw new UnexpectedStatusException(Name, ExpectedStatus, errorCode); + } + } + public byte[] ToBytes() + { + var list = new List + { + CLA, + INS + }; + if (!P1.HasValue) + throw new InvalidOperationException("P1 not provided"); + if (!P2.HasValue) + throw new InvalidOperationException("P2 not provided"); + + list.Add(P1.Value); + list.Add(P2.Value); + if (Data != null) + { + if (Lc.HasValue) + { + var realLc = Lc.Value; + if (CommMode is BoltCardTools.CommMode.Full) + { + var encDataSize = realLc - CommandHeaderSize; + realLc = (byte)CommandHeaderSize; + realLc += (byte)(16 - (encDataSize % 16)); // Padding + realLc += 8; // Add mac + } + if (CommMode is BoltCardTools.CommMode.MAC) + { + realLc += 8; // Add mac + } + + if (realLc != Data.Length) + throw new InvalidOperationException("Invalid Data length"); + } + list.Add((byte)(Data.Length)); + list.AddRange(Data); + } + if (Le.HasValue) + { + list.Add(Le.Value); + } + return list.ToArray(); + } + public override string ToString() + { + return ToBytes().ToHex(); + } + + internal NTagCommand Encode(Ntag424.Session currentSession) + { + if (CommMode is null) + throw new InvalidOperationException("CommMode isn't set"); + if (CommMode is BoltCardTools.CommMode.Plain) + return this; + var data = Data; + if (CommMode is BoltCardTools.CommMode.Full && data is not null) + { + var nonEncrypted = data[0..CommandHeaderSize]; + var encrypted = data[CommandHeaderSize..]; + data = Concat(nonEncrypted, currentSession.EncryptCommand(encrypted)); + } + var mac = currentSession.GetMac(INS, data); + data = Concat(data, mac); + return this with + { + Data = data + }; + } +} +internal class NtagCommands +{ + internal readonly static NTagCommand AuthenticateEV2FirstPart1 = new(Name: "AuthenticateEV2FirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain); + internal readonly static NTagCommand AuthenticateEV2FirstPart2 = new(Name: "AuthenticateEV2FirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain); + internal readonly static NTagCommand AuthenticateEV2NonFirstPart1 = new(Name: "AuthenticateEV2NonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain); + internal readonly static NTagCommand AuthenticateEV2NonFirstPart2 = new(Name: "AuthenticateEV2NonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain); + internal readonly static NTagCommand AuthenticateLRPFirstPart1 = new(Name: "AuthenticateLRPFirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null); + internal readonly static NTagCommand AuthenticateLRPFirstPart2 = new(Name: "AuthenticateLRPFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null); + internal readonly static NTagCommand AuthenticateLRPNonFirstPart1 = new(Name: "AuthenticateLRPNonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null); + internal readonly static NTagCommand AuthenticateLRPNonFirstPart2 = new(Name: "AuthenticateLRPNonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null); + internal readonly static NTagCommand ChangeFileSettings = new(Name: "ChangeFileSettings", CLA: 0x90, INS: 0x5F, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1); + internal readonly static NTagCommand ChangeKey = new(Name: "ChangeKey", CLA: 0x90, INS: 0xC4, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1) + { + ErrorCodes = + { + new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."), + new NTagError(0x911E, "INTEGRITY_ERROR", "Integrity error in cryptogram or invalid secure messaging MAC (Secure Messaging)."), + new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."), + new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."), + new NTagError(0x9140, "NO_SUCH_KEY", "Targeted key does not exist."), + new NTagError(0x919D, "PERMISSION_DENIED", "At PICC level, targeting any OriginalityKey which cannot be changed."), + new NTagError(0x91AE, "AUTHENTICATION_ERROR", "At application level, missing active authentication with AppMasterKey while targeting any AppKey."), + new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.") + } + }; + internal readonly static NTagCommand GetCardUID = new(Name: "GetCardUID", CLA: 0x90, INS: 0x51, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full); + internal readonly static NTagCommand GetFileCounters = new(Name: "GetFileCounters", CLA: 0x90, INS: 0xF6, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full); + internal readonly static NTagCommand GetFileSettings = new(Name: "GetFileSettings", CLA: 0x90, INS: 0xF5, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC) + { + ErrorCodes = + { + new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."), + new NTagError(0x911E, "INTEGRITY_ERROR", "Invalid secure messaging MAC (only)."), + new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."), + new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."), + new NTagError(0x919D, "PERMISSION_DENIED", "PICC level (MF) is selected."), + new NTagError(0x91F0, "FILE_NOT_FOUND", "File with targeted FileNo does not exist for the targeted application."), + new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.") + } + }; + internal readonly static NTagCommand GetKeyVersion = new(Name: "GetKeyVersion", CLA: 0x90, INS: 0x64, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC); + internal readonly static NTagCommand GetVersionPart1 = new(Name: "GetVersionPart1", CLA: 0x90, INS: 0x60, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC); + internal readonly static NTagCommand GetVersionPart2 = new(Name: "GetVersionPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC); + internal readonly static NTagCommand GetVersionPart3 = new(Name: "GetVersionPart3", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC); + internal readonly static NTagCommand ISOReadBinary = new(Name: "ISOReadBinary", CLA: 0x00, INS: 0xB0, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain); + internal readonly static NTagCommand ReadData = new(Name: "ReadData", CLA: 0x90, INS: 0xAD, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7); + internal readonly static NTagCommand Read_Sig = new(Name: "Read_Sig", CLA: 0x90, INS: 0x3C, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full); + internal readonly static NTagCommand ISOSelectFile = new(Name: "ISOSelectFile", CLA: 0x00, INS: 0xA4, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain); + internal readonly static NTagCommand SetConfiguration = new(Name: "SetConfiguration", CLA: 0x90, INS: 0x5C, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1); + internal readonly static NTagCommand ISOUpdateBinary = new(Name: "ISOUpdateBinary", CLA: 0x00, INS: 0xD6, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain); + internal readonly static NTagCommand WriteData = new(Name: "WriteData", CLA: 0x90, INS: 0x8D, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7); } diff --git a/src/BTCPayServer.NTag424/NtagResponse.cs b/src/BTCPayServer.NTag424/NtagResponse.cs index 8b2e99a..b352075 100644 --- a/src/BTCPayServer.NTag424/NtagResponse.cs +++ b/src/BTCPayServer.NTag424/NtagResponse.cs @@ -1,19 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace BoltCardTools; -namespace BoltCardTools +public record NtagResponse(byte[] Data, ushort sw1sw2) { - public record NtagResponse(byte[] Data, ushort sw1sw2) - { - internal NtagResponse Decode(Ntag424.Session currentSession, CommMode commMode) - { - return this with - { - Data = currentSession.DecryptResponse((byte)sw1sw2, Data, commMode) - }; - } - } + internal NtagResponse Decode(Ntag424.Session currentSession, CommMode commMode) + { + return this with + { + Data = currentSession.DecryptResponse((byte)sw1sw2, Data, commMode) + }; + } } diff --git a/src/BTCPayServer.NTag424/PCSCAPDUTransport.cs b/src/BTCPayServer.NTag424/PCSCAPDUTransport.cs index 232cc2e..cece53a 100644 --- a/src/BTCPayServer.NTag424/PCSCAPDUTransport.cs +++ b/src/BTCPayServer.NTag424/PCSCAPDUTransport.cs @@ -1,53 +1,46 @@ -using PCSC; -using PCSC.Extensions; -using PCSC.Iso7816; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; +using PCSC; +using PCSC.Extensions; -namespace BoltCardTools +namespace BoltCardTools; + +public class PCSCAPDUTransport : IAPDUTransport { - public class PCSCAPDUTransport : IAPDUTransport - { - public readonly ISCardReader CardReader; - public PCSCAPDUTransport(ISCardReader cardReader) - { - ArgumentNullException.ThrowIfNull(cardReader); - CardReader = cardReader; - } + public readonly ISCardReader CardReader; + public PCSCAPDUTransport(ISCardReader cardReader) + { + ArgumentNullException.ThrowIfNull(cardReader); + CardReader = cardReader; + } - public Task SendAPDU(NTagCommand apdu) - { - //return Task.Factory.StartNew(() => - //{ - // var bytes = apdu.ToBytes(); - // Console.WriteLine(bytes.ToHex()); - // Console.WriteLine("---"); - // var resp = new byte[256]; - // int received = resp.Length; - // var sc = CardReader.Transmit(bytes, resp, ref received); - // if (sc != SCardError.Success) - // sc.Throw(); - // var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]); - // var data = resp[..(received - 2)]; - // return new NtagResponse(data, sw1sw2); - //}, TaskCreationOptions.LongRunning); + public Task SendAPDU(NTagCommand apdu) + { + //return Task.Factory.StartNew(() => + //{ + // var bytes = apdu.ToBytes(); + // Console.WriteLine(bytes.ToHex()); + // Console.WriteLine("---"); + // var resp = new byte[256]; + // int received = resp.Length; + // var sc = CardReader.Transmit(bytes, resp, ref received); + // if (sc != SCardError.Success) + // sc.Throw(); + // var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]); + // var data = resp[..(received - 2)]; + // return new NtagResponse(data, sw1sw2); + //}, TaskCreationOptions.LongRunning); - var bytes = apdu.ToBytes(); - Console.WriteLine("Command APDU : " + bytes.ToHex()); - var resp = new byte[512]; - int received = resp.Length; - var sc = CardReader.Transmit(bytes, resp, ref received); - if (sc != SCardError.Success) - sc.Throw(); - Console.WriteLine("Response APDU : " + resp[..received].ToHex()); - var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]); - var data = resp[..(received - 2)]; - return Task.FromResult(new NtagResponse(data, sw1sw2)); - } - } + var bytes = apdu.ToBytes(); + Console.WriteLine("Command APDU : " + bytes.ToHex()); + var resp = new byte[512]; + int received = resp.Length; + var sc = CardReader.Transmit(bytes, resp, ref received); + if (sc != SCardError.Success) + sc.Throw(); + Console.WriteLine("Response APDU : " + resp[..received].ToHex()); + var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]); + var data = resp[..(received - 2)]; + return Task.FromResult(new NtagResponse(data, sw1sw2)); + } } diff --git a/src/BTCPayServer.NTag424/PICCData.cs b/src/BTCPayServer.NTag424/PICCData.cs index 8f54ba0..746c7b2 100644 --- a/src/BTCPayServer.NTag424/PICCData.cs +++ b/src/BTCPayServer.NTag424/PICCData.cs @@ -1,28 +1,28 @@ - + using System; namespace BoltCardTools; public record PICCData(byte[]? Uid, int? Counter) { - public static PICCData Create(ReadOnlySpan data) - { - bool hasUid = (data[0] & 0b1000_0000) != 0; - bool hasCounter = (data[0] & 0b0100_0000) != 0; - if (hasUid && ((data[0] & 0b0000_0111) != 0b0000_0111)) - throw new InvalidOperationException("Invalid PICCData"); - int i = 1; - byte[]? uid = null; - int? counter = null; - if (hasUid) - { - uid = data[i..(i + 7)].ToArray(); - i += 7; - } - if (hasCounter) - { - counter = data[i] | data[i + 1] << 8 | data[i + 2] << 16; - } - return new PICCData(uid, counter); - } + public static PICCData Create(ReadOnlySpan data) + { + bool hasUid = (data[0] & 0b1000_0000) != 0; + bool hasCounter = (data[0] & 0b0100_0000) != 0; + if (hasUid && ((data[0] & 0b0000_0111) != 0b0000_0111)) + throw new InvalidOperationException("Invalid PICCData"); + int i = 1; + byte[]? uid = null; + int? counter = null; + if (hasUid) + { + uid = data[i..(i + 7)].ToArray(); + i += 7; + } + if (hasCounter) + { + counter = data[i] | data[i + 1] << 8 | data[i + 2] << 16; + } + return new PICCData(uid, counter); + } } diff --git a/src/BTCPayServer.NTag424/Program.cs b/src/BTCPayServer.NTag424/Program.cs index 90dda35..f38d75a 100644 --- a/src/BTCPayServer.NTag424/Program.cs +++ b/src/BTCPayServer.NTag424/Program.cs @@ -1,54 +1,49 @@ -using BoltCardTools; +using System.Linq; +using System.Threading.Tasks; +using BoltCardTools; using NdefLibrary.Ndef; using PCSC; using PCSC.Extensions; -using PCSC.Iso7816; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; // https://github.com/boltcard/boltcard-wallet/blob/master/class/Ntag424.js // https://github.com/boltcard/boltcard-wallet/blob/master/screen/boltcard/create.js#L201 // https://github.com/danm-de/pcsc-sharp public class Program { - public static async Task Main(string[] args) - { - var message = NdefMessage.FromByteArray("D1012C5503746573742E636F6D3F703D303030303030303030303030303026633D303030303030303030303030303030".HexToBytes()); + public static async Task Main(string[] args) + { + var message = NdefMessage.FromByteArray("D1012C5503746573742E636F6D3F703D303030303030303030303030303026633D303030303030303030303030303030".HexToBytes()); - // http://test.com?p=00000000000000&c=000000000000000, description=, uriType=sms, mirrorFlags=null, mUri=http://test.com?p=00000000000000&c=000000000000000 + // http://test.com?p=00000000000000&c=000000000000000, description=, uriType=sms, mirrorFlags=null, mUri=http://test.com?p=00000000000000&c=000000000000000 - var contextFactory = ContextFactory.Instance; - using (var ctx = contextFactory.Establish(SCardScope.System)) - { - var readerName = ctx.GetReaders().FirstOrDefault(); - if (readerName != null) - { - using var reader = new SCardReader(ctx); - reader.Connect(readerName, SCardShareMode.Exclusive, SCardProtocol.Any).ThrowIfNotSuccess(); - var transport = new PCSCAPDUTransport(reader); - var ntag = new Ntag424(transport); - var key = new AESKey(new byte[16]); - await ntag.AuthenticateEV2First(0, key); - await ntag.AuthenticateEV2NonFirst(0, key); + var contextFactory = ContextFactory.Instance; + using var ctx = contextFactory.Establish(SCardScope.System); + var readerName = ctx.GetReaders().FirstOrDefault(); + if (readerName != null) + { + using var reader = new SCardReader(ctx); + reader.Connect(readerName, SCardShareMode.Exclusive, SCardProtocol.Any).ThrowIfNotSuccess(); + var transport = new PCSCAPDUTransport(reader); + var ntag = new Ntag424(transport); + var key = new AESKey(new byte[16]); + await ntag.AuthenticateEV2First(0, key); + await ntag.AuthenticateEV2NonFirst(0, key); - //Console.WriteLine("UID: " + (await ntag.GetCardUID()).ToHex()); - //await ntag.SetupBoltcard("http://test.com"); - //await ntag.ChangeFileSettings(); + //Console.WriteLine("UID: " + (await ntag.GetCardUID()).ToHex()); + //await ntag.SetupBoltcard("http://test.com"); + //await ntag.ChangeFileSettings(); - //await ntag.ReadFile(DataFile.NDEF, 0, 10); - //await ntag.WriteNDef(message); - //await ntag.ReadFile(DataFile.NDEF, 0, 10); - //await ntag.WriteNDef(message); - //await ntag.IsoSelectFile(DataFile.CC); - //var d = await ntag.ReadFile(DataFile.NDEF, 0, 0); - //Console.WriteLine(d.ToHex()); - //await ntag.GetCardUID - //var access = await ntag.GetFileSettings(); - } - } - } + //await ntag.ReadFile(DataFile.NDEF, 0, 10); + //await ntag.WriteNDef(message); + //await ntag.ReadFile(DataFile.NDEF, 0, 10); + //await ntag.WriteNDef(message); + //await ntag.IsoSelectFile(DataFile.CC); + //var d = await ntag.ReadFile(DataFile.NDEF, 0, 0); + //Console.WriteLine(d.ToHex()); + //await ntag.GetCardUID + //var access = await ntag.GetFileSettings(); + } + } - -} \ No newline at end of file + +} diff --git a/src/BTCPayServer.NTag424/Properties.cs b/src/BTCPayServer.NTag424/Properties.cs index 3a161ab..93784c3 100644 --- a/src/BTCPayServer.NTag424/Properties.cs +++ b/src/BTCPayServer.NTag424/Properties.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; -[assembly:InternalsVisibleTo("BoltCardTools.Tests")] +[assembly: InternalsVisibleTo("BoltCardTools.Tests")] diff --git a/src/BTCPayServer.NTag424/UnexpectedStatusException.cs b/src/BTCPayServer.NTag424/UnexpectedStatusException.cs index fb0a283..efc3e87 100644 --- a/src/BTCPayServer.NTag424/UnexpectedStatusException.cs +++ b/src/BTCPayServer.NTag424/UnexpectedStatusException.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System; -namespace BoltCardTools +namespace BoltCardTools; + +public class UnexpectedResponseException : Exception { - public class UnexpectedResponseException : Exception - { - public UnexpectedResponseException(string? message) : base(message) - { + public UnexpectedResponseException(string? message) : base(message) + { - } - } - public class UnexpectedStatusException : UnexpectedResponseException - { - public UnexpectedStatusException(string commandName, int expectedStatus, NTagError error) - : base($"Error for {commandName}: Expected {expectedStatus:X4}, Actual: {error}") - { - Details = error; - } - public NTagError? Details { get; } - public UnexpectedStatusException(string commandName, int expectedStatus, int actualStatus) - : base($"Unexpected status for {commandName}: Expected: {expectedStatus:X4}, Actual: {actualStatus:X4}") - { - } - } + } +} +public class UnexpectedStatusException : UnexpectedResponseException +{ + public UnexpectedStatusException(string commandName, int expectedStatus, NTagError error) + : base($"Error for {commandName}: Expected {expectedStatus:X4}, Actual: {error}") + { + Details = error; + } + public NTagError? Details { get; } + public UnexpectedStatusException(string commandName, int expectedStatus, int actualStatus) + : base($"Unexpected status for {commandName}: Expected: {expectedStatus:X4}, Actual: {actualStatus:X4}") + { + } } diff --git a/tests/BoltCardTools.Tests.csproj b/tests/BoltCardTools.Tests.csproj index 95c1d5c..664479a 100644 --- a/tests/BoltCardTools.Tests.csproj +++ b/tests/BoltCardTools.Tests.csproj @@ -23,7 +23,6 @@ - diff --git a/tests/CardReaderContext.cs b/tests/CardReaderContext.cs index 9dbbddf..679e4e6 100644 --- a/tests/CardReaderContext.cs +++ b/tests/CardReaderContext.cs @@ -1,39 +1,32 @@ -using PCSC; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.PortableExecutable; -using System.Text; -using System.Threading.Tasks; +using PCSC; -namespace BoltCardTools.Tests +namespace BoltCardTools.Tests; + +public record CardReaderContext(ISCardReader CardReader, IContextFactory ContextFactory, ISCardContext Context) : IDisposable { - public record CardReaderContext(ISCardReader CardReader, IContextFactory ContextFactory, ISCardContext Context) : IDisposable - { - public static CardReaderContext Create() - { - var contextFactory = PCSC.ContextFactory.Instance; - var context = contextFactory.Establish(SCardScope.System); - var readerNames = context.GetReaders(); - var readerName = readerNames.FirstOrDefault(); - if (readerName is null) - { - throw new InvalidOperationException("No readers found"); - } - var reader = new SCardReader(context); - reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any); - return new CardReaderContext(reader, contextFactory, context); - } - public void Dispose() - { - CardReader.Dispose(); - Context.Dispose(); - } + public static CardReaderContext Create() + { + var contextFactory = PCSC.ContextFactory.Instance; + var context = contextFactory.Establish(SCardScope.System); + var readerNames = context.GetReaders(); + var readerName = readerNames.FirstOrDefault(); + if (readerName is null) + { + throw new InvalidOperationException("No readers found"); + } + var reader = new SCardReader(context); + reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any); + return new CardReaderContext(reader, contextFactory, context); + } + public void Dispose() + { + CardReader.Dispose(); + Context.Dispose(); + } - public Ntag424 CreateNTag424() - { - var transport = new PCSCAPDUTransport(this.CardReader); - return new Ntag424(transport); - } - } + public Ntag424 CreateNTag424() + { + var transport = new PCSCAPDUTransport(this.CardReader); + return new Ntag424(transport); + } } diff --git a/tests/GlobalUsings.cs b/tests/GlobalUsings.cs index 8c927eb..c802f44 100644 --- a/tests/GlobalUsings.cs +++ b/tests/GlobalUsings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; diff --git a/tests/UnitTest1.cs b/tests/UnitTest1.cs index bff9ba1..7362500 100644 --- a/tests/UnitTest1.cs +++ b/tests/UnitTest1.cs @@ -1,196 +1,190 @@ -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using NdefLibrary.Ndef; -using PCSC; -using PCSC.Extensions; -using PCSC.Iso7816; -using System.Reflection.Emit; -using System.Text; using System.Text.RegularExpressions; +using NdefLibrary.Ndef; namespace BoltCardTools.Tests; public class UnitTest1 { - [Fact] - public void CanCreateAPDUFromNtagCommand() - { - var actual = (NtagCommands.ISOSelectFile with - { - P1 = 0x04, - P2 = 0x00, - Data = "d2760000850101".HexToBytes() - }).ToBytes().ToHex(); - var expected = "00A4040007D2760000850101".ToLowerInvariant(); - Assert.Equal(expected, actual); + [Fact] + public void CanCreateAPDUFromNtagCommand() + { + var actual = (NtagCommands.ISOSelectFile with + { + P1 = 0x04, + P2 = 0x00, + Data = "d2760000850101".HexToBytes() + }).ToBytes().ToHex(); + var expected = "00A4040007D2760000850101".ToLowerInvariant(); + Assert.Equal(expected, actual); - actual = (NtagCommands.ISOSelectFile with - { - P1 = 0x04, - P2 = 0x00, - Data = "d2760000850101".HexToBytes(), - Le = 0 - }).ToBytes().ToHex(); - expected = "00A4040007D276000085010100".ToLowerInvariant(); - Assert.Equal(expected, actual); - } + actual = (NtagCommands.ISOSelectFile with + { + P1 = 0x04, + P2 = 0x00, + Data = "d2760000850101".HexToBytes(), + Le = 0 + }).ToBytes().ToHex(); + expected = "00A4040007D276000085010100".ToLowerInvariant(); + Assert.Equal(expected, actual); + } - //from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md - [Theory] - [InlineData("E19CCB1FED8892CE", "04996c6a926980", 3)] - [InlineData("66B4826EA4C155B4", "04996c6a926980", 5)] - [InlineData("CC61660C020B4D96", "04996c6a926980", 7)] - public void CanCalculateSunMac(string expected, string uid, int ctr) - { - var key = new AESKey(Convert.FromHexString("b45775776cb224c75bcde7ca3704e933")); - var actual = key.GetSunMac(uid.HexToBytes(), ctr); - Assert.Equal(expected.ToLowerInvariant(), actual.ToHex()); - } + //from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md + [Theory] + [InlineData("E19CCB1FED8892CE", "04996c6a926980", 3)] + [InlineData("66B4826EA4C155B4", "04996c6a926980", 5)] + [InlineData("CC61660C020B4D96", "04996c6a926980", 7)] + public void CanCalculateSunMac(string expected, string uid, int ctr) + { + var key = new AESKey(Convert.FromHexString("b45775776cb224c75bcde7ca3704e933")); + var actual = key.GetSunMac(uid.HexToBytes(), ctr); + Assert.Equal(expected.ToLowerInvariant(), actual.ToHex()); + } - //from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md - [Theory] - [InlineData("4E2E289D945A66BB13377A728884E867", "04996c6a926980", 3)] - [InlineData("00F48C4F8E386DED06BCDC78FA92E2FE", "04996c6a926980", 5)] - [InlineData("0DBF3C59B59B0638D60B5842A997D4D1", "04996c6a926980", 7)] - public void CanDecryptSunPICCData(string encrypted, string uid, int ctr) - { - var key = new AESKey(Convert.FromHexString("0c3b25d92b38ae443229dd59ad34b85d")); - var picc = key.DecryptSun(encrypted.HexToBytes()); - Assert.Equal(ctr, picc.Counter); - Assert.Equal(uid.ToLowerInvariant(), picc.Uid?.ToHex()); - } + //from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md + [Theory] + [InlineData("4E2E289D945A66BB13377A728884E867", "04996c6a926980", 3)] + [InlineData("00F48C4F8E386DED06BCDC78FA92E2FE", "04996c6a926980", 5)] + [InlineData("0DBF3C59B59B0638D60B5842A997D4D1", "04996c6a926980", 7)] + public void CanDecryptSunPICCData(string encrypted, string uid, int ctr) + { + var key = new AESKey(Convert.FromHexString("0c3b25d92b38ae443229dd59ad34b85d")); + var picc = key.DecryptSun(encrypted.HexToBytes()); + Assert.Equal(ctr, picc.Counter); + Assert.Equal(uid.ToLowerInvariant(), picc.Uid?.ToHex()); + } - [Theory] - [InlineData("01020304050607080910111213141516", "0102030405060708091011121314151680000000000000000000000000000000")] - [InlineData("010203040506070809101112131415", "01020304050607080910111213141580")] - [InlineData("01", "01800000000000000000000000000000")] - public void CanDoPadding(string data, string padded) - { - var actual = Ntag424.Session.PaddingForEnc(data.HexToBytes()).ToHex(); - Assert.Equal(padded, actual); - } + [Theory] + [InlineData("01020304050607080910111213141516", "0102030405060708091011121314151680000000000000000000000000000000")] + [InlineData("010203040506070809101112131415", "01020304050607080910111213141580")] + [InlineData("01", "01800000000000000000000000000000")] + public void CanDoPadding(string data, string padded) + { + var actual = Ntag424.Session.PaddingForEnc(data.HexToBytes()).ToHex(); + Assert.Equal(padded, actual); + } - [Fact] - public void CanCreateCommModeMAC() - { - var session = new Ntag424.Session(0, new AESKey(new byte[16]), new AESKey("8248134A386E86EB7FAF54A52E536CB6".HexToBytes()), "7A21085E".HexToBytes()); - var command = NtagCommands.GetFileSettings with - { - CommMode = CommMode.MAC, - Data = new byte[] { 0x02 } - }; - command = command.Encode(session); - var apdu = command.ToBytes().ToHex(); - Assert.Equal("90F5000009026597A457C8CD442C00".ToLower(), apdu); - var resp = new NtagResponse("0040EEEE000100D1FE001F00004400004400002000006A00002A474282E7A47986".HexToBytes(), 0x9100); - command.ThrowIfUnexpected(resp); - session.Counter++; - var respData = resp.Decode(session, CommMode.MAC).Data.ToHex(); - Assert.Equal("0040EEEE000100D1FE001F00004400004400002000006A0000".ToLower(), respData); - } - [Fact] - public void CanCreateCommModeFull() - { - var session = new Ntag424.Session(0, new AESKey("7305E2CCA5B0377617CDBFEB96C9B358".HexToBytes()), new AESKey("8B485037C8C2FB400D79BF0AB956F28F".HexToBytes()), "856C1841".HexToBytes()); - var command = NtagCommands.WriteData with - { - CommMode = CommMode.Full, - Data = "02000000800000005ED1015B5500687474703A2F2F7777772E6D69666172652E6E65742F70726F64756374732F6E746167733F265549443D3034323136353441434634433830264374723D30303030303126436D61633D323145323336303832363645334345410000000000000000000000000000000000000000000000000000000000000000".HexToBytes() - }; - command = command.Encode(session); - var apdu = command.ToBytes().ToHex(); - // Why there are 4 more bytes in the doc?? - // Original: 908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F500000000D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00 - Assert.Equal("908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F5D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00".ToLower(), apdu); + [Fact] + public void CanCreateCommModeMAC() + { + var session = new Ntag424.Session(0, new AESKey(new byte[16]), new AESKey("8248134A386E86EB7FAF54A52E536CB6".HexToBytes()), "7A21085E".HexToBytes()); + var command = NtagCommands.GetFileSettings with + { + CommMode = CommMode.MAC, + Data = new byte[] { 0x02 } + }; + command = command.Encode(session); + var apdu = command.ToBytes().ToHex(); + Assert.Equal("90F5000009026597A457C8CD442C00".ToLower(), apdu); + var resp = new NtagResponse("0040EEEE000100D1FE001F00004400004400002000006A00002A474282E7A47986".HexToBytes(), 0x9100); + command.ThrowIfUnexpected(resp); + session.Counter++; + var respData = resp.Decode(session, CommMode.MAC).Data.ToHex(); + Assert.Equal("0040EEEE000100D1FE001F00004400004400002000006A0000".ToLower(), respData); + } + [Fact] + public void CanCreateCommModeFull() + { + var session = new Ntag424.Session(0, new AESKey("7305E2CCA5B0377617CDBFEB96C9B358".HexToBytes()), new AESKey("8B485037C8C2FB400D79BF0AB956F28F".HexToBytes()), "856C1841".HexToBytes()); + var command = NtagCommands.WriteData with + { + CommMode = CommMode.Full, + Data = "02000000800000005ED1015B5500687474703A2F2F7777772E6D69666172652E6E65742F70726F64756374732F6E746167733F265549443D3034323136353441434634433830264374723D30303030303126436D61633D323145323336303832363645334345410000000000000000000000000000000000000000000000000000000000000000".HexToBytes() + }; + command = command.Encode(session); + var apdu = command.ToBytes().ToHex(); + // Why there are 4 more bytes in the doc?? + // Original: 908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F500000000D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00 + Assert.Equal("908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F5D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00".ToLower(), apdu); - var resp = new NtagResponse("DDDB9EC959B3EFEB".HexToBytes(), 0x9100); - command.ThrowIfUnexpected(resp); + var resp = new NtagResponse("DDDB9EC959B3EFEB".HexToBytes(), 0x9100); + command.ThrowIfUnexpected(resp); - session.Counter++; - var respData = resp.Decode(session, CommMode.MAC).Data.ToHex(); - Assert.Empty(respData); - } + session.Counter++; + var respData = resp.Decode(session, CommMode.MAC).Data.ToHex(); + Assert.Empty(respData); + } - [Fact] - public void CanCreateFileSettings() - { - var actual = new FileSettings(DataFile.NDEF) - { - AccessRights = new() - { - ReadWrite = AccessCondition.Key0, - Change = AccessCondition.Key0, - Write = AccessCondition.Key0, - Read = AccessCondition.Free - }, - SDMMirroring = true, - SDMUID = true, - SDMReadCtr = true, - SDMAccessRights = new() - { - MetaRead = AccessCondition.Key1, - FileRead = AccessCondition.Key2, - CtrRet = AccessCondition.Never - }, - PICCDataOffset = 3, - SDMMACOffset = 2, - SDMMACInputOffset = 1 - }.ToBytes().ToHex(); + [Fact] + public void CanCreateFileSettings() + { + var actual = new FileSettings(DataFile.NDEF) + { + AccessRights = new() + { + ReadWrite = AccessCondition.Key0, + Change = AccessCondition.Key0, + Write = AccessCondition.Key0, + Read = AccessCondition.Free + }, + SDMMirroring = true, + SDMUID = true, + SDMReadCtr = true, + SDMAccessRights = new() + { + MetaRead = AccessCondition.Key1, + FileRead = AccessCondition.Key2, + CtrRet = AccessCondition.Never + }, + PICCDataOffset = 3, + SDMMACOffset = 2, + SDMMACInputOffset = 1 + }.ToBytes().ToHex(); - Assert.Equal("4000E0C1FF12030000010000020000".ToLower(), actual); - } + Assert.Equal("4000E0C1FF12030000010000020000".ToLower(), actual); + } - [Fact] - public async Task CanAuthenticate() - { - using var ctx = CardReaderContext.Create(); - var ntag = ctx.CreateNTag424(); - var key = new AESKey(new byte[16]); - await ntag.AuthenticateEV2First(0, key); - var uid1 = await ntag.GetCardUID(); - await ntag.AuthenticateEV2NonFirst(0, key); - var uid2 = await ntag.GetCardUID(); - Assert.Equal(uid1.ToHex(), uid2.ToHex()); - } + [Fact] + public async Task CanAuthenticate() + { + using var ctx = CardReaderContext.Create(); + var ntag = ctx.CreateNTag424(); + var key = new AESKey(new byte[16]); + await ntag.AuthenticateEV2First(0, key); + var uid1 = await ntag.GetCardUID(); + await ntag.AuthenticateEV2NonFirst(0, key); + var uid2 = await ntag.GetCardUID(); + Assert.Equal(uid1.ToHex(), uid2.ToHex()); + } - [Fact] - public async Task CanChangeKey() - { - using var ctx = CardReaderContext.Create(); - var ntag = ctx.CreateNTag424(); - var key1 = new AESKey(new byte[16]); - var key2b = new byte[16]; - key2b[^1] = 1; - var key2 = new AESKey(key2b); - await ntag.AuthenticateEV2First(0, key1); - await ntag.ChangeKey(0, key1); + [Fact] + public async Task CanChangeKey() + { + using var ctx = CardReaderContext.Create(); + var ntag = ctx.CreateNTag424(); + var key1 = new AESKey(new byte[16]); + var key2b = new byte[16]; + key2b[^1] = 1; + var key2 = new AESKey(key2b); + await ntag.AuthenticateEV2First(0, key1); + await ntag.ChangeKey(0, key1); - await ntag.AuthenticateEV2First(0, key1); - await ntag.ChangeKey(1, key1); - await ntag.ChangeKey(1, key2, key1); - await ntag.ChangeKey(1, key1, key2); - } + await ntag.AuthenticateEV2First(0, key1); + await ntag.ChangeKey(1, key1); + await ntag.ChangeKey(1, key2, key1); + await ntag.ChangeKey(1, key1, key2); + } - [Fact] - public async Task CanDoBoltcard() - { - using var ctx = CardReaderContext.Create(); - var ntag = ctx.CreateNTag424(); - var key = new AESKey(new byte[16]); - await ntag.AuthenticateEV2First(0, key); - await ntag.SetupBoltcard("http://test.com"); - var message = await ntag.ReadNDef(); - var uri = new NdefUriRecord(message[0]).Uri; - var p = Regex.Match(uri, "p=([^&]*)&").Groups[1].Value.ToLowerInvariant(); - var c = Regex.Match(uri, "c=(.*)").Groups[1].Value.ToLowerInvariant(); - var piccData = key.DecryptSun(p.HexToBytes()); - Assert.Equal(c, key.GetSunMac(piccData).ToHex()); - } + [Fact] + public async Task CanDoBoltcard() + { + using var ctx = CardReaderContext.Create(); + var ntag = ctx.CreateNTag424(); + var key = new AESKey(new byte[16]); + await ntag.AuthenticateEV2First(0, key); + await ntag.SetupBoltcard("http://test.com"); + var message = await ntag.ReadNDef(); + var uri = new NdefUriRecord(message[0]).Uri; + var p = Regex.Match(uri, "p=([^&]*)&").Groups[1].Value.ToLowerInvariant(); + var c = Regex.Match(uri, "c=(.*)").Groups[1].Value.ToLowerInvariant(); + var piccData = key.DecryptSun(p.HexToBytes()); + Assert.Equal(c, key.GetSunMac(piccData).ToHex()); + } - [Fact] - public void CanCalculateCRC() - { - var bytes = new byte[] { 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 }; - var result = Helpers.CRCJam(bytes); - Assert.Equal(unchecked((uint)(-0xd4a1186)), result); - } -} \ No newline at end of file + [Fact] + public void CanCalculateCRC() + { + var bytes = new byte[] { 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 }; + var result = Helpers.CRCJam(bytes); + Assert.Equal(unchecked((uint)(-0xd4a1186)), result); + } +}