Format
This commit is contained in:
parent
99467d8e7a
commit
daeb09a907
168
.editorconfig
Normal file
168
.editorconfig
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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<byte> bytes)
|
||||
{
|
||||
AssertKeySize(bytes);
|
||||
_bytes = bytes.ToArray();
|
||||
}
|
||||
|
||||
private static void AssertKeySize(ReadOnlySpan<byte> 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<byte> 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<byte>();
|
||||
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<byte> 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<byte> 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<byte> 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<byte>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Computes a Cipher-based Message Authentication Code (CMAC) by using the symmetric key AES block cipher.
|
||||
/// </summary>
|
||||
public sealed class AesCmac
|
||||
: KeyedHashAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a Cipher-based Message Authentication Code (CMAC) by using the symmetric key AES block cipher.
|
||||
/// </summary>
|
||||
public sealed class AesCmac
|
||||
: KeyedHashAlgorithm
|
||||
{
|
||||
const int BLOCKSIZE = 16; // bytes
|
||||
const int BLOCKSIZE = 16; // bytes
|
||||
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Create()" />
|
||||
/// <remarks>This static override defaults to <see cref="AesCmac" />.</remarks>
|
||||
public static new KeyedHashAlgorithm Create() => new AesCmac();
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Create()" />
|
||||
/// <remarks>This static override defaults to <see cref="AesCmac" />.</remarks>
|
||||
public static new KeyedHashAlgorithm Create() => new AesCmac();
|
||||
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Create(string)" />
|
||||
public static new KeyedHashAlgorithm? Create(string algorithmName)
|
||||
{
|
||||
if (algorithmName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(algorithmName));
|
||||
}
|
||||
return algorithmName == nameof(AesCmac) ? Create() : null;
|
||||
}
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Create(string)" />
|
||||
public static new KeyedHashAlgorithm? Create(string algorithmName)
|
||||
{
|
||||
if (algorithmName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(algorithmName));
|
||||
}
|
||||
return algorithmName == nameof(AesCmac) ? Create() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AesCmac" /> class with a randomly generated key.
|
||||
/// </summary>
|
||||
public AesCmac()
|
||||
{
|
||||
AesEcb = Aes.Create();
|
||||
AesEcb.Mode = CipherMode.ECB; // DevSkim: ignore DS187371
|
||||
AesEcb.Padding = PaddingMode.None;
|
||||
CryptoTransform = AesEcb.CreateEncryptor();
|
||||
HashSizeValue = BLOCKSIZE * 8;
|
||||
}
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AesCmac" /> class with a randomly generated key.
|
||||
/// </summary>
|
||||
public AesCmac()
|
||||
{
|
||||
AesEcb = Aes.Create();
|
||||
AesEcb.Mode = CipherMode.ECB; // DevSkim: ignore DS187371
|
||||
AesEcb.Padding = PaddingMode.None;
|
||||
CryptoTransform = AesEcb.CreateEncryptor();
|
||||
HashSizeValue = BLOCKSIZE * 8;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AesCmac" /> class with the specified key data.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key for AES-CMAC algorithm.</param>
|
||||
public AesCmac(byte[] key)
|
||||
: this()
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AesCmac" /> class with the specified key data.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key for AES-CMAC algorithm.</param>
|
||||
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;
|
||||
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Dispose(bool)" />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
CryptoTransform.Dispose();
|
||||
AesEcb.Dispose();
|
||||
ZeroizeState();
|
||||
}
|
||||
IsDisposed = true;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
#endregion
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Dispose(bool)" />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
CryptoTransform.Dispose();
|
||||
AesEcb.Dispose();
|
||||
ZeroizeState();
|
||||
}
|
||||
IsDisposed = true;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Key" />
|
||||
public override byte[] Key
|
||||
{
|
||||
get => AesEcb.Key;
|
||||
set
|
||||
{
|
||||
CryptoTransform.Dispose();
|
||||
AesEcb.Key = value;
|
||||
CryptoTransform = AesEcb.CreateEncryptor();
|
||||
}
|
||||
}
|
||||
/// <inheritdoc cref="KeyedHashAlgorithm.Key" />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="HashAlgorithm.Initialize" />
|
||||
public override void Initialize()
|
||||
{
|
||||
// See: NIST SP 800-38B, Section 6.2, Step 5
|
||||
ZeroizeState();
|
||||
/// <inheritdoc cref="HashAlgorithm.Initialize" />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="HashAlgorithm.HashCore(byte[], int, int)" />
|
||||
protected override void HashCore(byte[] array, int ibStart, int cbSize)
|
||||
{
|
||||
if (cbSize == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
/// <inheritdoc cref="HashAlgorithm.HashCore(byte[], int, int)" />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="HashAlgorithm.HashFinal" />
|
||||
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;
|
||||
/// <inheritdoc cref="HashAlgorithm.HashFinal" />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<byte> 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<byte> 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<byte> output = new List<byte>();
|
||||
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<byte> 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<byte> 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<byte> output = new List<byte>();
|
||||
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; }
|
||||
}
|
||||
|
||||
@ -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<byte> 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<byte> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<NtagResponse> SendAPDU(NTagCommand apdu);
|
||||
}
|
||||
Task<NtagResponse> SendAPDU(NTagCommand apdu);
|
||||
}
|
||||
|
||||
@ -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<byte>());
|
||||
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<Session> AuthenticateEV2NonFirst(int keyNo, AESKey key)
|
||||
{
|
||||
return AuthenticateEV2(keyNo, key, false);
|
||||
}
|
||||
public Task<Session> AuthenticateEV2First(int keyNo, AESKey key)
|
||||
{
|
||||
return AuthenticateEV2(keyNo, key, true);
|
||||
}
|
||||
public async Task<Session> 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<NtagResponse> 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<byte[]> GetCardUID()
|
||||
{
|
||||
return (await SendAPDU(NtagCommands.GetCardUID)).Data;
|
||||
}
|
||||
|
||||
public async Task<FileSettings> 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<NdefMessage> 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<byte[]> 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<CommMode> 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<byte>());
|
||||
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<Session> AuthenticateEV2NonFirst(int keyNo, AESKey key)
|
||||
{
|
||||
return AuthenticateEV2(keyNo, key, false);
|
||||
}
|
||||
public Task<Session> AuthenticateEV2First(int keyNo, AESKey key)
|
||||
{
|
||||
return AuthenticateEV2(keyNo, key, true);
|
||||
}
|
||||
public async Task<Session> 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<NtagResponse> 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<byte[]> GetCardUID()
|
||||
{
|
||||
return (await SendAPDU(NtagCommands.GetCardUID)).Data;
|
||||
}
|
||||
|
||||
public async Task<FileSettings> 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<NdefMessage> 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<byte[]> 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<CommMode> 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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<NTagError> ErrorCodes = new List<NTagError>();
|
||||
static List<NTagError> DefaultErrorCodes = new List<NTagError>()
|
||||
{
|
||||
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<byte>
|
||||
{
|
||||
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<NTagError> ErrorCodes = new List<NTagError>();
|
||||
static List<NTagError> DefaultErrorCodes = new List<NTagError>()
|
||||
{
|
||||
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<byte>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<NtagResponse> 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<NtagResponse> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
|
||||
|
||||
using System;
|
||||
|
||||
namespace BoltCardTools;
|
||||
|
||||
public record PICCData(byte[]? Uid, int? Counter)
|
||||
{
|
||||
public static PICCData Create(ReadOnlySpan<byte> 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<byte> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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}")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\BoltCardTools.csproj" />
|
||||
<ProjectReference Include="..\src\BTCPayServer.NTag424\BTCPayServer.NTag424.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
global using Xunit;
|
||||
global using Xunit;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user